diff --git a/packages/plugins/typescript-documents/tests/ts-documents.spec.ts b/packages/plugins/typescript-documents/tests/ts-documents.spec.ts index 91aa89a0d99e..8d683ab60c1a 100644 --- a/packages/plugins/typescript-documents/tests/ts-documents.spec.ts +++ b/packages/plugins/typescript-documents/tests/ts-documents.spec.ts @@ -711,5 +711,139 @@ describe('TypeScript Documents Plugin', async () => { `) ); }); + + it('should use __typename in fragments when requested', async () => { + const testSchema = makeExecutableSchema({ + typeDefs: parse(/* GraphQL */ ` + type Post { + title: String + } + type Query { + post: Post! + } + `) + }); + const query = parse(/* GraphQL */ ` + query Post { + post { + ... on Post { + __typename + } + } + } + `); + + const content = await plugin( + testSchema, + [{ filePath: '', content: query }], + {}, + { + outputFile: 'graphql.ts' + } + ); + + expect(format(content)).toBeSimilarStringTo( + format(` + export type PostQuery = { __typename?: 'Query' } & { post: { __typename?: 'Post' } & ({ __typename: 'Post' }) }; + `) + ); + }); + + it('should handle introspection types (__schema)', async () => { + const testSchema = makeExecutableSchema({ + typeDefs: parse(/* GraphQL */ ` + type Post { + title: String + } + type Query { + post: Post! + } + `) + }); + const query = parse(/* GraphQL */ ` + query Info { + __schema { + queryType { + fields { + name + } + } + } + } + `); + + const content = await plugin( + testSchema, + [{ filePath: '', content: query }], + {}, + { + outputFile: 'graphql.ts' + } + ); + + expect(format(content)).toBeSimilarStringTo( + format(` + export type InfoQuery = { __typename?: 'Query' } & { + __schema: { __typename?: '__Schema' } & { + queryType: { __typename?: '__Type' } & { fields: Maybe>> }; + }; + }; + `) + ); + }); + + it('should handle introspection types (__type)', async () => { + const testSchema = makeExecutableSchema({ + typeDefs: parse(/* GraphQL */ ` + type Post { + title: String + } + type Query { + post: Post! + } + `) + }); + const query = parse(/* GraphQL */ ` + query Info { + __type(name: "Post") { + name + fields { + name + type { + name + kind + } + } + } + } + `); + + const content = await plugin( + testSchema, + [{ filePath: '', content: query }], + {}, + { + outputFile: 'graphql.ts' + } + ); + + expect(format(content)).toBeSimilarStringTo( + format(` + export type InfoQuery = { __typename?: 'Query' } & { + __type: Maybe< + { __typename?: '__Type' } & Pick<__Type, 'name'> & { + fields: Maybe< + Array< + { __typename?: '__Field' } & Pick<__Field, 'name'> & { + type: { __typename?: '__Type' } & Pick<__Type, 'name' | 'kind'>; + } + > + >; + } + >; + }; + `) + ); + }); }); }); diff --git a/packages/plugins/visitor-plugin-common/src/base-documents-visitor.ts b/packages/plugins/visitor-plugin-common/src/base-documents-visitor.ts index 73992f5d5984..a219c11cd7e4 100644 --- a/packages/plugins/visitor-plugin-common/src/base-documents-visitor.ts +++ b/packages/plugins/visitor-plugin-common/src/base-documents-visitor.ts @@ -8,11 +8,23 @@ import { FragmentDefinitionNode, GraphQLObjectType, OperationDefinitionNode, - VariableDefinitionNode + VariableDefinitionNode, + OperationTypeNode } from 'graphql'; import { SelectionSetToObject } from './selection-set-to-object'; import { OperationVariablesToObject } from './variables-to-object'; +function getRootType(operation: OperationTypeNode, schema: GraphQLSchema) { + switch (operation) { + case 'query': + return schema.getQueryType(); + case 'mutation': + return schema.getMutationType(); + case 'subscription': + return schema.getSubscriptionType(); + } +} + export interface ParsedDocumentsConfig { scalars: ScalarsMap; convert: (str: string) => string; @@ -108,7 +120,7 @@ export class BaseDocumentsVisitor< OperationDefinition = (node: OperationDefinitionNode): string => { const name = this.handleAnonymouseOperation(node.name && node.name.value ? node.name.value : null); - const operationRootType = this._schema.getType(toPascalCase(node.operation)) as GraphQLObjectType; + const operationRootType = getRootType(node.operation, this._schema); const selectionSet = this._selectionSetToObject.createNext(operationRootType, node.selectionSet); const visitedOperationVariables = this._variablesTransfomer.transform( node.variableDefinitions diff --git a/packages/plugins/visitor-plugin-common/src/selection-set-to-object.ts b/packages/plugins/visitor-plugin-common/src/selection-set-to-object.ts index 19bac25df908..d4445e8b6ae9 100644 --- a/packages/plugins/visitor-plugin-common/src/selection-set-to-object.ts +++ b/packages/plugins/visitor-plugin-common/src/selection-set-to-object.ts @@ -9,7 +9,11 @@ import { isUnionType, isInterfaceType, isEnumType, - GraphQLSchema + GraphQLSchema, + isEqualType, + GraphQLField, + SchemaMetaFieldDef, + TypeMetaFieldDef } from 'graphql'; import { getBaseType, quoteIfNeeded } from './utils'; import { ScalarsMap, ConvertNameFn } from './types'; @@ -21,6 +25,23 @@ export type LinkField = { alias: string; name: string; type: string; selectionSe export type FragmentSpreadField = string; export type InlineFragmentField = { [onType: string]: string[] }; +function isMetadataFieldName(name: string) { + return ['__schema', '__type'].includes(name); +} + +function isRootType(type: GraphQLNamedType, schema: GraphQLSchema): type is GraphQLObjectType { + return ( + isEqualType(type, schema.getQueryType()) || + isEqualType(type, schema.getMutationType()) || + isEqualType(type, schema.getSubscriptionType()) + ); +} + +const metadataFieldMap: Record> = { + __schema: SchemaMetaFieldDef, + __type: TypeMetaFieldDef +}; + export class SelectionSetToObject { protected _primitiveFields: PrimitiveField[] = []; protected _primitiveAliasedFields: PrimitiveAliasedFields[] = []; @@ -57,7 +78,14 @@ export class SelectionSetToObject { } if (isObjectType(this._parentSchemaType) || isInterfaceType(this._parentSchemaType)) { - const schemaField = this._parentSchemaType.getFields()[field.name.value]; + let schemaField: GraphQLField; + + if (isRootType(this._parentSchemaType, this._schema) && isMetadataFieldName(field.name.value)) { + schemaField = metadataFieldMap[field.name.value]; + } else { + schemaField = this._parentSchemaType.getFields()[field.name.value]; + } + const rawType = schemaField.type as any; const baseType = getBaseType(rawType); const typeName = baseType.name;