From ad9e270dbd6051d5abd93767f96d68adb64ba181 Mon Sep 17 00:00:00 2001 From: Ford Filer Date: Thu, 8 Oct 2020 11:45:05 -0700 Subject: [PATCH 1/3] Add terminateCircularRelationships option --- .gitignore | 3 ++ jest.config.js | 1 + package.json | 2 +- src/index.ts | 47 ++++++++++++++++--- .../typescript-mock-data.spec.ts.snap | 41 ++++++++++++++++ tests/circular-mocks/create-mocks.ts | 21 +++++++++ tests/circular-mocks/spec.ts | 12 +++++ tests/circular-mocks/types.ts | 12 +++++ tests/typescript-mock-data.spec.ts | 10 ++++ 9 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 tests/circular-mocks/create-mocks.ts create mode 100644 tests/circular-mocks/spec.ts create mode 100644 tests/circular-mocks/types.ts diff --git a/.gitignore b/.gitignore index f09b75b..3cb29f5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# generated test file +/tests/circular-mocks/mocks.ts + diff --git a/jest.config.js b/jest.config.js index 0e1a379..9ece963 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, + globalSetup: './tests/circular-mocks/create-mocks.ts', }; diff --git a/package.json b/package.json index 16da0f1..33d98ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-codegen-typescript-mock-data", - "version": "1.2.1", + "version": "1.3.0", "description": "GraphQL Codegen plugin for building mock data", "main": "dist/commonjs/index.js", "module": "dist/esnext/index.js", diff --git a/src/index.ts b/src/index.ts index b38a7d0..a2d5ab9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,6 +74,7 @@ const getNamedType = ( types: TypeItem[], typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, + terminateCircularRelationships: boolean, prefix?: string, namedType?: NamedTypeNode, customScalars?: ScalarMap, @@ -113,6 +114,7 @@ const getNamedType = ( types, typenamesConvention, enumValuesConvention, + terminateCircularRelationships, prefix, foundType.types && foundType.types[0], ); @@ -150,7 +152,15 @@ const getNamedType = ( throw `foundType is unknown: ${foundType.name}: ${foundType.type}`; } } - return `${toMockName(name, name, prefix)}()`; + if (terminateCircularRelationships) { + return `relationshipsToOmit.has('${name}') ? {} as ${name} : ${toMockName( + name, + name, + prefix, + )}({}, relationshipsToOmit)`; + } else { + return `${toMockName(name, name, prefix)}()`; + } } } }; @@ -161,6 +171,7 @@ const generateMockValue = ( types: TypeItem[], typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, + terminateCircularRelationships: boolean, prefix: string | undefined, currentType: TypeNode, customScalars: ScalarMap, @@ -173,6 +184,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + terminateCircularRelationships, prefix, currentType as NamedTypeNode, customScalars, @@ -184,6 +196,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + terminateCircularRelationships, prefix, currentType.type, customScalars, @@ -195,6 +208,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + terminateCircularRelationships, prefix, currentType.type, customScalars, @@ -208,22 +222,38 @@ const getMockString = ( typeName: string, fields: string, typenamesConvention: NamingConvention, + terminateCircularRelationships: boolean, addTypename = false, prefix, typesPrefix = '', ) => { const casedName = createNameConverter(typenamesConvention)(typeName); const typename = addTypename ? `\n __typename: '${casedName}',` : ''; - return ` + + if (terminateCircularRelationships) { + return ` +export const ${toMockName( + typeName, + casedName, + prefix, + )} = (overrides?: Partial<${typesPrefix}${casedName}>, relationshipsToOmit: Set = new Set()): ${typesPrefix}${casedName} => { + relationshipsToOmit.add('${casedName}'); + return {${typename} +${fields} + }; +};`; + } else { + return ` export const ${toMockName( - typeName, - casedName, - prefix, - )} = (overrides?: Partial<${typesPrefix}${casedName}>): ${typesPrefix}${casedName} => { + typeName, + casedName, + prefix, + )} = (overrides?: Partial<${typesPrefix}${casedName}>): ${typesPrefix}${casedName} => { return {${typename} ${fields} }; };`; + } }; type ScalarGeneratorName = keyof Casual.Casual | keyof Casual.functions; @@ -243,6 +273,7 @@ export interface TypescriptMocksPluginConfig { addTypename?: boolean; prefix?: string; scalars?: ScalarMap; + terminateCircularRelationships?: boolean; typesPrefix?: string; } @@ -299,6 +330,7 @@ export const plugin: PluginFunction = (schema, docu types, typenamesConvention, enumValuesConvention, + !!config.terminateCircularRelationships, config.prefix, node.type, config.scalars, @@ -323,6 +355,7 @@ export const plugin: PluginFunction = (schema, docu types, typenamesConvention, enumValuesConvention, + !!config.terminateCircularRelationships, config.prefix, field.type, config.scalars, @@ -337,6 +370,7 @@ export const plugin: PluginFunction = (schema, docu fieldName, mockFields, typenamesConvention, + !!config.terminateCircularRelationships, false, config.prefix, config.typesPrefix, @@ -362,6 +396,7 @@ export const plugin: PluginFunction = (schema, docu typeName, mockFields, typenamesConvention, + !!config.terminateCircularRelationships, !!config.addTypename, config.prefix, config.typesPrefix, diff --git a/tests/__snapshots__/typescript-mock-data.spec.ts.snap b/tests/__snapshots__/typescript-mock-data.spec.ts.snap index 60747ae..620795b 100644 --- a/tests/__snapshots__/typescript-mock-data.spec.ts.snap +++ b/tests/__snapshots__/typescript-mock-data.spec.ts.snap @@ -670,3 +670,44 @@ export const aUSER = (overrides?: Partial): USER => { }; " `; + +exports[`should use relationshipsToOmit argument to terminate circular relationships with terminateCircularRelationships enabled 1`] = ` +" +export const anAbcType = (overrides?: Partial, relationshipsToOmit: Set = new Set()): AbcType => { + relationshipsToOmit.add('AbcType'); + return { + abc: overrides && overrides.hasOwnProperty('abc') ? overrides.abc! : 'sit', + }; +}; + +export const anAvatar = (overrides?: Partial, relationshipsToOmit: Set = new Set()): Avatar => { + relationshipsToOmit.add('Avatar'); + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '0550ff93-dd31-49b4-8c38-ff1cb68bdc38', + url: overrides && overrides.hasOwnProperty('url') ? overrides.url! : 'aliquid', + }; +}; + +export const anUpdateUserInput = (overrides?: Partial, relationshipsToOmit: Set = new Set()): UpdateUserInput => { + relationshipsToOmit.add('UpdateUserInput'); + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '1d6a9360-c92b-4660-8e5f-04155047bddc', + login: overrides && overrides.hasOwnProperty('login') ? overrides.login! : 'qui', + avatar: overrides && overrides.hasOwnProperty('avatar') ? overrides.avatar! : relationshipsToOmit.has('Avatar') ? {} as Avatar : anAvatar({}, relationshipsToOmit), + }; +}; + +export const aUser = (overrides?: Partial, relationshipsToOmit: Set = new Set()): User => { + relationshipsToOmit.add('User'); + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'a5756f00-41a6-422a-8a7d-d13ee6a63750', + creationDate: overrides && overrides.hasOwnProperty('creationDate') ? overrides.creationDate! : '1970-01-09T16:33:21.532Z', + login: overrides && overrides.hasOwnProperty('login') ? overrides.login! : 'libero', + avatar: overrides && overrides.hasOwnProperty('avatar') ? overrides.avatar! : relationshipsToOmit.has('Avatar') ? {} as Avatar : anAvatar({}, relationshipsToOmit), + status: overrides && overrides.hasOwnProperty('status') ? overrides.status! : Status.Online, + customStatus: overrides && overrides.hasOwnProperty('customStatus') ? overrides.customStatus! : AbcStatus.HasXyzStatus, + scalarValue: overrides && overrides.hasOwnProperty('scalarValue') ? overrides.scalarValue! : 'neque', + }; +}; +" +`; diff --git a/tests/circular-mocks/create-mocks.ts b/tests/circular-mocks/create-mocks.ts new file mode 100644 index 0000000..6a56b75 --- /dev/null +++ b/tests/circular-mocks/create-mocks.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import { buildSchema } from 'graphql'; +import { plugin } from '../../src'; +export default async () => { + const circularSchema = buildSchema(/* GraphQL */ ` + type A { + B: B! + C: C! + } + type B { + A: A! + } + type C { + aCollection: [A!]! + } + `); + + const output = await plugin(circularSchema, [], { typesFile: './types.ts', terminateCircularRelationships: true }); + + fs.writeFileSync('./tests/circular-mocks/mocks.ts', output.toString()); +}; diff --git a/tests/circular-mocks/spec.ts b/tests/circular-mocks/spec.ts new file mode 100644 index 0000000..f5d6460 --- /dev/null +++ b/tests/circular-mocks/spec.ts @@ -0,0 +1,12 @@ +import { aB, aC, anA } from './mocks'; + +it('should terminate circular relationships when terminateCircularRelationships is true', () => { + const a = anA(); + expect(a).toEqual({ B: { A: {} }, C: { aCollection: [{}] } }); + + const b = aB(); + expect(b).toEqual({ A: { B: {}, C: { aCollection: [{}] } } }); + + const c = aC(); + expect(c).toEqual({ aCollection: [{ B: { A: {} }, C: {} }] }); +}); diff --git a/tests/circular-mocks/types.ts b/tests/circular-mocks/types.ts new file mode 100644 index 0000000..767b681 --- /dev/null +++ b/tests/circular-mocks/types.ts @@ -0,0 +1,12 @@ +export type A = { + B: B; + C: C; +}; + +export type B = { + A: A; +}; + +export type C = { + aCollection: A[]; +}; diff --git a/tests/typescript-mock-data.spec.ts b/tests/typescript-mock-data.spec.ts index 605f203..817bf84 100644 --- a/tests/typescript-mock-data.spec.ts +++ b/tests/typescript-mock-data.spec.ts @@ -232,3 +232,13 @@ it('should add typesPrefix to all types when option is specified', async () => { expect(result).not.toMatch(/: User/); expect(result).toMatchSnapshot(); }); + +it('should use relationshipsToOmit argument to terminate circular relationships with terminateCircularRelationships enabled', async () => { + const result = await plugin(testSchema, [], { terminateCircularRelationships: true }); + + expect(result).toBeDefined(); + expect(result).toMatch(/relationshipsToOmit.add\('User'\)/); + expect(result).toMatch(/relationshipsToOmit.has\('Avatar'\) \? {} as Avatar : anAvatar\({}, relationshipsToOmit\)/); + expect(result).not.toMatch(/: anAvatar\(\)/); + expect(result).toMatchSnapshot(); +}); From 52d5bd33ff5f4ebb8185e99d4635d067d9b2aa83 Mon Sep 17 00:00:00 2001 From: Ford Filer Date: Thu, 8 Oct 2020 12:02:43 -0700 Subject: [PATCH 2/3] Update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d680fcf..fc8b436 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ Defines the file path containing all GraphQL types. This file can also be genera Adds `__typename` property to mock data +### terminateCircularRelationships (`boolean`, defaultValue: `false`) + +When enabled, prevents circular relationships from triggering infinite recursion. After the first resolution of a +specific type in a particular call stack, subsequent resolutions will return an empty object cast to the correct type. + ### prefix (`string`, defaultValue: `a` for constants & `an` for vowels) The prefix to add to the mock function name. Cannot be empty since it will clash with the associated From c0bfaa2948b50ff186da00e0dab4c616ef69613e Mon Sep 17 00:00:00 2001 From: Ford Filer Date: Thu, 8 Oct 2020 12:26:45 -0700 Subject: [PATCH 3/3] Reset version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33d98ff..16da0f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-codegen-typescript-mock-data", - "version": "1.3.0", + "version": "1.2.1", "description": "GraphQL Codegen plugin for building mock data", "main": "dist/commonjs/index.js", "module": "dist/esnext/index.js",