From 5b1038787da21f80726a5f5d1a79408e0cd5a91a Mon Sep 17 00:00:00 2001 From: Maor Daniel Date: Thu, 23 Mar 2023 14:15:46 +0200 Subject: [PATCH] add support for useImplementingTypes --- src/index.ts | 104 +++++++++++++----- .../__snapshots__/spec.ts.snap | 83 ++++++++++++++ tests/useImplementingTypes/schema.ts | 35 ++++++ tests/useImplementingTypes/spec.ts | 26 +++++ 4 files changed, 222 insertions(+), 26 deletions(-) create mode 100644 tests/useImplementingTypes/__snapshots__/spec.ts.snap create mode 100644 tests/useImplementingTypes/schema.ts create mode 100644 tests/useImplementingTypes/spec.ts diff --git a/src/index.ts b/src/index.ts index cdfc874..437c9c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,12 @@ -import { ASTKindToNode, ListTypeNode, NamedTypeNode, parse, printSchema, TypeNode } from 'graphql'; +import { + parse, + printSchema, + TypeNode, + ASTKindToNode, + ListTypeNode, + NamedTypeNode, + ObjectTypeDefinitionNode, +} from 'graphql'; import { faker } from '@faker-js/faker'; import casual from 'casual'; import { oldVisit, PluginFunction, resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; @@ -26,6 +34,7 @@ type Options = { generateLibrary: 'casual' | 'faker'; fieldGeneration?: TypeFieldMap; enumsAsTypes?: boolean; + useImplementingTypes: boolean; }; const convertName = (value: string, fn: (v: string) => string, transformUnderscore: boolean): string => { @@ -230,6 +239,16 @@ const handleValueGeneration = ( return baseGenerator(); }; +const getNamedImplementType = (opts: Options): string => { + if (!opts.currentType || !('name' in opts.currentType)) { + return ''; + } + + const name = opts.currentType.name.value; + const casedName = createNameConverter(opts.typeNamesConvention, opts.transformUnderscore)(name); + return `${toMockName(name, casedName, opts.prefix)}()`; +}; + const getNamedType = (opts: Options): string | number | boolean => { if (!opts.currentType) { return ''; @@ -264,8 +283,14 @@ const getNamedType = (opts: Options): string | number | boolean = return handleValueGeneration(opts, customScalar, mockValueGenerator.integer); } default: { - const foundType = opts.types.find((enumType: TypeItem) => enumType.name === name); - if (foundType) { + const foundTypes = opts.types.filter((enumType: TypeItem) => { + if (enumType.types && 'interfaces' in enumType.types) + return enumType.types.interfaces.every((item) => item.name.value === name); + return enumType.name === name; + }); + + if (foundTypes.length) { + const foundType = foundTypes[0]; switch (foundType.type) { case 'enum': { // It's an enum @@ -300,23 +325,27 @@ const getNamedType = (opts: Options): string | number | boolean = foundType.name === 'Date' ? mockValueGenerator.date : mockValueGenerator.word, ); } + case 'implement': + return foundTypes + .map((implementType: TypeItem) => + getNamedImplementType({ + ...opts, + currentType: implementType.types, + }), + ) + .join(' || '); default: throw `foundType is unknown: ${foundType.name}: ${foundType.type}`; } } if (opts.terminateCircularRelationships) { - return handleValueGeneration( - opts, - null, - () => - `relationshipsToOmit.has('${casedName}') ? {} as ${casedName} : ${toMockName( - name, - casedName, - opts.prefix, - )}({}, relationshipsToOmit)`, - ); + return `relationshipsToOmit.has('${casedName}') ? {} as ${casedName} : ${toMockName( + name, + casedName, + opts.prefix, + )}({}, relationshipsToOmit)`; } else { - return handleValueGeneration(opts, null, () => `${toMockName(name, casedName, opts.prefix)}()`); + return `${toMockName(name, casedName, opts.prefix)}()`; } } } @@ -470,13 +499,14 @@ export interface TypescriptMocksPluginConfig { fieldGeneration?: TypeFieldMap; locale?: string; enumsAsTypes?: boolean; + useImplementingTypes?: boolean; } interface TypeItem { name: string; - type: 'enum' | 'scalar' | 'union'; + type: 'enum' | 'scalar' | 'union' | 'implement'; values?: string[]; - types?: readonly NamedTypeNode[]; + types?: readonly NamedTypeNode[] | ObjectTypeDefinitionNode; } type VisitFn = ( @@ -516,6 +546,7 @@ export const plugin: PluginFunction = (schema, docu const dynamicValues = !!config.dynamicValues; const generateLibrary = config.generateLibrary || 'casual'; const enumsAsTypes = config.enumsAsTypes ?? false; + const useImplementingTypes = config.useImplementingTypes ?? false; if (generateLibrary === 'faker' && config.locale) { faker.setLocale(config.locale); @@ -523,7 +554,7 @@ export const plugin: PluginFunction = (schema, docu // List of types that are enums const types: TypeItem[] = []; - const visitor: VisitorType = { + const typeVisitor: VisitorType = { EnumTypeDefinition: (node) => { const name = node.name.value; if (!types.find((enumType: TypeItem) => enumType.name === name)) { @@ -544,6 +575,32 @@ export const plugin: PluginFunction = (schema, docu }); } }, + ObjectTypeDefinition: (node) => { + // This function triggered per each type + const typeName = node.name.value; + + if (config.useImplementingTypes) { + if (!types.find((enumType) => enumType.name === typeName)) { + node.interfaces.length && + types.push({ + name: typeName, + type: 'implement', + types: node, + }); + } + } + }, + ScalarTypeDefinition: (node) => { + const name = node.name.value; + if (!types.find((enumType) => enumType.name === name)) { + types.push({ + name, + type: 'scalar', + }); + } + }, + }; + const visitor: VisitorType = { FieldDefinition: (node) => { const fieldName = node.name.value; @@ -568,6 +625,7 @@ export const plugin: PluginFunction = (schema, docu generateLibrary, fieldGeneration: config.fieldGeneration, enumsAsTypes, + useImplementingTypes, }); return ` ${fieldName}: overrides && overrides.hasOwnProperty('${fieldName}') ? overrides.${fieldName}! : ${value},`; @@ -601,6 +659,7 @@ export const plugin: PluginFunction = (schema, docu generateLibrary, fieldGeneration: config.fieldGeneration, enumsAsTypes, + useImplementingTypes, }); return ` ${field.name.value}: overrides && overrides.hasOwnProperty('${field.name.value}') ? overrides.${field.name.value}! : ${value},`; @@ -665,17 +724,10 @@ export const plugin: PluginFunction = (schema, docu }, }; }, - ScalarTypeDefinition: (node) => { - const name = node.name.value; - if (!types.find((enumType) => enumType.name === name)) { - types.push({ - name, - type: 'scalar', - }); - } - }, }; + // run on the types first + oldVisit(astNode, { leave: typeVisitor }); const result = oldVisit(astNode, { leave: visitor }); const definitions = result.definitions.filter((definition: any) => !!definition); const typesFile = config.typesFile ? config.typesFile.replace(/\.[\w]+$/, '') : null; diff --git a/tests/useImplementingTypes/__snapshots__/spec.ts.snap b/tests/useImplementingTypes/__snapshots__/spec.ts.snap new file mode 100644 index 0000000..2d0640c --- /dev/null +++ b/tests/useImplementingTypes/__snapshots__/spec.ts.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should support useImplementingTypes 1`] = ` +" +export const mockAConfig = (overrides?: Partial): AConfig => { + return { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + }; +}; + +export const mockA = (overrides?: Partial): A => { + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'cae147b0-1c04-459e-82db-624dd87433b4', + str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'ea', + obj: overrides && overrides.hasOwnProperty('obj') ? overrides.obj! : mockB(), + config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockTestAConfig() || mockTestTwoAConfig(), + }; +}; + +export const mockB = (overrides?: Partial): B => { + return { + int: overrides && overrides.hasOwnProperty('int') ? overrides.int! : 696, + flt: overrides && overrides.hasOwnProperty('flt') ? overrides.flt! : 7.55, + bool: overrides && overrides.hasOwnProperty('bool') ? overrides.bool! : false, + }; +}; + +export const mockTestAConfig = (overrides?: Partial): TestAConfig => { + return { + detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test], + active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true, + }; +}; + +export const mockTestTwoAConfig = (overrides?: Partial): TestTwoAConfig => { + return { + detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test], + username: overrides && overrides.hasOwnProperty('username') ? overrides.username! : 'et', + }; +}; +" +`; + +exports[`shouldn't support useImplementingTypes 1`] = ` +" +export const mockAConfig = (overrides?: Partial): AConfig => { + return { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + }; +}; + +export const mockA = (overrides?: Partial): A => { + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'cae147b0-1c04-459e-82db-624dd87433b4', + str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'ea', + obj: overrides && overrides.hasOwnProperty('obj') ? overrides.obj! : mockB(), + config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockAConfig(), + }; +}; + +export const mockB = (overrides?: Partial): B => { + return { + int: overrides && overrides.hasOwnProperty('int') ? overrides.int! : 696, + flt: overrides && overrides.hasOwnProperty('flt') ? overrides.flt! : 7.55, + bool: overrides && overrides.hasOwnProperty('bool') ? overrides.bool! : false, + }; +}; + +export const mockTestAConfig = (overrides?: Partial): TestAConfig => { + return { + detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test], + active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true, + }; +}; + +export const mockTestTwoAConfig = (overrides?: Partial): TestTwoAConfig => { + return { + detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test], + username: overrides && overrides.hasOwnProperty('username') ? overrides.username! : 'et', + }; +}; +" +`; diff --git a/tests/useImplementingTypes/schema.ts b/tests/useImplementingTypes/schema.ts new file mode 100644 index 0000000..6cd59ea --- /dev/null +++ b/tests/useImplementingTypes/schema.ts @@ -0,0 +1,35 @@ +import { buildSchema } from 'graphql'; + +export default buildSchema(/* GraphQL */ ` + interface AConfig { + configTypes: [configTypes!]! + } + + enum configTypes { + TEST + TEST2 + } + + type A { + id: ID! + str: String! + obj: B! + config: AConfig! + } + + type B { + int: Int! + flt: Float! + bool: Boolean! + } + + type TestAConfig implements AConfig { + detectionTypes: [configTypes!]! + active: Boolean! + } + + type TestTwoAConfig implements AConfig { + detectionTypes: [configTypes!]! + username: String! + } +`); diff --git a/tests/useImplementingTypes/spec.ts b/tests/useImplementingTypes/spec.ts new file mode 100644 index 0000000..1645a49 --- /dev/null +++ b/tests/useImplementingTypes/spec.ts @@ -0,0 +1,26 @@ +import { plugin } from '../../src'; +import testSchema from './schema'; + +it('should support useImplementingTypes', async () => { + const result = await plugin(testSchema, [], { prefix: 'mock', useImplementingTypes: true }); + + expect(result).toBeDefined(); + // Boolean + expect(result).toContain( + "config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockTestAConfig() || mockTestTwoAConfig(),", + ); + + expect(result).toMatchSnapshot(); +}); + +it(`shouldn't support useImplementingTypes`, async () => { + const result = await plugin(testSchema, [], { prefix: 'mock' }); + + expect(result).toBeDefined(); + // Boolean + expect(result).toContain( + "config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockAConfig(),", + ); + + expect(result).toMatchSnapshot(); +});