From a4e62e85da97104c405fc665fb1a9a19b70350a8 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Wed, 1 Nov 2023 23:41:59 -0700 Subject: [PATCH 1/5] [WIP] Validate the type of context argument --- TODO.md | 1 - src/Errors.ts | 32 ++++++- src/Extractor.ts | 85 ++++++++++++++++++- src/TypeContext.ts | 6 ++ ...WithoutTypeOrInterface.invalid.ts.expected | 2 +- .../ClassMethodWithContextValue.ts | 12 +++ .../ClassMethodWithContextValue.ts.expected | 30 +++++++ .../ClassMethodWithContextValueExported.ts | 12 +++ ...MethodWithContextValueExported.ts.expected | 30 +++++++ ...MethodWithContextValueInArgsPos.invalid.ts | 12 +++ ...hContextValueInArgsPos.invalid.ts.expected | 23 +++++ ...ntextValueMissingTypeAnnotation.invalid.ts | 7 ++ ...eMissingTypeAnnotation.invalid.ts.expected | 18 ++++ .../resolver_context/ContextValueOptional.ts | 13 +++ .../ContextValueOptional.ts.expected | 31 +++++++ .../ContextValueSpread.invalid.ts | 10 +++ .../ContextValueSpread.invalid.ts.expected | 21 +++++ .../ContextValueTypeNotDefined.ts | 7 ++ .../ContextValueTypeNotDefined.ts.expected | 18 ++++ .../ContextValueTypedAsAny.invalid.ts | 7 ++ ...ContextValueTypedAsAny.invalid.ts.expected | 18 ++++ .../ContextValueTypedAsLiteral.invalid.ts | 7 ++ ...extValueTypedAsLiteral.invalid.ts.expected | 18 ++++ .../ContextValueTypedAsNever.ts | 7 ++ .../ContextValueTypedAsNever.ts.expected | 18 ++++ .../ContextValueTypedAsString.invalid.ts | 7 ++ ...textValueTypedAsString.invalid.ts.expected | 18 ++++ .../FunctionWithContextValue.ts | 12 +++ .../FunctionWithContextValue.ts.expected | 26 ++++++ .../MultipleContextTags.invalid.ts | 9 ++ .../MultipleContextTags.invalid.ts.expected | 25 ++++++ ...rfaceWithDeprecatedTag.invalid.ts.expected | 2 +- ...ypeImplementsInterface.invalid.ts.expected | 2 +- ...rfaceWithDeprecatedTag.invalid.ts.expected | 2 +- .../user_error/GqlTagDoesNotExist.ts.expected | 2 +- .../user_error/WrongCaseGqlTag.ts.expected | 2 +- website/docs/04-dockblock-tags/09-context.mdx | 41 +++++++++ 37 files changed, 582 insertions(+), 11 deletions(-) create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueOptional.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/FunctionWithContextValue.ts create mode 100644 src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected create mode 100644 src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected create mode 100644 website/docs/04-dockblock-tags/09-context.mdx diff --git a/TODO.md b/TODO.md index 79a89d30..9ddd4aba 100644 --- a/TODO.md +++ b/TODO.md @@ -50,7 +50,6 @@ - [ ] Improve playground - Could we actually evaluate the resolvers? Maybe in a worker? - Could we hook up GraphiQL 2? -- [ ] Can we ensure the context and ast arguments of resolvers are correct? - [ ] Can we use TypeScript's inference to infer types? - [ ] For example, a method which returns a string, or a property that has a default value. - [ ] Define resolvers? diff --git a/src/Errors.ts b/src/Errors.ts index 85812088..d0220699 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -248,7 +248,7 @@ export function pluralTypeMissingParameter() { return `Expected type reference to have type arguments.`; } -export function expectedIdentifer() { +export function expectedIdentifier() { return "Expected an identifier."; } @@ -323,3 +323,33 @@ export function invalidTypePassedToFieldFunction() { export function unresolvedTypeReference() { return "This type is not a valid GraphQL type. Did you mean to annotate it's definition with a `/** @gql */` tag such as `/** @gqlType */` or `/** @gqlInput **/`?"; } + +export function expectedTypeAnnotationOnContext() { + return "Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; +} + +export function expectedTypeAnnotationOfReferenceOnContext() { + return "Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; +} + +export function expectedTypeAnnotationOnContextToBeResolvable() { + // TODO: Provide guidance? + return "Unable to resolve the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; +} + +export function expectedTypeAnnotationOnContextToHaveDeclaration() { + // TODO: Provide guidance? + return "Unable to locate the declaration of the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. Did you mean to import or define this type?"; +} + +export function expectedTypeAnnotationOnContextToHaveContextTag() { + return "Expected the definition of the context type to be annotated with `/** @gqlContext */`. Did you mean to add that annotation?"; +} + +export function duplicateContextDeclaration() { + return "Unexpected duplicate declaration of `/** @gqlContext */`. Grats expects there to be only one context type."; +} + +export function unexpectedParamSpreadForContextParam() { + return "Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument."; +} diff --git a/src/Extractor.ts b/src/Extractor.ts index 38263875..6ff1ebb6 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -49,6 +49,7 @@ export const INTERFACE_TAG = "gqlInterface"; export const ENUM_TAG = "gqlEnum"; export const UNION_TAG = "gqlUnion"; export const INPUT_TAG = "gqlInput"; +export const CONTEXT_TAG = "gqlContext"; export const IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; export const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; @@ -62,6 +63,7 @@ export const ALL_TAGS = [ ENUM_TAG, UNION_TAG, INPUT_TAG, + CONTEXT_TAG, ]; const DEPRECATED_TAG = "deprecated"; @@ -148,6 +150,24 @@ export class Extractor { } break; } + case CONTEXT_TAG: { + if (this.ctx.contextDeclaration == null) { + this.ctx.contextDeclaration = { node: tag }; + } else { + this.report(tag.tagName, E.duplicateContextDeclaration(), [ + this.related( + this.ctx.contextDeclaration.node, + "Previous context declaration", + ), + ]); + } + // TODO: Validate that this is some kind of declaration + // type alias? + // interface? + // class? + // export of one of the above? + break; + } default: { const lowerCaseTag = tag.tagName.text.toLowerCase(); @@ -358,6 +378,11 @@ export class Extractor { args = this.collectArgs(argsParam); } + const context = node.parameters[2]; + if (context != null) { + this.validateContextParameter(context); + } + const description = this.collectDescription(funcName); if (!ts.isSourceFile(node.parent)) { @@ -980,7 +1005,7 @@ export class Extractor { } else if (node.kind === ts.SyntaxKind.FalseKeyword) { return this.gql.boolean(node, false); } else if (ts.isObjectLiteralExpression(node)) { - return this.cellectObjectLiteral(node); + return this.collectObjectLiteral(node); } else if (ts.isArrayLiteralExpression(node)) { return this.collectArrayLiteral(node); } @@ -1010,7 +1035,7 @@ export class Extractor { return this.gql.list(node, values); } - cellectObjectLiteral( + collectObjectLiteral( node: ts.ObjectLiteralExpression, ): ConstObjectValueNode | null { const fields: ConstObjectFieldNode[] = []; @@ -1301,6 +1326,55 @@ export class Extractor { return this.gql.name(id, id.text); } + // Ensure the type of the ctx param resolves to the declaration + // annotated with `@gqlContext`. + validateContextParameter(node: ts.ParameterDeclaration) { + if (node.type == null) { + return this.report(node, E.expectedTypeAnnotationOnContext()); + } + + if (!ts.isTypeReferenceNode(node.type)) { + return this.report( + node.type, + E.expectedTypeAnnotationOfReferenceOnContext(), + ); + } + + // Check for ... + if (node.dotDotDotToken != null) { + return this.report( + node.dotDotDotToken, + E.unexpectedParamSpreadForContextParam(), + ); + } + + const symbol = this.ctx.checker.getSymbolAtLocation(node.type.typeName); + if (symbol == null) { + return this.report( + node.type.typeName, + E.expectedTypeAnnotationOnContextToBeResolvable(), + ); + } + + const declaration = this.ctx.findSymbolDeclaration(symbol); + if (declaration == null) { + return this.report( + node.type.typeName, + E.expectedTypeAnnotationOnContextToHaveDeclaration(), + ); + } + + const contextTag = this.findTag(declaration, CONTEXT_TAG); + + if (contextTag == null) { + return this.report( + node.type.typeName, + E.expectedTypeAnnotationOnContextToHaveContextTag(), + [this.related(declaration, "The type was declared here")], + ); + } + } + methodDeclaration( node: ts.MethodDeclaration | ts.MethodSignature, ): FieldDefinitionNode | null { @@ -1325,6 +1399,11 @@ export class Extractor { args = this.collectArgs(argsParam); } + const context = node.parameters[1]; + if (context != null) { + this.validateContextParameter(context); + } + const description = this.collectDescription(node.name); const id = this.expectIdentifier(node.name); @@ -1556,7 +1635,7 @@ export class Extractor { if (ts.isIdentifier(node)) { return node; } - return this.report(node, E.expectedIdentifer()); + return this.report(node, E.expectedIdentifier()); } findTag(node: ts.Node, tagName: string): ts.JSDocTag | null { diff --git a/src/TypeContext.ts b/src/TypeContext.ts index 5539c688..4dbdb43b 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -43,6 +43,10 @@ export type AbstractFieldDefinitionNode = { readonly field: FieldDefinitionNode; }; +type ContextDeclaration = { + node: ts.Node; +}; + /** * Used to track TypeScript references. * @@ -62,6 +66,8 @@ export class TypeContext { _options: ts.ParsedCommandLine; _symbolToName: Map = new Map(); _unresolvedTypes: Map = new Map(); + // The `@gqlContext` declaration, if it has been encountered. + contextDeclaration: ContextDeclaration | null = null; hasTypename: Set = new Set(); constructor( diff --git a/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected b/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected index 46615c6d..fb9edc7f 100644 --- a/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected +++ b/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected @@ -9,7 +9,7 @@ function hello() { ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts:1:6 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts:1:6 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 1 /** @gqlImplements Node */ ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts new file mode 100644 index 00000000..68dba538 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts @@ -0,0 +1,12 @@ +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected new file mode 100644 index 00000000..b6be0138 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected @@ -0,0 +1,30 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + greeting: String +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts new file mode 100644 index 00000000..ee739b4f --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts @@ -0,0 +1,12 @@ +/** @gqlContext */ +export type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected new file mode 100644 index 00000000..b6183e69 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected @@ -0,0 +1,30 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +export type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + greeting: String +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts new file mode 100644 index 00000000..2626cca9 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts @@ -0,0 +1,12 @@ +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(ctx: GratsContext): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected new file mode 100644 index 00000000..e2f2e3e0 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected @@ -0,0 +1,23 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(ctx: GratsContext): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts:9:17 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. + +9 greeting(ctx: GratsContext): string { + ~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts new file mode 100644 index 00000000..d620a31a --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected new file mode 100644 index 00000000..22aa98ce --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts:4:25 - error: Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. + +4 greeting(args: never, ctx): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts b/src/tests/fixtures/resolver_context/ContextValueOptional.ts new file mode 100644 index 00000000..36ecbe8c --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts @@ -0,0 +1,13 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx?: SomeType): string { + // This is fine since Grats will always pass ctx. It's fine for + // the resolver to _also_ work _without_ ctx, as long as it's + // safe for Grats to pass ctx. + return ctx?.greeting ?? "Hello, World!"; + } +} + +/** @gqlContext */ +type SomeType = { greeting: string }; diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected new file mode 100644 index 00000000..9816dd23 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected @@ -0,0 +1,31 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx?: SomeType): string { + // This is fine since Grats will always pass ctx. It's fine for + // the resolver to _also_ work _without_ ctx, as long as it's + // safe for Grats to pass ctx. + return ctx?.greeting ?? "Hello, World!"; + } +} + +/** @gqlContext */ +type SomeType = { greeting: string }; + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + greeting: String +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts new file mode 100644 index 00000000..77e1cdce --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts @@ -0,0 +1,10 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ...ctx: SomeType): string { + return ctx[0].greeting; + } +} + +/** @gqlContext */ +type SomeType = { greeting: string }; diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected new file mode 100644 index 00000000..59c6135d --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected @@ -0,0 +1,21 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ...ctx: SomeType): string { + return ctx[0].greeting; + } +} + +/** @gqlContext */ +type SomeType = { greeting: string }; + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts:4:25 - error: Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument. + +4 greeting(args: never, ...ctx: SomeType): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts new file mode 100644 index 00000000..ad0f10fd --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: ThisIsNeverDefined): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected new file mode 100644 index 00000000..2a3658bc --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: ThisIsNeverDefined): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts:4:30 - error: Unable to locate the declaration of the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. Did you mean to import or define this type? + +4 greeting(args: never, ctx: ThisIsNeverDefined): string { + ~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts new file mode 100644 index 00000000..c300b979 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: any): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected new file mode 100644 index 00000000..3c2edca4 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: any): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. + +4 greeting(args: never, ctx: any): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts new file mode 100644 index 00000000..5c6ad453 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: { greeting: string }): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected new file mode 100644 index 00000000..cec03853 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: { greeting: string }): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. + +4 greeting(args: never, ctx: { greeting: string }): string { + ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts new file mode 100644 index 00000000..9fe0ab4e --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: never): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected new file mode 100644 index 00000000..2682a5b6 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: never): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. + +4 greeting(args: never, ctx: never): string { + ~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts new file mode 100644 index 00000000..4664dc39 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: string): string { + return ctx; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected new file mode 100644 index 00000000..eb01260b --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: string): string { + return ctx; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. + +4 greeting(args: never, ctx: string): string { + ~~~~~~ diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts new file mode 100644 index 00000000..c02a33e3 --- /dev/null +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts @@ -0,0 +1,12 @@ +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class User {} + +/** @gqlField */ +export function greeting(_: User, args: never, ctx: GratsContext): string { + return ctx.greeting; +} diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected new file mode 100644 index 00000000..42005bb5 --- /dev/null +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected @@ -0,0 +1,26 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class User {} + +/** @gqlField */ +export function greeting(_: User, args: never, ctx: GratsContext): string { + return ctx.greeting; +} + +----------------- +OUTPUT +----------------- +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type User { + greeting: String @exported(filename: "tests/fixtures/resolver_context/FunctionWithContextValue.js", functionName: "greeting") +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts new file mode 100644 index 00000000..da989a4c --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts @@ -0,0 +1,9 @@ +/** @gqlContext */ +export type GratsContext = { + greeting: string; +}; + +/** @gqlContext */ +export type AlsoGratsContext = { + greeting: string; +}; diff --git a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected new file mode 100644 index 00000000..f581829e --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected @@ -0,0 +1,25 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +export type GratsContext = { + greeting: string; +}; + +/** @gqlContext */ +export type AlsoGratsContext = { + greeting: string; +}; + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts:6:6 - error: Unexpected duplicate declaration of `/** @gqlContext */`. Grats expects there to be only one context type. + +6 /** @gqlContext */ + ~~~~~~~~~~ + + src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts:1:5 + 1 /** @gqlContext */ + ~~~~~~~~~~~~ + Previous context declaration diff --git a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected index 1b55699a..12820cae 100644 --- a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected @@ -26,7 +26,7 @@ src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWi ~~~~~~~~~~~~~~~~~~~~~ 10 */ ~ -src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 9 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected b/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected index 4c36ca70..1ca34526 100644 --- a/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected @@ -26,7 +26,7 @@ src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.inva ~~~~~~~~~~~~~~~~~~~~~ 4 */ ~ -src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts:3:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts:3:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 3 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected index fbb48e40..453273c5 100644 --- a/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected @@ -27,7 +27,7 @@ src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterf ~~~~~~~~~~~~~~~~~~~~~ 10 */ ~ -src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 9 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected b/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected index 6da39e5b..230cbb67 100644 --- a/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected +++ b/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected @@ -6,7 +6,7 @@ INPUT ----------------- OUTPUT ----------------- -src/tests/fixtures/user_error/GqlTagDoesNotExist.ts:1:6 - error: `@gqlFiled` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/user_error/GqlTagDoesNotExist.ts:1:6 - error: `@gqlFiled` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 1 /** @gqlFiled */ ~~~~~~~~ diff --git a/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected b/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected index 97a6cf92..5f55898e 100644 --- a/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected +++ b/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected @@ -11,7 +11,7 @@ src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: Incorrect casing f 1 /** @GQLField */ ~~~~~~~~ -src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: `@GQLField` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: `@GQLField` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 1 /** @GQLField */ ~~~~~~~~ diff --git a/website/docs/04-dockblock-tags/09-context.mdx b/website/docs/04-dockblock-tags/09-context.mdx new file mode 100644 index 00000000..b6b5a919 --- /dev/null +++ b/website/docs/04-dockblock-tags/09-context.mdx @@ -0,0 +1,41 @@ +# Context + +The TypeScript type of your expected GraphQL execution context can be defined by placing a `@gqlContext` docblock directly before a: + +- Declaration + +During execution, each resolver will be passed a [context object](https://graphql.org/learn/execution/#root-fields-resolvers). Context is typically used to pass around information about the current request as well as inject database connections and other per-request caches, such as [DataLoader](https://github.com/graphql/dataloader)s into your resolvers. + +Ensuring your resolvers functions/methods are expecting the context object to have the correct type can be difficult. Grats solves this by expecting you to annotate your context type's declaration with a `@gqlContext` docblock: + +:::caution +It is an error to have more than one `@gqlContext` docblock in your project. Grats will error if it detects more than one. +::: + +```ts +/** @gqlContext */ +type GQLCtx = { + req: express.Request; + userID: string; + db: Database; +}; + +/** @gqlField */ +export function me(_: Query, args: never, ctx: GQLCtx): User { + return ctx.db.users.getById(ctx.userID); +} +``` + +The context argument is passed as the third argument of resovler _functions_ and the second argument of resolver _methods_. If your resolver does not need access to the context object, you can omit the context argument. + +:::tip +If you need to access the context object in your resolver, but your field does not define any args, you can type your args parameter as `never`. +::: + +## Constructing your context object + +The mechanism by which you construct your context object will vary depending upon the GraphQL server library you are using. See your GraphQL server library's documentation for more information. + +:::caution +Grats can ensure that every resolver is expecting the correct context type, but it cannot ensure that your context object is constructed correctly. **It is up to you to ensure that your context object is constructed correctly and passed to the GraphQL execution engine.** +::: From fd74e79a4d30758e7802ee433b2f2594d8c70e4d Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 2 Nov 2023 16:09:40 -0700 Subject: [PATCH 2/5] Fix broken test --- src/tests/fixtures/arguments/NoArgsWithNever.ts | 4 ++-- src/tests/fixtures/arguments/NoArgsWithNever.ts.expected | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/fixtures/arguments/NoArgsWithNever.ts b/src/tests/fixtures/arguments/NoArgsWithNever.ts index 203dbba7..b0d4a24a 100644 --- a/src/tests/fixtures/arguments/NoArgsWithNever.ts +++ b/src/tests/fixtures/arguments/NoArgsWithNever.ts @@ -1,8 +1,8 @@ /** @gqlType */ export default class Query { /** @gqlField */ - hello(args: never, ctx: any): string { - console.log(ctx); + hello(args: never): string { + console.log("hello"); return "Hello world!"; } } diff --git a/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected b/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected index 52092e9d..1aa189b2 100644 --- a/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected +++ b/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected @@ -4,8 +4,8 @@ INPUT /** @gqlType */ export default class Query { /** @gqlField */ - hello(args: never, ctx: any): string { - console.log(ctx); + hello(args: never): string { + console.log("hello"); return "Hello world!"; } } From 75255ac9fa75e3d079dd4220b057c6e24c77d60d Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 3 Nov 2023 08:34:50 -0700 Subject: [PATCH 3/5] Check that all context types reference the same type --- src/Errors.ts | 22 ++++------ src/Extractor.ts | 41 ++++++------------- src/TypeContext.ts | 19 +++++++-- ...WithoutTypeOrInterface.invalid.ts.expected | 2 +- .../ClassMethodWithContextValue.ts | 1 - .../ClassMethodWithContextValue.ts.expected | 1 - .../ClassMethodWithContextValueExported.ts | 1 - ...MethodWithContextValueExported.ts.expected | 1 - ...MethodWithContextValueInArgsPos.invalid.ts | 1 - ...hContextValueInArgsPos.invalid.ts.expected | 5 +-- ...eMissingTypeAnnotation.invalid.ts.expected | 2 +- .../resolver_context/ContextValueOptional.ts | 1 - .../ContextValueOptional.ts.expected | 1 - .../ContextValueSpread.invalid.ts | 1 - .../ContextValueSpread.invalid.ts.expected | 1 - .../ContextValueTypeNotDefined.ts.expected | 2 +- ...ContextValueTypedAsAny.invalid.ts.expected | 2 +- ...extValueTypedAsLiteral.invalid.ts.expected | 2 +- .../ContextValueTypedAsNever.ts.expected | 2 +- ...textValueTypedAsString.invalid.ts.expected | 2 +- .../FunctionWithContextValue.ts | 1 - .../FunctionWithContextValue.ts.expected | 1 - ...ipleClassMethodsReferencingContextValue.ts | 16 ++++++++ ...MethodsReferencingContextValue.ts.expected | 35 ++++++++++++++++ .../MultipleContextTags.invalid.ts | 9 ---- .../MultipleContextTags.invalid.ts.expected | 25 ----------- .../MultipleContextValuesUsed.invalid.ts | 19 +++++++++ ...tipleContextValuesUsed.invalid.ts.expected | 35 ++++++++++++++++ ...rfaceWithDeprecatedTag.invalid.ts.expected | 2 +- ...ypeImplementsInterface.invalid.ts.expected | 2 +- ...rfaceWithDeprecatedTag.invalid.ts.expected | 2 +- .../user_error/GqlTagDoesNotExist.ts.expected | 2 +- .../user_error/WrongCaseGqlTag.ts.expected | 2 +- website/docs/04-dockblock-tags/04-context.mdx | 40 ++++++++++++++++++ .../{04-interfaces.mdx => 05-interfaces.mdx} | 0 .../{05-unions.mdx => 08-unions.mdx} | 0 website/docs/04-dockblock-tags/09-context.mdx | 41 ------------------- .../{08-inputs.mdx => 09-inputs.mdx} | 0 38 files changed, 196 insertions(+), 146 deletions(-) create mode 100644 src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts create mode 100644 src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected delete mode 100644 src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts delete mode 100644 src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected create mode 100644 src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts create mode 100644 src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected create mode 100644 website/docs/04-dockblock-tags/04-context.mdx rename website/docs/04-dockblock-tags/{04-interfaces.mdx => 05-interfaces.mdx} (100%) rename website/docs/04-dockblock-tags/{05-unions.mdx => 08-unions.mdx} (100%) delete mode 100644 website/docs/04-dockblock-tags/09-context.mdx rename website/docs/04-dockblock-tags/{08-inputs.mdx => 09-inputs.mdx} (100%) diff --git a/src/Errors.ts b/src/Errors.ts index d0220699..e2833816 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -325,31 +325,27 @@ export function unresolvedTypeReference() { } export function expectedTypeAnnotationOnContext() { - return "Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; + return "Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; } export function expectedTypeAnnotationOfReferenceOnContext() { - return "Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; + return "Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; } export function expectedTypeAnnotationOnContextToBeResolvable() { // TODO: Provide guidance? - return "Unable to resolve the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`."; + // TODO: I don't think we have a test case that triggers this error. + return "Unable to resolve context parameter type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; } export function expectedTypeAnnotationOnContextToHaveDeclaration() { - // TODO: Provide guidance? - return "Unable to locate the declaration of the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. Did you mean to import or define this type?"; -} - -export function expectedTypeAnnotationOnContextToHaveContextTag() { - return "Expected the definition of the context type to be annotated with `/** @gqlContext */`. Did you mean to add that annotation?"; -} - -export function duplicateContextDeclaration() { - return "Unexpected duplicate declaration of `/** @gqlContext */`. Grats expects there to be only one context type."; + return "Unable to locate the declaration of the context parameter's type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. Did you forget to import or define this type?"; } export function unexpectedParamSpreadForContextParam() { return "Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument."; } + +export function multipleContextTypes() { + return "Context argument's type does not match. Grats expects all resolvers that read the context argument to use the same type for that argument. Did you use the incorrect type in one of your resolvers?"; +} diff --git a/src/Extractor.ts b/src/Extractor.ts index 6ff1ebb6..ed753b38 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -49,7 +49,6 @@ export const INTERFACE_TAG = "gqlInterface"; export const ENUM_TAG = "gqlEnum"; export const UNION_TAG = "gqlUnion"; export const INPUT_TAG = "gqlInput"; -export const CONTEXT_TAG = "gqlContext"; export const IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; export const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; @@ -63,7 +62,6 @@ export const ALL_TAGS = [ ENUM_TAG, UNION_TAG, INPUT_TAG, - CONTEXT_TAG, ]; const DEPRECATED_TAG = "deprecated"; @@ -150,24 +148,6 @@ export class Extractor { } break; } - case CONTEXT_TAG: { - if (this.ctx.contextDeclaration == null) { - this.ctx.contextDeclaration = { node: tag }; - } else { - this.report(tag.tagName, E.duplicateContextDeclaration(), [ - this.related( - this.ctx.contextDeclaration.node, - "Previous context declaration", - ), - ]); - } - // TODO: Validate that this is some kind of declaration - // type alias? - // interface? - // class? - // export of one of the above? - break; - } default: { const lowerCaseTag = tag.tagName.text.toLowerCase(); @@ -1364,14 +1344,19 @@ export class Extractor { ); } - const contextTag = this.findTag(declaration, CONTEXT_TAG); - - if (contextTag == null) { - return this.report( - node.type.typeName, - E.expectedTypeAnnotationOnContextToHaveContextTag(), - [this.related(declaration, "The type was declared here")], - ); + if (this.ctx.gqlContext == null) { + // This is the first typed context value we've seen... + this.ctx.gqlContext = { + declaration: declaration, + firstReference: node.type.typeName, + }; + } else if (this.ctx.gqlContext.declaration !== declaration) { + return this.report(node.type.typeName, E.multipleContextTypes(), [ + this.related( + this.ctx.gqlContext.firstReference, + "A different type reference was used here", + ), + ]); } } diff --git a/src/TypeContext.ts b/src/TypeContext.ts index 4dbdb43b..ed720b28 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -43,8 +43,18 @@ export type AbstractFieldDefinitionNode = { readonly field: FieldDefinitionNode; }; -type ContextDeclaration = { - node: ts.Node; +/** + * Information about the GraphQL context type. We track the first value we see, + * and then validate that any other values we see are the same. + */ +type GqlContext = { + // If we follow the context type back to its source, this is the declaration + // we find. + declaration: ts.Node; + + // The first reference to the context type that we encountered. Used for + // reporting errors if a different context type is encountered. + firstReference: ts.Node; }; /** @@ -66,8 +76,9 @@ export class TypeContext { _options: ts.ParsedCommandLine; _symbolToName: Map = new Map(); _unresolvedTypes: Map = new Map(); - // The `@gqlContext` declaration, if it has been encountered. - contextDeclaration: ContextDeclaration | null = null; + // The resolver context declaration, if it has been encountered. + // Gets mutated by Extractor. + gqlContext: GqlContext | null = null; hasTypename: Set = new Set(); constructor( diff --git a/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected b/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected index fb9edc7f..46615c6d 100644 --- a/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected +++ b/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected @@ -9,7 +9,7 @@ function hello() { ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts:1:6 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts:1:6 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 1 /** @gqlImplements Node */ ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts index 68dba538..fa12ae88 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts @@ -1,4 +1,3 @@ -/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected index b6be0138..8e019e26 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected @@ -1,7 +1,6 @@ ----------------- INPUT ----------------- -/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts index ee739b4f..b2296dab 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts @@ -1,4 +1,3 @@ -/** @gqlContext */ export type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected index b6183e69..c0950a2e 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected @@ -1,7 +1,6 @@ ----------------- INPUT ----------------- -/** @gqlContext */ export type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts index 2626cca9..58d5f5c0 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts @@ -1,4 +1,3 @@ -/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected index e2f2e3e0..a238d7e7 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected @@ -1,7 +1,6 @@ ----------------- INPUT ----------------- -/** @gqlContext */ type GratsContext = { greeting: string; }; @@ -17,7 +16,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts:9:17 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. +src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts:8:17 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. -9 greeting(ctx: GratsContext): string { +8 greeting(ctx: GratsContext): string { ~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected index 22aa98ce..a9068d75 100644 --- a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts:4:25 - error: Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. +src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts:4:25 - error: Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. 4 greeting(args: never, ctx): string { ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts b/src/tests/fixtures/resolver_context/ContextValueOptional.ts index 36ecbe8c..21f8a2cb 100644 --- a/src/tests/fixtures/resolver_context/ContextValueOptional.ts +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts @@ -9,5 +9,4 @@ export class Query { } } -/** @gqlContext */ type SomeType = { greeting: string }; diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected index 9816dd23..a78c4605 100644 --- a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected @@ -12,7 +12,6 @@ export class Query { } } -/** @gqlContext */ type SomeType = { greeting: string }; ----------------- diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts index 77e1cdce..bad3d2f2 100644 --- a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts @@ -6,5 +6,4 @@ export class Query { } } -/** @gqlContext */ type SomeType = { greeting: string }; diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected index 59c6135d..1632e787 100644 --- a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected @@ -9,7 +9,6 @@ export class Query { } } -/** @gqlContext */ type SomeType = { greeting: string }; ----------------- diff --git a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected index 2a3658bc..c148e2c2 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts:4:30 - error: Unable to locate the declaration of the type of the context parameter. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. Did you mean to import or define this type? +src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts:4:30 - error: Unable to locate the declaration of the context parameter's type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. Did you forget to import or define this type? 4 greeting(args: never, ctx: ThisIsNeverDefined): string { ~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected index 3c2edca4..0a7dc328 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. +src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. 4 greeting(args: never, ctx: any): string { ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected index cec03853..a522ee72 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. +src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. 4 greeting(args: never, ctx: { greeting: string }): string { ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected index 2682a5b6..3fe39fe2 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. +src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. 4 greeting(args: never, ctx: never): string { ~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected index eb01260b..3aecc90f 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference. Grats validates that your context parameter is type-safe by checking that it references the type declaration annotated with `/** @gqlContext */`. +src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. 4 greeting(args: never, ctx: string): string { ~~~~~~ diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts index c02a33e3..dbab962a 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts @@ -1,4 +1,3 @@ -/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected index 42005bb5..3d3f5a18 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected @@ -1,7 +1,6 @@ ----------------- INPUT ----------------- -/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts new file mode 100644 index 00000000..983b9f02 --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts @@ -0,0 +1,16 @@ +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } + + /** @gqlField */ + alsoGreeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected new file mode 100644 index 00000000..9fd8dbef --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected @@ -0,0 +1,35 @@ +----------------- +INPUT +----------------- +type GratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } + + /** @gqlField */ + alsoGreeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + greeting: String + alsoGreeting: String +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts deleted file mode 100644 index da989a4c..00000000 --- a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @gqlContext */ -export type GratsContext = { - greeting: string; -}; - -/** @gqlContext */ -export type AlsoGratsContext = { - greeting: string; -}; diff --git a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected b/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected deleted file mode 100644 index f581829e..00000000 --- a/src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts.expected +++ /dev/null @@ -1,25 +0,0 @@ ------------------ -INPUT ------------------ -/** @gqlContext */ -export type GratsContext = { - greeting: string; -}; - -/** @gqlContext */ -export type AlsoGratsContext = { - greeting: string; -}; - ------------------ -OUTPUT ------------------ -src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts:6:6 - error: Unexpected duplicate declaration of `/** @gqlContext */`. Grats expects there to be only one context type. - -6 /** @gqlContext */ - ~~~~~~~~~~ - - src/tests/fixtures/resolver_context/MultipleContextTags.invalid.ts:1:5 - 1 /** @gqlContext */ - ~~~~~~~~~~~~ - Previous context declaration diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts new file mode 100644 index 00000000..20c44bde --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts @@ -0,0 +1,19 @@ +type GratsContext = { + greeting: string; +}; + +type AlsoGratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } + /** @gqlField */ + alsoGreeting(args: never, ctx: AlsoGratsContext): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected new file mode 100644 index 00000000..fe3d3f08 --- /dev/null +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected @@ -0,0 +1,35 @@ +----------------- +INPUT +----------------- +type GratsContext = { + greeting: string; +}; + +type AlsoGratsContext = { + greeting: string; +}; + +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: never, ctx: GratsContext): string { + return ctx.greeting; + } + /** @gqlField */ + alsoGreeting(args: never, ctx: AlsoGratsContext): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:16:34 - error: Context argument's type does not match. Grats expects all resolvers that read the context argument to use the same type for that argument. Did you use the incorrect type in one of your resolvers? + +16 alsoGreeting(args: never, ctx: AlsoGratsContext): string { + ~~~~~~~~~~~~~~~~ + + src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:12:30 + 12 greeting(args: never, ctx: GratsContext): string { + ~~~~~~~~~~~~ + A different type reference was used here diff --git a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected index 12820cae..1b55699a 100644 --- a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected @@ -26,7 +26,7 @@ src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWi ~~~~~~~~~~~~~~~~~~~~~ 10 */ ~ -src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 9 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected b/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected index 1ca34526..4c36ca70 100644 --- a/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts.expected @@ -26,7 +26,7 @@ src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.inva ~~~~~~~~~~~~~~~~~~~~~ 4 */ ~ -src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts:3:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/type_definitions_from_alias/AliasTypeImplementsInterface.invalid.ts:3:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 3 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected index 453273c5..fbb48e40 100644 --- a/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected +++ b/src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts.expected @@ -27,7 +27,7 @@ src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterf ~~~~~~~~~~~~~~~~~~~~~ 10 */ ~ -src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/type_definitions_from_interface/InterfaceTypeExtendsGqlInterfaceWithDeprecatedTag.invalid.ts:9:5 - error: `@gqlImplements` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 9 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected b/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected index 230cbb67..6da39e5b 100644 --- a/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected +++ b/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected @@ -6,7 +6,7 @@ INPUT ----------------- OUTPUT ----------------- -src/tests/fixtures/user_error/GqlTagDoesNotExist.ts:1:6 - error: `@gqlFiled` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/user_error/GqlTagDoesNotExist.ts:1:6 - error: `@gqlFiled` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 1 /** @gqlFiled */ ~~~~~~~~ diff --git a/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected b/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected index 5f55898e..97a6cf92 100644 --- a/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected +++ b/src/tests/fixtures/user_error/WrongCaseGqlTag.ts.expected @@ -11,7 +11,7 @@ src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: Incorrect casing f 1 /** @GQLField */ ~~~~~~~~ -src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: `@GQLField` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. +src/tests/fixtures/user_error/WrongCaseGqlTag.ts:1:6 - error: `@GQLField` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. 1 /** @GQLField */ ~~~~~~~~ diff --git a/website/docs/04-dockblock-tags/04-context.mdx b/website/docs/04-dockblock-tags/04-context.mdx new file mode 100644 index 00000000..6be578b6 --- /dev/null +++ b/website/docs/04-dockblock-tags/04-context.mdx @@ -0,0 +1,40 @@ +# Context + +After the [arguments object](./03-arguments.mdx), each resolver method/function is passed a [context value](https://graphql.org/learn/execution/#root-fields-resolvers). Context is the standard way to implement depednecy injection for GraphQL resolvers. Typically the context value will be an object includiding the current request, information about the requesting user, as well as a database connection and per-request caches, such as [DataLoaders](https://github.com/graphql/dataloader). + +Becuase GraphQL invokes resolver functions dynamically, there are generally no static calls to your resolver methods/functions that are visible to TypeScript. This means that there is no typecheck that can confirm that the context arugment is typed correctly. + +Grats helps to mitigate this risk by alidating that **every resolver that specifies a context argument references _the same defintion_ for the context value's type**. This at least ensures all your resolvers match. + +```ts +type GQLCtx = { + req: Request; + userID: string; + db: Database; +}; + +/** @gqlField */ +// highlight-start +export function me(_: Query, args: never, ctx: GQLCtx): User { + // highlight-end + return ctx.db.users.getById(ctx.userID); +} +``` + +The context argument is passed as the third argument of resovler _functions_ and the second argument of resolver _methods_. If your resolver does not need access to the context object, you can omit the context argument. + +:::tip +If you need to access the context object in your resolver, but your field does not define any args, you can type your args parameter as `never`. +::: + +:::info +Due to limitations in the TypeScript compiler, Grats is not able to [structurally](https://en.wikipedia.org/wiki/Structural_type_system) typecheck the context value. Instead is simply checks that every resolver that specifies a context argument references the same _type definition_. +::: + +## Constructing your context object + +The mechanism by which you construct your context object will vary depending upon the GraphQL server library you are using. See your GraphQL server library's documentation for more information. + +:::caution +Grats can ensure that every resolver is expecting the same context type, but it cannot ensure that the context value you construct and pass in matches that type. **It is up to you to ensure that your context value is constructed correctly and passed to the GraphQL execution engine.** +::: diff --git a/website/docs/04-dockblock-tags/04-interfaces.mdx b/website/docs/04-dockblock-tags/05-interfaces.mdx similarity index 100% rename from website/docs/04-dockblock-tags/04-interfaces.mdx rename to website/docs/04-dockblock-tags/05-interfaces.mdx diff --git a/website/docs/04-dockblock-tags/05-unions.mdx b/website/docs/04-dockblock-tags/08-unions.mdx similarity index 100% rename from website/docs/04-dockblock-tags/05-unions.mdx rename to website/docs/04-dockblock-tags/08-unions.mdx diff --git a/website/docs/04-dockblock-tags/09-context.mdx b/website/docs/04-dockblock-tags/09-context.mdx deleted file mode 100644 index b6b5a919..00000000 --- a/website/docs/04-dockblock-tags/09-context.mdx +++ /dev/null @@ -1,41 +0,0 @@ -# Context - -The TypeScript type of your expected GraphQL execution context can be defined by placing a `@gqlContext` docblock directly before a: - -- Declaration - -During execution, each resolver will be passed a [context object](https://graphql.org/learn/execution/#root-fields-resolvers). Context is typically used to pass around information about the current request as well as inject database connections and other per-request caches, such as [DataLoader](https://github.com/graphql/dataloader)s into your resolvers. - -Ensuring your resolvers functions/methods are expecting the context object to have the correct type can be difficult. Grats solves this by expecting you to annotate your context type's declaration with a `@gqlContext` docblock: - -:::caution -It is an error to have more than one `@gqlContext` docblock in your project. Grats will error if it detects more than one. -::: - -```ts -/** @gqlContext */ -type GQLCtx = { - req: express.Request; - userID: string; - db: Database; -}; - -/** @gqlField */ -export function me(_: Query, args: never, ctx: GQLCtx): User { - return ctx.db.users.getById(ctx.userID); -} -``` - -The context argument is passed as the third argument of resovler _functions_ and the second argument of resolver _methods_. If your resolver does not need access to the context object, you can omit the context argument. - -:::tip -If you need to access the context object in your resolver, but your field does not define any args, you can type your args parameter as `never`. -::: - -## Constructing your context object - -The mechanism by which you construct your context object will vary depending upon the GraphQL server library you are using. See your GraphQL server library's documentation for more information. - -:::caution -Grats can ensure that every resolver is expecting the correct context type, but it cannot ensure that your context object is constructed correctly. **It is up to you to ensure that your context object is constructed correctly and passed to the GraphQL execution engine.** -::: diff --git a/website/docs/04-dockblock-tags/08-inputs.mdx b/website/docs/04-dockblock-tags/09-inputs.mdx similarity index 100% rename from website/docs/04-dockblock-tags/08-inputs.mdx rename to website/docs/04-dockblock-tags/09-inputs.mdx From 03dd662f05a782fed7ee6fe88e4c11345dba49e3 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 3 Nov 2023 15:31:54 -0700 Subject: [PATCH 4/5] Allow `unknown` for ctx and prefer `unknown` for args over `never` --- src/Errors.ts | 4 +-- src/Extractor.ts | 8 +++++- .../arguments/NoArgsWithNever.ts.expected | 13 +++------- .../fixtures/arguments/NoArgsWithUnknown.ts | 8 ++++++ .../arguments/NoArgsWithUnknown.ts.expected | 26 +++++++++++++++++++ .../NoTypeAnnotation.invalid.ts.expected | 2 +- .../OpaqueArgType.invalid.ts.expected | 2 +- .../ClassMethodWithContextValue.ts | 2 +- .../ClassMethodWithContextValue.ts.expected | 2 +- ...MethodWithContextValueExported.ts.expected | 13 +++------- ...hContextValueInArgsPos.invalid.ts.expected | 2 +- ...ntextValueMissingTypeAnnotation.invalid.ts | 2 +- ...eMissingTypeAnnotation.invalid.ts.expected | 8 +++--- .../resolver_context/ContextValueOptional.ts | 2 +- .../ContextValueOptional.ts.expected | 2 +- .../ContextValueSpread.invalid.ts | 2 +- .../ContextValueSpread.invalid.ts.expected | 8 +++--- .../ContextValueTypeNotDefined.ts | 2 +- .../ContextValueTypeNotDefined.ts.expected | 8 +++--- .../ContextValueTypedAsAny.invalid.ts | 2 +- ...ContextValueTypedAsAny.invalid.ts.expected | 8 +++--- .../ContextValueTypedAsLiteral.invalid.ts | 2 +- ...extValueTypedAsLiteral.invalid.ts.expected | 8 +++--- .../ContextValueTypedAsNever.ts | 2 +- .../ContextValueTypedAsNever.ts.expected | 8 +++--- .../ContextValueTypedAsString.invalid.ts | 2 +- ...textValueTypedAsString.invalid.ts.expected | 8 +++--- .../ContextValueTypedAsUnknown.ts | 7 +++++ .../ContextValueTypedAsUnknown.ts.expected | 25 ++++++++++++++++++ .../FunctionWithContextValue.ts | 2 +- .../FunctionWithContextValue.ts.expected | 2 +- ...ipleClassMethodsReferencingContextValue.ts | 4 +-- ...MethodsReferencingContextValue.ts.expected | 4 +-- .../MultipleContextValuesUsed.invalid.ts | 4 +-- ...tipleContextValuesUsed.invalid.ts.expected | 16 ++++++------ website/docs/04-dockblock-tags/04-context.mdx | 4 +-- .../components/PlaygroundFeatures/store.ts | 3 ++- 37 files changed, 143 insertions(+), 84 deletions(-) create mode 100644 src/tests/fixtures/arguments/NoArgsWithUnknown.ts create mode 100644 src/tests/fixtures/arguments/NoArgsWithUnknown.ts.expected create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts create mode 100644 src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts.expected diff --git a/src/Errors.ts b/src/Errors.ts index e2833816..3144ec23 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -161,11 +161,11 @@ export function typeNameDoesNotMatchExpected(expected: string) { } export function argumentParamIsMissingType() { - return "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."; + return "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: unknown`."; } export function argumentParamIsNotObject() { - return "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."; + return "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`."; } export function argIsNotProperty() { diff --git a/src/Extractor.ts b/src/Extractor.ts index ed753b38..b270060f 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -936,7 +936,7 @@ export class Extractor { if (argsType == null) { return this.report(argsParam, E.argumentParamIsMissingType()); } - if (argsType.kind === ts.SyntaxKind.NeverKeyword) { + if (argsType.kind === ts.SyntaxKind.UnknownKeyword) { return []; } if (!ts.isTypeLiteralNode(argsType)) { @@ -1313,6 +1313,12 @@ export class Extractor { return this.report(node, E.expectedTypeAnnotationOnContext()); } + if (node.type.kind === ts.SyntaxKind.UnknownKeyword) { + // If the user just needs to define the argument to get to a later parameter, + // they can use `ctx: unknown` to safely avoid triggering a Grats error. + return; + } + if (!ts.isTypeReferenceNode(node.type)) { return this.report( node.type, diff --git a/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected b/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected index 1aa189b2..66516f36 100644 --- a/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected +++ b/src/tests/fixtures/arguments/NoArgsWithNever.ts.expected @@ -13,14 +13,7 @@ export default class Query { ----------------- OUTPUT ----------------- -schema { - query: Query -} - -directive @methodName(name: String!) on FIELD_DEFINITION - -directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION +src/tests/fixtures/arguments/NoArgsWithNever.ts:4:15 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`. -type Query { - hello: String -} \ No newline at end of file +4 hello(args: never): string { + ~~~~~ diff --git a/src/tests/fixtures/arguments/NoArgsWithUnknown.ts b/src/tests/fixtures/arguments/NoArgsWithUnknown.ts new file mode 100644 index 00000000..8b6e36c2 --- /dev/null +++ b/src/tests/fixtures/arguments/NoArgsWithUnknown.ts @@ -0,0 +1,8 @@ +/** @gqlType */ +export default class Query { + /** @gqlField */ + hello(args: unknown): string { + console.log("hello"); + return "Hello world!"; + } +} diff --git a/src/tests/fixtures/arguments/NoArgsWithUnknown.ts.expected b/src/tests/fixtures/arguments/NoArgsWithUnknown.ts.expected new file mode 100644 index 00000000..1b409d18 --- /dev/null +++ b/src/tests/fixtures/arguments/NoArgsWithUnknown.ts.expected @@ -0,0 +1,26 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export default class Query { + /** @gqlField */ + hello(args: unknown): string { + console.log("hello"); + return "Hello world!"; + } +} + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + hello: String +} \ No newline at end of file diff --git a/src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts.expected b/src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts.expected index 1404f6a8..3db796a7 100644 --- a/src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts.expected +++ b/src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts.expected @@ -12,7 +12,7 @@ export default class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts:4:9 - error: Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`. +src/tests/fixtures/arguments/NoTypeAnnotation.invalid.ts:4:9 - error: Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: unknown`. 4 hello(args): string { ~~~~ diff --git a/src/tests/fixtures/arguments/OpaqueArgType.invalid.ts.expected b/src/tests/fixtures/arguments/OpaqueArgType.invalid.ts.expected index da3586ff..a3cd5130 100644 --- a/src/tests/fixtures/arguments/OpaqueArgType.invalid.ts.expected +++ b/src/tests/fixtures/arguments/OpaqueArgType.invalid.ts.expected @@ -14,7 +14,7 @@ export default class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/arguments/OpaqueArgType.invalid.ts:6:23 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. +src/tests/fixtures/arguments/OpaqueArgType.invalid.ts:6:23 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`. 6 hello({ greeting }: SomeType): string { ~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts index fa12ae88..93023eaf 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts @@ -5,7 +5,7 @@ type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected index 8e019e26..50b70cf3 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected @@ -8,7 +8,7 @@ type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected index c0950a2e..5663742e 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected @@ -16,14 +16,7 @@ export class Query { ----------------- OUTPUT ----------------- -schema { - query: Query -} - -directive @methodName(name: String!) on FIELD_DEFINITION - -directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION +src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts:8:18 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`. -type Query { - greeting: String -} \ No newline at end of file +8 greeting(args: never, ctx: GratsContext): string { + ~~~~~ diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected index a238d7e7..52581ae7 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts.expected @@ -16,7 +16,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts:8:17 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. +src/tests/fixtures/resolver_context/ClassMethodWithContextValueInArgsPos.invalid.ts:8:17 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`. 8 greeting(ctx: GratsContext): string { ~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts index d620a31a..ee788e54 100644 --- a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx): string { + greeting(args: unknown, ctx): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected index a9068d75..cad7ece9 100644 --- a/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx): string { + greeting(args: unknown, ctx): string { return ctx.greeting; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts:4:25 - error: Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. +src/tests/fixtures/resolver_context/ContextValueMissingTypeAnnotation.invalid.ts:4:27 - error: Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. -4 greeting(args: never, ctx): string { - ~~~ +4 greeting(args: unknown, ctx): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts b/src/tests/fixtures/resolver_context/ContextValueOptional.ts index 21f8a2cb..e5688a0e 100644 --- a/src/tests/fixtures/resolver_context/ContextValueOptional.ts +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx?: SomeType): string { + greeting(args: unknown, ctx?: SomeType): string { // This is fine since Grats will always pass ctx. It's fine for // the resolver to _also_ work _without_ ctx, as long as it's // safe for Grats to pass ctx. diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected index a78c4605..e955004a 100644 --- a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx?: SomeType): string { + greeting(args: unknown, ctx?: SomeType): string { // This is fine since Grats will always pass ctx. It's fine for // the resolver to _also_ work _without_ ctx, as long as it's // safe for Grats to pass ctx. diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts index bad3d2f2..5d27b72d 100644 --- a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ...ctx: SomeType): string { + greeting(args: unknown, ...ctx: SomeType): string { return ctx[0].greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected index 1632e787..9c0a5376 100644 --- a/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ...ctx: SomeType): string { + greeting(args: unknown, ...ctx: SomeType): string { return ctx[0].greeting; } } @@ -14,7 +14,7 @@ type SomeType = { greeting: string }; ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts:4:25 - error: Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument. +src/tests/fixtures/resolver_context/ContextValueSpread.invalid.ts:4:27 - error: Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument. -4 greeting(args: never, ...ctx: SomeType): string { - ~~~ +4 greeting(args: unknown, ...ctx: SomeType): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts index ad0f10fd..d74641d0 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts +++ b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: ThisIsNeverDefined): string { + greeting(args: unknown, ctx: ThisIsNeverDefined): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected index c148e2c2..940176e8 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: ThisIsNeverDefined): string { + greeting(args: unknown, ctx: ThisIsNeverDefined): string { return ctx.greeting; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts:4:30 - error: Unable to locate the declaration of the context parameter's type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. Did you forget to import or define this type? +src/tests/fixtures/resolver_context/ContextValueTypeNotDefined.ts:4:32 - error: Unable to locate the declaration of the context parameter's type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. Did you forget to import or define this type? -4 greeting(args: never, ctx: ThisIsNeverDefined): string { - ~~~~~~~~~~~~~~~~~~ +4 greeting(args: unknown, ctx: ThisIsNeverDefined): string { + ~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts index c300b979..283f8c0d 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: any): string { + greeting(args: unknown, ctx: any): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected index 0a7dc328..d7301a1d 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: any): string { + greeting(args: unknown, ctx: any): string { return ctx.greeting; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. +src/tests/fixtures/resolver_context/ContextValueTypedAsAny.invalid.ts:4:32 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. -4 greeting(args: never, ctx: any): string { - ~~~ +4 greeting(args: unknown, ctx: any): string { + ~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts index 5c6ad453..5eb8b98b 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: { greeting: string }): string { + greeting(args: unknown, ctx: { greeting: string }): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected index a522ee72..2b6e4b09 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: { greeting: string }): string { + greeting(args: unknown, ctx: { greeting: string }): string { return ctx.greeting; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. +src/tests/fixtures/resolver_context/ContextValueTypedAsLiteral.invalid.ts:4:32 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. -4 greeting(args: never, ctx: { greeting: string }): string { - ~~~~~~~~~~~~~~~~~~~~ +4 greeting(args: unknown, ctx: { greeting: string }): string { + ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts index 9fe0ab4e..6cbef22a 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: never): string { + greeting(args: unknown, ctx: never): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected index 3fe39fe2..8896b90e 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: never): string { + greeting(args: unknown, ctx: never): string { return ctx.greeting; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. +src/tests/fixtures/resolver_context/ContextValueTypedAsNever.ts:4:32 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. -4 greeting(args: never, ctx: never): string { - ~~~~~ +4 greeting(args: unknown, ctx: never): string { + ~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts index 4664dc39..46148a5d 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: string): string { + greeting(args: unknown, ctx: string): string { return ctx; } } diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected index 3aecc90f..83d5bf7c 100644 --- a/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts.expected @@ -4,7 +4,7 @@ INPUT /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: string): string { + greeting(args: unknown, ctx: string): string { return ctx; } } @@ -12,7 +12,7 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts:4:30 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. +src/tests/fixtures/resolver_context/ContextValueTypedAsString.invalid.ts:4:32 - error: Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. -4 greeting(args: never, ctx: string): string { - ~~~~~~ +4 greeting(args: unknown, ctx: string): string { + ~~~~~~ diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts b/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts new file mode 100644 index 00000000..27da8846 --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts @@ -0,0 +1,7 @@ +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: unknown, ctx: unknown): string { + return ctx.greeting; + } +} diff --git a/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts.expected b/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts.expected new file mode 100644 index 00000000..c696a0cc --- /dev/null +++ b/src/tests/fixtures/resolver_context/ContextValueTypedAsUnknown.ts.expected @@ -0,0 +1,25 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +export class Query { + /** @gqlField */ + greeting(args: unknown, ctx: unknown): string { + return ctx.greeting; + } +} + +----------------- +OUTPUT +----------------- +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION + +type Query { + greeting: String +} \ No newline at end of file diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts index dbab962a..7297070a 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts @@ -6,6 +6,6 @@ type GratsContext = { export class User {} /** @gqlField */ -export function greeting(_: User, args: never, ctx: GratsContext): string { +export function greeting(_: User, args: unknown, ctx: GratsContext): string { return ctx.greeting; } diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected index 3d3f5a18..d27f4466 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected @@ -9,7 +9,7 @@ type GratsContext = { export class User {} /** @gqlField */ -export function greeting(_: User, args: never, ctx: GratsContext): string { +export function greeting(_: User, args: unknown, ctx: GratsContext): string { return ctx.greeting; } diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts index 983b9f02..acd579bc 100644 --- a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts @@ -5,12 +5,12 @@ type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } /** @gqlField */ - alsoGreeting(args: never, ctx: GratsContext): string { + alsoGreeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected index 9fd8dbef..c808d8e8 100644 --- a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected @@ -8,12 +8,12 @@ type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } /** @gqlField */ - alsoGreeting(args: never, ctx: GratsContext): string { + alsoGreeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts index 20c44bde..312d497b 100644 --- a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts @@ -9,11 +9,11 @@ type AlsoGratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } /** @gqlField */ - alsoGreeting(args: never, ctx: AlsoGratsContext): string { + alsoGreeting(args: unknown, ctx: AlsoGratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected index fe3d3f08..c44cd4a6 100644 --- a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected @@ -12,11 +12,11 @@ type AlsoGratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } /** @gqlField */ - alsoGreeting(args: never, ctx: AlsoGratsContext): string { + alsoGreeting(args: unknown, ctx: AlsoGratsContext): string { return ctx.greeting; } } @@ -24,12 +24,12 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:16:34 - error: Context argument's type does not match. Grats expects all resolvers that read the context argument to use the same type for that argument. Did you use the incorrect type in one of your resolvers? +src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:16:36 - error: Context argument's type does not match. Grats expects all resolvers that read the context argument to use the same type for that argument. Did you use the incorrect type in one of your resolvers? -16 alsoGreeting(args: never, ctx: AlsoGratsContext): string { - ~~~~~~~~~~~~~~~~ +16 alsoGreeting(args: unknown, ctx: AlsoGratsContext): string { + ~~~~~~~~~~~~~~~~ - src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:12:30 - 12 greeting(args: never, ctx: GratsContext): string { - ~~~~~~~~~~~~ + src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:12:32 + 12 greeting(args: unknown, ctx: GratsContext): string { + ~~~~~~~~~~~~ A different type reference was used here diff --git a/website/docs/04-dockblock-tags/04-context.mdx b/website/docs/04-dockblock-tags/04-context.mdx index 6be578b6..aba8d431 100644 --- a/website/docs/04-dockblock-tags/04-context.mdx +++ b/website/docs/04-dockblock-tags/04-context.mdx @@ -15,7 +15,7 @@ type GQLCtx = { /** @gqlField */ // highlight-start -export function me(_: Query, args: never, ctx: GQLCtx): User { +export function me(_: Query, args: unknown, ctx: GQLCtx): User { // highlight-end return ctx.db.users.getById(ctx.userID); } @@ -24,7 +24,7 @@ export function me(_: Query, args: never, ctx: GQLCtx): User { The context argument is passed as the third argument of resovler _functions_ and the second argument of resolver _methods_. If your resolver does not need access to the context object, you can omit the context argument. :::tip -If you need to access the context object in your resolver, but your field does not define any args, you can type your args parameter as `never`. +If you need to access the context object in your resolver, but your field does not define any args, you can type your args parameter as `unknown`. ::: :::info diff --git a/website/src/components/PlaygroundFeatures/store.ts b/website/src/components/PlaygroundFeatures/store.ts index 4faaef57..cefd396b 100644 --- a/website/src/components/PlaygroundFeatures/store.ts +++ b/website/src/components/PlaygroundFeatures/store.ts @@ -76,8 +76,9 @@ function reducer(state: State = stateFromUrl(), action: Action) { doc: action.value, }; } - default: + default: { const _: never = action; + } } return state; From 8ee1d65f67c20f04d425bbf341b7c6432bf19916 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 3 Nov 2023 15:47:14 -0700 Subject: [PATCH 5/5] Fix links (and another never -> unknown update) --- .../ClassMethodWithContextValueExported.ts | 2 +- ...MethodWithContextValueExported.ts.expected | 15 +++++++++++---- website/docs/04-dockblock-tags/01-types.mdx | 6 +++--- .../{08-unions.mdx => 06-unions.mdx} | 0 .../{06-scalars.mdx => 08-scalars.mdx} | 0 website/docs/04-dockblock-tags/index.md | 19 ++++++++++--------- website/docusaurus.config.js | 2 +- 7 files changed, 26 insertions(+), 18 deletions(-) rename website/docs/04-dockblock-tags/{08-unions.mdx => 06-unions.mdx} (100%) rename website/docs/04-dockblock-tags/{06-scalars.mdx => 08-scalars.mdx} (100%) diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts index b2296dab..33cc23bf 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts @@ -5,7 +5,7 @@ export type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } diff --git a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected index 5663742e..05cf3db4 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected @@ -8,7 +8,7 @@ export type GratsContext = { /** @gqlType */ export class Query { /** @gqlField */ - greeting(args: never, ctx: GratsContext): string { + greeting(args: unknown, ctx: GratsContext): string { return ctx.greeting; } } @@ -16,7 +16,14 @@ export class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts:8:18 - error: Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`. +schema { + query: Query +} + +directive @methodName(name: String!) on FIELD_DEFINITION + +directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION -8 greeting(args: never, ctx: GratsContext): string { - ~~~~~ +type Query { + greeting: String +} \ No newline at end of file diff --git a/website/docs/04-dockblock-tags/01-types.mdx b/website/docs/04-dockblock-tags/01-types.mdx index 84af96a2..e7cde5a8 100644 --- a/website/docs/04-dockblock-tags/01-types.mdx +++ b/website/docs/04-dockblock-tags/01-types.mdx @@ -49,7 +49,7 @@ Like GraphQL's schema definition language, each type which implemnts of an inter ### Classes -If you are using classes to model your GraphQL resolvers, you can define your types as implementing a GraphQL interface by declaring that your class `implements` an interface which has been annotated with [`@gqlInterface`](./04-interfaces.mdx). +If you are using classes to model your GraphQL resolvers, you can define your types as implementing a GraphQL interface by declaring that your class `implements` an interface which has been annotated with [`@gqlInterface`](./05-interfaces.mdx). @@ -59,7 +59,7 @@ Will generate the following GraphQL schema: ### TypeScript Interface -If you are using interfaces to model your GraphQL resolvers, you can define your types as implementing a GraphQL interface by declaring that your class `extends` an interface which has been annotated with [`@gqlInterface`](./04-interfaces.mdx). +If you are using interfaces to model your GraphQL resolvers, you can define your types as implementing a GraphQL interface by declaring that your class `extends` an interface which has been annotated with [`@gqlInterface`](./05-interfaces.mdx). @@ -74,7 +74,7 @@ Types declared using a type alias _may not_ implement a GraphQL interface. Inste --- :::info -See [Interfaces](./04-interfaces.mdx) for more information about defining interfaces. +See [Interfaces](./05-interfaces.mdx) for more information about defining interfaces. ::: :::note diff --git a/website/docs/04-dockblock-tags/08-unions.mdx b/website/docs/04-dockblock-tags/06-unions.mdx similarity index 100% rename from website/docs/04-dockblock-tags/08-unions.mdx rename to website/docs/04-dockblock-tags/06-unions.mdx diff --git a/website/docs/04-dockblock-tags/06-scalars.mdx b/website/docs/04-dockblock-tags/08-scalars.mdx similarity index 100% rename from website/docs/04-dockblock-tags/06-scalars.mdx rename to website/docs/04-dockblock-tags/08-scalars.mdx diff --git a/website/docs/04-dockblock-tags/index.md b/website/docs/04-dockblock-tags/index.md index 3adcb9cb..e1e1ce80 100644 --- a/website/docs/04-dockblock-tags/index.md +++ b/website/docs/04-dockblock-tags/index.md @@ -8,16 +8,17 @@ special JSDoc tags such as `/** @gqlType */` or `/** @gqlField */`. JSDocs must being with `/**` (two asterix). However, they may be consolidated into a single line `/** Like this */`. ::: -Each tag maps directly to a concept in the GraphQL [Schema Definition Language](https://graphql.org/learn/schema/) (SDL). The following JSDoc tags are supported: - -* [`@gqlType`](./01-types.mdx) -* [`@gqlInterface`](./04-interfaces.mdx) -* [`@gqlField`](./02-fields.mdx) -* [`@gqlUnion`](./05-unions.mdx) -* [`@gqlScalar`](./06-scalars.mdx) -* [`@gqlEnum`](./07-enums.mdx) -* [`@gqlInput`](./08-inputs.mdx) +Each tag maps directly to a concept in the GraphQL [Schema Definition Language](https://graphql.org/learn/schema/) (SDL). The following JSDoc tags are supported: +- [`@gqlType`](./01-types.mdx) +- [`@gqlField`](./02-fields.mdx) + - [Arguments](./03-arguments.mdx) + - [Context](./04-context.mdx) +- [`@gqlInterface`](./05-interfaces.mdx) +- [`@gqlUnion`](./06-unions.mdx) +- [`@gqlEnum`](./07-enums.mdx) +- [`@gqlScalar`](./08-scalars.mdx) +- [`@gqlInput`](./09-inputs.mdx) :::tip This documentaiton aims diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index fdd0185c..aa04eb8b 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -22,7 +22,7 @@ const config = { projectName: "grats", // Usually your repo name. onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", + onBrokenMarkdownLinks: "throw", // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want