diff --git a/.changeset/tidy-jobs-unite.md b/.changeset/tidy-jobs-unite.md new file mode 100644 index 00000000000..c4eb57e9cd1 --- /dev/null +++ b/.changeset/tidy-jobs-unite.md @@ -0,0 +1,9 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-operations': patch +'@graphql-codegen/typescript': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/client-preset': patch +--- + +Abstract how enum imports are generated into visitor-plugin-common package diff --git a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts index 68bb9328daa..b4d9a405e85 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts @@ -43,6 +43,7 @@ import { } from './utils.js'; import { OperationVariablesToObject } from './variables-to-object.js'; import { buildEnumValuesBlock } from './convert-schema-enum-to-declaration-block-string.js'; +import { buildTypeImport, getEnumsImports } from './imports.js'; export interface ParsedTypesConfig extends ParsedConfig { enumValues: ParsedEnumValuesMap; @@ -559,12 +560,24 @@ export class BaseTypesVisitor< const mappedValue = this.config.scalars[enumName]; if (mappedValue.input.isExternal) { - res.push(this._buildTypeImport(mappedValue.input.import, mappedValue.input.source, mappedValue.input.default)); + res.push( + buildTypeImport({ + identifier: mappedValue.input.import, + source: mappedValue.input.source, + asDefault: mappedValue.input.default, + useTypeImports: this.config.useTypeImports, + }) + ); } if (mappedValue.output.isExternal) { res.push( - this._buildTypeImport(mappedValue.output.import, mappedValue.output.source, mappedValue.output.default) + buildTypeImport({ + identifier: mappedValue.output.import, + source: mappedValue.output.source, + asDefault: mappedValue.output.default, + useTypeImports: this.config.useTypeImports, + }) ); } @@ -578,7 +591,12 @@ export class BaseTypesVisitor< const mappedValue = this.config.directiveArgumentAndInputFieldMappings[directive]; if (mappedValue.isExternal) { - return this._buildTypeImport(mappedValue.import, mappedValue.source, mappedValue.default); + return buildTypeImport({ + identifier: mappedValue.import, + source: mappedValue.source, + asDefault: mappedValue.default, + useTypeImports: this.config.useTypeImports, + }); } return null; @@ -811,58 +829,11 @@ export class BaseTypesVisitor< return ''; } - protected _buildTypeImport(identifier: string, source: string, asDefault = false): string { - const { useTypeImports } = this.config; - if (asDefault) { - if (useTypeImports) { - return `import type { default as ${identifier} } from '${source}';`; - } - return `import ${identifier} from '${source}';`; - } - return `import${useTypeImports ? ' type' : ''} { ${identifier} } from '${source}';`; - } - - protected handleEnumValueMapper( - typeIdentifier: string, - importIdentifier: string | null, - sourceIdentifier: string | null, - sourceFile: string | null - ): string[] { - if (importIdentifier !== sourceIdentifier) { - // use namespace import to dereference nested enum - // { enumValues: { MyEnum: './my-file#NS.NestedEnum' } } - return [ - this._buildTypeImport(importIdentifier || sourceIdentifier, sourceFile), - `import ${typeIdentifier} = ${sourceIdentifier};`, - ]; - } - if (sourceIdentifier !== typeIdentifier) { - return [this._buildTypeImport(`${sourceIdentifier} as ${typeIdentifier}`, sourceFile)]; - } - return [this._buildTypeImport(importIdentifier || sourceIdentifier, sourceFile)]; - } - public getEnumsImports(): string[] { - return Object.keys(this.config.enumValues) - .flatMap(enumName => { - const mappedValue = this.config.enumValues[enumName]; - - if (mappedValue.sourceFile) { - if (mappedValue.isDefault) { - return [this._buildTypeImport(mappedValue.typeIdentifier, mappedValue.sourceFile, true)]; - } - - return this.handleEnumValueMapper( - mappedValue.typeIdentifier, - mappedValue.importIdentifier, - mappedValue.sourceIdentifier, - mappedValue.sourceFile - ); - } - - return []; - }) - .filter(Boolean); + return getEnumsImports({ + enumValues: this.config.enumValues, + useTypeImports: this.config.useTypeImports, + }); } EnumTypeDefinition(node: EnumTypeDefinitionNode): string { diff --git a/packages/plugins/other/visitor-plugin-common/src/imports.ts b/packages/plugins/other/visitor-plugin-common/src/imports.ts index b9e4cbde6ba..1a04f48e7f9 100644 --- a/packages/plugins/other/visitor-plugin-common/src/imports.ts +++ b/packages/plugins/other/visitor-plugin-common/src/imports.ts @@ -1,5 +1,6 @@ import { dirname, isAbsolute, join, relative, resolve } from 'path'; import parse from 'parse-filepath'; +import type { ParsedEnumValuesMap } from './types'; export type ImportDeclaration = { outputPath: string; @@ -98,3 +99,88 @@ export function clearExtension(path: string): string { export function fixLocalFilePath(path: string): string { return path.startsWith('..') ? path : `./${path}`; } + +export function getEnumsImports({ + enumValues, + useTypeImports, +}: { + enumValues: ParsedEnumValuesMap; + useTypeImports: boolean; +}): string[] { + function handleEnumValueMapper({ + typeIdentifier, + importIdentifier, + sourceIdentifier, + sourceFile, + useTypeImports, + }: { + typeIdentifier: string; + importIdentifier: string | null; + sourceIdentifier: string | null; + sourceFile: string | null; + useTypeImports: boolean; + }): string[] { + if (importIdentifier !== sourceIdentifier) { + // use namespace import to dereference nested enum + // { enumValues: { MyEnum: './my-file#NS.NestedEnum' } } + return [ + buildTypeImport({ identifier: importIdentifier || sourceIdentifier, source: sourceFile, useTypeImports }), + `import ${typeIdentifier} = ${sourceIdentifier};`, + ]; + } + if (sourceIdentifier !== typeIdentifier) { + return [ + buildTypeImport({ identifier: `${sourceIdentifier} as ${typeIdentifier}`, source: sourceFile, useTypeImports }), + ]; + } + return [buildTypeImport({ identifier: importIdentifier || sourceIdentifier, source: sourceFile, useTypeImports })]; + } + + return Object.keys(enumValues) + .flatMap(enumName => { + const mappedValue = enumValues[enumName]; + if (mappedValue.sourceFile) { + if (mappedValue.isDefault) { + return [ + buildTypeImport({ + identifier: mappedValue.typeIdentifier, + source: mappedValue.sourceFile, + asDefault: true, + useTypeImports, + }), + ]; + } + + return handleEnumValueMapper({ + typeIdentifier: mappedValue.typeIdentifier, + importIdentifier: mappedValue.importIdentifier, + sourceIdentifier: mappedValue.sourceIdentifier, + sourceFile: mappedValue.sourceFile, + useTypeImports, + }); + } + + return []; + }) + .filter(Boolean); +} + +export function buildTypeImport({ + identifier, + source, + useTypeImports, + asDefault = false, +}: { + identifier: string; + source: string; + useTypeImports: boolean; + asDefault?: boolean; +}): string { + if (asDefault) { + if (useTypeImports) { + return `import type { default as ${identifier} } from '${source}';`; + } + return `import ${identifier} from '${source}';`; + } + return `import${useTypeImports ? ' type' : ''} { ${identifier} } from '${source}';`; +} diff --git a/packages/plugins/typescript/operations/src/index.ts b/packages/plugins/typescript/operations/src/index.ts index 8dd2c1b3439..3e1497bc8f2 100644 --- a/packages/plugins/typescript/operations/src/index.ts +++ b/packages/plugins/typescript/operations/src/index.ts @@ -63,6 +63,7 @@ export const plugin: PluginFunction = { [K in keyof T]: T[K] };', ], diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index 3e4ae45692b..406cf788237 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -5,6 +5,7 @@ import { DeclarationKind, generateFragmentImportStatement, getConfigValue, + getEnumsImports, LoadedFragment, normalizeAvoidOptionals, NormalizedAvoidOptionalsConfig, @@ -168,6 +169,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< ); this._declarationBlockConfig = { ignoreExport: this.config.noExport, + enumNameValueSeparator: ' =', }; } @@ -245,4 +247,11 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< return usedInputTypes; } + + public getEnumsImports(): string[] { + return getEnumsImports({ + enumValues: this.config.enumValues, + useTypeImports: this.config.useTypeImports, + }); + } } diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts new file mode 100644 index 00000000000..b00857d7364 --- /dev/null +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts @@ -0,0 +1,1350 @@ +import { mergeOutputs } from '@graphql-codegen/plugin-helpers'; +import { validateTs } from '@graphql-codegen/testing'; +import { buildSchema, parse } from 'graphql'; +import { plugin } from '../src/index.js'; + +describe('TypeScript Operations Plugin - Enum', () => { + it('does not generate enums if not used in variables and result', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], {})]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type MeQueryVariables = Exact<{ [key: string]: never; }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `native-numeric` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native-numeric' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + Admin = 0, + Customer = 1 + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `const` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + A_B_C + X_Y_Z + _TEST + My_Value + _123 + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'const' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export const UserRole = { + ABC: 'A_B_C', + XYZ: 'X_Y_Z', + Test: '_TEST', + MyValue: 'My_Value', + '123': '_123' + } as const; + + export type UserRole = typeof UserRole[keyof typeof UserRole]; + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `native-const` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + """ + Multiline comment test + """ + enum UserRole { + ADMIN + CUSTOMER @deprecated(reason: "Enum value CUSTOMER has been deprecated.") + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native-const' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + /** Multiline comment test */ + export const enum UserRole { + Admin = 'ADMIN', + /** @deprecated Enum value CUSTOMER has been deprecated. */ + Customer = 'CUSTOMER' + }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `native` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + Admin = 'ADMIN', + Customer = 'CUSTOMER' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` with `string-literal` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + A_B_C + X_Y_Z + _TEST + My_Value + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'string-literal', + enumValues: { + UserRole: { + A_B_C: 0, + X_Y_Z: 'Foo', + _TEST: 'Bar', + My_Value: 1, + }, + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserRole = + | 0 + | 'Foo' + | 'Bar' + | 1; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` with `const` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + A_B_C + X_Y_Z + _TEST + My_Value + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'const', + enumValues: { + UserRole: { + A_B_C: 0, + X_Y_Z: 'Foo', + _TEST: 'Bar', + My_Value: 1, + }, + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export const UserRole = { + ABC: 0, + XYZ: 'Foo', + Test: 'Bar', + MyValue: 1 + } as const; + + export type UserRole = typeof UserRole[keyof typeof UserRole]; + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` with `native` enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'native', + enumValues: { + UserRole: { + ADMIN: 0, + CUSTOMER: 'test', + }, + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + Admin = 0, + Customer = 'test' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` as file import', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumValues: { + UserRole: './my-file#MyEnum', + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import { MyEnum as UserRole } from './my-file'; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` with custom imported enum from namespace with different name', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumValues: { + UserRole: './my-file#NS.ETest', + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import { NS } from './my-file'; + import UserRole = NS.ETest; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` with custom imported enum from namespace with the same name', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumValues: { + UserRole: './my-file#NS.UserRole', + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import { NS } from './my-file'; + import UserRole = NS.UserRole; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` from a single file', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + enum UserStatus { + ACTIVE + PENDING + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!, $status: UserStatus!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'native', + enumValues: './my-file', + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import { UserRole } from './my-file'; + import { UserStatus } from './my-file'; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export { UserStatus }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + status: UserStatus; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles `enumValues` from a single file when specified as string', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + enum UserStatus { + ACTIVE + PENDING + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!, $status: UserStatus!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'native', + enumValues: { UserRole: './my-file#UserRole', UserStatus: './my-file#UserStatus2X' }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import { UserRole } from './my-file'; + import { UserStatus2X as UserStatus } from './my-file'; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export { UserStatus }; + + export type MeQueryVariables = Exact<{ + role: UserRole; + status: UserStatus; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('removes underscore from enum values', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + A_B_C + X_Y_Z + _TEST + My_Value + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + ABC = 'A_B_C', + XYZ = 'X_Y_Z', + Test = '_TEST', + MyValue = 'My_Value' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('keeps underscores in enum values when the value is only underscores', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + _ + __ + _TEST + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + _ = '_', + __ = '__', + Test = '_TEST' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('adds typesPrefix to enum when enumPrefix is true', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: true })]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type IUserRole = + | 'ADMIN' + | 'CUSTOMER'; + + export type IMeQueryVariables = Exact<{ + role: IUserRole; + }>; + + + export type IMeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('does not add typesPrefix to enum when enumPrefix is false', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: false })]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserRole = + | 'ADMIN' + | 'CUSTOMER'; + + export type IMeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type IMeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('adds typesSuffix to enum when enumSuffix is true', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: true })]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserRoleZ = + | 'ADMIN' + | 'CUSTOMER'; + + export type MeQueryVariablesZ = Exact<{ + role: UserRoleZ; + }>; + + + export type MeQueryZ = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('does not add typesSuffix to enum when enumSuffix is false', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: false })]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserRole = + | 'ADMIN' + | 'CUSTOMER'; + + export type MeQueryVariablesZ = Exact<{ + role: UserRole; + }>; + + + export type MeQueryZ = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('keeps enum value naming convention when namingConvention.enumValues is `keep`', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + namingConvention: { + typeNames: 'change-case-all#lowerCase', + enumValues: 'keep', + }, + }), + ]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type userrole = + | 'ADMIN' + | 'CUSTOMER'; + + export type mequeryvariables = Exact<{ + role: userrole; + }>; + + + export type mequery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('uses custom enum naming convention when namingConvention.enumValues is provided and enumType is native', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + enumType: 'native', + namingConvention: { + typeNames: 'keep', + enumValues: 'change-case-all#lowerCase', + }, + }), + ]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + admin = 'ADMIN', + customer = 'CUSTOMER' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('does not contain "export" when noExport is set to true', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + noExport: true, + }), + ]); + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + type UserRole = + | 'ADMIN' + | 'CUSTOMER'; + + type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); + + it('handles enumValues and named default import', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + typesPrefix: 'I', + namingConvention: { enumValues: 'change-case-all#constantCase' }, + enumValues: { + UserRole: './files#default as UserRole', + }, + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import UserRole from './files'; + type Exact = { [K in keyof T]: T[K] }; + export { UserRole }; + + export type IMeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type IMeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + }); + + it('enum members should be quoted if numeric when enumType is native', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + AXB + _1X2 + _3X4 + } + + scalar DateTime + `); + + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export enum UserRole { + Axb = 'AXB', + '1X2' = '_1X2', + '3X4' = '_3X4' + } + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + }); +}); + +describe('TypeScript Operations Plugin - Enum `%future added value`', () => { + it('adds `%future added value` to the type when enumType is `string-literal` and futureProofEnums is true', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + me: User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query Me($role: UserRole!) { + me { + id + } + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], { futureProofEnums: true })]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserRole = + | 'ADMIN' + | 'CUSTOMER' + | '%future added value'; + + export type MeQueryVariables = Exact<{ + role: UserRole; + }>; + + + export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); +}); diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts index 06dd1752443..f3106cf72a1 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts @@ -29,13 +29,19 @@ describe('TypeScript Operations Plugin - Standalone', () => { createdAt: DateTime! } + "UserRole Description" enum UserRole { + "UserRole ADMIN" ADMIN + "UserRole CUSTOMER" CUSTOMER } + "UsersInput Description" input UsersInput { + "UsersInput from" from: DateTime + "UsersInput to" to: DateTime role: UserRole } @@ -88,8 +94,11 @@ describe('TypeScript Operations Plugin - Standalone', () => { expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; + /** UserRole Description */ export type UserRole = + /** UserRole ADMIN */ | 'ADMIN' + /** UserRole CUSTOMER */ | 'CUSTOMER'; export type UserQueryVariables = Exact<{ @@ -163,13 +172,3 @@ describe('TypeScript Operations Plugin - Standalone', () => { `); }); }); - -describe('TypeScript Operations Plugin - Enum', () => { - it.todo('does not generate unused enum in variables and result'); - it.todo('handles native numeric enum correctly'); - it.todo('handles const enum correctly'); - it.todo('handles native const enum correctly'); - it.todo('handles native enum correctly'); - it.todo('handles EnumValues correctly'); - // Bring over tests from https://github.com/dotansimha/graphql-code-generator/blob/accdab69106605241933e9d66d64dc7077656f30/packages/plugins/typescript/typescript/tests/typescript.spec.ts -});