diff --git a/.changeset/curly-trees-lead.md b/.changeset/curly-trees-lead.md new file mode 100644 index 00000000000..e6aab78bdca --- /dev/null +++ b/.changeset/curly-trees-lead.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/typescript-operations': major +--- + +BREAKING CHANGE: typescript-operations plugin now generates enum if it is used in operation. diff --git a/dev-test/githunt/typed-document-nodes.ts b/dev-test/githunt/typed-document-nodes.ts index 4eed5751e4f..1de574e175e 100644 --- a/dev-test/githunt/typed-document-nodes.ts +++ b/dev-test/githunt/typed-document-nodes.ts @@ -169,6 +169,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.avoidOptionals.ts b/dev-test/githunt/types.avoidOptionals.ts index 66c403d7758..0d19fae050d 100644 --- a/dev-test/githunt/types.avoidOptionals.ts +++ b/dev-test/githunt/types.avoidOptionals.ts @@ -168,6 +168,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.d.ts b/dev-test/githunt/types.d.ts index b84825f3808..cf2dc2f2014 100644 --- a/dev-test/githunt/types.d.ts +++ b/dev-test/githunt/types.d.ts @@ -163,6 +163,18 @@ export type Vote = { /** The type of vote to record, when submitting a vote */ export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.enumsAsTypes.ts b/dev-test/githunt/types.enumsAsTypes.ts index b84825f3808..cf2dc2f2014 100644 --- a/dev-test/githunt/types.enumsAsTypes.ts +++ b/dev-test/githunt/types.enumsAsTypes.ts @@ -163,6 +163,18 @@ export type Vote = { /** The type of vote to record, when submitting a vote */ export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.flatten.preResolveTypes.ts b/dev-test/githunt/types.flatten.preResolveTypes.ts index 90348f33215..d41d3fc8780 100644 --- a/dev-test/githunt/types.flatten.preResolveTypes.ts +++ b/dev-test/githunt/types.flatten.preResolveTypes.ts @@ -168,6 +168,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.immutableTypes.ts b/dev-test/githunt/types.immutableTypes.ts index f708b3c3dce..42cf26e9d56 100644 --- a/dev-test/githunt/types.immutableTypes.ts +++ b/dev-test/githunt/types.immutableTypes.ts @@ -168,6 +168,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts index 29bff30b9dd..e54e8ab576f 100644 --- a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts @@ -31,6 +31,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.preResolveTypes.ts b/dev-test/githunt/types.preResolveTypes.ts index c63166be345..a6114cf2411 100644 --- a/dev-test/githunt/types.preResolveTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.ts @@ -168,6 +168,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.ts b/dev-test/githunt/types.ts index c63166be345..a6114cf2411 100644 --- a/dev-test/githunt/types.ts +++ b/dev-test/githunt/types.ts @@ -168,6 +168,18 @@ export enum VoteType { Up = 'UP', } +/** A list of options for the sort order of the feed */ +export type FeedType = + /** Sort by a combination of freshness and score, using Reddit's algorithm */ + | 'HOT' + /** Newest entries first */ + | 'NEW' + /** Highest score entries first */ + | 'TOP'; + +/** The type of vote to record, when submitting a vote */ +export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; + export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/star-wars/types.avoidOptionals.ts b/dev-test/star-wars/types.avoidOptionals.ts index 24b23e24f0c..96a96ea0b4d 100644 --- a/dev-test/star-wars/types.avoidOptionals.ts +++ b/dev-test/star-wars/types.avoidOptionals.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.excludeQueryAlpha.ts b/dev-test/star-wars/types.excludeQueryAlpha.ts index 40703b13094..7ccbc0a787c 100644 --- a/dev-test/star-wars/types.excludeQueryAlpha.ts +++ b/dev-test/star-wars/types.excludeQueryAlpha.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.excludeQueryBeta.ts b/dev-test/star-wars/types.excludeQueryBeta.ts index 5b38889e9ac..9f20261977e 100644 --- a/dev-test/star-wars/types.excludeQueryBeta.ts +++ b/dev-test/star-wars/types.excludeQueryBeta.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.globallyAvailable.d.ts b/dev-test/star-wars/types.globallyAvailable.d.ts index 2efbb00482e..0e376af86bc 100644 --- a/dev-test/star-wars/types.globallyAvailable.d.ts +++ b/dev-test/star-wars/types.globallyAvailable.d.ts @@ -238,6 +238,15 @@ type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.immutableTypes.ts b/dev-test/star-wars/types.immutableTypes.ts index 5849e8f6d04..bb485fff875 100644 --- a/dev-test/star-wars/types.immutableTypes.ts +++ b/dev-test/star-wars/types.immutableTypes.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts index d5430785c67..9597c939d8f 100644 --- a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts @@ -49,6 +49,15 @@ export type ReviewInput = { stars: Scalars['Int']['input']; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.preResolveTypes.ts b/dev-test/star-wars/types.preResolveTypes.ts index 5e82a72b7e0..99c11f7e757 100644 --- a/dev-test/star-wars/types.preResolveTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.skipSchema.ts b/dev-test/star-wars/types.skipSchema.ts index 5e82a72b7e0..99c11f7e757 100644 --- a/dev-test/star-wars/types.skipSchema.ts +++ b/dev-test/star-wars/types.skipSchema.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.ts b/dev-test/star-wars/types.ts index 5e82a72b7e0..99c11f7e757 100644 --- a/dev-test/star-wars/types.ts +++ b/dev-test/star-wars/types.ts @@ -240,6 +240,15 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; +/** The episodes in the Star Wars trilogy */ +export type Episode = + /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ + | 'EMPIRE' + /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ + | 'JEDI' + /** Star Wars Episode IV: A New Hope, released in 1977. */ + | 'NEWHOPE'; + export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/packages/plugins/other/visitor-plugin-common/src/convert-schema-enum-to-declaration-block-string.ts b/packages/plugins/other/visitor-plugin-common/src/convert-schema-enum-to-declaration-block-string.ts index 2e456b50df3..30279ebed3a 100644 --- a/packages/plugins/other/visitor-plugin-common/src/convert-schema-enum-to-declaration-block-string.ts +++ b/packages/plugins/other/visitor-plugin-common/src/convert-schema-enum-to-declaration-block-string.ts @@ -9,7 +9,7 @@ import { wrapWithSingleQuotes, } from './utils.js'; -interface ConvertSchemaEnumToDeclarationBlockString { +export interface ConvertSchemaEnumToDeclarationBlockString { schema: GraphQLSchema; node: EnumTypeDefinitionNode; enumName: string; diff --git a/packages/plugins/typescript/operations/package.json b/packages/plugins/typescript/operations/package.json index a890d6a6af8..2e1f8f8424c 100644 --- a/packages/plugins/typescript/operations/package.json +++ b/packages/plugins/typescript/operations/package.json @@ -15,6 +15,7 @@ "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/typescript": "^5.0.4", + "@graphql-codegen/schema-ast": "^5.0.0", "@graphql-codegen/visitor-plugin-common": "6.1.2", "auto-bind": "~4.0.0", "tslib": "~2.6.0" diff --git a/packages/plugins/typescript/operations/src/config.ts b/packages/plugins/typescript/operations/src/config.ts index ec83928eecc..44fb213b786 100644 --- a/packages/plugins/typescript/operations/src/config.ts +++ b/packages/plugins/typescript/operations/src/config.ts @@ -1,4 +1,9 @@ -import { AvoidOptionalsConfig, RawDocumentsConfig } from '@graphql-codegen/visitor-plugin-common'; +import { + AvoidOptionalsConfig, + type ConvertSchemaEnumToDeclarationBlockString, + type EnumValuesMap, + RawDocumentsConfig, +} from '@graphql-codegen/visitor-plugin-common'; /** * @description This plugin generates TypeScript types based on your GraphQLSchema _and_ your GraphQL operations and fragments. @@ -336,4 +341,121 @@ export interface TypeScriptDocumentsPluginConfig extends RawDocumentsConfig { nullability?: { errorHandlingClient: boolean; }; + + /** + * @description Controls the enum output type. Options: `string-literal` | `native-numeric` | `const` | `native-const` | `native`; + * @default `string-literal` + * + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli' + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file.ts': { + * plugins: ['typescript-operations'], + * config: { + * enumType: 'string-literal', + * } + * } + * } + * } + * export default config + */ + enumType?: ConvertSchemaEnumToDeclarationBlockString['outputType']; + + /** + * @description Overrides the default value of enum values declared in your GraphQL schema. + * You can also map the entire enum to an external type by providing a string that of `module#type`. + * + * @exampleMarkdown + * ## With Custom Values + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file': { + * // plugins... + * config: { + * enumValues: { + * MyEnum: { + * A: 'foo' + * } + * } + * }, + * }, + * }, + * }; + * export default config; + * ``` + * + * ## With External Enum + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file': { + * // plugins... + * config: { + * enumValues: { + * MyEnum: './my-file#MyCustomEnum', + * } + * }, + * }, + * }, + * }; + * export default config; + * ``` + * + * ## Import All Enums from a file + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file': { + * // plugins... + * config: { + * enumValues: { + * MyEnum: './my-file', + * } + * }, + * }, + * }, + * }; + * export default config; + * ``` + */ + enumValues?: EnumValuesMap; + + /** + * @description This option controls whether or not a catch-all entry is added to enum type definitions for values that may be added in the future. + * This is useful if you are using `relay`. + * @default false + * + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli' + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file.ts': { + * plugins: ['typescript-operations'], + * config: { + * futureProofEnums: true + * } + * } + * } + * } + * export default config + * ``` + */ + futureProofEnums?: boolean; } diff --git a/packages/plugins/typescript/operations/src/index.ts b/packages/plugins/typescript/operations/src/index.ts index 3b9cd42ddfa..8dd2c1b3439 100644 --- a/packages/plugins/typescript/operations/src/index.ts +++ b/packages/plugins/typescript/operations/src/index.ts @@ -1,6 +1,7 @@ import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; -import { LoadedFragment, optimizeOperations } from '@graphql-codegen/visitor-plugin-common'; -import { concatAST, FragmentDefinitionNode, GraphQLSchema, Kind } from 'graphql'; +import { transformSchemaAST } from '@graphql-codegen/schema-ast'; +import { optimizeOperations } from '@graphql-codegen/visitor-plugin-common'; +import { concatAST, GraphQLSchema } from 'graphql'; import { TypeScriptDocumentsPluginConfig } from './config.js'; import { TypeScriptDocumentsVisitor } from './visitor.js'; @@ -20,25 +21,11 @@ export const plugin: PluginFunction v.document)); - const allFragments: LoadedFragment[] = [ - ...(allAst.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION) as FragmentDefinitionNode[]).map( - fragmentDef => ({ - node: fragmentDef, - name: fragmentDef.name.value, - onType: fragmentDef.typeCondition.name.value, - isExternal: false, - }) - ), - ...(config.externalFragments || []), - ]; - - const visitor = new TypeScriptDocumentsVisitor(schema, config, allFragments); + const visitor = new TypeScriptDocumentsVisitor(schema, config, allAst); - const visitorResult = oldVisit(allAst, { - leave: visitor, - }); + const operationsResult = oldVisit(allAst, { leave: visitor }); - let content = visitorResult.definitions.join('\n'); + let operationsContent = operationsResult.definitions.join('\n'); if (config.addOperationExport) { const exportConsts = []; @@ -49,23 +36,37 @@ export const plugin: PluginFunction typeof def === 'string').join('\n'); + + const content: string[] = []; + if (schemaTypesContent) { + content.push(schemaTypesContent); + } + content.push(operationsContent); + return { prepend: [ ...visitor.getImports(), ...visitor.getGlobalDeclarations(visitor.config.noExport), 'type Exact = { [K in keyof T]: T[K] };', ], - content, + content: content.join('\n'), }; }; diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index f6121c0ce00..3e4ae45692b 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -1,5 +1,7 @@ import { BaseDocumentsVisitor, + type ConvertSchemaEnumToDeclarationBlockString, + convertSchemaEnumToDeclarationBlockString, DeclarationKind, generateFragmentImportStatement, getConfigValue, @@ -7,13 +9,30 @@ import { normalizeAvoidOptionals, NormalizedAvoidOptionalsConfig, ParsedDocumentsConfig, + type ParsedEnumValuesMap, + parseEnumValues, PreResolveTypesProcessor, SelectionSetProcessorConfig, SelectionSetToObject, wrapTypeWithModifiers, } from '@graphql-codegen/visitor-plugin-common'; import autoBind from 'auto-bind'; -import { GraphQLNamedType, GraphQLOutputType, GraphQLSchema, isEnumType, isNonNullType } from 'graphql'; +import { + type DocumentNode, + EnumTypeDefinitionNode, + type FragmentDefinitionNode, + GraphQLEnumType, + GraphQLInputObjectType, + type GraphQLNamedInputType, + type GraphQLNamedType, + type GraphQLOutputType, + GraphQLScalarType, + type GraphQLSchema, + isEnumType, + isNonNullType, + Kind, + visit, +} from 'graphql'; import { TypeScriptDocumentsPluginConfig } from './config.js'; import { TypeScriptOperationVariablesToObject } from './ts-operation-variables-to-object.js'; import { TypeScriptSelectionSetProcessor } from './ts-selection-set-processor.js'; @@ -25,13 +44,19 @@ export interface TypeScriptDocumentsParsedConfig extends ParsedDocumentsConfig { noExport: boolean; maybeValue: string; allowUndefinedQueryVariables: boolean; + enumType: ConvertSchemaEnumToDeclarationBlockString['outputType']; + futureProofEnums: boolean; + enumValues: ParsedEnumValuesMap; } +type UsedNamedInputTypes = Record; + export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< TypeScriptDocumentsPluginConfig, TypeScriptDocumentsParsedConfig > { - constructor(schema: GraphQLSchema, config: TypeScriptDocumentsPluginConfig, allFragments: LoadedFragment[]) { + protected _usedNamedInputTypes: UsedNamedInputTypes = {}; + constructor(schema: GraphQLSchema, config: TypeScriptDocumentsPluginConfig, documentNode: DocumentNode) { super( config, { @@ -43,6 +68,13 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< preResolveTypes: getConfigValue(config.preResolveTypes, true), mergeFragmentTypes: getConfigValue(config.mergeFragmentTypes, false), allowUndefinedQueryVariables: getConfigValue(config.allowUndefinedQueryVariables, false), + enumType: getConfigValue(config.enumType, 'string-literal'), + enumValues: parseEnumValues({ + schema, + mapOrStr: config.enumValues, + ignoreEnumValuesFromSchema: config.ignoreEnumValuesFromSchema, + }), + futureProofEnums: getConfigValue(config.futureProofEnums, false), } as TypeScriptDocumentsParsedConfig, schema ); @@ -76,6 +108,20 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< return (this.config.immutableTypes ? `readonly ${name}` : name) + (optional ? '?' : ''); }; + const allFragments: LoadedFragment[] = [ + ...(documentNode.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION) as FragmentDefinitionNode[]).map( + fragmentDef => ({ + node: fragmentDef, + name: fragmentDef.name.value, + onType: fragmentDef.typeCondition.name.value, + isExternal: false, + }) + ), + ...(config.externalFragments || []), + ]; + + this._usedNamedInputTypes = this.collectUsedInputTypes({ schema, documentNode }); + const processorConfig: SelectionSetProcessorConfig = { namespacedImportName: this.config.namespacedImportName, convertName: this.convertName.bind(this), @@ -125,6 +171,31 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< }; } + EnumTypeDefinition(node: EnumTypeDefinitionNode): string | null { + const enumName = node.name.value; + if (!this._usedNamedInputTypes[enumName]) { + return null; + } + + return convertSchemaEnumToDeclarationBlockString({ + schema: this._schema, + node, + declarationBlockConfig: this._declarationBlockConfig, + enumName, + enumValues: this.config.enumValues, + futureProofEnums: this.config.futureProofEnums, + ignoreEnumValuesFromSchema: this.config.ignoreEnumValuesFromSchema, + outputType: this.config.enumType, + naming: { + convert: this.config.convert, + typesPrefix: this.config.typesPrefix, + typesSuffix: this.config.typesSuffix, + useTypesPrefix: this.config.enumPrefix, + useTypesSuffix: this.config.enumSuffix, + }, + }); + } + public getImports(): Array { return !this.config.globalNamespace && (this.config.inlineFragmentTypes === 'combine' || this.config.inlineFragmentTypes === 'mask') @@ -142,4 +213,36 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< return `${prefix}Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; } + + private collectUsedInputTypes({ + schema, + documentNode, + }: { + schema: GraphQLSchema; + documentNode: DocumentNode; + }): UsedNamedInputTypes { + const schemaTypes = schema.getTypeMap(); + + const usedInputTypes: UsedNamedInputTypes = {}; + + visit(documentNode, { + VariableDefinition: variableDefinitionNode => { + visit(variableDefinitionNode, { + NamedType: namedTypeNode => { + const foundInputType = schemaTypes[namedTypeNode.name.value]; + if ( + foundInputType && + (foundInputType instanceof GraphQLInputObjectType || + foundInputType instanceof GraphQLScalarType || + foundInputType instanceof GraphQLEnumType) + ) { + usedInputTypes[namedTypeNode.name.value] = foundInputType; + } + }, + }); + }, + }); + + return usedInputTypes; + } } 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 50057f007c6..06dd1752443 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts @@ -37,6 +37,7 @@ describe('TypeScript Operations Plugin - Standalone', () => { input UsersInput { from: DateTime to: DateTime + role: UserRole } type UsersResponseOk { @@ -69,8 +70,8 @@ describe('TypeScript Operations Plugin - Standalone', () => { } } - query UsersWithScalarInput($from: DateTime!, $to: DateTime) { - users(input: { from: $from, to: $to }) { + query UsersWithScalarInput($from: DateTime!, $to: DateTime, $role: UserRole) { + users(input: { from: $from, to: $to, role: $role }) { ... on UsersResponseOk { result { __typename @@ -87,6 +88,10 @@ describe('TypeScript Operations Plugin - Standalone', () => { expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; + export type UserRole = + | 'ADMIN' + | 'CUSTOMER'; + export type UserQueryVariables = Exact<{ id: string; }>; @@ -107,6 +112,7 @@ describe('TypeScript Operations Plugin - Standalone', () => { export type UsersWithScalarInputQueryVariables = Exact<{ from: any; to?: any | null; + role?: UserRole | null; }>; @@ -157,3 +163,13 @@ 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 +});