diff --git a/README.md b/README.md index 22b7fa9..ce194f3 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,11 @@ When disabled, underscores will be retained for type names when the case is chan When enabled, values will be generated dynamically when the mock function is called rather than statically when the mock function is generated. The values are generated consistently from a [casual seed](https://github.com/boo1ean/casual#seeding) that can be manually configured using the generated `seedMocks(seed: number)` function, as shown in [this test](https://github.com/JimmyPaolini/graphql-codegen-typescript-mock-data/blob/dynamic-mode/tests/dynamicValues/spec.ts#L13). +### useImplementingTypes (`boolean`, defaultValue: `false`) + +When enabled, it will support the useImplementingTypes GraphQL codegen configuration. +- When a GraphQL interface is used for a field, this flag will use the implementing types, instead of the interface itself. + ### fieldGeneration (`{ [typeName: string]: { [fieldName: string]: GeneratorOptions } }`, defaultValue: `undefined`) This setting allows you to add specific generation to a field for a given type. For example if you have a type called `User` and a field called `birthDate` you can override any generated value there as follows: diff --git a/src/index.ts b/src/index.ts index cdfc874..3927167 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,17 @@ 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 +284,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((foundType: TypeItem) => { + if (foundType.types && 'interfaces' in foundType.types) + return foundType.types.interfaces.every((item) => item.name.value === name); + return foundType.name === name; + }); + + if (foundTypes.length) { + const foundType = foundTypes[0]; switch (foundType.type) { case 'enum': { // It's an enum @@ -300,6 +326,22 @@ const getNamedType = (opts: Options): string | number | boolean = foundType.name === 'Date' ? mockValueGenerator.date : mockValueGenerator.word, ); } + case 'implement': + if ( + opts.fieldGeneration && + opts.fieldGeneration[opts.typeName] && + opts.fieldGeneration[opts.typeName][opts.fieldName] + ) + break; + + return foundTypes + .map((implementType: TypeItem) => + getNamedImplementType({ + ...opts, + currentType: implementType.types, + }), + ) + .join(' || '); default: throw `foundType is unknown: ${foundType.name}: ${foundType.type}`; } @@ -470,13 +512,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 +559,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 +567,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 +588,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((objectType) => objectType.name === typeName)) { + node.interfaces.length && + types.push({ + name: typeName, + type: 'implement', + types: node, + }); + } + } + }, + ScalarTypeDefinition: (node) => { + const name = node.name.value; + if (!types.find((scalarType) => scalarType.name === name)) { + types.push({ + name, + type: 'scalar', + }); + } + }, + }; + const visitor: VisitorType = { FieldDefinition: (node) => { const fieldName = node.name.value; @@ -568,6 +638,7 @@ export const plugin: PluginFunction = (schema, docu generateLibrary, fieldGeneration: config.fieldGeneration, enumsAsTypes, + useImplementingTypes, }); return ` ${fieldName}: overrides && overrides.hasOwnProperty('${fieldName}') ? overrides.${fieldName}! : ${value},`; @@ -601,6 +672,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 +737,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..071e5fa --- /dev/null +++ b/tests/useImplementingTypes/__snapshots__/spec.ts.snap @@ -0,0 +1,124 @@ +// 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 { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true, + }; +}; + +export const mockTestTwoAConfig = (overrides?: Partial): TestTwoAConfig => { + return { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [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 { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true, + }; +}; + +export const mockTestTwoAConfig = (overrides?: Partial): TestTwoAConfig => { + return { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + username: overrides && overrides.hasOwnProperty('username') ? overrides.username! : 'et', + }; +}; +" +`; + +exports[`support useImplementingTypes with fieldGeneration prop 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! : 'Karelle_Kassulke@Carolyne.io', + }; +}; + +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 { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test], + active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true, + }; +}; + +export const mockTestTwoAConfig = (overrides?: Partial): TestTwoAConfig => { + return { + configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [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..f5a7e21 --- /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 { + configTypes: [configTypes!]! + active: Boolean! + } + + type TestTwoAConfig implements AConfig { + configTypes: [configTypes!]! + username: String! + } +`); diff --git a/tests/useImplementingTypes/spec.ts b/tests/useImplementingTypes/spec.ts new file mode 100644 index 0000000..d907994 --- /dev/null +++ b/tests/useImplementingTypes/spec.ts @@ -0,0 +1,62 @@ +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(); + + 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(); + + expect(result).toContain( + "config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockAConfig(),", + ); + + expect(result).toMatchSnapshot(); +}); + +it(`support useImplementingTypes with fieldGeneration prop`, async () => { + let result = await plugin(testSchema, [], { + prefix: 'mock', + useImplementingTypes: true, + fieldGeneration: { + A: { str: 'email' }, + }, + }); + expect(result).toBeDefined(); + + expect(result).toContain( + "str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'Krystel.Farrell@Frederique.biz'", + ); + + expect(result).toContain( + "config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockTestAConfig() || mockTestTwoAConfig(),", + ); + + result = await plugin(testSchema, [], { + prefix: 'mock', + useImplementingTypes: true, + fieldGeneration: { + A: { config: 'email' }, + }, + }); + expect(result).toBeDefined(); + + expect(result).toContain("str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'ea'"); + + expect(result).toContain( + "config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : 'Karelle_Kassulke@Carolyne.io',", + ); + + expect(result).toMatchSnapshot(); +});