Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-trees-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/typescript-operations': major
---

BREAKING CHANGE: typescript-operations plugin now generates enum if it is used in operation.
1 change: 1 addition & 0 deletions packages/plugins/typescript/operations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion packages/plugins/typescript/operations/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AvoidOptionalsConfig, RawDocumentsConfig } from '@graphql-codegen/visitor-plugin-common';
import { AvoidOptionalsConfig, 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.
Expand Down Expand Up @@ -336,4 +336,8 @@ export interface TypeScriptDocumentsPluginConfig extends RawDocumentsConfig {
nullability?: {
errorHandlingClient: boolean;
};

enumType?: 'string-literal' | 'native-numeric' | 'const' | 'native-const' | 'native';
enumValues?: EnumValuesMap;
futureProofEnums?: boolean;
}
47 changes: 24 additions & 23 deletions packages/plugins/typescript/operations/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -20,25 +21,11 @@ export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.Compl
: rawDocuments;
const allAst = concatAST(documents.map(v => 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 = [];
Expand All @@ -49,23 +36,37 @@ export const plugin: PluginFunction<TypeScriptDocumentsPluginConfig, Types.Compl
}
}

content = visitorResult.definitions.concat(exportConsts).join('\n');
operationsContent = operationsResult.definitions.concat(exportConsts).join('\n');
}

if (config.globalNamespace) {
content = `
operationsContent = `
declare global {
${content}
${operationsContent}
}`;
}

const schemaTypes = oldVisit(transformSchemaAST(schema, config).ast, { leave: visitor });

// IMPORTANT: when a visitor leaves a node with no transformation logic,
// It will leave the node as an object.
// Here, we filter in nodes that have been turned into strings, i.e. they have been transformed
// This way, we do not have to explicitly declare a method for every node type to convert them to null
const schemaTypesContent = schemaTypes.definitions.filter(def => 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<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };',
],
content,
content: content.join('\n'),
};
};

Expand Down
106 changes: 104 additions & 2 deletions packages/plugins/typescript/operations/src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import {
BaseDocumentsVisitor,
convertSchemaEnumToDeclarationBlockString,
DeclarationKind,
generateFragmentImportStatement,
getConfigValue,
LoadedFragment,
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';
Expand All @@ -25,13 +43,19 @@ export interface TypeScriptDocumentsParsedConfig extends ParsedDocumentsConfig {
noExport: boolean;
maybeValue: string;
allowUndefinedQueryVariables: boolean;
enumType: 'string-literal' | 'native-numeric' | 'const' | 'native-const' | 'native';
futureProofEnums: boolean;
enumValues: ParsedEnumValuesMap;
}

type UsedNamedInputTypes = Record<string, GraphQLNamedInputType>;

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,
{
Expand All @@ -43,6 +67,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
);
Expand Down Expand Up @@ -76,6 +107,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),
Expand Down Expand Up @@ -125,6 +170,31 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
};
}

EnumTypeDefinition(node: EnumTypeDefinitionNode): string {
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<string> {
return !this.config.globalNamespace &&
(this.config.inlineFragmentTypes === 'combine' || this.config.inlineFragmentTypes === 'mask')
Expand All @@ -142,4 +212,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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('TypeScript Operations Plugin - Standalone', () => {
input UsersInput {
from: DateTime
to: DateTime
role: UserRole
}

type UsersResponseOk {
Expand Down Expand Up @@ -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
Expand All @@ -87,6 +88,10 @@ describe('TypeScript Operations Plugin - Standalone', () => {

expect(result).toMatchInlineSnapshot(`
"type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type UserRole =
| 'ADMIN'
| 'CUSTOMER';

export type UserQueryVariables = Exact<{
id: string;
}>;
Expand All @@ -107,6 +112,7 @@ describe('TypeScript Operations Plugin - Standalone', () => {
export type UsersWithScalarInputQueryVariables = Exact<{
from: any;
to?: any | null;
role?: UserRole | null;
}>;


Expand Down Expand Up @@ -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
});
Comment on lines +167 to +175
Copy link
Collaborator Author

@eddeee888 eddeee888 Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests will be tested in a follow-up PR. I want to get the logic of detecting used input types into the feature branch first.

Loading