diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..5e55bea1 --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +- [x] SDL should extend type for external types - I guess marking types in SDL + - [x] can't generate graphql-js stuff, don't want to do it for externs - don't support graphql-js for this? +- [x] all imported types (so support interfaces etc) +- [x] Read SDL to actually do validation + - [x] reenable global validations +- [x] "modular" mode? like no full schema, but parts of schema but with full validation by resolving it? + - [?] treat query/mutation/subscription as "import" type and extend it +- [ ] all tests to add fixtures for metadata/resolver map +- [ ] pluggable module resolution - too many variables there, use filepath by default, let users customize it + - [ ] first try ts project resolution +- [ ] how to handle overimporting? Improting whole SDL module "infects" the schema with types that might not be requested. +- [ ] another check on error handling - I think eg enums and scalars accept stuff they shouldn't accept? diff --git a/src/Errors.ts b/src/Errors.ts index f41ee240..f26b539c 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -12,7 +12,10 @@ import { UNION_TAG, CONTEXT_TAG, INFO_TAG, + EXTERNAL_TAG, + AllTags, DIRECTIVE_TAG, + EXTERNAL_TAG_VALID_TAGS, } from "./Extractor"; export const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; @@ -153,6 +156,10 @@ export function typeTagOnAliasOfNonObjectOrUnknown() { return `Expected \`@${TYPE_TAG}\` type to be an object type literal (\`{ }\`) or \`unknown\`. For example: \`type Foo = { bar: string }\` or \`type Query = unknown\`.`; } +export function nonExternalTypeAlias(tag: AllTags) { + return `Expected \`@${tag}\` to be a type alias only if used with \`@${EXTERNAL_TAG}\``; +} + // TODO: Add code action export function typeNameNotDeclaration() { return `Expected \`__typename\` to be a property declaration. For example: \`__typename: "MyType"\`.`; @@ -638,3 +645,23 @@ export function directiveFunctionNotNamed() { export function directiveArgumentNotObject() { return "Expected first argument of a `@gqlDirective` function to be typed using an inline object literal."; } + +export function noModuleInGqlExternal() { + return `\`@${EXTERNAL_TAG}\` must include a module name in double quotes. For example: /** @gqlExternal "myModule" */`; +} + +export function externalNotInResolverMapMode() { + return `Unexpected \`@${EXTERNAL_TAG}\` tag. \`@${EXTERNAL_TAG}\` is only supported when the \`EXPERIMENTAL__emitResolverMap\` Grats configuration option is enabled.`; +} + +export function externalOnWrongNode(existingTag?: string) { + if (existingTag) { + return `Unexpected \`@${EXTERNAL_TAG}\` on type with \`@${existingTag}\`. \`@${EXTERNAL_TAG}\` can only be used with ${EXTERNAL_TAG_VALID_TAGS.map( + (tag) => `\`@${tag}\``, + ).join(", ")}.`; + } else { + return `Unexpected \`@${EXTERNAL_TAG}\` without a Grats tag. \`@${EXTERNAL_TAG}\` must be used with ${EXTERNAL_TAG_VALID_TAGS.map( + (tag) => `\`@${tag}\``, + ).join(", ")}.`; + } +} diff --git a/src/Extractor.ts b/src/Extractor.ts index 87c9e612..57702a53 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -46,6 +46,8 @@ import { InputValueDefinitionNodeOrResolverArg, ResolverArgument, } from "./resolverSignature"; +import path = require("path"); +import { GratsConfig } from "./gratsConfig"; import { Parser } from "graphql/language/parser"; export const LIBRARY_IMPORT_NAME = "grats"; @@ -71,6 +73,8 @@ export const INFO_TAG = "gqlInfo"; export const IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; export const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; +export const EXTERNAL_TAG = "gqlExternal"; + // All the tags that start with gql export const ALL_TAGS = [ TYPE_TAG, @@ -82,6 +86,17 @@ export const ALL_TAGS = [ INPUT_TAG, DIRECTIVE_TAG, ANNOTATE_TAG, + EXTERNAL_TAG, +] as const; +export type AllTags = (typeof ALL_TAGS)[number]; + +export const EXTERNAL_TAG_VALID_TAGS = [ + TYPE_TAG, + INPUT_TAG, + INTERFACE_TAG, + UNION_TAG, + SCALAR_TAG, + ENUM_TAG, ]; const DEPRECATED_TAG = "deprecated"; @@ -120,8 +135,9 @@ type FieldTypeContext = { */ export function extract( sourceFile: ts.SourceFile, + options: GratsConfig, ): DiagnosticsResult { - const extractor = new Extractor(); + const extractor = new Extractor(options); return extractor.extract(sourceFile); } @@ -139,7 +155,7 @@ class Extractor { errors: ts.DiagnosticWithLocation[] = []; gql: GraphQLConstructor; - constructor() { + constructor(private _options: GratsConfig) { this.gql = new GraphQLConstructor(); } @@ -151,8 +167,9 @@ class Extractor { node: ts.DeclarationStatement, name: NameNode, kind: NameDefinition["kind"], + externalImportPath: string | null = null, ): void { - this.nameDefinitions.set(node, { name, kind }); + this.nameDefinitions.set(node, { name, kind, externalImportPath }); } // Traverse all nodes, checking each one for its JSDoc tags. @@ -277,6 +294,26 @@ class Extractor { ], }); break; + case EXTERNAL_TAG: + if (!this._options.EXPERIMENTAL__emitResolverMap) { + this.report(tag.tagName, E.externalNotInResolverMapMode()); + } else if ( + !EXTERNAL_TAG_VALID_TAGS.some((tag) => this.hasTag(node, tag)) + ) { + this.report( + tag.tagName, + E.externalOnWrongNode( + ts + .getJSDocTags(node) + .filter( + (t) => + t.tagName.text !== EXTERNAL_TAG && + ALL_TAGS.includes(t.tagName.text as AllTags), + )[0]?.tagName.text, + ), + ); + } + break; default: { const lowerCaseTag = tag.tagName.text.toLowerCase(); @@ -557,6 +594,8 @@ class Extractor { extractInterface(node: ts.Node, tag: ts.JSDocTag) { if (ts.isInterfaceDeclaration(node)) { this.interfaceInterfaceDeclaration(node, tag); + } else if (ts.isTypeAliasDeclaration(node)) { + this.interfaceTypeAliasDeclaration(node, tag); } else { this.report(tag, E.invalidInterfaceTagUsage()); } @@ -648,6 +687,9 @@ class Extractor { types.push(this.unionMemberDeclaration(member)); } } else if (ts.isTypeReferenceNode(node.type)) { + if (this.hasTag(node, EXTERNAL_TAG)) { + return this.externalModule(node, name, "UNION"); + } types.push(this.unionMemberDeclaration(node.type)); } else { return this.report(node, E.expectedUnionTypeNode()); @@ -1031,6 +1073,11 @@ class Extractor { if (name == null) return null; const description = this.collectDescription(node); + + if (this.hasTag(node, EXTERNAL_TAG)) { + return this.externalModule(node, name, "SCALAR"); + } + this.recordTypeName(node, name, "SCALAR"); // TODO: Can a scalar be deprecated? @@ -1047,32 +1094,43 @@ class Extractor { if (name == null) return null; const description = this.collectDescription(node); - this.recordTypeName(node, name, "INPUT_OBJECT"); - let fields: InputValueDefinitionNode[] | null = null; + if ( + node.type.kind === ts.SyntaxKind.TypeReference && + this.hasTag(node, EXTERNAL_TAG) + ) { + return this.externalModule(node, name, "INPUT_OBJECT"); + } else { + this.recordTypeName(node, name, "INPUT_OBJECT"); + let fields: InputValueDefinitionNode[] | null = null; - const directives = this.collectDirectives(node); - if (ts.isUnionTypeNode(node.type)) { - directives.push( - this.gql.constDirective(node, this.gql.name(node.type, ONE_OF_TAG), []), - ); + const directives = this.collectDirectives(node); + if (ts.isUnionTypeNode(node.type)) { + directives.push( + this.gql.constDirective( + node, + this.gql.name(node.type, ONE_OF_TAG), + [], + ), + ); - fields = this.extractOneOfInputFields(node.type); - } else { - fields = this.collectInputFields(node); - } + fields = this.extractOneOfInputFields(node.type); + } else { + fields = this.collectInputFields(node); + } - if (fields == null) return; + if (fields == null) return; - this.definitions.push( - this.gql.inputObjectTypeDefinition( - node, - name, - fields, - directives, - description, - ), - ); + this.definitions.push( + this.gql.inputObjectTypeDefinition( + node, + name, + fields, + directives, + description, + ), + ); + } } inputInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag) { @@ -1334,6 +1392,11 @@ class Extractor { // This is fine, we just don't know what it is. This should be the expected // case for operation types such as `Query`, `Mutation`, and `Subscription` // where there is not strong convention around. + } else if ( + node.type.kind === ts.SyntaxKind.TypeReference && + this.hasTag(node, EXTERNAL_TAG) + ) { + return this.externalModule(node, name, "TYPE"); } else { return this.report(node.type, E.typeTagOnAliasOfNonObjectOrUnknown()); } @@ -1641,6 +1704,24 @@ class Extractor { ); } + interfaceTypeAliasDeclaration( + node: ts.TypeAliasDeclaration, + tag: ts.JSDocTag, + ) { + const name = this.entityName(node, tag); + if (name == null || name.value == null) { + return; + } + + if ( + node.type.kind === ts.SyntaxKind.TypeReference && + this.hasTag(node, EXTERNAL_TAG) + ) { + return this.externalModule(node, name, "INTERFACE"); + } + return this.report(node.type, E.nonExternalTypeAlias(INTERFACE_TAG)); + } + collectFields( members: ReadonlyArray, ): Array { @@ -1934,7 +2015,7 @@ class Extractor { ); } - enumEnumDeclaration(node: ts.EnumDeclaration, tag: ts.JSDocTag): void { + enumEnumDeclaration(node: ts.EnumDeclaration, tag: ts.JSDocTag) { const name = this.entityName(node, tag); if (name == null || name.value == null) { return; @@ -1952,15 +2033,16 @@ class Extractor { ); } - enumTypeAliasDeclaration( - node: ts.TypeAliasDeclaration, - tag: ts.JSDocTag, - ): void { + enumTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag) { const name = this.entityName(node, tag); if (name == null || name.value == null) { return; } + if (this.hasTag(node, EXTERNAL_TAG)) { + return this.externalModule(node, name, "ENUM"); + } + const values = this.enumTypeAliasVariants(node); if (values == null) return; @@ -2131,6 +2213,38 @@ class Extractor { return this.gql.name(id, id.text); } + externalModule( + node: ts.DeclarationStatement, + name: NameNode, + kind: NameDefinition["kind"], + ) { + const tag = this.findTag(node, EXTERNAL_TAG); + if (!tag) { + return this.report(node, E.noModuleInGqlExternal()); + } + + let externalModule; + if (tag.comment != null) { + const commentText = ts.getTextOfJSDocComment(tag.comment); + if (commentText) { + const match = commentText.match(/^\s*"(.*)"\s*$/); + + if (match && match[1]) { + externalModule = match[1]; + } + } + } + if (!externalModule) { + return this.report(node, E.noModuleInGqlExternal()); + } + return this.recordTypeName( + node, + name, + kind, + path.resolve(path.dirname(node.getSourceFile().fileName), externalModule), + ); + } + methodDeclaration( node: ts.MethodDeclaration | ts.MethodSignature | ts.GetAccessorDeclaration, ): FieldDefinitionNode | null { diff --git a/src/GraphQLAstExtensions.ts b/src/GraphQLAstExtensions.ts index 8e18fd77..8ab81bbf 100644 --- a/src/GraphQLAstExtensions.ts +++ b/src/GraphQLAstExtensions.ts @@ -37,13 +37,23 @@ declare module "graphql" { tsModulePath: string; exportName: string | null; }; + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; } + export interface UnionTypeDefinitionNode { /** * Grats metadata: Indicates that the type was materialized as part of * generic type resolution. */ wasSynthesized?: boolean; + + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; } export interface InterfaceTypeDefinitionNode { /** @@ -51,6 +61,11 @@ declare module "graphql" { * generic type resolution. */ wasSynthesized?: boolean; + + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; } export interface ObjectTypeExtensionNode { /** @@ -58,6 +73,11 @@ declare module "graphql" { * or a type. */ mayBeInterface?: boolean; + + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; } export interface FieldDefinitionNode { @@ -71,6 +91,26 @@ declare module "graphql" { killsParentOnException?: NameNode; } + export interface ScalarTypeDefinitionNode { + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; + } + + export interface EnumTypeDefinitionNode { + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; + } + + export interface InputObjectTypeDefinitionNode { + /** + * Grats metadata: Indicates whether this definition was imported from external module + */ + isExternalType?: boolean; + } export interface DirectiveNode { /** * Grats metadata: Indicates that the directive was added by Grats diff --git a/src/TypeContext.ts b/src/TypeContext.ts index e0c7e4eb..6a2bc01e 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -40,6 +40,7 @@ export type NameDefinition = { | "ENUM" | "CONTEXT" | "INFO"; + externalImportPath: string | null; }; export type DeclarationDefinition = NameDefinition | DerivedResolverDefinition; @@ -130,6 +131,21 @@ export class TypeContext { return this._declarationToDefinition.values(); } + allNameDefinitions(): Array { + return Array.from(this._declarationToDefinition.values()).filter( + (decl): decl is NameDefinition => !!decl.kind, + ); + } + + getNameDefinition(name: NameNode): NameDefinition | null { + for (const def of this.allDefinitions()) { + if (def.kind && def.name.value === name.value) { + return def as NameDefinition; + } + } + return null; + } + findSymbolDeclaration(startSymbol: ts.Symbol): ts.Declaration | null { const symbol = this.resolveSymbol(startSymbol); const declaration = symbol.declarations?.[0]; diff --git a/src/lib.ts b/src/lib.ts index caad756c..1d7f07e6 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -35,6 +35,7 @@ import { resolveResolverParams } from "./transforms/resolveResolverParams"; import { customSpecValidations } from "./validations/customSpecValidations"; import { makeResolverSignature } from "./transforms/makeResolverSignature"; import { addImplicitRootTypes } from "./transforms/addImplicitRootTypes"; +import { addImportedSchemas } from "./transforms/addImportedSchemas"; import { Metadata } from "./metadata"; import { validateDirectiveArguments } from "./validations/validateDirectiveArguments"; @@ -124,30 +125,31 @@ export function extractSchemaAndDoc( // `@gqlQueryField` and friends. .map((doc) => addImplicitRootTypes(doc)) // Merge any `extend` definitions into their base definitions. - .map((doc) => mergeExtensions(doc)) + .map((doc) => mergeExtensions(ctx, doc)) // Perform custom validations that reimplement spec validation rules // with more tailored error messages. .andThen((doc) => customSpecValidations(doc)) // Sort the definitions in the document to ensure a stable output. .map((doc) => sortSchemaAst(doc)) - .andThen((doc) => specValidateSDL(doc)) + .andThen((doc) => addImportedSchemas(ctx, doc)) + .andThen((docs) => specValidateSDL(docs)) .result(); if (docResult.kind === "ERROR") { return docResult; } - const doc = docResult.value; - const resolvers = makeResolverSignature(doc); + const { gratsDoc, externalDocs } = docResult.value; + const resolvers = makeResolverSignature(gratsDoc); // Build and validate the schema with regards to the GraphQL spec. return ( - new ResultPipe(buildSchema(doc)) + new ResultPipe(buildSchema([gratsDoc, ...externalDocs])) // Apply the "Type Validation" sub-sections of the specification's // "Type System" section. .andThen((schema) => specSchemaValidation(schema)) // The above spec validation fails to catch type errors in directive // arguments, so Grats checks these manually. - .andThen((schema) => validateDirectiveArguments(schema, doc)) + .andThen((schema) => validateDirectiveArguments(schema, gratsDoc)) // Ensure that every type which implements an interface or is a member of a // union has a __typename field. .andThen((schema) => validateTypenames(schema, typesWithTypename)) @@ -155,7 +157,7 @@ export function extractSchemaAndDoc( // with type nullability. .andThen((schema) => validateSemanticNullability(schema, config)) // Combine the schema and document into a single result. - .map((schema) => ({ schema, doc, resolvers })) + .map((schema) => ({ schema, doc: gratsDoc, resolvers })) .result() ); }) @@ -163,19 +165,39 @@ export function extractSchemaAndDoc( } function buildSchema( - doc: DocumentNode, + docs: DocumentNode[], ): DiagnosticsWithoutLocationResult { + const doc: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: docs.flatMap((doc) => doc.definitions), + }; return ok(buildASTSchema(doc, { assumeValidSDL: true })); } -function specValidateSDL( - doc: DocumentNode, -): DiagnosticsWithoutLocationResult { +function specValidateSDL(docs: { + gratsDoc: DocumentNode; + externalDocs: DocumentNode[]; +}): DiagnosticsWithoutLocationResult<{ + gratsDoc: DocumentNode; + externalDocs: DocumentNode[]; +}> { // TODO: Currently this does not detect definitions that shadow builtins // (`String`, `Int`, etc). However, if we pass a second param (extending an // existing schema) we do! So, we should find a way to validate that we don't // shadow builtins. - return asDiagnostics(doc, validateSDL); + const doc: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [docs.gratsDoc, ...docs.externalDocs].flatMap( + (doc) => doc.definitions, + ), + }; + + const result = asDiagnostics(doc, validateSDL); + if (result.kind === "OK") { + return ok(docs); + } else { + return result; + } } function specSchemaValidation( diff --git a/src/printSchema.ts b/src/printSchema.ts index 186576dc..223207f9 100644 --- a/src/printSchema.ts +++ b/src/printSchema.ts @@ -49,9 +49,11 @@ export function applySDLHeader(config: GratsConfig, sdl: string): string { export function printSDLWithoutMetadata(doc: DocumentNode): string { const trimmed = visit(doc, { ScalarTypeDefinition(t) { - return specifiedScalarTypes.some((scalar) => scalar.name === t.name.value) - ? null - : t; + if (specifiedScalarTypes.some((scalar) => scalar.name === t.name.value)) { + return null; + } else { + return t; + } }, }); return print(trimmed); diff --git a/src/tests/TestRunner.ts b/src/tests/TestRunner.ts index 7cc519cf..56202b66 100644 --- a/src/tests/TestRunner.ts +++ b/src/tests/TestRunner.ts @@ -36,10 +36,12 @@ export default class TestRunner { const filterRegex = filter != null ? new RegExp(filter) : null; for (const fileName of readdirSyncRecursive(fixturesDir)) { if (testFilePattern.test(fileName)) { - this._testFixtures.push(fileName); - const filePath = path.join(fixturesDir, fileName); - if (filterRegex != null && !filePath.match(filterRegex)) { - this._skip.add(fileName); + if (!(ignoreFilePattern && ignoreFilePattern.test(fileName))) { + this._testFixtures.push(fileName); + const filePath = path.join(fixturesDir, fileName); + if (filterRegex != null && !filePath.match(filterRegex)) { + this._skip.add(fileName); + } } } else if (!ignoreFilePattern || !ignoreFilePattern.test(fileName)) { this._otherFiles.add(fileName); diff --git a/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected b/src/tests/fixtures/comments/detachedBlockCommentWithInvalidTagName.invalid.ts.expected index 3c1107d7..c76507d9 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 2 * @gqlTyp ~~~~~~~ diff --git a/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected b/src/tests/fixtures/comments/invalidTagInLinecomment.ts.expected index 4b14b183..a47c23ea 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 1 // @gqlTyp ~~~~~~~ diff --git a/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected b/src/tests/fixtures/comments/lineCommentWrongCasing.invalid.ts.expected index 8753f6a6..5f77f69e 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 3 // @gqlfield ~~~~~~~~~ diff --git a/src/tests/fixtures/externals/fundamentalErrors.ts b/src/tests/fixtures/externals/fundamentalErrors.ts new file mode 100644 index 00000000..bf4c2186 --- /dev/null +++ b/src/tests/fixtures/externals/fundamentalErrors.ts @@ -0,0 +1,30 @@ +// { "EXPERIMENTAL__emitResolverMap": false } + +import { + SomeType as _SomeType, + SomeInterface as _SomeInterface, +} from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** + * @gqlType MyType + * @gqlExternal + */ +export type OtherType = _SomeType; + +/** + * @gqlField + */ +export function someField(parent: SomeType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} diff --git a/src/tests/fixtures/externals/fundamentalErrors.ts.expected b/src/tests/fixtures/externals/fundamentalErrors.ts.expected new file mode 100644 index 00000000..c0e3db95 --- /dev/null +++ b/src/tests/fixtures/externals/fundamentalErrors.ts.expected @@ -0,0 +1,49 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": false } + +import { + SomeType as _SomeType, + SomeInterface as _SomeInterface, +} from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** + * @gqlType MyType + * @gqlExternal + */ +export type OtherType = _SomeType; + +/** + * @gqlField + */ +export function someField(parent: SomeType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/externals/fundamentalErrors.ts:10:5 - error: Unexpected `@gqlExternal` tag. `@gqlExternal` is only supported when the `EXPERIMENTAL__emitResolverMap` Grats configuration option is enabled. + +10 * @gqlExternal "./test-sdl.ignore.graphql" + ~~~~~~~~~~~ +src/tests/fixtures/externals/fundamentalErrors.ts:18:1 - error: `@gqlExternal` must include a module name in double quotes. For example: /** @gqlExternal "myModule" */ + +18 export type OtherType = _SomeType; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/fixtures/externals/fundamentalErrors.ts:16:5 - error: Unexpected `@gqlExternal` tag. `@gqlExternal` is only supported when the `EXPERIMENTAL__emitResolverMap` Grats configuration option is enabled. + +16 * @gqlExternal + ~~~~~~~~~~~ diff --git a/src/tests/fixtures/externals/inputTypes.ts b/src/tests/fixtures/externals/inputTypes.ts new file mode 100644 index 00000000..7e94f653 --- /dev/null +++ b/src/tests/fixtures/externals/inputTypes.ts @@ -0,0 +1,22 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { Int } from "../../../Types"; +import { SomeInputType as _SomeInputType } from "./nonGratsPackage.ignore"; + +/** + * @gqlInput MyInput + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type MyInputType = _SomeInputType; + +/** + * @gqlInput + */ +type NestedInput = { + my: MyInputType; +}; + +/** @gqlQueryField */ +export function myRoot(my: MyInputType, nested: NestedInput): Int | null { + return null; +} diff --git a/src/tests/fixtures/externals/inputTypes.ts.expected b/src/tests/fixtures/externals/inputTypes.ts.expected new file mode 100644 index 00000000..45247c78 --- /dev/null +++ b/src/tests/fixtures/externals/inputTypes.ts.expected @@ -0,0 +1,49 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { Int } from "../../../Types"; +import { SomeInputType as _SomeInputType } from "./nonGratsPackage.ignore"; + +/** + * @gqlInput MyInput + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type MyInputType = _SomeInputType; + +/** + * @gqlInput + */ +type NestedInput = { + my: MyInputType; +}; + +/** @gqlQueryField */ +export function myRoot(my: MyInputType, nested: NestedInput): Int | null { + return null; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +input NestedInput { + my: MyInput! +} + +type Query { + myRoot(my: MyInput!, nested: NestedInput!): Int +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { myRoot as queryMyRootResolver } from "./inputTypes"; +export function getResolverMap(): IResolvers { + return { + Query: { + myRoot(_source, args) { + return queryMyRootResolver(args.my, args.nested); + } + } + }; +} diff --git a/src/tests/fixtures/externals/interface.ts b/src/tests/fixtures/externals/interface.ts new file mode 100644 index 00000000..15666f0c --- /dev/null +++ b/src/tests/fixtures/externals/interface.ts @@ -0,0 +1,36 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { ID } from "../../../Types"; +import { SomeInterface as _SomeInterface } from "./nonGratsPackage.ignore"; + +/** + * @gqlInterface MyInterface + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeInterface; + +/** + * @gqlType + */ +interface ImplementingType extends SomeType { + __typename: "ImplementingType"; + /** + * @gqlField + * @killsParentOnException + */ + id: ID; + /** @gqlField */ + otherField: string; +} + +/** + * @gqlField + */ +export function someField(parent: ImplementingType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): ImplementingType | null { + return null; +} diff --git a/src/tests/fixtures/externals/interface.ts.expected b/src/tests/fixtures/externals/interface.ts.expected new file mode 100644 index 00000000..3f320b9c --- /dev/null +++ b/src/tests/fixtures/externals/interface.ts.expected @@ -0,0 +1,70 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { ID } from "../../../Types"; +import { SomeInterface as _SomeInterface } from "./nonGratsPackage.ignore"; + +/** + * @gqlInterface MyInterface + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeInterface; + +/** + * @gqlType + */ +interface ImplementingType extends SomeType { + __typename: "ImplementingType"; + /** + * @gqlField + * @killsParentOnException + */ + id: ID; + /** @gqlField */ + otherField: string; +} + +/** + * @gqlField + */ +export function someField(parent: ImplementingType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): ImplementingType | null { + return null; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type ImplementingType implements MyInterface { + id: ID! + otherField: String + someField: String +} + +type Query { + myRoot: ImplementingType +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { someField as implementingTypeSomeFieldResolver, myRoot as queryMyRootResolver } from "./interface"; +export function getResolverMap(): IResolvers { + return { + ImplementingType: { + someField(source) { + return implementingTypeSomeFieldResolver(source); + } + }, + Query: { + myRoot() { + return queryMyRootResolver(); + } + } + }; +} diff --git a/src/tests/fixtures/externals/nonGratsPackage.ignore.ts b/src/tests/fixtures/externals/nonGratsPackage.ignore.ts new file mode 100644 index 00000000..cbe6d31f --- /dev/null +++ b/src/tests/fixtures/externals/nonGratsPackage.ignore.ts @@ -0,0 +1,24 @@ +export type SomeType = { + __typename: "MyType"; + id: string; +}; + +export type SomeOtherType = { + __typename: "MyOtherType"; + id: string; +}; + +export type SomeInterface = { + id: string; +}; + +export type SomeInputType = { + foo: number; + id: string; +}; + +export type SomeUnion = SomeType | SomeOtherType; + +export type SomeEnum = "A" | "B"; + +export type SomeScalar = string; diff --git a/src/tests/fixtures/externals/objectType.ts b/src/tests/fixtures/externals/objectType.ts new file mode 100644 index 00000000..b21b30ac --- /dev/null +++ b/src/tests/fixtures/externals/objectType.ts @@ -0,0 +1,21 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeType as _SomeType } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** + * @gqlField + */ +export function someField(parent: SomeType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} diff --git a/src/tests/fixtures/externals/objectType.ts.expected b/src/tests/fixtures/externals/objectType.ts.expected new file mode 100644 index 00000000..85a5aabf --- /dev/null +++ b/src/tests/fixtures/externals/objectType.ts.expected @@ -0,0 +1,53 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeType as _SomeType } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** + * @gqlField + */ +export function someField(parent: SomeType): string { + return parent.id; +} + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Query { + myRoot: MyType +} + +extend type MyType { + someField: String +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { myRoot as queryMyRootResolver, someField as myTypeSomeFieldResolver } from "./objectType"; +export function getResolverMap(): IResolvers { + return { + Query: { + myRoot() { + return queryMyRootResolver(); + } + }, + MyType: { + someField(source) { + return myTypeSomeFieldResolver(source); + } + } + }; +} diff --git a/src/tests/fixtures/externals/objectTypeWithoutFields.ts b/src/tests/fixtures/externals/objectTypeWithoutFields.ts new file mode 100644 index 00000000..bef60e88 --- /dev/null +++ b/src/tests/fixtures/externals/objectTypeWithoutFields.ts @@ -0,0 +1,14 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeType as _SomeType } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} diff --git a/src/tests/fixtures/externals/objectTypeWithoutFields.ts.expected b/src/tests/fixtures/externals/objectTypeWithoutFields.ts.expected new file mode 100644 index 00000000..309e4e96 --- /dev/null +++ b/src/tests/fixtures/externals/objectTypeWithoutFields.ts.expected @@ -0,0 +1,37 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeType as _SomeType } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyType + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeType = _SomeType; + +/** @gqlQueryField */ +export function myRoot(): SomeType | null { + return null; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Query { + myRoot: MyType +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { myRoot as queryMyRootResolver } from "./objectTypeWithoutFields"; +export function getResolverMap(): IResolvers { + return { + Query: { + myRoot() { + return queryMyRootResolver(); + } + } + }; +} diff --git a/src/tests/fixtures/externals/scalar.ts b/src/tests/fixtures/externals/scalar.ts new file mode 100644 index 00000000..c890584f --- /dev/null +++ b/src/tests/fixtures/externals/scalar.ts @@ -0,0 +1,44 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { + SomeScalar as _SomeScalar, + SomeEnum as _SomeEnum, +} from "./nonGratsPackage.ignore"; + +/** + * @gqlInput MyScalar + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type SomeScalar = _SomeScalar; + +/** + * @gqlInput MyEnum + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type SomeEnum = _SomeEnum; + +/** + * @gqlInput + */ +type NestedInput = { + my: SomeScalar; + enum: SomeEnum; +}; + +/** + * @gqlType + */ +type NestedObject = { + /** @gqlField */ + my: SomeScalar; + /** @gqlField */ + enum: SomeEnum; +}; + +/** @gqlQueryField */ +export function myRoot( + my: SomeScalar, + nested: NestedInput, +): NestedObject | null { + return null; +} diff --git a/src/tests/fixtures/externals/scalar.ts.expected b/src/tests/fixtures/externals/scalar.ts.expected new file mode 100644 index 00000000..9e0511de --- /dev/null +++ b/src/tests/fixtures/externals/scalar.ts.expected @@ -0,0 +1,77 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { + SomeScalar as _SomeScalar, + SomeEnum as _SomeEnum, +} from "./nonGratsPackage.ignore"; + +/** + * @gqlInput MyScalar + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type SomeScalar = _SomeScalar; + +/** + * @gqlInput MyEnum + * @gqlExternal "./test-sdl.ignore.graphql" + */ +type SomeEnum = _SomeEnum; + +/** + * @gqlInput + */ +type NestedInput = { + my: SomeScalar; + enum: SomeEnum; +}; + +/** + * @gqlType + */ +type NestedObject = { + /** @gqlField */ + my: SomeScalar; + /** @gqlField */ + enum: SomeEnum; +}; + +/** @gqlQueryField */ +export function myRoot( + my: SomeScalar, + nested: NestedInput, +): NestedObject | null { + return null; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +input NestedInput { + enum: MyEnum! + my: MyScalar! +} + +type NestedObject { + enum: MyEnum + my: MyScalar +} + +type Query { + myRoot(my: MyScalar!, nested: NestedInput!): NestedObject +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { myRoot as queryMyRootResolver } from "./scalar"; +export function getResolverMap(): IResolvers { + return { + Query: { + myRoot(_source, args) { + return queryMyRootResolver(args.my, args.nested); + } + } + }; +} diff --git a/src/tests/fixtures/externals/test-sdl.ignore.graphql b/src/tests/fixtures/externals/test-sdl.ignore.graphql new file mode 100644 index 00000000..77611a10 --- /dev/null +++ b/src/tests/fixtures/externals/test-sdl.ignore.graphql @@ -0,0 +1,25 @@ +type MyType { + id: ID! +} + +type MyOtherType { + id: ID! +} + +interface MyInterface { + id: ID! +} + +input MyInput { + id: ID! + foo: Int! +} + +union MyUnion = MyType | MyOtherType + +enum MyEnum { + A + B +} + +scalar MyScalar diff --git a/src/tests/fixtures/externals/union.ts b/src/tests/fixtures/externals/union.ts new file mode 100644 index 00000000..45fbfa28 --- /dev/null +++ b/src/tests/fixtures/externals/union.ts @@ -0,0 +1,17 @@ +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeUnion as _SomeUnion } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyUnion + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeUnion = _SomeUnion; + +/** @gqlQueryField */ +export function myRoot(): SomeUnion { + return { + __typename: "MyType", + id: "foo", + }; +} diff --git a/src/tests/fixtures/externals/union.ts.expected b/src/tests/fixtures/externals/union.ts.expected new file mode 100644 index 00000000..32099c1c --- /dev/null +++ b/src/tests/fixtures/externals/union.ts.expected @@ -0,0 +1,40 @@ +----------------- +INPUT +----------------- +// { "EXPERIMENTAL__emitResolverMap": true } + +import { SomeUnion as _SomeUnion } from "./nonGratsPackage.ignore"; + +/** + * @gqlType MyUnion + * @gqlExternal "./test-sdl.ignore.graphql" + */ +export type SomeUnion = _SomeUnion; + +/** @gqlQueryField */ +export function myRoot(): SomeUnion { + return { + __typename: "MyType", + id: "foo", + }; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Query { + myRoot: MyUnion +} +-- TypeScript -- +import { IResolvers } from "@graphql-tools/utils"; +import { myRoot as queryMyRootResolver } from "./union"; +export function getResolverMap(): IResolvers { + return { + Query: { + myRoot() { + return queryMyRootResolver(); + } + } + }; +} diff --git a/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected b/src/tests/fixtures/interfaces/tag/ImplementsTagWithoutTypeOrInterface.invalid.ts.expected index 511e28af..5674a1aa 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 1 /** @gqlImplements Node */ ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected index 55206074..8e7854f5 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 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 afcf4976..31b1277c 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 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 e46505e9..cbc824d0 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 9 * @gqlImplements Person ~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected b/src/tests/fixtures/user_error/GqlTagDoesNotExist.ts.expected index bea4c810..d585bd6e 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`, `@gqlDirective`, `@gqlAnnotate`. +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`, `@gqlDirective`, `@gqlAnnotate`, `@gqlExternal`. 1 /** @gqlFiled */ ~~~~~~~~ diff --git a/src/tests/test.ts b/src/tests/test.ts index cb762eed..5d485787 100644 --- a/src/tests/test.ts +++ b/src/tests/test.ts @@ -5,14 +5,7 @@ import { buildSchemaAndDocResultWithHost, } from "../lib"; import * as ts from "typescript"; -import { - buildASTSchema, - graphql, - GraphQLSchema, - print, - printSchema, - specifiedScalarTypes, -} from "graphql"; +import { buildASTSchema, graphql, GraphQLSchema, printSchema } from "graphql"; import { Command } from "commander"; import { locate } from "../Locate"; import { gqlErr, ReportableDiagnostics } from "../utils/DiagnosticError"; @@ -26,7 +19,7 @@ import { validateGratsOptions, } from "../gratsConfig"; import { SEMANTIC_NON_NULL_DIRECTIVE } from "../publicDirectives"; -import { applySDLHeader, applyTypeScriptHeader } from "../printSchema"; +import { printExecutableSchema, printGratsSDL } from "../printSchema"; import { extend } from "../utils/helpers"; const TS_VERSION = ts.version; @@ -77,7 +70,7 @@ const testDirs = [ { fixturesDir, testFilePattern: /\.ts$/, - ignoreFilePattern: null, + ignoreFilePattern: /\.ignore\.(ts|graphql)$/, transformer: (code: string, fileName: string): string | false => { const firstLine = code.split("\n")[0]; let config: Partial = { @@ -136,14 +129,11 @@ const testDirs = [ const { schema, doc, resolvers } = schemaResult.value; // We run codegen here just ensure that it doesn't throw. - const executableSchema = applyTypeScriptHeader( + const executableSchema = printExecutableSchema( + schema, + resolvers, parsedOptions.raw.grats, - codegen( - schema, - resolvers, - parsedOptions.raw.grats, - `${fixturesDir}/${fileName}`, - ), + `${fixturesDir}/${fileName}`, ); const LOCATION_REGEX = /^\/\/ Locate: (.*)/; @@ -158,23 +148,9 @@ const testDirs = [ gqlErr({ loc: locResult.value }, "Located here"), ]).formatDiagnosticsWithContext(); } else { - const docSansDirectives = { - ...doc, - definitions: doc.definitions.filter((def) => { - if (def.kind === "ScalarTypeDefinition") { - return !specifiedScalarTypes.some( - (scalar) => scalar.name === def.name.value, - ); - } - return true; - }), - }; - const sdl = applySDLHeader( - parsedOptions.raw.grats, - print(docSansDirectives), - ); + const sdl = printGratsSDL(doc, parsedOptions.raw.grats); - return `-- SDL --\n${sdl}\n-- TypeScript --\n${executableSchema}`; + return `-- SDL --\n${sdl}-- TypeScript --\n${executableSchema}`; } }, }, diff --git a/src/transforms/addImportedSchemas.ts b/src/transforms/addImportedSchemas.ts new file mode 100644 index 00000000..0eb86bbd --- /dev/null +++ b/src/transforms/addImportedSchemas.ts @@ -0,0 +1,64 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as ts from "typescript"; +import { + DocumentNode, + GraphQLError, + isTypeDefinitionNode, + Kind, + parse, + Source, +} from "graphql"; +import { TypeContext } from "../TypeContext"; +import { + DiagnosticsWithoutLocationResult, + graphQlErrorToDiagnostic, +} from "../utils/DiagnosticError"; +import { err, ok } from "../utils/Result"; + +export function addImportedSchemas( + ctx: TypeContext, + doc: DocumentNode, +): DiagnosticsWithoutLocationResult<{ + gratsDoc: DocumentNode; + externalDocs: DocumentNode[]; +}> { + const importedSchemas: Set = new Set(); + for (const name of ctx.allNameDefinitions()) { + if (name.externalImportPath) { + importedSchemas.add(name.externalImportPath); + } + } + const externalDocs: DocumentNode[] = []; + const errors: ts.Diagnostic[] = []; + for (const schemaPath of importedSchemas) { + const text = fs.readFileSync(path.resolve(schemaPath), "utf-8"); + try { + const parsedAst = parse(new Source(text, schemaPath)); + externalDocs.push({ + kind: Kind.DOCUMENT, + definitions: parsedAst.definitions.map((def) => { + if (isTypeDefinitionNode(def)) { + return { + ...def, + isExternalType: true, + }; + } else { + return def; + } + }), + }); + } catch (e) { + if (e instanceof GraphQLError) { + errors.push(graphQlErrorToDiagnostic(e)); + } + } + } + if (errors.length > 0) { + return err(errors); + } + return ok({ + gratsDoc: doc, + externalDocs, + }); +} diff --git a/src/transforms/makeResolverSignature.ts b/src/transforms/makeResolverSignature.ts index 1770cbaa..02042618 100644 --- a/src/transforms/makeResolverSignature.ts +++ b/src/transforms/makeResolverSignature.ts @@ -15,13 +15,20 @@ export function makeResolverSignature(documentAst: DocumentNode): Metadata { }; for (const declaration of documentAst.definitions) { - if (declaration.kind !== Kind.OBJECT_TYPE_DEFINITION) { + if ( + declaration.kind !== Kind.OBJECT_TYPE_DEFINITION && + declaration.kind !== Kind.OBJECT_TYPE_EXTENSION + ) { continue; } if (declaration.fields == null) { continue; } + if (declaration.isExternalType) { + continue; + } + const fieldResolvers: Record = {}; for (const fieldAst of declaration.fields) { diff --git a/src/transforms/mergeExtensions.ts b/src/transforms/mergeExtensions.ts index 48a4320f..8df657ba 100644 --- a/src/transforms/mergeExtensions.ts +++ b/src/transforms/mergeExtensions.ts @@ -1,11 +1,17 @@ import { DocumentNode, FieldDefinitionNode, visit } from "graphql"; import { extend } from "../utils/helpers"; +import { TypeContext } from "../TypeContext"; /** * Takes every example of `extend type Foo` and `extend interface Foo` and * merges them into the original type/interface definition. + * + * Do not merge the ones that are external */ -export function mergeExtensions(doc: DocumentNode): DocumentNode { +export function mergeExtensions( + ctx: TypeContext, + doc: DocumentNode, +): DocumentNode { const fields = new MultiMap(); // Collect all the fields from the extensions and trim them from the AST. @@ -14,8 +20,13 @@ export function mergeExtensions(doc: DocumentNode): DocumentNode { if (t.directives != null || t.interfaces != null) { throw new Error("Unexpected directives or interfaces on Extension"); } - fields.extend(t.name.value, t.fields); - return null; + const nameDef = ctx.getNameDefinition(t.name); + if (nameDef && nameDef.externalImportPath) { + return t; + } else { + fields.extend(t.name.value, t.fields); + return null; + } }, InterfaceTypeExtension(t) { if (t.directives != null || t.interfaces != null) { diff --git a/src/transforms/snapshotsFromProgram.ts b/src/transforms/snapshotsFromProgram.ts index 262eebe2..a5c6f85f 100644 --- a/src/transforms/snapshotsFromProgram.ts +++ b/src/transforms/snapshotsFromProgram.ts @@ -48,7 +48,7 @@ export function extractSnapshotsFromProgram( } const extractResults = gratsSourceFiles.map((sourceFile) => { - return extract(sourceFile); + return extract(sourceFile, options.raw.grats); }); return collectResults(extractResults); diff --git a/src/utils/Result.ts b/src/utils/Result.ts index 9d205649..3e0794bc 100644 --- a/src/utils/Result.ts +++ b/src/utils/Result.ts @@ -49,6 +49,8 @@ export class ResultPipe { } } +export type PromiseOrValue = T | Promise; + export function collectResults( results: DiagnosticsResult[], ): DiagnosticsResult { diff --git a/src/validations/validateTypenames.ts b/src/validations/validateTypenames.ts index 87efedc8..8810598e 100644 --- a/src/validations/validateTypenames.ts +++ b/src/validations/validateTypenames.ts @@ -39,7 +39,11 @@ export function validateTypenames( ? E.genericTypeImplementsInterface() : E.genericTypeUsedAsUnionMember(); errors.push(gqlErr(ast.name, message)); - } else if (!hasTypename.has(implementor.name) && ast.exported == null) { + } else if ( + !ast.isExternalType && + !hasTypename.has(implementor.name) && + ast.exported == null + ) { const message = type instanceof GraphQLInterfaceType ? E.concreteTypenameImplementingInterfaceCannotBeResolved(