diff --git a/e2e/esm/index.e2e.ts b/e2e/esm/index.e2e.ts index e9002f9..c852c91 100644 --- a/e2e/esm/index.e2e.ts +++ b/e2e/esm/index.e2e.ts @@ -119,49 +119,67 @@ describe('GraphQL features test', () => { field4: ({ field: string } | null)[]; }>(); }); - it('interface', async () => { - const TypeWithInterfaceField = defineInterfaceTest_TypeWithInterfaceFieldFactory({ - defaultFields: { - interface: undefined, - }, - }); - const ImplementingTypeFactory = defineInterfaceTest_ImplementingTypeFactory({ - defaultFields: { - id: 'ImplementingType-0', - field: 'field', - }, - }); - const typeWithInterfaceField = await TypeWithInterfaceField.build({ - interface: await ImplementingTypeFactory.build(), + describe('interface', () => { + it('basic', async () => { + const TypeWithInterfaceField = defineInterfaceTest_TypeWithInterfaceFieldFactory({ + defaultFields: { + interface: undefined, + }, + }); + const ImplementingTypeFactory = defineInterfaceTest_ImplementingTypeFactory({ + defaultFields: { + id: 'ImplementingType-0', + field: 'field', + }, + }); + const typeWithInterfaceField = await TypeWithInterfaceField.build({ + interface: await ImplementingTypeFactory.build(), + }); + expect(typeWithInterfaceField).toStrictEqual({ + interface: { + id: 'ImplementingType-0', + field: 'field', + }, + }); + expectTypeOf(typeWithInterfaceField).toEqualTypeOf<{ interface: { id: string; field: string } }>(); }); - expect(typeWithInterfaceField).toStrictEqual({ - interface: { - id: 'ImplementingType-0', - field: 'field', - }, + it('the fields of a union field is optional', async () => { + defineInterfaceTest_TypeWithInterfaceFieldFactory({ + defaultFields: { + interface: {}, + }, + }); }); - expectTypeOf(typeWithInterfaceField).toEqualTypeOf<{ interface: { id: string; field: string } }>(); }); - it('union', async () => { - const TypeFactory = defineUnionTest_TypeFactory({ - defaultFields: { - union: undefined, - }, - }); - const Member1Factory = defineUnionTest_Member1Factory({ - defaultFields: { - field1: 'field1', - }, - }); - const type = await TypeFactory.build({ - union: await Member1Factory.build(), + describe('union', () => { + it('basic', async () => { + const TypeFactory = defineUnionTest_TypeFactory({ + defaultFields: { + union: undefined, + }, + }); + const Member1Factory = defineUnionTest_Member1Factory({ + defaultFields: { + field1: 'field1', + }, + }); + const type = await TypeFactory.build({ + union: await Member1Factory.build(), + }); + expect(type).toStrictEqual({ + union: { + field1: 'field1', + }, + }); + expectTypeOf(type).toEqualTypeOf<{ union: { field1: string } }>(); }); - expect(type).toStrictEqual({ - union: { - field1: 'field1', - }, + it('the fields of a union field is optional', async () => { + defineUnionTest_TypeFactory({ + defaultFields: { + union: {}, + }, + }); }); - expectTypeOf(type).toEqualTypeOf<{ union: { field1: string } }>(); }); it('enum', async () => { const TypeFactory = defineEnumTest_TypeFactory({ diff --git a/src/__snapshots__/code-generator.test.ts.snap b/src/__snapshots__/code-generator.test.ts.snap index c7be731..b1c4e47 100644 --- a/src/__snapshots__/code-generator.test.ts.snap +++ b/src/__snapshots__/code-generator.test.ts.snap @@ -8,7 +8,7 @@ exports[`generateCode > generates code 1`] = ` type FieldsResolver, defineTypeFactoryInternal, } from '@mizdra/graphql-codegen-typescript-fabbrica/helper'; -import type { Maybe, Book, Author } from './types'; +import type { Maybe, Book, Author, Node } from './types'; export * from '@mizdra/graphql-codegen-typescript-fabbrica/helper'; export type OptionalBook = { @@ -101,5 +101,7 @@ export function defineAuthorFactory< return defineAuthorFactoryInternal(options); } +export type OptionalNode = OptionalBook | OptionalAuthor; + " `; diff --git a/src/code-generator.test.ts b/src/code-generator.test.ts index a517af3..581103c 100644 --- a/src/code-generator.test.ts +++ b/src/code-generator.test.ts @@ -6,7 +6,8 @@ import { fakeConfig, oneOf } from './test/util.js'; describe('generateOptionalTypeDefinitionCode', () => { it('generates description comment', () => { - const typeInfo: TypeInfo = { + const typeInfo1: TypeInfo = { + type: 'object', name: 'Book', fields: [ { name: 'id', typeString: 'string | undefined' }, @@ -14,8 +15,7 @@ describe('generateOptionalTypeDefinitionCode', () => { ], comment: transformComment('The book'), }; - const actual = generateOptionalTypeDefinitionCode(typeInfo); - expect(actual).toMatchInlineSnapshot(` + expect(generateOptionalTypeDefinitionCode(typeInfo1)).toMatchInlineSnapshot(` "/** The book */ export type OptionalBook = { id?: string | undefined; @@ -24,6 +24,17 @@ describe('generateOptionalTypeDefinitionCode', () => { }; " `); + const typeInfo2: TypeInfo = { + type: 'abstract', + name: 'Node', + possibleTypes: ['Book', 'Author'], + comment: transformComment('The node'), + }; + expect(generateOptionalTypeDefinitionCode(typeInfo2)).toMatchInlineSnapshot(` + "/** The node */ + export type OptionalNode = OptionalBook | OptionalAuthor; + " + `); }); }); @@ -35,6 +46,7 @@ describe('generateCode', () => { }); const typeInfos: TypeInfo[] = [ { + type: 'object', name: 'Book', fields: [ { name: 'id', typeString: 'string | undefined' }, @@ -43,6 +55,7 @@ describe('generateCode', () => { ], }, { + type: 'object', name: 'Author', fields: [ { name: 'id', typeString: 'string | undefined' }, @@ -50,6 +63,11 @@ describe('generateCode', () => { { name: 'books', typeString: 'Book[] | undefined' }, ], }, + { + type: 'abstract', + name: 'Node', + possibleTypes: ['Book', 'Author'], + }, ]; const actual = generateCode(config, typeInfos); expect(actual).toMatchSnapshot(); diff --git a/src/code-generator.ts b/src/code-generator.ts index 87ff578..5b4c9d9 100644 --- a/src/code-generator.ts +++ b/src/code-generator.ts @@ -1,5 +1,5 @@ import { Config } from './config.js'; -import { TypeInfo } from './schema-scanner.js'; +import { ObjectTypeInfo, TypeInfo } from './schema-scanner.js'; function generatePreludeCode(config: Config, typeInfos: TypeInfo[]): string { const joinedTypeNames = typeInfos.map(({ name }) => name).join(', '); @@ -19,28 +19,37 @@ export * from '@mizdra/graphql-codegen-typescript-fabbrica/helper'; } export function generateOptionalTypeDefinitionCode(typeInfo: TypeInfo): string { - const { name, fields } = typeInfo; - const comment = typeInfo.comment ?? ''; - const joinedPropDefinitions = fields - .map((field) => { - const comment = field.comment ? ` ${field.comment}` : ''; - return `${comment} ${field.name}?: ${field.typeString};`; - }) - .join('\n'); - return ` + if (typeInfo.type === 'object') { + const { name, fields } = typeInfo; + const comment = typeInfo.comment ?? ''; + const joinedPropDefinitions = fields + .map((field) => { + const comment = field.comment ? ` ${field.comment}` : ''; + return `${comment} ${field.name}?: ${field.typeString};`; + }) + .join('\n'); + return ` ${comment}export type Optional${name} = { ${joinedPropDefinitions} }; `.trimStart(); + } else { + const { name, possibleTypes } = typeInfo; + const comment = typeInfo.comment ?? ''; + const joinedPossibleTypes = possibleTypes.map((type) => `Optional${type}`).join(' | '); + return ` +${comment}export type Optional${name} = ${joinedPossibleTypes}; +`.trimStart(); + } } -function generateFieldNamesDefinitionCode(typeInfo: TypeInfo): string { +function generateFieldNamesDefinitionCode(typeInfo: ObjectTypeInfo): string { const { name, fields } = typeInfo; const joinedFieldNames = fields.map((field) => `'${field.name}'`).join(', '); return `const ${name}FieldNames = [${joinedFieldNames}] as const;\n`; } -function generateTypeFactoryCode(config: Config, typeInfo: TypeInfo): string { +function generateTypeFactoryCode(config: Config, typeInfo: ObjectTypeInfo): string { const { name } = typeInfo; function wrapRequired(str: string) { return config.nonOptionalDefaultFields ? `Required<${str}>` : str; @@ -91,10 +100,12 @@ export function generateCode(config: Config, typeInfos: TypeInfo[]): string { for (const typeInfo of typeInfos) { code += generateOptionalTypeDefinitionCode(typeInfo); code += '\n'; - code += generateFieldNamesDefinitionCode(typeInfo); - code += '\n'; - code += generateTypeFactoryCode(config, typeInfo); - code += '\n'; + if (typeInfo.type === 'object') { + code += generateFieldNamesDefinitionCode(typeInfo); + code += '\n'; + code += generateTypeFactoryCode(config, typeInfo); + code += '\n'; + } } return code; } diff --git a/src/schema-scanner.test.ts b/src/schema-scanner.test.ts index 57a8e4c..b2aeb0f 100644 --- a/src/schema-scanner.test.ts +++ b/src/schema-scanner.test.ts @@ -4,9 +4,13 @@ import { convertFactory } from '@graphql-codegen/visitor-plugin-common'; import { buildSchema } from 'graphql/index.js'; import { describe, expect, it } from 'vitest'; import { Config } from './config.js'; -import { getTypeInfos } from './schema-scanner.js'; +import { ObjectTypeInfo, TypeInfo, getTypeInfos } from './schema-scanner.js'; import { fakeConfig } from './test/util.js'; +function isObjectTypeInfo(x: TypeInfo): x is ObjectTypeInfo { + return x.type === 'object'; +} + describe('getTypeInfos', () => { it('returns typename and field names', () => { const schema = buildSchema(` @@ -44,6 +48,7 @@ describe('getTypeInfos', () => { }, ], "name": "Book", + "type": "object", }, { "comment": undefined, @@ -65,6 +70,7 @@ describe('getTypeInfos', () => { }, ], "name": "Author", + "type": "object", }, ] `); @@ -98,6 +104,7 @@ describe('getTypeInfos', () => { }, ], "name": "Book", + "type": "object", }, ] `); @@ -120,6 +127,7 @@ describe('getTypeInfos', () => { }, ], "name": "Argument", + "type": "object", } `); }); @@ -163,6 +171,7 @@ describe('getTypeInfos', () => { }, ], "name": "Type", + "type": "object", } `); }); @@ -179,49 +188,42 @@ describe('getTypeInfos', () => { fieldB: String! } `); - expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "comment": undefined, - "name": "fieldA", - "typeString": "ImplementingType['fieldA'] | undefined", - }, - { - "comment": undefined, - "name": "fieldB", - "typeString": "ImplementingType['fieldB'] | undefined", - }, - ], - "name": "ImplementingType", - } - `); - expect(getTypeInfos(fakeConfig({ skipIsAbstractType: false }), schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "name": "__isInterface1", - "typeString": "'ImplementingType'", - }, - { - "name": "__isInterface2", - "typeString": "'ImplementingType'", - }, - { - "comment": undefined, - "name": "fieldA", - "typeString": "ImplementingType['fieldA'] | undefined", - }, - { - "comment": undefined, - "name": "fieldB", - "typeString": "ImplementingType['fieldB'] | undefined", - }, - ], - "name": "ImplementingType", - } + expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)).toMatchInlineSnapshot(` + [ + { + "comment": undefined, + "name": "Interface1", + "possibleTypes": [ + "ImplementingType", + ], + "type": "abstract", + }, + { + "comment": undefined, + "name": "Interface2", + "possibleTypes": [ + "ImplementingType", + ], + "type": "abstract", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "fieldA", + "typeString": "ImplementingType['fieldA'] | undefined", + }, + { + "comment": undefined, + "name": "fieldB", + "typeString": "ImplementingType['fieldB'] | undefined", + }, + ], + "name": "ImplementingType", + "type": "object", + }, + ] `); }); it('union', () => { @@ -235,39 +237,51 @@ describe('getTypeInfos', () => { field2: String! } `); - expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "comment": undefined, - "name": "field1", - "typeString": "Member1['field1'] | undefined", - }, - ], - "name": "Member1", - } - `); - expect(getTypeInfos(fakeConfig({ skipIsAbstractType: false }), schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "name": "__isUnion1", - "typeString": "'Member1'", - }, - { - "name": "__isUnion2", - "typeString": "'Member1'", - }, - { - "comment": undefined, - "name": "field1", - "typeString": "Member1['field1'] | undefined", - }, - ], - "name": "Member1", - } + expect(getTypeInfos(fakeConfig({ skipIsAbstractType: true }), schema)).toMatchInlineSnapshot(` + [ + { + "comment": undefined, + "name": "Union1", + "possibleTypes": [ + "Member1", + "Member2", + ], + "type": "abstract", + }, + { + "comment": undefined, + "name": "Union2", + "possibleTypes": [ + "Member1", + "Member2", + ], + "type": "abstract", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field1", + "typeString": "Member1['field1'] | undefined", + }, + ], + "name": "Member1", + "type": "object", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field2", + "typeString": "Member2['field2'] | undefined", + }, + ], + "name": "Member2", + "type": "object", + }, + ] `); }); it('input', () => { @@ -297,6 +311,7 @@ describe('getTypeInfos', () => { }, ], "name": "Input", + "type": "object", } `); }); @@ -310,7 +325,7 @@ describe('getTypeInfos', () => { } `); const config: Config = fakeConfig({ skipTypename: false }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "name": "__typename", @@ -331,7 +346,7 @@ describe('getTypeInfos', () => { } `); const config: Config = fakeConfig({ skipTypename: true }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "comment": undefined, @@ -351,7 +366,7 @@ describe('getTypeInfos', () => { union Union = Type `); const config: Config = fakeConfig({ skipIsAbstractType: false }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "name": "__isUnion", @@ -367,18 +382,27 @@ describe('getTypeInfos', () => { }); it('does not include __typename if skipIsAbstractType is true', () => { const schema = buildSchema(` - type Type { - field: String! + type Type implements Node { + field1: String! + field2: String! + } + interface Node { + field1: String! } union Union = Type `); const config: Config = fakeConfig({ skipIsAbstractType: true }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "comment": undefined, - "name": "field", - "typeString": "Type['field'] | undefined", + "name": "field1", + "typeString": "Type['field1'] | undefined", + }, + { + "comment": undefined, + "name": "field2", + "typeString": "Type['field2'] | undefined", }, ] `); @@ -387,34 +411,69 @@ describe('getTypeInfos', () => { describe('namingConvention', () => { it('renames type by namingConvention', () => { const schema = buildSchema(` - type Type { + type Type implements Interface { field1: String! field2: SubType! } type SubType { field: String! } + interface Interface { + field2: String! + } + union Union = Type `); const config: Config = fakeConfig({ convert: convertFactory({ namingConvention: 'change-case-all#lowerCase' }), }); - expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "comment": undefined, - "name": "field1", - "typeString": "type['field1'] | undefined", - }, - { - "comment": undefined, - "name": "field2", - "typeString": "Optionalsubtype | undefined", - }, - ], - "name": "type", - } + expect(getTypeInfos(config, schema)).toMatchInlineSnapshot(` + [ + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field1", + "typeString": "type['field1'] | undefined", + }, + { + "comment": undefined, + "name": "field2", + "typeString": "Optionalsubtype | undefined", + }, + ], + "name": "type", + "type": "object", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field", + "typeString": "subtype['field'] | undefined", + }, + ], + "name": "subtype", + "type": "object", + }, + { + "comment": undefined, + "name": "interface", + "possibleTypes": [ + "type", + ], + "type": "abstract", + }, + { + "comment": undefined, + "name": "union", + "possibleTypes": [ + "type", + ], + "type": "abstract", + }, + ] `); }); it('does not effect to __typename and __is', () => { @@ -429,7 +488,7 @@ describe('getTypeInfos', () => { skipIsAbstractType: false, convert: convertFactory({ namingConvention: 'change-case-all#lowerCase' }), }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "name": "__typename", @@ -451,32 +510,67 @@ describe('getTypeInfos', () => { describe('typesPrefix', () => { it('renames type by typesPrefix', () => { const schema = buildSchema(` - type Type { + type Type implements Interface { field1: String! field2: SubType! } type SubType { field: String! } + interface Interface { + field1: String! + } + union Union = Type `); const config: Config = fakeConfig({ typesPrefix: 'I' }); - expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "comment": undefined, - "name": "field1", - "typeString": "IType['field1'] | undefined", - }, - { - "comment": undefined, - "name": "field2", - "typeString": "OptionalISubType | undefined", - }, - ], - "name": "IType", - } + expect(getTypeInfos(config, schema)).toMatchInlineSnapshot(` + [ + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field1", + "typeString": "IType['field1'] | undefined", + }, + { + "comment": undefined, + "name": "field2", + "typeString": "OptionalISubType | undefined", + }, + ], + "name": "IType", + "type": "object", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field", + "typeString": "ISubType['field'] | undefined", + }, + ], + "name": "ISubType", + "type": "object", + }, + { + "comment": undefined, + "name": "IInterface", + "possibleTypes": [ + "IType", + ], + "type": "abstract", + }, + { + "comment": undefined, + "name": "IUnion", + "possibleTypes": [ + "IType", + ], + "type": "abstract", + }, + ] `); }); it('does not effect to __typename and __is', () => { @@ -491,7 +585,7 @@ describe('getTypeInfos', () => { skipIsAbstractType: false, typesPrefix: 'I', }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "name": "__typename", @@ -513,32 +607,67 @@ describe('getTypeInfos', () => { describe('typesSuffix', () => { it('renames type by typesSuffix', () => { const schema = buildSchema(` - type Type { + type Type implements Interface { field1: String! field2: SubType! } type SubType { field: String! } + interface Interface { + field1: String! + } + union Union = Type `); const config: Config = fakeConfig({ typesSuffix: 'I' }); - expect(getTypeInfos(config, schema)[0]).toMatchInlineSnapshot(` - { - "comment": undefined, - "fields": [ - { - "comment": undefined, - "name": "field1", - "typeString": "TypeI['field1'] | undefined", - }, - { - "comment": undefined, - "name": "field2", - "typeString": "OptionalSubTypeI | undefined", - }, - ], - "name": "TypeI", - } + expect(getTypeInfos(config, schema)).toMatchInlineSnapshot(` + [ + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field1", + "typeString": "TypeI['field1'] | undefined", + }, + { + "comment": undefined, + "name": "field2", + "typeString": "OptionalSubTypeI | undefined", + }, + ], + "name": "TypeI", + "type": "object", + }, + { + "comment": undefined, + "fields": [ + { + "comment": undefined, + "name": "field", + "typeString": "SubTypeI['field'] | undefined", + }, + ], + "name": "SubTypeI", + "type": "object", + }, + { + "comment": undefined, + "name": "InterfaceI", + "possibleTypes": [ + "TypeI", + ], + "type": "abstract", + }, + { + "comment": undefined, + "name": "UnionI", + "possibleTypes": [ + "TypeI", + ], + "type": "abstract", + }, + ] `); }); it('does not effect to __typename and __is', () => { @@ -553,7 +682,7 @@ describe('getTypeInfos', () => { skipIsAbstractType: false, typesSuffix: 'I', }); - expect(getTypeInfos(config, schema)[0]?.fields).toMatchInlineSnapshot(` + expect(getTypeInfos(config, schema).find(isObjectTypeInfo)?.fields).toMatchInlineSnapshot(` [ { "name": "__typename", diff --git a/src/schema-scanner.ts b/src/schema-scanner.ts index 2fb3ecb..de8a336 100644 --- a/src/schema-scanner.ts +++ b/src/schema-scanner.ts @@ -9,6 +9,7 @@ import { InputObjectTypeDefinitionNode, InputValueDefinitionNode, UnionTypeDefinitionNode, + InterfaceTypeDefinitionNode, } from 'graphql'; import { Config } from './config.js'; @@ -68,12 +69,13 @@ function parseObjectTypeOrInputObjectTypeDefinition( config: Config, userDefinedTypeNames: string[], getAbstractTypeNames: (type: ObjectTypeDefinitionNode) => string[], -): TypeInfo { +): ObjectTypeInfo { const originalTypeName = node.name.value; const convertedTypeName = convertName(originalTypeName, config); const comment = node.description ? transformComment(node.description) : undefined; const abstractTypeNames = node.kind === Kind.OBJECT_TYPE_DEFINITION ? getAbstractTypeNames(node) : []; return { + type: 'object', name: convertedTypeName, fields: [ ...(!config.skipTypename ? [{ name: '__typename', typeString: `'${originalTypeName}'` }] : []), @@ -90,17 +92,46 @@ function parseObjectTypeOrInputObjectTypeDefinition( } type FieldInfo = { name: string; typeString: string; comment?: string | undefined }; -export type TypeInfo = { name: string; fields: FieldInfo[]; comment?: string | undefined }; +export type ObjectTypeInfo = { + type: 'object'; + name: string; + fields: FieldInfo[]; + comment?: string | undefined; +}; +export type AbstractTypeInfo = { + type: 'abstract'; + name: string; + possibleTypes: string[]; + comment?: string | undefined; +}; +export type TypeInfo = ObjectTypeInfo | AbstractTypeInfo; export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[] { const types = Object.values(schema.getTypeMap()); - const objectTypeOrInputObjectTypeDefinitions = types + const userDefinedTypeDefinitions = types .map((type) => type.astNode) - .filter((node): node is ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode => { - if (!node) return false; - return node.kind === Kind.OBJECT_TYPE_DEFINITION || node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION; - }); + .filter( + ( + node, + ): node is + | ObjectTypeDefinitionNode + | InputObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode => { + if (!node) return false; + return ( + node.kind === Kind.OBJECT_TYPE_DEFINITION || + node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || + node.kind === Kind.INTERFACE_TYPE_DEFINITION || + node.kind === Kind.UNION_TYPE_DEFINITION + ); + }, + ); + const objectTypeDefinitions = userDefinedTypeDefinitions.filter((node): node is ObjectTypeDefinitionNode => { + if (!node) return false; + return node.kind === Kind.OBJECT_TYPE_DEFINITION; + }); const unionTypeDefinitions = types .map((type) => type.astNode) .filter((node): node is UnionTypeDefinitionNode => { @@ -115,11 +146,48 @@ export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[] return [...interfaceNames, ...unionNames]; } - const userDefinedTypeNames = objectTypeOrInputObjectTypeDefinitions.map((type) => type.name.value); - - const typeInfos = objectTypeOrInputObjectTypeDefinitions.map((node) => - parseObjectTypeOrInputObjectTypeDefinition(node, config, userDefinedTypeNames, getAbstractTypeNames), - ); + const userDefinedTypeNames = userDefinedTypeDefinitions.map((node) => node.name.value); - return typeInfos; + return types + .map((type) => type.astNode) + .filter( + ( + node, + ): node is + | ObjectTypeDefinitionNode + | InputObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode => { + if (!node) return false; + return ( + node.kind === Kind.OBJECT_TYPE_DEFINITION || + node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || + node.kind === Kind.INTERFACE_TYPE_DEFINITION || + node.kind === Kind.UNION_TYPE_DEFINITION + ); + }, + ) + .map((node) => { + if (node?.kind === Kind.OBJECT_TYPE_DEFINITION || node?.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + return parseObjectTypeOrInputObjectTypeDefinition(node, config, userDefinedTypeNames, getAbstractTypeNames); + } else if (node?.kind === Kind.INTERFACE_TYPE_DEFINITION) { + return { + type: 'abstract', + name: convertName(node.name.value, config), + possibleTypes: objectTypeDefinitions + .filter((objectTypeDefinitionNode) => + (objectTypeDefinitionNode.interfaces ?? []).some((i) => i.name.value === node.name.value), + ) + .map((objectTypeDefinitionNode) => convertName(objectTypeDefinitionNode.name.value, config)), + comment: node.description ? transformComment(node.description) : undefined, + }; + } else { + return { + type: 'abstract', + name: convertName(node.name.value, config), + possibleTypes: (node.types ?? []).map((type) => convertName(type.name.value, config)), + comment: node.description ? transformComment(node.description) : undefined, + }; + } + }); }