Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support positional resolver arguments #23

Open
captbaritone opened this issue Nov 2, 2023 · 5 comments · May be fixed by #109
Open

Support positional resolver arguments #23

captbaritone opened this issue Nov 2, 2023 · 5 comments · May be fixed by #109
Labels
design Issue/concern about the design of the system enhancement New feature or request

Comments

@captbaritone
Copy link
Owner

captbaritone commented Nov 2, 2023

GraphQL JS expects GraphQL field arguments to be passed to the resolver as a single args object where each argument is a named property. This makes sense in a world where your executor does not know anything about the implementation of your resolvers, but in Grats I think we can offer a more ergonomic alternative: positional arguments.

So, instead of:

/** @gqlField */
export function greeting(_: Query, args: {name: string, greeting: string}): string {
  return `${args.greeting}, ${args.name}`;
}

We could support the simpler:

/** @gqlField */
export function greeting(_: Query, name: string, greeting: string): string {
  return `${greeting}, ${name}`;
}

This becomes especially nice when you want to have descriptions or @deprecated on your args:

/** @gqlField */
export function greeting(
  _: Query,
  /** Name by which to greet the person */
  name: string,
  /** Salutation to use when greeting */
  greeting: string,
  /** @deprecated Prefer the `name` arg */
  userName: string
): string {
  return `${greeting}, ${name}`;
}

Implementation

At extraction time Grats could annotate the fields with directives defining which field name maps to which position. That mapping could then be implemented using the same type of resolver wrapper we use for @methodName.

Context

One reason not to use positional arguments is that it interferes with your non-GraphQL arguments like context and the extra metadata argument. However, if Grats knew which type was your context type (for example via a @gqlContext tag), it could just know which position you were expecting your context in, and pass it in this position using a similar mapping strategy:

/** @gqlContext */
type RequestContext = { username: string };

/** @gqlField */
export function greeting(
  _: Query,
  greeting: string,
  ctx: GqlContext
): string {
  return `${greeting}, ${ctx.username}`;
}

v.s the more verbose

/** @gqlField */
export function greeting(
  _: Query,
  args: {
    /** Name by which to greet the person */
    name: string,
    /** Salutation to use when greeting */
    greeting: string,
    /** @deprecated Prefer the `name` arg */
    userName: string
  }
): string {
  return `${args.greeting}, ${args.name}`;
}

Now even the context object does not need to be defined at a fixed postion. Maybe you prefer getting your context first?

/** @gqlContext */
type RequestContext = { username: string };

/** @gqlField */
export function greeting(
  _: Query,
  ctx: GqlContext,
  greeting: string
): string {
  return `${greeting}, ${ctx.username}`;
}

(On the other hand, maybe having a strong convention around context going after all GraphQL arguments is better for clarity in the code base. In which case that last option might be a bridge too far.)

@captbaritone captbaritone added enhancement New feature or request design Issue/concern about the design of the system labels Nov 2, 2023
@captbaritone
Copy link
Owner Author

captbaritone commented Nov 3, 2023

After some discussion in other issues (#21 (comment)) and on Discord, I think this is probably not great to use positional arguments. GraphQL arguments are named/keyed so, while the object syntax is awkward in JS, I think it's best to stick with it since it more semantically matches GraphQL. At some point in the future we could come back to this if we think the ergonomic value outweighs modeling GraphQL semantics accurately.

@captbaritone
Copy link
Owner Author

captbaritone commented Nov 3, 2023

Another point in favor of this approach is the ergonomics of default values, especially when combined with descriptions.

/** @gqlField */
export async function searchItems(
  _: Query,
  {
    query,
    first = 100,
  }: {
    query: string;
    /** Must be between 100 and 10,000 */
    first?: Int;
  }
): Promise<SearchItemsConnection> {
  // ...
}

Is much more painful than:

/** @gqlField */
export async function searchItems(
  _: Query,
  query: string,
  /** Must be between 100 and 10,000 */
  first?: Int = 10
): Promise<SearchItemsConnection> {
  // ...
}

@captbaritone
Copy link
Owner Author

Another argument in favor of this approach came up in Discord and I figured I'd document it here:

One appealing pattern which is uniquely enabled by implementation first solutions like Grats is that you can reuse your model types as the skeleton of your GraphQL Schema.

For example, if you had a user model:

class User {
  name: string;
  greet(salutation: string): string;
}

And you wanted to expose it in the graph:

/** @gqlType */
class User {
   /** @gqlField */
  name: string;
   /** @gqlField */
  greet(salutation: string): string  {
    return `${salutation} ${this.name}`
  };
}

Today you'd either need to add a new field for greet that accepted its args as an object, or change the signature of the existing greet field to be less ergonomic for internal use. However, if we supported positional arguments, you could expose it as written without needing to duplicate it or modify it.

@captbaritone
Copy link
Owner Author

As I think more about this, one of the key value Grats unlocks, is to expose existing methods as fields. I think enabling that type of developer experience might bring this feature back into the set of things we should consider doing.

@captbaritone captbaritone reopened this Dec 12, 2023
@captbaritone
Copy link
Owner Author

I took a stab at migrating a TypeGraphQL app to Grats this evening, and found that not having support for positional args made it rather difficult. Ideally, one could just:

  1. Add Grats annotations to existing code to match the TypeGraphQL decorators
  2. Validate that the Grats schema and TypeGraphQL schema match
  3. Delete all the TypeGraphQL directives

Right now that's not possible due to Grats requiring object shaped args.

@captbaritone captbaritone linked a pull request Jan 19, 2024 that will close this issue
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Issue/concern about the design of the system enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant