diff --git a/examples/production-app/ViewerContext.ts b/examples/production-app/ViewerContext.ts index 11e796b4..7c6548eb 100644 --- a/examples/production-app/ViewerContext.ts +++ b/examples/production-app/ViewerContext.ts @@ -49,4 +49,5 @@ export class VC { } } +/** @gqlContext */ export type Ctx = YogaInitialContext & { vc: VC }; diff --git a/src/Errors.ts b/src/Errors.ts index b9acfe76..6aa29995 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -11,6 +11,7 @@ import { TYPE_TAG, UNION_TAG, SPECIFIED_BY_TAG, + CONTEXT_TAG, } from "./Extractor"; export const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; @@ -377,6 +378,15 @@ 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?"; } +export function contextTagOnWrongNode() { + // TODO: Word this better + return `Expected \`@${CONTEXT_TAG}\` tag to be applied to a type alias declaration..`; +} + +export function multipleContextDefinitions() { + return `Unexpected multiple \`@${CONTEXT_TAG}\` definitions. Only one type may be annotated with \`@${CONTEXT_TAG}\` in a Grats project.`; +} + export function graphQLNameHasLeadingNewlines( name: string, tagName: string, diff --git a/src/Extractor.ts b/src/Extractor.ts index f236284e..fa9d4eed 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -45,6 +45,7 @@ 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"; @@ -57,6 +58,7 @@ export const ALL_TAGS = [ ENUM_TAG, UNION_TAG, INPUT_TAG, + CONTEXT_TAG, ]; const DEPRECATED_TAG = "deprecated"; @@ -70,6 +72,7 @@ export type ExtractionSnapshot = { readonly unresolvedNames: Map; readonly nameDefinitions: Map; readonly contextReferences: Array; + readonly contextDefinitions: Array; readonly typesWithTypename: Set; readonly interfaceDeclarations: Array; }; @@ -95,6 +98,11 @@ export function extract( return extractor.extract(sourceFile); } +type ContextNodeType = + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | ts.ClassDeclaration; + class Extractor { definitions: DefinitionNode[] = []; @@ -102,6 +110,7 @@ class Extractor { unresolvedNames: Map = new Map(); nameDefinitions: Map = new Map(); contextReferences: Array = []; + contextDefinitions: Array = []; typesWithTypename: Set = new Set(); interfaceDeclarations: Array = []; @@ -195,6 +204,20 @@ class Extractor { } } break; + case CONTEXT_TAG: { + if ( + !( + ts.isTypeAliasDeclaration(node) || + ts.isInterfaceDeclaration(node) || + ts.isClassDeclaration(node) + ) + ) { + console.log("node kind", node.kind); + return this.report(node, E.contextTagOnWrongNode()); + } + this.contextDefinitions.push(node); + break; + } case KILLS_PARENT_ON_EXCEPTION_TAG: { if (!this.hasTag(node, FIELD_TAG)) { this.report( @@ -257,6 +280,7 @@ class Extractor { unresolvedNames: this.unresolvedNames, nameDefinitions: this.nameDefinitions, contextReferences: this.contextReferences, + contextDefinitions: this.contextDefinitions, typesWithTypename: this.typesWithTypename, interfaceDeclarations: this.interfaceDeclarations, }); diff --git a/src/lib.ts b/src/lib.ts index fbc20d22..80458637 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -88,7 +88,7 @@ export function extractSchemaAndDoc( // Collect validation errors const validationResult = concatResults( validateMergedInterfaces(checker, snapshot.interfaceDeclarations), - validateContextReferences(ctx, snapshot.contextReferences), + validateContextReferences(ctx, snapshot), ); const docResult = new ResultPipe(validationResult) @@ -167,6 +167,7 @@ function combineSnapshots(snapshots: ExtractionSnapshot[]): ExtractionSnapshot { nameDefinitions: new Map(), unresolvedNames: new Map(), contextReferences: [], + contextDefinitions: [], typesWithTypename: new Set(), interfaceDeclarations: [], }; @@ -188,6 +189,10 @@ function combineSnapshots(snapshots: ExtractionSnapshot[]): ExtractionSnapshot { result.contextReferences.push(contextReference); } + for (const contextDefinition of snapshot.contextDefinitions) { + result.contextDefinitions.push(contextDefinition); + } + for (const typeName of snapshot.typesWithTypename) { result.typesWithTypename.add(typeName); } diff --git a/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected b/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected index ae647709..1a978b59 100644 --- a/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected +++ b/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected @@ -11,7 +11,7 @@ INPUT ----------------- OUTPUT ----------------- -src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts:2:4 - error: `@gqlTyp` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts:2:4 - error: `@gqlTyp` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 2 * @gqlTyp ~~~~~~~ diff --git a/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected b/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected index 8fdae20e..74448578 100644 --- a/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected +++ b/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected @@ -12,7 +12,7 @@ export default class Composer { OUTPUT ----------------- -- Error Report -- -src/tests/fixtures/comments/invalidTagInLinecomment.ts:1:4 - error: `@gqlTyp` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/comments/invalidTagInLinecomment.ts:1:4 - error: `@gqlTyp` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 1 // @gqlTyp ~~~~~~~ diff --git a/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected b/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected index ede3c554..9f70ccaa 100644 --- a/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected +++ b/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected @@ -13,7 +13,7 @@ export default class Composer { OUTPUT ----------------- -- Error Report -- -src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts:1:4 - error: `@GQLtYPE` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts:1:4 - error: `@GQLtYPE` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 1 // @GQLtYPE ~~~~~~~~ @@ -22,7 +22,7 @@ src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts:1:4 - error: Unexp 1 // @GQLtYPE ~~~~~~~~ -src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts:3:6 - error: `@gqlfield` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`. +src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts:3:6 - error: `@gqlfield` is not a valid Grats tag. Valid tags are: `@gqlType`, `@gqlField`, `@gqlScalar`, `@gqlInterface`, `@gqlEnum`, `@gqlUnion`, `@gqlInput`, `@gqlContext`. 3 // @gqlfield ~~~~~~~~~ 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 index f15cd3ff..793042ad 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts @@ -1,3 +1,4 @@ +/** @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 c6cb7896..24df56ab 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValue.ts.expected @@ -1,6 +1,7 @@ ----------------- 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 5098dc47..4e25a45f 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts @@ -1,3 +1,4 @@ +/** @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 20850f64..2935786b 100644 --- a/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected +++ b/src/tests/fixtures/resolver_context/ClassMethodWithContextValueExported.ts.expected @@ -1,6 +1,7 @@ ----------------- INPUT ----------------- +/** @gqlContext */ export type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts b/src/tests/fixtures/resolver_context/ContextValueOptional.ts index 2aed4ade..9ea5ae2a 100644 --- a/src/tests/fixtures/resolver_context/ContextValueOptional.ts +++ b/src/tests/fixtures/resolver_context/ContextValueOptional.ts @@ -1,7 +1,7 @@ /** @gqlType */ export class SomeType { /** @gqlField */ - greeting(args: unknown, ctx?: SomeType): string { + greeting(args: unknown, ctx?: SomeCtx): 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. @@ -9,4 +9,5 @@ export class SomeType { } } -type SomeType = { greeting: string }; +/** @gqlContext */ +type SomeCtx = { greeting: string }; diff --git a/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected b/src/tests/fixtures/resolver_context/ContextValueOptional.ts.expected index 4dabc989..0875b997 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 SomeType { /** @gqlField */ - greeting(args: unknown, ctx?: SomeType): string { + greeting(args: unknown, ctx?: SomeCtx): 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. @@ -12,7 +12,8 @@ export class SomeType { } } -type SomeType = { greeting: string }; +/** @gqlContext */ +type SomeCtx = { greeting: string }; ----------------- OUTPUT diff --git a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts index 7297070a..88e022c0 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts @@ -1,3 +1,4 @@ +/** @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 2812e353..287a858b 100644 --- a/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/FunctionWithContextValue.ts.expected @@ -1,6 +1,7 @@ ----------------- INPUT ----------------- +/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts index 2487ccd7..493be366 100644 --- a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts @@ -1,3 +1,4 @@ +/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected index dd49a3f4..02332992 100644 --- a/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected +++ b/src/tests/fixtures/resolver_context/MultipleClassMethodsReferencingContextValue.ts.expected @@ -1,6 +1,7 @@ ----------------- INPUT ----------------- +/** @gqlContext */ type GratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts index 1a5de9c3..77d530a1 100644 --- a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts @@ -1,7 +1,9 @@ +/** @gqlContext */ type GratsContext = { greeting: string; }; +/** @gqlContext */ type AlsoGratsContext = { greeting: string; }; diff --git a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected index 4972ad51..6d271171 100644 --- a/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected +++ b/src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts.expected @@ -1,10 +1,12 @@ ----------------- INPUT ----------------- +/** @gqlContext */ type GratsContext = { greeting: string; }; +/** @gqlContext */ type AlsoGratsContext = { greeting: string; }; @@ -24,12 +26,20 @@ export class SomeType { ----------------- OUTPUT ----------------- -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? +src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:7:1 - error: Unexpected multiple `@gqlContext` definitions. Only one type may be annotated with `@gqlContext` in a Grats project. -16 alsoGreeting(args: unknown, ctx: AlsoGratsContext): string { - ~~~~~~~~~~~~~~~~ +7 type AlsoGratsContext = { + ~~~~~~~~~~~~~~~~~~~~~~~~~ +8 greeting: string; + ~~~~~~~~~~~~~~~~~~~ +9 }; + ~~ - src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:12:32 - 12 greeting(args: unknown, ctx: GratsContext): string { - ~~~~~~~~~~~~ - A different type reference was used here + src/tests/fixtures/resolver_context/MultipleContextValuesUsed.invalid.ts:2:1 + 2 type GratsContext = { + ~~~~~~~~~~~~~~~~~~~~~ + 3 greeting: string; + ~~~~~~~~~~~~~~~~~~~ + 4 }; + ~~ + A different context definition was found here diff --git a/src/tests/fixtures/resolver_context/contextTag.ts b/src/tests/fixtures/resolver_context/contextTag.ts new file mode 100644 index 00000000..59b6e76b --- /dev/null +++ b/src/tests/fixtures/resolver_context/contextTag.ts @@ -0,0 +1,2 @@ +/** @gqlContext */ +type Context = {}; diff --git a/src/tests/fixtures/resolver_context/contextTag.ts.expected b/src/tests/fixtures/resolver_context/contextTag.ts.expected new file mode 100644 index 00000000..93584ff4 --- /dev/null +++ b/src/tests/fixtures/resolver_context/contextTag.ts.expected @@ -0,0 +1,18 @@ +----------------- +INPUT +----------------- +/** @gqlContext */ +type Context = {}; + +----------------- +OUTPUT +----------------- +-- SDL -- + +-- TypeScript -- +import { GraphQLSchema } from "graphql"; +export function getSchema(): GraphQLSchema { + return new GraphQLSchema({ + types: [] + }); +} diff --git a/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts b/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts new file mode 100644 index 00000000..193f8da6 --- /dev/null +++ b/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts @@ -0,0 +1,9 @@ +type Ctx = {}; + +/** @gqlType */ +class User { + /** @gqlField */ + greeting(_args: unknown, _ctx: Ctx): string { + return "Hello"; + } +} diff --git a/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts.expected b/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts.expected new file mode 100644 index 00000000..3ee59f12 --- /dev/null +++ b/src/tests/fixtures/resolver_context/contextTypeNotTagged.ts.expected @@ -0,0 +1,34 @@ +----------------- +INPUT +----------------- +type Ctx = {}; + +/** @gqlType */ +class User { + /** @gqlField */ + greeting(_args: unknown, _ctx: Ctx): string { + return "Hello"; + } +} + +----------------- +OUTPUT +----------------- +-- Error Report -- +src/tests/fixtures/resolver_context/contextTypeNotTagged.ts:6:34 - error: Add a @gqlContext tag + +6 greeting(_args: unknown, _ctx: Ctx): string { + ~~~ + + src/tests/fixtures/resolver_context/contextTypeNotTagged.ts:1:1 + 1 type Ctx = {}; + ~~~~~~~~~~~~~~ + This is the type declaration + +-- Code Action: "Tag context declaration with @gqlContext" (add-context-tag) -- +- Original ++ Fixed + +@@ -1,1 +1,2 @@ ++ /** @gqlContext */ + type Ctx = {}; \ No newline at end of file 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 b317882b..be7f6e6b 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/validations/validateContextReferences.ts b/src/validations/validateContextReferences.ts index 1b2f9b91..a6a51d89 100644 --- a/src/validations/validateContextReferences.ts +++ b/src/validations/validateContextReferences.ts @@ -7,6 +7,8 @@ import { } from "../utils/DiagnosticError"; import { err, ok } from "../utils/Result"; import { TypeContext } from "../TypeContext"; +import { ExtractionSnapshot } from "../Extractor"; +import { prefixNode } from "../CodeActions"; /** * Ensure that all context type references resolve to the same @@ -14,10 +16,24 @@ import { TypeContext } from "../TypeContext"; */ export function validateContextReferences( ctx: TypeContext, - references: ts.Node[], + snapshot: ExtractionSnapshot, ): DiagnosticsWithoutLocationResult { - let gqlContext: { declaration: ts.Node; firstReference: ts.Node } | null = - null; + const { contextReferences: references, contextDefinitions: definitions } = + snapshot; + + if (definitions.length > 1) { + return err([ + tsErr(definitions[1], E.multipleContextDefinitions(), [ + tsRelated( + definitions[0], + "A different context definition was found here", + ), + ]), + ]); + } + + const tagged: ts.Node | undefined = definitions[0]; + for (const typeName of references) { const symbol = ctx.checker.getSymbolAtLocation(typeName); if (symbol == null) { @@ -33,19 +49,26 @@ export function validateContextReferences( ]); } - if (gqlContext == null) { - // This is the first typed context value we've seen... - gqlContext = { - declaration: declaration, - firstReference: typeName, - }; - } else if (gqlContext.declaration !== declaration) { + if (tagged == null) { + return err([ + tsErr( + typeName, + "Add a @gqlContext tag", + [tsRelated(declaration, "This is the type declaration")], + { + fixName: "add-context-tag", + description: "Tag context declaration with @gqlContext", + changes: [prefixNode(declaration, "/** @gqlContext */\n")], + }, + ), + ]); + } + + if (tagged !== declaration) { return err([ + // TODO: Reword tsErr(typeName, E.multipleContextTypes(), [ - tsRelated( - gqlContext.firstReference, - "A different type reference was used here", - ), + tsRelated(declaration, "A different type reference was used here"), ]), ]); } diff --git a/website/docs/03-resolvers/snippets/02-resolver-signature-function.grats.ts b/website/docs/03-resolvers/snippets/02-resolver-signature-function.grats.ts index b9cd58bb..3136cf4b 100644 --- a/website/docs/03-resolvers/snippets/02-resolver-signature-function.grats.ts +++ b/website/docs/03-resolvers/snippets/02-resolver-signature-function.grats.ts @@ -12,6 +12,7 @@ export function friends( return context.db.getSortedFriends(user._bestFriendID, args.order); } +/** @gqlContext */ type GqlContext = { db: { getSortedFriends(id: number, order: string): User }; }; diff --git a/website/docs/03-resolvers/snippets/02-resolver-signature-function.out b/website/docs/03-resolvers/snippets/02-resolver-signature-function.out index e4c3dae1..69593331 100644 --- a/website/docs/03-resolvers/snippets/02-resolver-signature-function.out +++ b/website/docs/03-resolvers/snippets/02-resolver-signature-function.out @@ -12,6 +12,7 @@ export function friends( return context.db.getSortedFriends(user._bestFriendID, args.order); } +/** @gqlContext */ type GqlContext = { db: { getSortedFriends(id: number, order: string): User }; }; diff --git a/website/docs/03-resolvers/snippets/02-resolver-signature-method.grats.ts b/website/docs/03-resolvers/snippets/02-resolver-signature-method.grats.ts index 84c2aa08..7398fbd0 100644 --- a/website/docs/03-resolvers/snippets/02-resolver-signature-method.grats.ts +++ b/website/docs/03-resolvers/snippets/02-resolver-signature-method.grats.ts @@ -7,6 +7,7 @@ class User { } } +/** @gqlContext */ type GqlContext = { db: { getSortedFriends(id: number, order: string): User }; }; diff --git a/website/docs/03-resolvers/snippets/02-resolver-signature-method.out b/website/docs/03-resolvers/snippets/02-resolver-signature-method.out index 378c4217..505bcbd6 100644 --- a/website/docs/03-resolvers/snippets/02-resolver-signature-method.out +++ b/website/docs/03-resolvers/snippets/02-resolver-signature-method.out @@ -7,6 +7,7 @@ class User { } } +/** @gqlContext */ type GqlContext = { db: { getSortedFriends(id: number, order: string): User }; };