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 e956f89e..a1818548 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -13,6 +13,8 @@ import { SPECIFIED_BY_TAG, CONTEXT_TAG, INFO_TAG, + EXTERNAL_TAG, + AllTags, } from "./Extractor"; export const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; @@ -153,6 +155,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"\`.`; @@ -607,3 +613,15 @@ export function noTypesDefined() { export function tsConfigNotFound(cwd: string) { return `Grats: Could not find \`tsconfig.json\` searching in ${cwd}.\n\nSee https://www.typescriptlang.org/download/ for instructors on how to add TypeScript to your project. Then run \`npx tsc --init\` to create a \`tsconfig.json\` file.`; } + +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) { + return `Unexpected \`@${EXTERNAL_TAG}\` on type with \`${existingTag}\`. \`@${EXTERNAL_TAG}\` can only be used on type declarations.`; +} diff --git a/src/Extractor.ts b/src/Extractor.ts index 8e5ada8b..98b8a435 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -40,6 +40,8 @@ import { InputValueDefinitionNodeOrResolverArg, ResolverArgument, } from "./resolverSignature"; +import path = require("path"); +import { GratsConfig } from "./gratsConfig"; export const LIBRARY_IMPORT_NAME = "grats"; export const LIBRARY_NAME = "Grats"; @@ -62,6 +64,18 @@ export const INFO_TAG = "gqlInfo"; export const IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; export const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; +export const EXTERNAL_TAG = "gqlExternal"; + +export type AllTags = + | typeof TYPE_TAG + | typeof FIELD_TAG + | typeof SCALAR_TAG + | typeof INTERFACE_TAG + | typeof ENUM_TAG + | typeof UNION_TAG + | typeof INPUT_TAG + | typeof EXTERNAL_TAG; + // All the tags that start with gql export const ALL_TAGS = [ TYPE_TAG, @@ -71,6 +85,7 @@ export const ALL_TAGS = [ ENUM_TAG, UNION_TAG, INPUT_TAG, + EXTERNAL_TAG, ]; const DEPRECATED_TAG = "deprecated"; @@ -106,8 +121,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); } @@ -123,7 +139,7 @@ class Extractor { errors: ts.DiagnosticWithLocation[] = []; gql: GraphQLConstructor; - constructor() { + constructor(private _options: GratsConfig) { this.gql = new GraphQLConstructor(); } @@ -135,12 +151,12 @@ 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. - // If we find a tag we recognize, we extract the relevant information, + // Traverse all nodes, checking each one for its JSDoc tags. // If we find a tag we recognize, we extract the relevant information, // reporting an error if it is attached to a node where that tag is not // supported. extract(sourceFile: ts.SourceFile): DiagnosticsResult { @@ -231,6 +247,30 @@ class Extractor { } break; } + case EXTERNAL_TAG: + if (!this._options.EXPERIMENTAL__emitResolverMap) { + this.report(tag.tagName, E.externalNotInResolverMapMode()); + } + if ( + !this.hasTag(node, TYPE_TAG) && + !this.hasTag(node, INPUT_TAG) && + !this.hasTag(node, INTERFACE_TAG) && + !this.hasTag(node, UNION_TAG) && + !this.hasTag(node, SCALAR_TAG) && + !this.hasTag(node, ENUM_TAG) + ) { + this.report( + tag.tagName, + E.externalOnWrongNode( + ts + .getJSDocTags(node) + .filter((t) => t.tagName.text !== EXTERNAL_TAG)[0].tagName + .text, + ), + ); + } + break; + default: { const lowerCaseTag = tag.tagName.text.toLowerCase(); @@ -353,6 +393,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()); } @@ -426,7 +468,12 @@ class Extractor { const name = this.entityName(node, tag); if (name == null) return null; - if (!ts.isUnionTypeNode(node.type)) { + if (ts.isTypeReferenceNode(node)) { + if (this.hasTag(node, EXTERNAL_TAG)) { + return this.externalModule(node, name, "UNION"); + } + return this.report(node, E.nonExternalTypeAlias(UNION_TAG)); + } else if (!ts.isUnionTypeNode(node.type)) { return this.report(node, E.expectedUnionTypeNode()); } @@ -742,6 +789,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? @@ -763,21 +815,29 @@ class Extractor { if (name == null) return null; const description = this.collectDescription(node); - this.recordTypeName(node, name, "INPUT_OBJECT"); - const fields = this.collectInputFields(node); + if ( + node.type.kind === ts.SyntaxKind.TypeReference && + this.hasTag(node, EXTERNAL_TAG) + ) { + return this.externalModule(node, name, "INPUT_OBJECT"); + } else { + const fields = this.collectInputFields(node); - const deprecatedDirective = this.collectDeprecated(node); + const deprecatedDirective = this.collectDeprecated(node); - this.definitions.push( - this.gql.inputObjectTypeDefinition( - node, - name, - fields, - deprecatedDirective == null ? null : [deprecatedDirective], - description, - ), - ); + this.recordTypeName(node, name, "INPUT_OBJECT"); + + this.definitions.push( + this.gql.inputObjectTypeDefinition( + node, + name, + fields, + deprecatedDirective == null ? null : [deprecatedDirective], + description, + ), + ); + } } inputInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag) { @@ -1070,6 +1130,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()); } @@ -1371,6 +1436,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 { @@ -1668,7 +1751,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; @@ -1684,15 +1767,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; @@ -1858,6 +1942,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 f9c45074..ada4b397 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 { @@ -70,4 +90,25 @@ declare module "graphql" { resolver?: ResolverSignature; 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; + } } diff --git a/src/TypeContext.ts b/src/TypeContext.ts index 548d8473..17f61780 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -29,6 +29,7 @@ export type NameDefinition = { | "ENUM" | "CONTEXT" | "INFO"; + externalImportPath: string | null; }; type TsIdentifier = number; @@ -61,7 +62,12 @@ export class TypeContext { self._markUnresolvedType(node, typeName); } for (const [node, definition] of snapshot.nameDefinitions) { - self._recordTypeName(node, definition.name, definition.kind); + self._recordTypeName( + node, + definition.name, + definition.kind, + definition.externalImportPath, + ); } return self; } @@ -76,9 +82,10 @@ export class TypeContext { node: ts.Declaration, name: NameNode, kind: NameDefinition["kind"], + externalImportPath: string | null = null, ) { this._idToDeclaration.set(name.tsIdentifier, node); - this._declarationToName.set(node, { name, kind }); + this._declarationToName.set(node, { name, kind, externalImportPath }); } // Record that a type references `node` @@ -90,6 +97,15 @@ export class TypeContext { return this._declarationToName.values(); } + getNameDefinition(name: NameNode): NameDefinition | null { + for (const def of this.allNameDefinitions()) { + if (def.name.value === name.value) { + return def; + } + } + 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 cc89a4aa..2f2a1937 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -34,6 +34,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"; // Export the TypeScript plugin implementation used by @@ -118,29 +119,30 @@ 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) => addImportedSchemas(ctx, doc)) .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(buildSchemaFromDoc(doc)) + new ResultPipe(buildSchemaFromDoc([gratsDoc, ...externalDocs])) // Ensure that every type which implements an interface or is a member of a // union has a __typename field. .andThen((schema) => validateTypenames(schema, typesWithTypename)) .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() ); }) @@ -149,12 +151,16 @@ export function extractSchemaAndDoc( // Given a SDL AST, build and validate a GraphQLSchema. function buildSchemaFromDoc( - doc: DocumentNode, + docs: DocumentNode[], ): DiagnosticsWithoutLocationResult { // 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. + const doc: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: docs.flatMap((doc) => doc.definitions), + }; const validationErrors = validateSDL(doc); if (validationErrors.length > 0) { return err(validationErrors.map(graphQlErrorToDiagnostic)); @@ -164,7 +170,7 @@ function buildSchemaFromDoc( const diagnostics = validateSchema(schema) // FIXME: Handle case where query is not defined (no location) .filter((e) => e.source && e.locations && e.positions); - + // if (diagnostics.length > 0) { return err(diagnostics.map(graphQlErrorToDiagnostic)); } 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/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/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterfaceWithDeprecatedTag.invalid.ts.expected index 171d6d61..36deafd6 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`, `@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 b317882b..af83753a 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`, `@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 fbb48e40..c28c5877 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`, `@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 6da39e5b..3fac6760 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`, `@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..2c25e7f5 --- /dev/null +++ b/src/transforms/addImportedSchemas.ts @@ -0,0 +1,63 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as ts from "typescript"; +import { + DocumentNode, + GraphQLError, + isTypeDefinitionNode, + Kind, + parse, +} 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(text); + 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 a97e7507..a5d2afdc 100644 --- a/src/transforms/makeResolverSignature.ts +++ b/src/transforms/makeResolverSignature.ts @@ -14,13 +14,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..ff77c563 100644 --- a/src/utils/Result.ts +++ b/src/utils/Result.ts @@ -49,6 +49,60 @@ export class ResultPipe { } } +export type PromiseOrValue = T | Promise; + +/** + * Helper class for chaining together a series of `Result` operations that accepts also promises. + */ +export class ResultPipeAsync { + private readonly _result: Promise>; + constructor(result: PromiseOrValue>) { + this._result = Promise.resolve(result); + } + // Transform the value if OK, otherwise return the error. + map(fn: (value: T) => PromiseOrValue): ResultPipeAsync { + return new ResultPipeAsync( + this._result.then((result) => { + if (result.kind === "OK") { + return Promise.resolve(fn(result.value)).then(ok); + } + return result; + }), + ); + } + // Transform the error if ERROR, otherwise return the value. + mapErr(fn: (e: E) => PromiseOrValue): ResultPipeAsync { + return new ResultPipeAsync( + this._result.then((result) => { + if (result.kind === "ERROR") { + return Promise.resolve(fn(result.err)).then(err); + } + return result; + }), + ); + } + // Transform the value into a new result if OK, otherwise return the error. + // The new result may have a new value type, but must have the same error + // type. + andThen( + fn: (value: T) => PromiseOrValue>, + ): ResultPipeAsync { + return new ResultPipeAsync( + this._result.then((result) => { + if (result.kind === "OK") { + return Promise.resolve(fn(result.value)); + } else { + return result; + } + }), + ); + } + // Return the result + result(): Promise> { + return this._result; + } +} + 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(