Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/famous-rats-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/typescript-operations': patch
---

Fix types when introspection is used and values are not generated or imported correctly
53 changes: 43 additions & 10 deletions packages/plugins/typescript/operations/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { concatAST, GraphQLSchema, type DocumentNode } from 'graphql';
import {
concatAST,
GraphQLSchema,
parse,
printIntrospectionSchema,
type DocumentNode,
} from 'graphql';
import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
import { optimizeOperations } from '@graphql-codegen/visitor-plugin-common';
Expand Down Expand Up @@ -61,8 +67,7 @@ export const plugin: PluginFunction<
leave: visitor,
});

const operationsDefinitions = operationsResult.definitions;

const operationsDefinitions: string[] = operationsResult.definitions;
if (config.addOperationExport) {
for (const d of allDocumentsAST.definitions) {
if ('name' in d) {
Expand All @@ -73,15 +78,34 @@ export const plugin: PluginFunction<
}
}

// #region generateSchemaTypes
// When Input and Enum appear in Result selection sets, we need to
// generate those types so they can be referred to correctly
const schemaTypes = oldVisit(transformSchemaAST(schema, config).ast, { leave: visitor });
const schemaTypesDefinitions = findTransformedDefinitions(schemaTypes);
// #endregion

// #region generateIntrospectionTypesDefinitions
// It is possible for queries to refer to enums in introspection:
// - `__TypeKind`
// - `__DirectiveOperation`
//
// In such cases, we need to generate the used introspection types
// so the Result types can refer to them correctly (similar to how we do schema types)
let introspectionTypesDefinitions: string[] = [];
if (visitor.shouldVisitIntrospectionTypes()) {
const introspectionTypes = oldVisit(parse(printIntrospectionSchema(schema)), {
leave: visitor,
});
introspectionTypesDefinitions = findTransformedDefinitions(introspectionTypes);
}
// #endregion

// 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 schemaTypesDefinitions = schemaTypes.definitions.filter(def => typeof def === 'string');

let content = [...schemaTypesDefinitions, ...operationsDefinitions].join('\n');
let content = [
...schemaTypesDefinitions,
...introspectionTypesDefinitions,
...operationsDefinitions,
].join('\n');

if (config.globalNamespace) {
content = `
Expand Down Expand Up @@ -116,3 +140,12 @@ const semanticToStrict = async (schema: GraphQLSchema): Promise<GraphQLSchema> =
);
}
};

// IMPORTANT: when a visitor leaves a node with no transformation logic,
// It will leave the node as an object.
//
// This helper function filters 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 findTransformedDefinitions = (visitedResult: any): string[] => {
return visitedResult.definitions.filter(def => typeof def === 'string');
};
20 changes: 19 additions & 1 deletion packages/plugins/typescript/operations/src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import autoBind from 'auto-bind';
import {
EnumTypeDefinitionNode,
getNamedType,
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLScalarType,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
isEnumType,
isIntrospectionType,
Kind,
TypeInfo,
visit,
visitWithTypeInfo,
type DocumentNode,
type EnumTypeDefinitionNode,
type FragmentDefinitionNode,
type GraphQLNamedInputType,
type GraphQLSchema,
Expand Down Expand Up @@ -80,6 +81,14 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
> {
protected _usedSchemaTypes: UsedSchemaTypes = {};
protected _needsExactUtilityType: boolean = false;
/**
* _usedEnumIntrospectionType is a metadata value
* which tracks whether an introspection type enum (i.e. __TypeKind or __DirectiveLocation)
* has been referred to in selection sets
*
* If it is, we need to generate the enum values from introspection types
*/
protected _usedEnumIntrospectionType: boolean = false;
private _outputPath: string;

constructor(
Expand Down Expand Up @@ -687,6 +696,11 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
node: namedType,
tsType: this.convertName(namedType.name),
};

if (isIntrospectionType(namedType)) {
this._usedEnumIntrospectionType = true;
}

return;
}

Expand Down Expand Up @@ -739,6 +753,10 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
// 2. In Client Preset, it is used by fragment-masking.ts, so it needs `export`
return `${internalUtilityTypeWarning}export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };`;
}

shouldVisitIntrospectionTypes(): boolean {
return this._usedEnumIntrospectionType;
}
}

const internalUtilityTypeWarning = '/** Internal type. DO NOT USE DIRECTLY. */\n';
Expand Down
Loading
Loading