From 6ed53b35bc202284b29eed0f925de798a56994a6 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Wed, 17 Apr 2019 14:32:30 -0500 Subject: [PATCH] Add __typename to TS and Flow types --- packages/plugins/flow/flow/tests/flow.spec.ts | 65 ++++++++++++ .../src/base-documents-visitor.ts | 17 +-- .../src/base-resolvers-visitor.ts | 5 +- .../src/base-types-visitor.ts | 20 +++- .../visitor-plugin-common/src/base-visitor.ts | 17 +++ .../other/visitor-plugin-common/src/utils.ts | 13 ++- .../typescript/tests/typescript.spec.ts | 100 ++++++++++++++++++ 7 files changed, 215 insertions(+), 22 deletions(-) diff --git a/packages/plugins/flow/flow/tests/flow.spec.ts b/packages/plugins/flow/flow/tests/flow.spec.ts index 9cae14ace4a..5bf0e4ebf77 100644 --- a/packages/plugins/flow/flow/tests/flow.spec.ts +++ b/packages/plugins/flow/flow/tests/flow.spec.ts @@ -217,6 +217,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type B = { + __typename?: "B", /** the id */ id?: ?$ElementType, };`); @@ -235,6 +236,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type Node = { + __typename?: "Node", /** the id */ id: $ElementType, };`); @@ -298,6 +300,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = {| + __typename?: "MyInterface", foo?: ?$ElementType, bar: $ElementType, |}; @@ -322,6 +325,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", +foo?: ?$ElementType, +bar: $ElementType, }; @@ -354,6 +358,7 @@ describe('Flow Plugin', () => { `); expect(result).toBeSimilarStringTo(` export type mytype = { + __typename?: "MyType", foo?: ?$ElementType, }; `); @@ -397,6 +402,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type Imytype = { + __typename?: "MyType", foo?: ?$ElementType, }; `); @@ -459,6 +465,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(`export type myenum = $Values;`); expect(result).toBeSimilarStringTo(`export type mytype = { + __typename?: "MyType", f?: ?$ElementType, bar?: ?myenum, b_a_r?: ?$ElementType, @@ -466,28 +473,34 @@ describe('Flow Plugin', () => { };`); expect(result).toBeSimilarStringTo(`export type my_type = { + __typename?: "My_Type", linkTest?: ?mytype, };`); expect(result).toBeSimilarStringTo(`export type myunion = my_type | mytype;`); expect(result).toBeSimilarStringTo(`export type some_interface = { + __typename?: "Some_Interface", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type impl1 = some_interface & { + __typename?: "Impl1", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type impl_2 = some_interface & { + __typename?: "Impl_2", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type impl_3 = some_interface & { + __typename?: "impl_3", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type query = { + __typename?: "Query", something?: ?myunion, use_interface?: ?some_interface, };`); @@ -508,6 +521,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(`export type MyEnum = $Values;`); expect(result).toBeSimilarStringTo(`export type MyType = { + __typename?: "MyType", f?: ?$ElementType, bar?: ?MyEnum, b_a_r?: ?$ElementType, @@ -515,28 +529,34 @@ describe('Flow Plugin', () => { };`); expect(result).toBeSimilarStringTo(`export type My_Type = { + __typename?: "My_Type", linkTest?: ?MyType, };`); expect(result).toBeSimilarStringTo(`export type MyUnion = My_Type | MyType;`); expect(result).toBeSimilarStringTo(`export type Some_Interface = { + __typename?: "Some_Interface", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type Impl1 = Some_Interface & { + __typename?: "Impl1", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type Impl_2 = Some_Interface & { + __typename?: "Impl_2", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type Impl_3 = Some_Interface & { + __typename?: "impl_3", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type Query = { + __typename?: "Query", something?: ?MyUnion, use_interface?: ?Some_Interface, };`); @@ -556,6 +576,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(`export type IMyEnum = $Values;`); expect(result).toBeSimilarStringTo(`export type IMyType = { + __typename?: "MyType", f?: ?$ElementType, bar?: ?IMyEnum, b_a_r?: ?$ElementType, @@ -563,28 +584,34 @@ describe('Flow Plugin', () => { };`); expect(result).toBeSimilarStringTo(`export type IMy_Type = { + __typename?: "My_Type", linkTest?: ?IMyType, };`); expect(result).toBeSimilarStringTo(`export type IMyUnion = IMy_Type | IMyType;`); expect(result).toBeSimilarStringTo(`export type ISome_Interface = { + __typename?: "Some_Interface", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type IImpl1 = ISome_Interface & { + __typename?: "Impl1", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type IImpl_2 = ISome_Interface & { + __typename?: "Impl_2", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type IImpl_3 = ISome_Interface & { + __typename?: "impl_3", id: $ElementType, };`); expect(result).toBeSimilarStringTo(`export type IQuery = { + __typename?: "Query", something?: ?IMyUnion, use_interface?: ?ISome_Interface, };`); @@ -654,6 +681,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type TMutation = { + __typename?: "Mutation", foo?: ?$ElementType, }; @@ -819,6 +847,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: ?$ElementType, bar: $ElementType, }; @@ -840,11 +869,13 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo: $ElementType, }; `); expect(result).toBeSimilarStringTo(` export type MyType = MyInterface & { + __typename?: "MyType", foo: $ElementType, }; `); @@ -870,16 +901,19 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo: $ElementType, }; `); expect(result).toBeSimilarStringTo(` export type MyOtherInterface = { + __typename?: "MyOtherInterface", bar: $ElementType, }; `); expect(result).toBeSimilarStringTo(` export type MyType = MyInterface & MyOtherInterface & { + __typename?: "MyType", foo: $ElementType, bar: $ElementType, }; @@ -901,11 +935,13 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo: MyOtherType, }; `); expect(result).toBeSimilarStringTo(` export type MyOtherType = { + __typename?: "MyOtherType", bar: $ElementType, }; `); @@ -947,6 +983,7 @@ describe('Flow Plugin', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo?: ?$ElementType, bar: $ElementType, }; @@ -1003,4 +1040,32 @@ describe('Flow Plugin', () => { validateFlow(result); }); }); + + it('should not contain __typename', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: Int! + name: String! + email: String! + } + type QueryRoot { + allUsers: [User]! + userById(id: Int!): User + # Generates a new answer for the guessing game + answer: [Int!]! + } + type SubscriptionRoot { + newUser: User + } + schema { + query: QueryRoot + subscription: SubscriptionRoot + } + `); + + const content = await plugin(schema, [], { skipTypename: true }, { outputFile: '' }); + expect(content).not.toContain('__typename'); + + validateFlow(content); + }); }); diff --git a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts index 522505b3746..985f7506719 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts @@ -22,22 +22,7 @@ export interface ParsedDocumentsConfig extends ParsedConfig { addTypename: boolean; } -export interface RawDocumentsConfig extends RawConfig { - /** - * @name skipTypename - * @type boolean - * @default false - * @description Automatically adds `__typename` field to the generated types, even when they are not specified - * in the selection set. - * - * @example - * ```yml - * config: - * skipTypename: true - * ``` - */ - skipTypename?: boolean; -} +export interface RawDocumentsConfig extends RawConfig {} export class BaseDocumentsVisitor extends BaseVisitor { protected _unnamedCounter = 1; diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index 9e3375764d3..ef5a4b738ea 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -267,7 +267,10 @@ export class BaseResolversVisitor `'${f.fieldName}'`).join(' | ')}> & { ${relevantFields.map(f => `${f.fieldName}: ${f.replaceWithType}`).join(', ')} }`; + const omitFields = relevantFields.map(f => `'${f.fieldName}'`).join(' | '); + const keepFields = relevantFields.map(f => `${f.fieldName}: ${f.replaceWithType}`).join(', '); + + return `Omit<${typeName}, ${omitFields}> & { ${keepFields} }`; } protected applyMaybe(str: string): string { diff --git a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts index 797263043f6..92ad15e0020 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts @@ -15,6 +15,8 @@ import { ScalarTypeDefinitionNode, UnionTypeDefinitionNode, StringValueNode, + isUnionType, + isInterfaceType, } from 'graphql'; import { BaseVisitor, ParsedConfig, RawConfig } from './base-visitor'; import { parseMapper } from './mappers'; @@ -141,13 +143,18 @@ export class BaseTypesVisitor 0 ? originalNode.interfaces.map(i => this.convertName(i)).join(' & ') + (node.fields.length ? ' & ' : '') : ''; - const typeDefinition = new DeclarationBlock(this._declarationBlockConfig) + let declarationBlock = new DeclarationBlock(this._declarationBlockConfig) .export() .asKind('type') .withName(this.convertName(node)) .withContent(interfaces) .withComment((node.description as any) as string) - .withBlock(node.fields.join('\n')).string; + .withComment((node.description as any) as string); + + if (this._parsedConfig.addTypename) { + declarationBlock = declarationBlock.withTypeName(node.name); + } + const typeDefinition = declarationBlock.withBlock(node.fields.join('\n')).string; const argumentsBlock = this.buildArgumentsBlock(originalNode); @@ -157,12 +164,17 @@ export class BaseTypesVisitor f).join('\n\n'); } diff --git a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts index de2a0f1e4d7..4fd497d0e96 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts @@ -13,6 +13,7 @@ export interface ParsedConfig { scalars: ScalarsMap; convert: ConvertFn; typesPrefix: string; + addTypename: boolean; } export interface RawConfig { @@ -72,6 +73,21 @@ export interface RawConfig { * ``` */ typesPrefix?: string; + + /** + * @name skipTypename + * @type boolean + * @default false + * @description Automatically adds `__typename` field to the generated types, even when they are not specified + * in the selection set. + * + * @example + * ```yml + * config: + * skipTypename: true + * ``` + */ + skipTypename?: boolean; } export class BaseVisitor { @@ -83,6 +99,7 @@ export class BaseVisitor !!val).join('\n'); if (this._methodName) { result += `${this._methodName}(${this._config.blockTransformer!(block)})`; @@ -216,6 +223,10 @@ export class DeclarationBlock { } } +function buildTypeName(name: NameNode | null): string | null { + return name ? indent(`__typename?: "${name}",`) : null; +} + export function getBaseTypeNode(typeNode: TypeNode): NamedTypeNode { if (typeNode.kind === Kind.LIST_TYPE || typeNode.kind === Kind.NON_NULL_TYPE) { return getBaseTypeNode(typeNode.type); diff --git a/packages/plugins/typescript/typescript/tests/typescript.spec.ts b/packages/plugins/typescript/typescript/tests/typescript.spec.ts index 1a6fbe8574e..7598ee41551 100644 --- a/packages/plugins/typescript/typescript/tests/typescript.spec.ts +++ b/packages/plugins/typescript/typescript/tests/typescript.spec.ts @@ -137,6 +137,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type B = { + __typename?: "B", /** the id */ id?: Maybe, };`); @@ -153,6 +154,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type Node = { + __typename?: "Node", /** the id */ id: Scalars['ID'], };`); @@ -254,6 +256,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(`export type Any = Scalars['String'] | Scalars['Int'] | Scalars['Float'] | Scalars['ID'];`); expect(result).toBeSimilarStringTo(` export type CardEdge = { + __typename?: "CardEdge", count: Scalars['Int'], };`); validateTs(result); @@ -271,6 +274,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo: Maybe, bar: Scalars['String'], }; @@ -287,6 +291,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", readonly foo: ReadonlyArray, }; `); @@ -375,6 +380,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type mytype = { + __typename?: "MyType", foo?: Maybe, }; `); @@ -450,6 +456,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: Maybe, }; `); @@ -534,6 +541,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: Maybe, bar: Scalars['String'], };`); @@ -562,6 +570,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: Maybe, bar: Scalars['MyScalar'], };`); @@ -590,6 +599,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: Maybe, bar: Scalars['MyScalar'], };`); @@ -608,6 +618,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo?: Maybe, bar: Scalars['String'], }; @@ -629,11 +640,13 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo: Scalars['String'], }; `); expect(result).toBeSimilarStringTo(` export type MyType = MyInterface & { + __typename?: "MyType", foo: Scalars['String'], }; `); @@ -659,16 +672,19 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo: Scalars['String'], }; `); expect(result).toBeSimilarStringTo(` export type MyOtherInterface = { + __typename?: "MyOtherInterface", bar: Scalars['String'], }; `); expect(result).toBeSimilarStringTo(` export type MyType = MyInterface & MyOtherInterface & { + __typename?: "MyType", foo: Scalars['String'], bar: Scalars['String'], }; @@ -688,6 +704,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo: Scalars['String'], }; `); @@ -711,11 +728,13 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", foo: MyOtherType, }; `); expect(result).toBeSimilarStringTo(` export type MyOtherType = { + __typename?: "MyOtherType", bar: Scalars['String'], }; `); @@ -756,6 +775,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type MyInterface = { + __typename?: "MyInterface", foo?: Maybe, bar: Scalars['String'], }; @@ -798,6 +818,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type mytype = { + __typename?: "MyType", foo?: Maybe, }; `); @@ -820,6 +841,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type Imytype = { + __typename?: "MyType", foo?: Maybe, }; `); @@ -881,6 +903,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type mytype = { + __typename?: "MyType", f?: Maybe, bar?: Maybe, b_a_r?: Maybe, @@ -889,6 +912,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type my_type = { + __typename?: "My_Type", linkTest?: Maybe, }; `); @@ -897,26 +921,31 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type some_interface = { + __typename?: "Some_Interface", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type impl1 = some_interface & { + __typename?: "Impl1", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type impl_2 = some_interface & { + __typename?: "Impl_2", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type impl_3 = some_interface & { + __typename?: "impl_3", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type query = { + __typename?: "Query", something?: Maybe, use_interface?: Maybe, }; @@ -937,6 +966,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type MyType = { + __typename?: "MyType", f?: Maybe, bar?: Maybe, b_a_r?: Maybe, @@ -945,6 +975,7 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type My_Type = { + __typename?: "My_Type", linkTest?: Maybe, }; `); @@ -953,26 +984,31 @@ describe('TypeScript', () => { `); expect(result).toBeSimilarStringTo(` export type Some_Interface = { + __typename?: "Some_Interface", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type Impl1 = Some_Interface & { + __typename?: "Impl1", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type Impl_2 = Some_Interface & { + __typename?: "Impl_2", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type Impl_3 = Some_Interface & { + __typename?: "impl_3", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type Query = { + __typename?: "Query", something?: Maybe, use_interface?: Maybe, }; @@ -993,6 +1029,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type IMyType = { + __typename?: "MyType", f?: Maybe, bar?: Maybe, b_a_r?: Maybe, @@ -1000,32 +1037,38 @@ describe('TypeScript', () => { };`); expect(result).toBeSimilarStringTo(` export type IMy_Type = { + __typename?: "My_Type", linkTest?: Maybe, }; `); expect(result).toBeSimilarStringTo(`export type IMyUnion = IMy_Type | IMyType;`); expect(result).toBeSimilarStringTo(` export type ISome_Interface = { + __typename?: "Some_Interface", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type IImpl1 = ISome_Interface & { + __typename?: "Impl1", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type IImpl_2 = ISome_Interface & { + __typename?: "Impl_2", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type IImpl_3 = ISome_Interface & { + __typename?: "impl_3", id: Scalars['ID'], }; `); expect(result).toBeSimilarStringTo(` export type IQuery = { + __typename?: "Query", something?: Maybe, use_interface?: Maybe, }; @@ -1097,6 +1140,7 @@ describe('TypeScript', () => { expect(result).toBeSimilarStringTo(` export type TMutation = { + __typename?: "Mutation", foo?: Maybe, }; @@ -1220,4 +1264,60 @@ describe('TypeScript', () => { validateTs(content); }); + + it('should contain __typename', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: Int! + name: String! + email: String! + } + type QueryRoot { + allUsers: [User]! + userById(id: Int!): User + # Generates a new answer for the guessing game + answer: [Int!]! + } + type SubscriptionRoot { + newUser: User + } + schema { + query: QueryRoot + subscription: SubscriptionRoot + } + `); + + const content = await plugin(schema, [], {}, { outputFile: '' }); + expect(content).toContain('__typename'); + + validateTs(content); + }); + + it('should not contain __typename', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: Int! + name: String! + email: String! + } + type QueryRoot { + allUsers: [User]! + userById(id: Int!): User + # Generates a new answer for the guessing game + answer: [Int!]! + } + type SubscriptionRoot { + newUser: User + } + schema { + query: QueryRoot + subscription: SubscriptionRoot + } + `); + + const content = await plugin(schema, [], { skipTypename: true }, { outputFile: '' }); + expect(content).not.toContain('__typename'); + + validateTs(content); + }); });