From 4b4a8cb937fd0443b20800a285160b96c9aec459 Mon Sep 17 00:00:00 2001 From: Aggelos Arvanitakis Date: Mon, 13 Jul 2020 16:20:09 +0300 Subject: [PATCH 1/2] feat: add `scalars` config option for custom GraphQL [scalar -> casual] mappings --- README.md | 12 ++++++ src/index.ts | 22 +++++++++-- .../typescript-mock-data.spec.ts.snap | 37 +++++++++++++++++++ tests/typescript-mock-data.spec.ts | 9 +++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe5c4d8..c6006e1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Changes the case of the enums. Accepts `upper-case#upperCase`, `pascal-case#pasc Changes the case of the enums. Accepts `upper-case#upperCase`, `pascal-case#pascalCase` or `keep` +### scalars (`{ [Scalar: string]: keyof Casual.Casual | Casual.functions }`, defaultValue: `undefined`) + +Allows you to define mappings for your custom scalars. Allows you to map any GraphQL Scalar to a + [casual](https://github.com/boo1ean/casual#embedded-generators) embedded generator (string or + function key) + ## Example of usage **codegen.yml** @@ -43,6 +49,8 @@ generates: typesFile: '../generated-types.ts' enumValues: upper-case#upperCase typenames: keep + scalars: + AWSTimestamp: unix_time # gets translated to casual.unix_time ``` ## Example or generated code @@ -50,6 +58,8 @@ generates: Given the following schema: ```graphql +scalar AWSTimestamp + type Avatar { id: ID! url: String! @@ -60,6 +70,7 @@ type User { login: String! avatar: Avatar status: Status! + updatedAt: AWSTimestamp } type Query { @@ -106,6 +117,7 @@ export const aUser = (overrides?: Partial): User => { login: overrides && overrides.hasOwnProperty('login') ? overrides.login! : 'libero', avatar: overrides && overrides.hasOwnProperty('avatar') ? overrides.avatar! : anAvatar(), status: overrides && overrides.hasOwnProperty('status') ? overrides.status! : Status.Online, + updatedAt: overrides && overrides.hasOwnProperty('updatedAt') ? overrides.updatedAt! : 1458071232, }; }; ``` diff --git a/src/index.ts b/src/index.ts index 38cb6a5..42e033c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,7 @@ const getNamedType = ( typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, namedType?: NamedTypeNode, + customScalars?: ScalarMap, ): string | number | boolean => { if (!namedType) { return ''; @@ -101,9 +102,15 @@ const getNamedType = ( foundType.types && foundType.types[0], ); case 'scalar': - // it's a scalar, let's use a string as a value. - // This could be improved with a custom scalar definition in the config - return `'${casual.word}'`; + // it's a scalar, let's use a string as a value if there is no custom + // mapping for this particular scalar + if (!customScalars || !customScalars[foundType.name]) { + return `'${casual.word}'`; + } + + // If there is a mapping to a `casual` type, then use this + const value = casual[customScalars[foundType.name]]; + return typeof value === 'function' ? value() : value; default: throw `foundType is unknown: ${foundType.name}: ${foundType.type}`; } @@ -120,6 +127,7 @@ const generateMockValue = ( typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, currentType: TypeNode, + customScalars: ScalarMap, ): string | number | boolean => { switch (currentType.kind) { case 'NamedType': @@ -130,6 +138,7 @@ const generateMockValue = ( typenamesConvention, enumValuesConvention, currentType as NamedTypeNode, + customScalars, ); case 'NonNullType': return generateMockValue( @@ -139,6 +148,7 @@ const generateMockValue = ( typenamesConvention, enumValuesConvention, currentType.type, + customScalars, ); case 'ListType': { const value = generateMockValue( @@ -148,6 +158,7 @@ const generateMockValue = ( typenamesConvention, enumValuesConvention, currentType.type, + customScalars, ); return `[${value}]`; } @@ -170,11 +181,14 @@ ${fields} };`; }; +type ScalarMap = { [name: string]: keyof (Casual.Casual | Casual.functions) }; + export interface TypescriptMocksPluginConfig { typesFile?: string; enumValues?: NamingConvention; typenames?: NamingConvention; addTypename?: boolean; + scalars?: ScalarMap; } interface TypeItem { @@ -231,6 +245,7 @@ export const plugin: PluginFunction = (schema, docu typenamesConvention, enumValuesConvention, node.type, + config.scalars, ); return ` ${fieldName}: overrides && overrides.hasOwnProperty('${fieldName}') ? overrides.${fieldName}! : ${value},`; @@ -253,6 +268,7 @@ export const plugin: PluginFunction = (schema, docu typenamesConvention, enumValuesConvention, field.type, + config.scalars, ); return ` ${field.name.value}: overrides && overrides.hasOwnProperty('${field.name.value}') ? overrides.${field.name.value}! : ${value},`; diff --git a/tests/__snapshots__/typescript-mock-data.spec.ts.snap b/tests/__snapshots__/typescript-mock-data.spec.ts.snap index e6c94b0..3422ab8 100644 --- a/tests/__snapshots__/typescript-mock-data.spec.ts.snap +++ b/tests/__snapshots__/typescript-mock-data.spec.ts.snap @@ -448,3 +448,40 @@ export const aUSER = (overrides?: Partial): USER => { }; " `; + +exports[`should generate the \`casual\` data for a particular scalar mapping 1`] = ` +" +export const anAbcType = (overrides?: Partial): AbcType => { + return { + abc: overrides && overrides.hasOwnProperty('abc') ? overrides.abc! : 'sit', + }; +}; + +export const anAvatar = (overrides?: Partial): Avatar => { + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '0550ff93-dd31-49b4-8c38-ff1cb68bdc38', + url: overrides && overrides.hasOwnProperty('url') ? overrides.url! : 'aliquid', + }; +}; + +export const aUpdateUserInput = (overrides?: Partial): 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! : anAvatar(), + }; +}; + +export const aUser = (overrides?: Partial): 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! : anAvatar(), + 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! : Mohamed.Nader@Kiehn.io, + }; +}; +" +`; diff --git a/tests/typescript-mock-data.spec.ts b/tests/typescript-mock-data.spec.ts index 58460f5..32f4deb 100644 --- a/tests/typescript-mock-data.spec.ts +++ b/tests/typescript-mock-data.spec.ts @@ -168,3 +168,12 @@ it('should generate mock data with as-is types and enums if typenames is "keep"' expect(result).not.toMatch(/ABC(TYPE|STATUS)/); expect(result).toMatchSnapshot(); }); + +it('should generate the `casual` data for a particular scalar mapping', async () => { + const result = await plugin(testSchema, [], { scalars: { AnyObject: 'email' } }); + + const emailRegex = /(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/; + expect(result).toBeDefined(); + expect(emailRegex.test(result as string)).toBeTruthy(); + expect(result).toMatchSnapshot(); +}); From 5bdb17bec478bb4ac2e354d53bcdeebe4d7d7f2e Mon Sep 17 00:00:00 2001 From: Aggelos Arvanitakis Date: Mon, 13 Jul 2020 17:01:04 +0300 Subject: [PATCH 2/2] feat: allow users to specify the `prefix` of mock builders --- README.md | 5 +++ src/index.ts | 29 ++++++++++++--- .../typescript-mock-data.spec.ts.snap | 37 +++++++++++++++++++ tests/typescript-mock-data.spec.ts | 9 +++++ 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe5c4d8..63d68c0 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 +### 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 +typescript definition from `@graphql-codegen/typescript` + ### enumValues (`string`, defaultValue: `pascal-case#pascalCase`) Changes the case of the enums. Accepts `upper-case#upperCase`, `pascal-case#pascalCase` or `keep` diff --git a/src/index.ts b/src/index.ts index 38cb6a5..d1902b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,10 @@ const createNameConverter = (convention: NamingConvention) => (value: string) => } }; -const toMockName = (name: string) => { +const toMockName = (name: string, prefix?: string) => { + if (prefix) { + return `${prefix}${name}`; + } const isVowel = name.match(/^[AEIO]/); return isVowel ? `an${name}` : `a${name}`; }; @@ -59,6 +62,7 @@ const getNamedType = ( types: TypeItem[], typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, + prefix?: string, namedType?: NamedTypeNode, ): string | number | boolean => { if (!namedType) { @@ -98,6 +102,7 @@ const getNamedType = ( types, typenamesConvention, enumValuesConvention, + prefix, foundType.types && foundType.types[0], ); case 'scalar': @@ -108,7 +113,7 @@ const getNamedType = ( throw `foundType is unknown: ${foundType.name}: ${foundType.type}`; } } - return `${toMockName(name)}()`; + return `${toMockName(name, prefix)}()`; } } }; @@ -119,6 +124,7 @@ const generateMockValue = ( types: TypeItem[], typenamesConvention: NamingConvention, enumValuesConvention: NamingConvention, + prefix: string | undefined, currentType: TypeNode, ): string | number | boolean => { switch (currentType.kind) { @@ -129,6 +135,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + prefix, currentType as NamedTypeNode, ); case 'NonNullType': @@ -138,6 +145,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + prefix, currentType.type, ); case 'ListType': { @@ -147,6 +155,7 @@ const generateMockValue = ( types, typenamesConvention, enumValuesConvention, + prefix, currentType.type, ); return `[${value}]`; @@ -159,11 +168,12 @@ const getMockString = ( fields: string, typenamesConvention: NamingConvention, addTypename = false, + prefix, ) => { const casedName = createNameConverter(typenamesConvention)(typeName); const typename = addTypename ? `\n __typename: '${casedName}',` : ''; return ` -export const ${toMockName(casedName)} = (overrides?: Partial<${casedName}>): ${casedName} => { +export const ${toMockName(casedName, prefix)} = (overrides?: Partial<${casedName}>): ${casedName} => { return {${typename} ${fields} }; @@ -175,6 +185,7 @@ export interface TypescriptMocksPluginConfig { enumValues?: NamingConvention; typenames?: NamingConvention; addTypename?: boolean; + prefix?: string; } interface TypeItem { @@ -230,6 +241,7 @@ export const plugin: PluginFunction = (schema, docu types, typenamesConvention, enumValuesConvention, + config.prefix, node.type, ); @@ -252,6 +264,7 @@ export const plugin: PluginFunction = (schema, docu types, typenamesConvention, enumValuesConvention, + config.prefix, field.type, ); @@ -260,7 +273,7 @@ export const plugin: PluginFunction = (schema, docu .join('\n') : ''; - return getMockString(fieldName, mockFields, typenamesConvention, false); + return getMockString(fieldName, mockFields, typenamesConvention, false, config.prefix); }, }; }, @@ -278,7 +291,13 @@ export const plugin: PluginFunction = (schema, docu mockFn: () => { const mockFields = fields ? fields.map(({ mockFn }: any) => mockFn(typeName)).join('\n') : ''; - return getMockString(typeName, mockFields, typenamesConvention, !!config.addTypename); + return getMockString( + typeName, + mockFields, + typenamesConvention, + !!config.addTypename, + config.prefix, + ); }, }; }, diff --git a/tests/__snapshots__/typescript-mock-data.spec.ts.snap b/tests/__snapshots__/typescript-mock-data.spec.ts.snap index e6c94b0..908a141 100644 --- a/tests/__snapshots__/typescript-mock-data.spec.ts.snap +++ b/tests/__snapshots__/typescript-mock-data.spec.ts.snap @@ -1,5 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should add custom prefix if the \`prefix\` config option is specified 1`] = ` +" +export const mockAbcType = (overrides?: Partial): AbcType => { + return { + abc: overrides && overrides.hasOwnProperty('abc') ? overrides.abc! : 'sit', + }; +}; + +export const mockAvatar = (overrides?: Partial): Avatar => { + return { + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '0550ff93-dd31-49b4-8c38-ff1cb68bdc38', + url: overrides && overrides.hasOwnProperty('url') ? overrides.url! : 'aliquid', + }; +}; + +export const mockUpdateUserInput = (overrides?: Partial): 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! : mockAvatar(), + }; +}; + +export const mockUser = (overrides?: Partial): 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! : mockAvatar(), + 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', + }; +}; +" +`; + exports[`should generate mock data functions 1`] = ` " export const anAbcType = (overrides?: Partial): AbcType => { diff --git a/tests/typescript-mock-data.spec.ts b/tests/typescript-mock-data.spec.ts index 58460f5..1a7d02b 100644 --- a/tests/typescript-mock-data.spec.ts +++ b/tests/typescript-mock-data.spec.ts @@ -168,3 +168,12 @@ it('should generate mock data with as-is types and enums if typenames is "keep"' expect(result).not.toMatch(/ABC(TYPE|STATUS)/); expect(result).toMatchSnapshot(); }); + +it('should add custom prefix if the `prefix` config option is specified', async () => { + const result = await plugin(testSchema, [], { prefix: 'mock' }); + + expect(result).toBeDefined(); + expect(result).toMatch(/const mockUser/); + expect(result).not.toMatch(/const aUser/); + expect(result).toMatchSnapshot(); +});