From c044cdf53ef5fda44358ecb24280226f8c255473 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 14 May 2024 15:57:19 +0000 Subject: [PATCH] feat(terminateCircularRelationships): Add 'immediate' as a value for the terminateCircularRelationships option. This value will make the mock not create a new Set of relationships to ignore and create a global Set. This will circumvent possible OOM due to large graphs. --- README.md | 4 +- src/index.ts | 23 ++++++----- .../__snapshots__/spec.ts.snap | 39 +++++++++++++++++++ .../schema.ts | 18 +++++++++ .../spec.ts | 12 ++++++ .../types.ts | 17 ++++++++ 6 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 tests/terminateCircularRelationshipsImmediately/__snapshots__/spec.ts.snap create mode 100644 tests/terminateCircularRelationshipsImmediately/schema.ts create mode 100644 tests/terminateCircularRelationshipsImmediately/spec.ts create mode 100644 tests/terminateCircularRelationshipsImmediately/types.ts diff --git a/README.md b/README.md index 331376b..444f01b 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,13 @@ Adds `__typename` property to mock data Changes enums to TypeScript string union types -### terminateCircularRelationships (`boolean`, defaultValue: `false`) +### terminateCircularRelationships (`boolean | 'immediate'`, 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. +When enabled with `immediate`, it will only resolve the relationship once, independently of the call stack. Use this option if you're experiencing `out of memory` errors while generating mocks. + ### prefix (`string`, defaultValue: `a` for consonants & `an` for vowels) The prefix to add to the mock function name. Cannot be empty since it will clash with the associated diff --git a/src/index.ts b/src/index.ts index 6ef0561..418d55d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ type Options = { types: TypeItem[]; typeNamesConvention: NamingConvention; enumValuesConvention: NamingConvention; - terminateCircularRelationships: boolean; + terminateCircularRelationships: boolean | 'immediate'; prefix: string | undefined; typesPrefix: string; enumsPrefix: string; @@ -40,6 +40,9 @@ type Options = { nonNull: boolean; }; +const getTerminateCircularRelationshipsConfig = ({ terminateCircularRelationships }: TypescriptMocksPluginConfig) => + terminateCircularRelationships ? terminateCircularRelationships : false; + const convertName = (value: string, fn: (v: string) => string, transformUnderscore: boolean): string => { if (transformUnderscore) { return fn(value); @@ -430,7 +433,7 @@ const getMockString = ( typeName: string, fields: string, typeNamesConvention: NamingConvention, - terminateCircularRelationships: boolean, + terminateCircularRelationships: boolean | 'immediate', addTypename = false, prefix, typesPrefix = '', @@ -443,13 +446,15 @@ const getMockString = ( const typenameReturnType = addTypename ? `{ __typename: '${typeName}' } & ` : ''; if (terminateCircularRelationships) { + const relationshipsToOmitInit = + terminateCircularRelationships === 'immediate' ? '_relationshipsToOmit' : 'new Set(_relationshipsToOmit)'; return ` export const ${toMockName( typeName, casedName, prefix, )} = (overrides?: Partial<${casedNameWithPrefix}>, _relationshipsToOmit: Set = new Set()): ${typenameReturnType}${casedNameWithPrefix} => { - const relationshipsToOmit: Set = new Set(_relationshipsToOmit); + const relationshipsToOmit: Set = ${relationshipsToOmitInit}; relationshipsToOmit.add('${casedName}'); return {${typename} ${fields} @@ -541,7 +546,7 @@ export interface TypescriptMocksPluginConfig { addTypename?: boolean; prefix?: string; scalars?: ScalarMap; - terminateCircularRelationships?: boolean; + terminateCircularRelationships?: boolean | 'immediate'; typesPrefix?: string; enumsPrefix?: string; transformUnderscore?: boolean; @@ -669,7 +674,7 @@ export const plugin: PluginFunction = (schema, docu types, typeNamesConvention, enumValuesConvention, - terminateCircularRelationships: !!config.terminateCircularRelationships, + terminateCircularRelationships: getTerminateCircularRelationshipsConfig(config), prefix: config.prefix, typesPrefix: config.typesPrefix, enumsPrefix: config.enumsPrefix, @@ -706,7 +711,7 @@ export const plugin: PluginFunction = (schema, docu types, typeNamesConvention, enumValuesConvention, - terminateCircularRelationships: !!config.terminateCircularRelationships, + terminateCircularRelationships: getTerminateCircularRelationshipsConfig(config), prefix: config.prefix, typesPrefix: config.typesPrefix, enumsPrefix: config.enumsPrefix, @@ -733,7 +738,7 @@ export const plugin: PluginFunction = (schema, docu fieldName, mockFields, typeNamesConvention, - !!config.terminateCircularRelationships, + getTerminateCircularRelationshipsConfig(config), false, config.prefix, config.typesPrefix, @@ -756,7 +761,7 @@ export const plugin: PluginFunction = (schema, docu typeName, mockFields, typeNamesConvention, - !!config.terminateCircularRelationships, + getTerminateCircularRelationshipsConfig(config), !!config.addTypename, config.prefix, config.typesPrefix, @@ -777,7 +782,7 @@ export const plugin: PluginFunction = (schema, docu typeName, mockFields, typeNamesConvention, - !!config.terminateCircularRelationships, + getTerminateCircularRelationshipsConfig(config), !!config.addTypename, config.prefix, config.typesPrefix, diff --git a/tests/terminateCircularRelationshipsImmediately/__snapshots__/spec.ts.snap b/tests/terminateCircularRelationshipsImmediately/__snapshots__/spec.ts.snap new file mode 100644 index 0000000..ce35500 --- /dev/null +++ b/tests/terminateCircularRelationshipsImmediately/__snapshots__/spec.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should support setting terminateCircularRelationships as imediate and not create a new set of relationships 1`] = ` +" +export const anA = (overrides?: Partial, _relationshipsToOmit: Set = new Set()): A => { + const relationshipsToOmit: Set = _relationshipsToOmit; + relationshipsToOmit.add('A'); + return { + B: overrides && overrides.hasOwnProperty('B') ? overrides.B! : relationshipsToOmit.has('B') ? {} as B : aB({}, relationshipsToOmit), + C: overrides && overrides.hasOwnProperty('C') ? overrides.C! : relationshipsToOmit.has('C') ? {} as C : aC({}, relationshipsToOmit), + }; +}; + +export const aB = (overrides?: Partial, _relationshipsToOmit: Set = new Set()): B => { + const relationshipsToOmit: Set = _relationshipsToOmit; + relationshipsToOmit.add('B'); + return { + A: overrides && overrides.hasOwnProperty('A') ? overrides.A! : relationshipsToOmit.has('A') ? {} as A : anA({}, relationshipsToOmit), + }; +}; + +export const aC = (overrides?: Partial, _relationshipsToOmit: Set = new Set()): C => { + const relationshipsToOmit: Set = _relationshipsToOmit; + relationshipsToOmit.add('C'); + return { + aCollection: overrides && overrides.hasOwnProperty('aCollection') ? overrides.aCollection! : [relationshipsToOmit.has('A') ? {} as A : anA({}, relationshipsToOmit)], + }; +}; + +export const aD = (overrides?: Partial, _relationshipsToOmit: Set = new Set()): D => { + const relationshipsToOmit: Set = _relationshipsToOmit; + relationshipsToOmit.add('D'); + return { + A: overrides && overrides.hasOwnProperty('A') ? overrides.A! : relationshipsToOmit.has('A') ? {} as A : anA({}, relationshipsToOmit), + B: overrides && overrides.hasOwnProperty('B') ? overrides.B! : relationshipsToOmit.has('B') ? {} as B : aB({}, relationshipsToOmit), + }; +}; +" +`; diff --git a/tests/terminateCircularRelationshipsImmediately/schema.ts b/tests/terminateCircularRelationshipsImmediately/schema.ts new file mode 100644 index 0000000..937d770 --- /dev/null +++ b/tests/terminateCircularRelationshipsImmediately/schema.ts @@ -0,0 +1,18 @@ +import { buildSchema } from 'graphql'; + +export default buildSchema(/* GraphQL */ ` + type A { + B: B! + C: C! + } + type B { + A: A! + } + type C { + aCollection: [A!]! + } + type D { + A: A! + B: B! + } +`); diff --git a/tests/terminateCircularRelationshipsImmediately/spec.ts b/tests/terminateCircularRelationshipsImmediately/spec.ts new file mode 100644 index 0000000..dd16979 --- /dev/null +++ b/tests/terminateCircularRelationshipsImmediately/spec.ts @@ -0,0 +1,12 @@ +import { plugin } from '../../src'; +import testSchema from './schema'; + +it('should support setting terminateCircularRelationships as imediate and not create a new set of relationships', async () => { + const result = await plugin(testSchema, [], { + terminateCircularRelationships: 'immediate', + }); + + expect(result).toBeDefined(); + expect(result).not.toContain('new Set(_relationshipsToOmit)'); + expect(result).toMatchSnapshot(); +}); diff --git a/tests/terminateCircularRelationshipsImmediately/types.ts b/tests/terminateCircularRelationshipsImmediately/types.ts new file mode 100644 index 0000000..2d7d94c --- /dev/null +++ b/tests/terminateCircularRelationshipsImmediately/types.ts @@ -0,0 +1,17 @@ +export type A = { + B: B; + C: C; +}; + +export type B = { + A: A; +}; + +export type C = { + aCollection: A[]; +}; + +export type D = { + A: A; + B: B; +};