diff --git a/src/Errors.ts b/src/Errors.ts index 02365ba4..deda2d91 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -255,8 +255,8 @@ export function methodMissingType() { return "Expected GraphQL field methods to have an explicitly defined return type. Grats needs to be able to see the type of the field to generate its type in the GraphQL schema."; } -export function wrapperMissingTypeArg() { - return `Expected wrapper type reference to have type arguments. Grats needs to be able to see the return type in order to generate a GraphQL schema.`; +export function wrapperMissingTypeArg(wrapperTypeName: string) { + return `Expected \`${wrapperTypeName}\` type to have exactly one type argument. Grats needs to be able to see the inner type in order to generate a GraphQL schema.`; } export function invalidWrapperOnInputType(wrapperName: string) { @@ -607,7 +607,7 @@ export function invalidDerivedContextArgType() { } export function missingReturnTypeForDerivedResolver() { - return 'Expected derived resolver to have an explicit return type. This is needed to allow Grats to "see" which type to treat as a derived context type.'; + return 'Expected derived resolver\'s return type to be a named type alias, e.g. `: SomeType`. This is needed to allow Grats to "see" which type declaration to treat as the derived context type.'; } export function derivedResolverInvalidReturnType() { diff --git a/src/Extractor.ts b/src/Extractor.ts index e6bdbd32..6cc40c72 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -411,8 +411,14 @@ class Extractor { if (returnType == null) { return this.report(node, E.missingReturnTypeForDerivedResolver()); } - if (!ts.isTypeReferenceNode(returnType)) { - return this.report(returnType, E.missingReturnTypeForDerivedResolver()); + + // Check if the return type is Promise and unwrap it + const unwrapped = this.maybeUnwrapPromiseType(returnType); + if (unwrapped === null) return null; + const { type: innerType, isAsync } = unwrapped; + + if (!ts.isTypeReferenceNode(innerType)) { + return this.report(innerType, E.missingReturnTypeForDerivedResolver()); } const funcName = this.namedFunctionExportName(node); @@ -434,8 +440,9 @@ class Extractor { path: tsModulePath, exportName: funcName?.text ?? null, args: paramResults.resolverParams, + async: isAsync, }, - returnType, + innerType, ); } @@ -2592,6 +2599,30 @@ class Extractor { return null; } + /** + * Unwraps a Promise type to T, tracking whether it was async. + * Returns null if there's an error (e.g., Promise without type arguments). + */ + maybeUnwrapPromiseType( + type: ts.TypeNode, + ): { type: ts.TypeNode; isAsync: boolean } | null { + if (!ts.isTypeReferenceNode(type)) { + return { type, isAsync: false }; + } + + const typeName = type.typeName; + if (ts.isIdentifier(typeName) && typeName.text === "Promise") { + if (type.typeArguments == null || type.typeArguments.length !== 1) { + // + this.report(type, E.wrapperMissingTypeArg(typeName.text)); + return null; + } + return { type: type.typeArguments[0], isAsync: true }; + } + + return { type, isAsync: false }; + } + typeReference( node: ts.TypeReferenceNode, ctx: FieldTypeContext, @@ -2632,10 +2663,9 @@ class Extractor { return this.gql.nonNullType(node, listType); } case "Promise": { - if (node.typeArguments == null) { - return this.report(node, E.wrapperMissingTypeArg()); - } - const element = this.collectType(node.typeArguments[0], ctx); + const unwrapped = this.maybeUnwrapPromiseType(node); + if (unwrapped === null) return null; + const element = this.collectType(unwrapped.type, ctx); if (element == null) return null; return element; } diff --git a/src/TypeContext.ts b/src/TypeContext.ts index e0c7e4eb..6b4ac6be 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -27,6 +27,7 @@ export type DerivedResolverDefinition = { exportName: string | null; args: ResolverArgument[]; kind: "DERIVED_CONTEXT"; + async: boolean; }; export type NameDefinition = { diff --git a/src/codegen/TSAstBuilder.ts b/src/codegen/TSAstBuilder.ts index b0c16858..403d1be5 100644 --- a/src/codegen/TSAstBuilder.ts +++ b/src/codegen/TSAstBuilder.ts @@ -56,9 +56,10 @@ export default class TSAstBuilder { name: string, params: ts.ParameterDeclaration[], statements: ts.Statement[], + isAsync: boolean = false, ): ts.MethodDeclaration { return F.createMethodDeclaration( - undefined, + isAsync ? [F.createModifier(ts.SyntaxKind.AsyncKeyword)] : undefined, undefined, name, undefined, diff --git a/src/codegen/resolverCodegen.ts b/src/codegen/resolverCodegen.ts index 07d26525..99fc748b 100644 --- a/src/codegen/resolverCodegen.ts +++ b/src/codegen/resolverCodegen.ts @@ -64,11 +64,14 @@ export default class ResolverCodegen { ), ), ], + false, ); case "method": { + const args = resolver.arguments ?? []; + const isAsync = args.some(usesAsyncDerivedContext); return this.ts.method( methodName, - extractUsedParams(resolver.arguments ?? [], true).map((name) => { + extractUsedParams(args, true).map((name) => { if (name === "source") { return this.ts.param("source", getSourceTypeRef()); } @@ -82,12 +85,13 @@ export default class ResolverCodegen { F.createIdentifier(resolver.name ?? fieldName), ), [], - (resolver.arguments ?? []).map((arg) => { + args.map((arg) => { return this.resolverParam(arg); }), ), ), ], + isAsync, ); } case "function": { @@ -101,9 +105,11 @@ export default class ResolverCodegen { resolverName, false, ); + const args = resolver.arguments ?? []; + const isAsync = args.some(usesAsyncDerivedContext); return this.ts.method( methodName, - extractUsedParams(resolver.arguments ?? []).map((name) => { + extractUsedParams(args).map((name) => { if (name === "source") { return this.ts.param("source", getSourceTypeRef()); } @@ -114,12 +120,13 @@ export default class ResolverCodegen { F.createCallExpression( F.createIdentifier(resolverName), undefined, - (resolver.arguments ?? []).map((arg) => { + args.map((arg) => { return this.resolverParam(arg); }), ), ), ], + isAsync, ); } case "staticMethod": { @@ -135,9 +142,11 @@ export default class ResolverCodegen { resolverName, false, ); + const args = resolver.arguments ?? []; + const isAsync = args.some(usesAsyncDerivedContext); return this.ts.method( methodName, - extractUsedParams(resolver.arguments ?? []).map((name) => { + extractUsedParams(args).map((name) => { if (name === "source") { return this.ts.param("source", getSourceTypeRef()); } @@ -151,12 +160,13 @@ export default class ResolverCodegen { F.createIdentifier(resolver.name), ), undefined, - (resolver.arguments ?? []).map((arg) => { + args.map((arg) => { return this.resolverParam(arg); }), ), ), ], + isAsync, ); } default: @@ -213,11 +223,16 @@ export default class ResolverCodegen { case "derivedContext": { const localName = this.getDerivedContextName(arg.path, arg.exportName); this.ts.importUserConstruct(arg.path, arg.exportName, localName, false); - return F.createCallExpression( + const callExpr = F.createCallExpression( F.createIdentifier(localName), undefined, arg.args.map((arg) => this.resolverParam(arg)), ); + // If the derived context is async, we need to await it + if (arg.async) { + return F.createAwaitExpression(callExpr); + } + return callExpr; } default: @@ -361,6 +376,16 @@ function usesContext(param: ResolverArgument) { } } +// Check if any param is or uses an async derived context +function usesAsyncDerivedContext(param: ResolverArgument): boolean { + switch (param.kind) { + case "derivedContext": + return param.async || param.args.some(usesAsyncDerivedContext); + default: + return false; + } +} + function fieldDirective( field: GraphQLField, name: string, diff --git a/src/metadata.ts b/src/metadata.ts index 0faae6ef..1450691c 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -114,6 +114,7 @@ export type DerivedContextArgument = { path: string; // Path to the module exportName: string | null; // Export name. If omitted, the class is the default export args: Array; + async: boolean; }; /** The GraphQL info object */ diff --git a/src/resolverSignature.ts b/src/resolverSignature.ts index 390e7508..fbb5a84b 100644 --- a/src/resolverSignature.ts +++ b/src/resolverSignature.ts @@ -66,6 +66,7 @@ export type DerivedContextResolverArgument = { exportName: string | null; args: Array; node: ts.Node; + async: boolean; }; export type InformationResolverArgument = { diff --git a/src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts.expected.md index 0582e48f..9d2ec830 100644 --- a/src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts.expected.md +++ b/src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts.expected.md @@ -29,7 +29,7 @@ export function greeting(_: Query, ctx: DerivedContext): string { ### Error Report ```text -src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts:11:1 - error: Expected derived resolver to have an explicit return type. This is needed to allow Grats to "see" which type to treat as a derived context type. +src/tests/fixtures/derived_context/derivedContextNoReturnType.invalid.ts:11:1 - error: Expected derived resolver's return type to be a named type alias, e.g. `: SomeType`. This is needed to allow Grats to "see" which type declaration to treat as the derived context type. 11 export function createDerivedContext(ctx: RootContext) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts.expected.md index f3c3fd53..97ddc709 100644 --- a/src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts.expected.md +++ b/src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts.expected.md @@ -29,7 +29,7 @@ export function greeting(_: Query, ctx: DerivedContext): string { ### Error Report ```text -src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts:11:57 - error: Expected derived resolver to have an explicit return type. This is needed to allow Grats to "see" which type to treat as a derived context type. +src/tests/fixtures/derived_context/derivedContextNonNamedReturnType.invalid.ts:11:57 - error: Expected derived resolver's return type to be a named type alias, e.g. `: SomeType`. This is needed to allow Grats to "see" which type declaration to treat as the derived context type. 11 export function createDerivedContext(ctx: RootContext): { greeting: string } { ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts b/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts new file mode 100644 index 00000000..4fdc6c3e --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts @@ -0,0 +1,18 @@ +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext(ctx: RootContext): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts.expected.md new file mode 100644 index 00000000..9c571c1c --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts.expected.md @@ -0,0 +1,33 @@ +## input + +```ts title="derived_context/derivedContextPromiseNoTypeArg.invalid.ts" +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext(ctx: RootContext): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} +``` + +## Output + +### Error Report + +```text +src/tests/fixtures/derived_context/derivedContextPromiseNoTypeArg.invalid.ts:11:63 - error: Expected `Promise` type to have exactly one type argument. Grats needs to be able to see the inner type in order to generate a GraphQL schema. + +11 export async function createDerivedContext(ctx: RootContext): Promise { + ~~~~~~~ +``` \ No newline at end of file diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts b/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts new file mode 100644 index 00000000..ace74ebc --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts @@ -0,0 +1,19 @@ +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, +): Promise<{ greeting: string }> { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function greeting(_: Query, ctx: { greeting: string }): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts.expected.md new file mode 100644 index 00000000..b9bfd24f --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts.expected.md @@ -0,0 +1,34 @@ +## input + +```ts title="derived_context/derivedContextPromiseNonReferenceInner.invalid.ts" +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, +): Promise<{ greeting: string }> { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function greeting(_: Query, ctx: { greeting: string }): string { + return ctx.greeting; +} +``` + +## Output + +### Error Report + +```text +src/tests/fixtures/derived_context/derivedContextPromiseNonReferenceInner.invalid.ts:9:12 - error: Expected derived resolver's return type to be a named type alias, e.g. `: SomeType`. This is needed to allow Grats to "see" which type declaration to treat as the derived context type. + +9 ): Promise<{ greeting: string }> { + ~~~~~~~~~~~~~~~~~~~~ +``` \ No newline at end of file diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts b/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts new file mode 100644 index 00000000..991db617 --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts @@ -0,0 +1,21 @@ +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, + // @ts-expect-error - Promise only takes one type argument +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts.expected.md new file mode 100644 index 00000000..bd9b18cf --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts.expected.md @@ -0,0 +1,36 @@ +## input + +```ts title="derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts" +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, + // @ts-expect-error - Promise only takes one type argument +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} +``` + +## Output + +### Error Report + +```text +src/tests/fixtures/derived_context/derivedContextPromiseTooManyTypeArgs.invalid.ts:14:4 - error: Expected `Promise` type to have exactly one type argument. Grats needs to be able to see the inner type in order to generate a GraphQL schema. + +14 ): Promise { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` \ No newline at end of file diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts b/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts new file mode 100644 index 00000000..fa59960b --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts @@ -0,0 +1,20 @@ +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts.expected.md b/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts.expected.md new file mode 100644 index 00000000..f3d4f81e --- /dev/null +++ b/src/tests/fixtures/derived_context/derivedContextPromiseValid.ts.expected.md @@ -0,0 +1,61 @@ +## input + +```ts title="derived_context/derivedContextPromiseValid.ts" +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext( + ctx: RootContext, +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext): string { + return ctx.greeting; +} +``` + +## Output + +### SDL + +```graphql +type Query { + greeting: String +} +``` + +### TypeScript + +```ts +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +import { greeting as queryGreetingResolver, createDerivedContext } from "./derivedContextPromiseValid"; +export function getSchema(): GraphQLSchema { + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString, + async resolve(_source, _args, context) { + return queryGreetingResolver(await createDerivedContext(context)); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType] + }); +} +``` \ No newline at end of file diff --git a/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts b/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts new file mode 100644 index 00000000..3a193f48 --- /dev/null +++ b/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts @@ -0,0 +1,30 @@ +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext1 = { + greeting: string; +}; +type DerivedContext2 = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext1( + ctx: RootContext, +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlContext */ +export async function createDerivedContext2( + ctx: DerivedContext1, +): Promise { + return { greeting: `Hello, ${ctx.greeting}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext2): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts.expected.md b/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts.expected.md new file mode 100644 index 00000000..7481cc56 --- /dev/null +++ b/src/tests/fixtures/derived_context/nestedAsyncDerivedContext.ts.expected.md @@ -0,0 +1,71 @@ +## input + +```ts title="derived_context/nestedAsyncDerivedContext.ts" +/** @gqlContext */ +type RootContext = { + userName: string; +}; + +type DerivedContext1 = { + greeting: string; +}; +type DerivedContext2 = { + greeting: string; +}; + +/** @gqlContext */ +export async function createDerivedContext1( + ctx: RootContext, +): Promise { + return { greeting: `Hello, ${ctx.userName}!` }; +} + +/** @gqlContext */ +export async function createDerivedContext2( + ctx: DerivedContext1, +): Promise { + return { greeting: `Hello, ${ctx.greeting}!` }; +} + +/** @gqlQueryField */ +export function greeting(ctx: DerivedContext2): string { + return ctx.greeting; +} +``` + +## Output + +### SDL + +```graphql +type Query { + greeting: String +} +``` + +### TypeScript + +```ts +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +import { greeting as queryGreetingResolver, createDerivedContext2, createDerivedContext1 } from "./nestedAsyncDerivedContext"; +export function getSchema(): GraphQLSchema { + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString, + async resolve(_source, _args, context) { + return queryGreetingResolver(await createDerivedContext2(await createDerivedContext1(context))); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType] + }); +} +``` \ No newline at end of file diff --git a/src/tests/integrationFixtures/derivedContextAsync/index.ts b/src/tests/integrationFixtures/derivedContextAsync/index.ts new file mode 100644 index 00000000..a30269a6 --- /dev/null +++ b/src/tests/integrationFixtures/derivedContextAsync/index.ts @@ -0,0 +1,53 @@ +/** @gqlContext */ +type Ctx = {}; + +type SomeCtx = { name: string }; + +/** @gqlContext */ +export async function derived(): Promise { + return { name: "Roger" }; +} + +/** @gqlType */ +export class User { + /** @gqlField */ + name: string; + + constructor(name: string) { + this.name = name; + } + + /** @gqlField */ + greeting(someCtx: SomeCtx): string { + return `Hello ${this.name}, from ${someCtx.name}`; + } + + /** @gqlQueryField */ + static currentUser(someCtx: SomeCtx): User { + return new User(someCtx.name); + } +} + +/** @gqlQueryField */ +export function hello(someCtx: SomeCtx): string { + return `Hello ${someCtx.name}`; +} + +/** @gqlQueryField */ +export function user(someCtx: SomeCtx): User { + return new User(someCtx.name); +} + +export const query = ` + query { + hello + user { + name + greeting + } + currentUser { + name + greeting + } + } + `; diff --git a/src/tests/integrationFixtures/derivedContextAsync/index.ts.expected.md b/src/tests/integrationFixtures/derivedContextAsync/index.ts.expected.md new file mode 100644 index 00000000..3a6fb1cf --- /dev/null +++ b/src/tests/integrationFixtures/derivedContextAsync/index.ts.expected.md @@ -0,0 +1,77 @@ +## input + +```ts title="derivedContextAsync/index.ts" +/** @gqlContext */ +type Ctx = {}; + +type SomeCtx = { name: string }; + +/** @gqlContext */ +export async function derived(): Promise { + return { name: "Roger" }; +} + +/** @gqlType */ +export class User { + /** @gqlField */ + name: string; + + constructor(name: string) { + this.name = name; + } + + /** @gqlField */ + greeting(someCtx: SomeCtx): string { + return `Hello ${this.name}, from ${someCtx.name}`; + } + + /** @gqlQueryField */ + static currentUser(someCtx: SomeCtx): User { + return new User(someCtx.name); + } +} + +/** @gqlQueryField */ +export function hello(someCtx: SomeCtx): string { + return `Hello ${someCtx.name}`; +} + +/** @gqlQueryField */ +export function user(someCtx: SomeCtx): User { + return new User(someCtx.name); +} + +export const query = ` + query { + hello + user { + name + greeting + } + currentUser { + name + greeting + } + } + `; +``` + +## Output + +### Query Result + +```json +{ + "data": { + "hello": "Hello Roger", + "user": { + "name": "Roger", + "greeting": "Hello Roger, from Roger" + }, + "currentUser": { + "name": "Roger", + "greeting": "Hello Roger, from Roger" + } + } +} +``` \ No newline at end of file diff --git a/src/tests/integrationFixtures/derivedContextAsync/schema.ts b/src/tests/integrationFixtures/derivedContextAsync/schema.ts new file mode 100644 index 00000000..44e8071b --- /dev/null +++ b/src/tests/integrationFixtures/derivedContextAsync/schema.ts @@ -0,0 +1,54 @@ +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +import { derived, User as queryCurrentUserResolver, hello as queryHelloResolver, user as queryUserResolver } from "./index"; +export function getSchema(): GraphQLSchema { + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString, + async resolve(source) { + return source.greeting(await derived()); + } + }, + name: { + name: "name", + type: GraphQLString + } + }; + } + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + currentUser: { + name: "currentUser", + type: UserType, + async resolve() { + return queryCurrentUserResolver.currentUser(await derived()); + } + }, + hello: { + name: "hello", + type: GraphQLString, + async resolve() { + return queryHelloResolver(await derived()); + } + }, + user: { + name: "user", + type: UserType, + async resolve() { + return queryUserResolver(await derived()); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType, UserType] + }); +} diff --git a/src/transforms/makeResolverSignature.ts b/src/transforms/makeResolverSignature.ts index 1770cbaa..8a215e13 100644 --- a/src/transforms/makeResolverSignature.ts +++ b/src/transforms/makeResolverSignature.ts @@ -99,6 +99,7 @@ function transformArg(arg: DirectiveResolverArgument): ResolverArgument { kind: "derivedContext", path: arg.path, exportName: arg.exportName, + async: arg.async, args: arg.args.map((arg): ContextArgs => { const newArg = transformArg(arg); invariant( diff --git a/src/transforms/resolveResolverParams.ts b/src/transforms/resolveResolverParams.ts index 95673ea6..8442668f 100644 --- a/src/transforms/resolveResolverParams.ts +++ b/src/transforms/resolveResolverParams.ts @@ -181,7 +181,7 @@ class ResolverParamsResolver { definition: DerivedResolverDefinition, seenDerivedContextValues?: Map, ): ResolverArgument | null { - const { path, exportName, args } = definition; + const { path, exportName, args, async } = definition; const key = `${path}:${exportName}`; if (seenDerivedContextValues == null) { // We're resolving the arg of a resolver. Initiate the map. @@ -220,7 +220,14 @@ class ResolverParamsResolver { ); } } - return { kind: "derivedContext", node, path, exportName, args: newArgs }; + return { + kind: "derivedContext", + node, + path, + exportName, + args: newArgs, + async, + }; } resolveToPositionalArg( diff --git a/website/docs/07-changelog/index.md b/website/docs/07-changelog/index.md index b38eb46c..8e06cdbb 100644 --- a/website/docs/07-changelog/index.md +++ b/website/docs/07-changelog/index.md @@ -1,8 +1,11 @@ # Changelog - +- **Features** + - Added support for async derived context functions. Derived context functions can now return `Promise` and Grats will automatically generate the necessary `await` expressions in resolver code. + +Changes in this section are not yet released. If you need access to these changes before we cut a release, check out our `@main` NPM releases. Each commit on the main branch is [published to NPM](https://www.npmjs.com/package/grats?activeTab=versions) under the `main` tag. ## 0.0.34