From 2e03d68482bdfef174892e57f2b09e4e5567a289 Mon Sep 17 00:00:00 2001 From: SanderVanRiessen <51707291+SanderVanRiessen@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:27:00 +0200 Subject: [PATCH] feat: allow renaming types (#171) Add `typeNamesMapping` option in order to update a type name --------- Co-authored-by: s.vanriessen --- README.md | 20 +++++++++ src/index.ts | 43 ++++++++++++++++--- tests/typeNamesMapping/schema.ts | 31 +++++++++++++ tests/typeNamesMapping/spec.ts | 36 ++++++++++++++++ .../useTypeImports/__snapshots__/spec.ts.snap | 8 +++- tests/useTypeImports/schema.ts | 4 ++ tests/useTypeImports/spec.ts | 2 +- 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 tests/typeNamesMapping/schema.ts create mode 100644 tests/typeNamesMapping/spec.ts diff --git a/README.md b/README.md index 19ee1d6..e7045ed 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,26 @@ export const aUser = (overrides?: Partial): User => { } ``` +### typeNamesMapping (`{ [typeName: string]: string }`, defaultValue: `{}`) + +Allows you to define mappings to rename the types. This is useful when you want to override the generated type name. For example, if you have a type called `User` and you want to rename it to `RenamedUser` you can do the following: + +``` +plugins: + - typescript-mock-data: + typesFile: '../generated-types.ts' + typeNamesMapping: + User: RenamedUser +``` + +This will generate the following mock function: + +``` +export const aUser = (overrides?: Partial): RenamedUser => { +``` + +**Note:** It is not possible to rename your enums using this option. + ### transformUnderscore (`boolean`, defaultValue: `true`) When disabled, underscores will be retained for type names when the case is changed. It has no effect if `typeNames` is set to `keep`. diff --git a/src/index.ts b/src/index.ts index a986971..53f88fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,7 @@ type Options = { useImplementingTypes: boolean; defaultNullableToNull: boolean; nonNull: boolean; + typeNamesMapping?: Record; }; const getTerminateCircularRelationshipsConfig = ({ terminateCircularRelationships }: TypescriptMocksPluginConfig) => @@ -64,6 +65,15 @@ const createNameConverter = return `${prefix}${convertName(value, resolveExternalModuleAndFn(convention), transformUnderscore)}`; }; +const renameImports = (list: string[], typeNamesMapping: Record) => { + return list.map((type) => { + if (typeNamesMapping && typeNamesMapping[type]) { + return `${type} as ${typeNamesMapping[type]}`; + } + return type; + }); +}; + const toMockName = (typedName: string, casedName: string, prefix?: string) => { if (prefix) { return `${prefix}${casedName}`; @@ -380,14 +390,20 @@ const getNamedType = (opts: Options): opts.typeNamesConvention, opts.transformUnderscore, ); - const casedNameWithPrefix = typeNameConverter(name, opts.typesPrefix); + const renamedType = renameImports([name], opts.typeNamesMapping)[0]; + const casedNameWithPrefix = typeNameConverter(renamedType, opts.typesPrefix); return `relationshipsToOmit.has('${casedName}') ? {} as ${casedNameWithPrefix} : ${toMockName( name, casedName, opts.prefix, )}({}, relationshipsToOmit)`; } else { - return `relationshipsToOmit.has('${casedName}') ? {} as ${casedName} : ${toMockName( + const renamedType = renameImports([name], opts.typeNamesMapping)[0]; + const renamedCasedName = createNameConverter( + opts.typeNamesConvention, + opts.transformUnderscore, + )(renamedType); + return `relationshipsToOmit.has('${casedName}') ? {} as ${renamedCasedName} : ${toMockName( name, casedName, opts.prefix, @@ -443,10 +459,12 @@ const getMockString = ( prefix, typesPrefix = '', transformUnderscore: boolean, + typeNamesMapping?: Record, ) => { const typeNameConverter = createNameConverter(typeNamesConvention, transformUnderscore); + const NewTypeName = typeNamesMapping[typeName] || typeName; const casedName = typeNameConverter(typeName); - const casedNameWithPrefix = typeNameConverter(typeName, typesPrefix); + const casedNameWithPrefix = typeNameConverter(NewTypeName, typesPrefix); const typename = addTypename ? `\n __typename: '${typeName}',` : ''; const typenameReturnType = addTypename ? `{ __typename: '${typeName}' } & ` : ''; @@ -489,6 +507,7 @@ const getImportTypes = ({ transformUnderscore, enumsAsTypes, useTypeImports, + typeNamesMapping, }: { typeNamesConvention: NamingConvention; definitions: any; @@ -499,6 +518,7 @@ const getImportTypes = ({ transformUnderscore: boolean; enumsAsTypes: boolean; useTypeImports: boolean; + typeNamesMapping?: Record; }) => { const typenameConverter = createNameConverter(typeNamesConvention, transformUnderscore); const typeImports = typesPrefix?.endsWith('.') @@ -506,12 +526,15 @@ const getImportTypes = ({ : definitions .filter(({ typeName }: { typeName: string }) => !!typeName) .map(({ typeName }: { typeName: string }) => typenameConverter(typeName, typesPrefix)); + const enumTypes = enumsPrefix?.endsWith('.') ? [enumsPrefix.slice(0, -1)] : types.filter(({ type }) => type === 'enum').map(({ name }) => typenameConverter(name, enumsPrefix)); + const renamedTypeImports = renameImports(typeImports, typeNamesMapping); + if (!enumsAsTypes || useTypeImports) { - typeImports.push(...enumTypes); + renamedTypeImports.push(...enumTypes); } function onlyUnique(value, index, self) { @@ -520,7 +543,9 @@ const getImportTypes = ({ const importPrefix = `import ${useTypeImports ? 'type ' : ''}`; - return typesFile ? `${importPrefix}{ ${typeImports.filter(onlyUnique).join(', ')} } from '${typesFile}';\n` : ''; + return typesFile + ? `${importPrefix}{ ${renamedTypeImports.filter(onlyUnique).join(', ')} } from '${typesFile}';\n` + : ''; }; type GeneratorName = keyof Casual.Casual | keyof Casual.functions | string; @@ -564,6 +589,7 @@ export interface TypescriptMocksPluginConfig { useImplementingTypes?: boolean; defaultNullableToNull?: boolean; useTypeImports?: boolean; + typeNamesMapping?: Record; } interface TypeItem { @@ -614,6 +640,7 @@ export const plugin: PluginFunction = (schema, docu const useImplementingTypes = config.useImplementingTypes ?? false; const defaultNullableToNull = config.defaultNullableToNull ?? false; const generatorLocale = config.locale || 'en'; + const typeNamesMapping = config.typeNamesMapping || {}; // List of types that are enums const types: TypeItem[] = []; @@ -693,6 +720,7 @@ export const plugin: PluginFunction = (schema, docu useImplementingTypes, defaultNullableToNull, nonNull: false, + typeNamesMapping: config.typeNamesMapping, }); return ` ${fieldName}: overrides && overrides.hasOwnProperty('${fieldName}') ? overrides.${fieldName}! : ${value},`; @@ -731,6 +759,7 @@ export const plugin: PluginFunction = (schema, docu useImplementingTypes, defaultNullableToNull, nonNull: false, + typeNamesMapping: config.typeNamesMapping, }); return ` ${field.name.value}: overrides && overrides.hasOwnProperty('${field.name.value}') ? overrides.${field.name.value}! : ${value},`; @@ -747,6 +776,7 @@ export const plugin: PluginFunction = (schema, docu config.prefix, config.typesPrefix, transformUnderscore, + typeNamesMapping, ); }, }; @@ -770,6 +800,7 @@ export const plugin: PluginFunction = (schema, docu config.prefix, config.typesPrefix, transformUnderscore, + typeNamesMapping, ); }, }; @@ -791,6 +822,7 @@ export const plugin: PluginFunction = (schema, docu config.prefix, config.typesPrefix, transformUnderscore, + typeNamesMapping, ); }, }; @@ -813,6 +845,7 @@ export const plugin: PluginFunction = (schema, docu transformUnderscore: transformUnderscore, useTypeImports: config.useTypeImports, enumsAsTypes, + typeNamesMapping, }); // Function that will generate the mocks. // We generate it after having visited because we need to distinct types from enums diff --git a/tests/typeNamesMapping/schema.ts b/tests/typeNamesMapping/schema.ts new file mode 100644 index 0000000..a7770e2 --- /dev/null +++ b/tests/typeNamesMapping/schema.ts @@ -0,0 +1,31 @@ +import { buildSchema } from 'graphql/index'; + +export default buildSchema(/* GraphQL */ ` + enum EnumExample { + LOREM + IPSUM + } + + type A { + id: ID! + str: String! + email: String! + } + + type B { + id: ID! + str: String! + email: String! + } + + type C { + id: ID! + str: String! + enum: EnumExample! + D: D! + } + + type D { + nested: C! + } +`); diff --git a/tests/typeNamesMapping/spec.ts b/tests/typeNamesMapping/spec.ts new file mode 100644 index 0000000..2363c37 --- /dev/null +++ b/tests/typeNamesMapping/spec.ts @@ -0,0 +1,36 @@ +import { plugin } from '../../src'; +import testSchema from './schema'; + +it('should support typeNamesMapping', async () => { + const result = await plugin(testSchema, [], { + typesFile: './types/graphql.ts', + typeNamesMapping: { A: 'RenamedAType' }, + }); + + expect(result).toBeDefined(); + expect(result).toContain("import { A as RenamedAType, B, C, D, EnumExample } from './types/graphql';"); +}); + +it('should support typeNamesMapping with circular relationships', async () => { + const result = await plugin(testSchema, [], { + typesFile: './types/graphql.ts', + typeNamesMapping: { D: 'RenamedDType' }, + terminateCircularRelationships: 'immediate', + }); + + expect(result).toBeDefined(); + expect(result).toContain("import { A, B, C, D as RenamedDType, EnumExample } from './types/graphql';"); + expect(result).toContain( + "D: overrides && overrides.hasOwnProperty('D') ? overrides.D! : relationshipsToOmit.has('D') ? {} as DAsRenamedDType : aD({}, relationshipsToOmit),", + ); +}); + +it('should not support typeNamesMapping when enum type is given', async () => { + const result = await plugin(testSchema, [], { + typesFile: './types/graphql.ts', + typeNamesMapping: { EnumExample: 'RenamedEnum' }, + }); + + expect(result).toBeDefined(); + expect(result).toContain("import { A, B, C, D, EnumExample } from './types/graphql';"); +}); diff --git a/tests/useTypeImports/__snapshots__/spec.ts.snap b/tests/useTypeImports/__snapshots__/spec.ts.snap index fed422e..645a7fe 100644 --- a/tests/useTypeImports/__snapshots__/spec.ts.snap +++ b/tests/useTypeImports/__snapshots__/spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should support useTypeImports 1`] = ` -"import type { Avatar, User, WithAvatar, CamelCaseThing, PrefixedResponse, AbcType, ListType, UpdateUserInput, Mutation, Query, AbcStatus, Status, PrefixedEnum } from './types/graphql'; +"import type { Avatar, User, Partial, WithAvatar, CamelCaseThing, PrefixedResponse, AbcType, ListType, UpdateUserInput, Mutation, Query, AbcStatus, Status, PrefixedEnum } from './types/graphql'; export const anAvatar = (overrides?: Partial): Avatar => { return { @@ -25,6 +25,12 @@ export const aUser = (overrides?: Partial): User => { }; }; +export const aPartial = (overrides?: Partial): Partial => { + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '262c8866-bf76-4ccf-b606-2a0b4742f81f', + }; +}; + export const aWithAvatar = (overrides?: Partial): WithAvatar => { return { id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '99f515e7-21e0-461d-b823-0d4c7f4dafc5', diff --git a/tests/useTypeImports/schema.ts b/tests/useTypeImports/schema.ts index 8e1ac4e..3484d14 100644 --- a/tests/useTypeImports/schema.ts +++ b/tests/useTypeImports/schema.ts @@ -22,6 +22,10 @@ export default buildSchema(/* GraphQL */ ` prefixedEnum: Prefixed_Enum } + type Partial { + id: ID! + } + interface WithAvatar { id: ID! avatar: Avatar diff --git a/tests/useTypeImports/spec.ts b/tests/useTypeImports/spec.ts index e98259c..8888e64 100644 --- a/tests/useTypeImports/spec.ts +++ b/tests/useTypeImports/spec.ts @@ -6,7 +6,7 @@ it('should support useTypeImports', async () => { expect(result).toBeDefined(); expect(result).toContain( - "import type { Avatar, User, WithAvatar, CamelCaseThing, PrefixedResponse, AbcType, ListType, UpdateUserInput, Mutation, Query, AbcStatus, Status, PrefixedEnum } from './types/graphql';", + "import type { Avatar, User, Partial, WithAvatar, CamelCaseThing, PrefixedResponse, AbcType, ListType, UpdateUserInput, Mutation, Query, AbcStatus, Status, PrefixedEnum } from './types/graphql';", ); expect(result).toMatchSnapshot(); });