UNPKG

@graphql-codegen/typescript-operations

Version:

GraphQL Code Generator plugin for generating TypeScript types for GraphQL queries, mutations, subscriptions and fragments

548 lines (547 loc) • 27.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeScriptDocumentsVisitor = void 0; const tslib_1 = require("tslib"); const auto_bind_1 = tslib_1.__importDefault(require("auto-bind")); const graphql_1 = require("graphql"); const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers"); const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common"); const ts_operation_variables_to_object_js_1 = require("./ts-operation-variables-to-object.js"); class TypeScriptDocumentsVisitor extends visitor_plugin_common_1.BaseDocumentsVisitor { _usedSchemaTypes = {}; _needsExactUtilityType = false; _outputPath; constructor(schema, config, documentNode, outputPath) { super(config, { arrayInputCoercion: (0, visitor_plugin_common_1.getConfigValue)(config.arrayInputCoercion, true), noExport: (0, visitor_plugin_common_1.getConfigValue)(config.noExport, false), immutableTypes: (0, visitor_plugin_common_1.getConfigValue)(config.immutableTypes, false), nonOptionalTypename: (0, visitor_plugin_common_1.getConfigValue)(config.nonOptionalTypename, false), mergeFragmentTypes: (0, visitor_plugin_common_1.getConfigValue)(config.mergeFragmentTypes, false), allowUndefinedQueryVariables: (0, visitor_plugin_common_1.getConfigValue)(config.allowUndefinedQueryVariables, false), enumType: (0, visitor_plugin_common_1.getConfigValue)(config.enumType, 'string-literal'), ignoreEnumValuesFromSchema: (0, visitor_plugin_common_1.getConfigValue)(config.ignoreEnumValuesFromSchema, false), futureProofEnums: (0, visitor_plugin_common_1.getConfigValue)(config.futureProofEnums, false), maybeValue: (0, visitor_plugin_common_1.getConfigValue)(config.maybeValue, 'T | null'), inputMaybeValue: (0, visitor_plugin_common_1.getConfigValue)(config.inputMaybeValue, 'T | null | undefined'), }, schema); this.config.enumValues = (0, visitor_plugin_common_1.parseEnumValues)({ schema, mapOrStr: config.enumValues, ignoreEnumValuesFromSchema: config.ignoreEnumValuesFromSchema, naming: { convert: this.config.convert, options: { typesPrefix: this.config.typesPrefix, typesSuffix: this.config.typesSuffix, useTypesPrefix: this.config.enumPrefix, useTypesSuffix: this.config.enumSuffix, }, }, }); this._outputPath = outputPath; (0, auto_bind_1.default)(this); const allFragments = [ ...documentNode.definitions.filter(d => d.kind === graphql_1.Kind.FRAGMENT_DEFINITION).map(fragmentDef => ({ node: fragmentDef, name: fragmentDef.name.value, onType: fragmentDef.typeCondition.name.value, isExternal: false, })), ...(config.externalFragments || []), ]; // Create a combined document that includes operations, internal and external fragments for enum collection const documentWithAllFragments = { ...documentNode, definitions: [ ...documentNode.definitions.filter(d => d.kind !== graphql_1.Kind.FRAGMENT_DEFINITION), ...allFragments.map(f => f.node), ], }; this._usedSchemaTypes = this.collectUsedSchemaTypesToGenerate({ schema, documentNode: documentWithAllFragments, }); const processorConfig = { namespacedImportName: this.config.namespacedImportName, convertName: this.convertName.bind(this), enumPrefix: this.config.enumPrefix, enumSuffix: this.config.enumSuffix, scalars: this.scalars, formatNamedField: ({ name, isOptional }) => { return (this.config.immutableTypes ? `readonly ${name}` : name) + (isOptional ? '?' : ''); }, wrapTypeWithModifiers: (baseType, type) => { return (0, visitor_plugin_common_1.wrapTypeWithModifiers)(baseType, type, { wrapOptional: type => (0, visitor_plugin_common_1.printTypeScriptMaybeType)({ type, pattern: this.config.maybeValue, }), wrapArray: type => { const listModifier = this.config.immutableTypes ? 'ReadonlyArray' : 'Array'; return `${listModifier}<${type}>`; }, }); }, printFieldsOnNewLines: this.config.printFieldsOnNewLines, }; this.setSelectionSetHandler(new visitor_plugin_common_1.SelectionSetToObject(new visitor_plugin_common_1.PreResolveTypesProcessor(processorConfig), this.scalars, this.schema, this.convertName.bind(this), this.getFragmentSuffix.bind(this), allFragments, this.config)); const enumsNames = Object.keys(schema.getTypeMap()).filter(typeName => (0, graphql_1.isEnumType)(schema.getType(typeName))); this.setVariablesTransformer(new ts_operation_variables_to_object_js_1.TypeScriptOperationVariablesToObject({ avoidOptionals: this.config.avoidOptionals, immutableTypes: this.config.immutableTypes, inputMaybeValue: this.config.inputMaybeValue, }, this.scalars, this.convertName.bind(this), this.config.namespacedImportName, enumsNames, this.config.enumPrefix, this.config.enumSuffix, this.config.enumValues, this.config.arrayInputCoercion)); this._declarationBlockConfig = { ignoreExport: this.config.noExport, enumNameValueSeparator: ' =', }; } EnumTypeDefinition(node) { const enumName = node.name.value; if (!this._usedSchemaTypes[enumName] || // If not used... this.config.importSchemaTypesFrom // ... Or, is imported from a shared file ) { return null; // ... then, don't generate in this file } return (0, visitor_plugin_common_1.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, options: { typesPrefix: this.config.typesPrefix, typesSuffix: this.config.typesSuffix, useTypesPrefix: this.config.enumPrefix, useTypesSuffix: this.config.enumSuffix, }, }, }); } InputObjectTypeDefinition(node) { const inputTypeName = node.name.value; if (!this._usedSchemaTypes[inputTypeName] || // If not used... this.config.importSchemaTypesFrom // ... Or, is imported from a shared file ) { return null; // ... then, don't generate in this file } // Note: we usually don't need to export this type, // however, it's not possible to know if another file is using this type e.g. using `importSchemaTypesFrom`, // so it's better export the types. if ((0, visitor_plugin_common_1.isOneOfInputObjectType)(this._schema.getType(inputTypeName))) { return new visitor_plugin_common_1.DeclarationBlock(this._declarationBlockConfig) .export() .asKind(this.config.declarationKind.input) .withName(this.convertName(node)) .withComment(node.description?.value) .withContent(`\n` + (node.fields || []).join('\n |')).string; } return new visitor_plugin_common_1.DeclarationBlock(this._declarationBlockConfig) .export() .asKind(this.config.declarationKind.input) .withName(this.convertName(node)) .withComment(node.description?.value) .withBlock((node.fields || []).join('\n')).string; } InputValueDefinition(node, _key, _parent, _path, ancestors) { const oneOfDetails = parseOneOfInputValue({ node, schema: this._schema, ancestors, }); // 1. Flatten GraphQL type nodes to make it easier to turn into string // GraphQL type nodes may have `NonNullType` type before each `ListType` or `NamedType` // This make it a bit harder to know whether a `ListType` or `Namedtype` is nullable without looking at the node before it. // Flattening it into an array where the nullability is in `ListType` and `NamedType` makes it easier to code, // // So, we recursively call `collectAndFlattenTypeNodes` to handle the following scenarios: // - [Thing] // - [Thing!] // - [Thing]! // - [Thing!]! const typeNodes = []; collectAndFlattenTypeNodes({ currentTypeNode: node.type, isPreviousNodeNonNullable: oneOfDetails.isOneOfInputValue, // If the InputValue is part of @oneOf input, we treat it as non-null (even if it must be null in the schema) typeNodes, }); // 2. Generate the type of a TypeScript field declaration // e.g. `field?: string`, then the `string` is the `typePart` let typePart = ''; // We call `.reverse()` here to get the base type node first for (const typeNode of typeNodes.reverse()) { if (typeNode.type === 'NamedType') { const usedInputType = this._usedSchemaTypes[typeNode.name]; if (!usedInputType) { continue; } typePart = usedInputType.tsType; // If the schema is correct, when reversing typeNodes, the first node would be `NamedType`, which means we can safely set it as the base for typePart if (!typeNode.isNonNullable) { typePart = (0, visitor_plugin_common_1.printTypeScriptMaybeType)({ type: typePart, pattern: this.config.inputMaybeValue, }); } continue; } if (typeNode.type === 'ListType') { typePart = `Array<${typePart}>`; if (!typeNode.isNonNullable) { typePart = (0, visitor_plugin_common_1.printTypeScriptMaybeType)({ type: typePart, pattern: this.config.inputMaybeValue, }); } } } const addOptionalSign = !oneOfDetails.isOneOfInputValue && !this.config.avoidOptionals.inputValue && (node.type.kind !== graphql_1.Kind.NON_NULL_TYPE || (!this.config.avoidOptionals.defaultValue && node.defaultValue !== undefined)); // 3. Generate the keyPart of the TypeScript field declaration // e.g. `field?: string`, then the `field?` is the `keyPart` const keyPart = `${node.name.value}${addOptionalSign ? '?' : ''}`; // 4. other parts of TypeScript field declaration const commentPart = (0, visitor_plugin_common_1.getNodeComment)(node); const readonlyPart = this.config.immutableTypes ? 'readonly ' : ''; const currentInputValue = commentPart + (0, visitor_plugin_common_1.indent)(`${readonlyPart}${keyPart}: ${typePart};`); // 5. Check if field is part of `@oneOf` input type // If yes, we must generate a union member where the current inputValue must be provieded, and the others are not // e.g. // ```graphql // input UserInput { // byId: ID // byEmail: String // byLegacyId: ID // } // ``` // // Then, the generated type is: // ```ts // type UserInput = // | { byId: string | number; byEmail?: never; byLegacyId?: never } // | { byId?: never; byEmail: string; byLegacyId?: never } // | { byId?: never; byEmail?: never; byLegacyId: string | number } // ``` if (oneOfDetails.isOneOfInputValue) { const fieldParts = []; for (const fieldName of Object.keys(oneOfDetails.parentType.getFields())) { if (fieldName === node.name.value) { fieldParts.push(currentInputValue); continue; } fieldParts.push(`${readonlyPart}${fieldName}?: never;`); } return (0, visitor_plugin_common_1.indent)(`{ ${fieldParts.join(' ')} }`); } // If field is not part of @oneOf input type, then it's a input value, just return as-is return currentInputValue; } getImports() { return !this.config.globalNamespace && (this.config.inlineFragmentTypes === 'combine' || this.config.inlineFragmentTypes === 'mask') ? this.config.fragmentImports.map(fragmentImport => (0, visitor_plugin_common_1.generateFragmentImportStatement)(fragmentImport, 'type')) : []; } getExternalSchemaTypeImports() { if (!this.config.importSchemaTypesFrom) { return []; } const hasTypesToImport = Object.values(this._usedSchemaTypes).filter(value => value.type === 'GraphQLEnumType' || value.type === 'GraphQLInputObjectType').length > 0; if (!hasTypesToImport) { return []; } return [ (0, visitor_plugin_common_1.generateImportStatement)({ baseDir: process.cwd(), baseOutputDir: '', outputPath: this._outputPath, importSource: { path: this.config.importSchemaTypesFrom, namespace: this.config.namespacedImportName, identifiers: [], }, typesImport: true, emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports, importExtension: (0, plugin_helpers_1.normalizeImportExtension)({ emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports, importExtension: this.config.importExtension, }), }), ]; } getEnumsImports() { const usedEnumMap = {}; for (const [enumName, enumDetails] of Object.entries(this.config.enumValues)) { if (this._usedSchemaTypes[enumName]) { usedEnumMap[enumName] = enumDetails; } } return (0, visitor_plugin_common_1.getEnumsImports)({ enumValues: usedEnumMap, useTypeImports: this.config.useTypeImports, }); } getScalarsImports() { const fileType = this.config.importSchemaTypesFrom ? 'multi-file-operation-file' : this.config.generateOperationTypes ? 'single-file-operation-file' : 'multi-file-shared-type-file'; const imports = {}; for (const [scalarName, parsedScalar] of Object.entries(this.config.scalars)) { const usedScalar = this._usedSchemaTypes[scalarName]; if (!usedScalar || usedScalar.type !== 'GraphQLScalarType') { continue; } if (parsedScalar.input.isExternal && (((usedScalar.useCases.input || usedScalar.useCases.variables) && fileType === 'single-file-operation-file') || (usedScalar.useCases.input && fileType === 'multi-file-shared-type-file') || (usedScalar.useCases.variables && fileType === 'multi-file-operation-file'))) { imports[parsedScalar.input.source] ||= { identifiers: {} }; imports[parsedScalar.input.source].identifiers[parsedScalar.input.import] = { asDefault: parsedScalar.input.default, }; } if (parsedScalar.output.isExternal && usedScalar.useCases.output && (fileType === 'single-file-operation-file' || fileType === 'multi-file-operation-file')) { imports[parsedScalar.output.source] ||= { identifiers: {} }; imports[parsedScalar.output.source].identifiers[parsedScalar.output.import] = { asDefault: parsedScalar.output.default, }; } } return Object.entries(imports).reduce((res, [importSource, importParams]) => { // One import statement cannot have multiple defaults. // So: // - split each defaults into its own statements // - the named imports can all go together, tracked by `namedImports` const namedImports = []; for (const [identifier, identifierMetadata] of Object.entries(importParams.identifiers)) { if (identifierMetadata.asDefault) { res.push((0, visitor_plugin_common_1.buildTypeImport)({ identifier, source: importSource, asDefault: true, useTypeImports: this.config.useTypeImports, })); continue; } namedImports.push(identifier); } if (namedImports.length > 0) { res.push((0, visitor_plugin_common_1.buildTypeImport)({ identifier: namedImports.join(', '), source: importSource, asDefault: false, useTypeImports: this.config.useTypeImports, })); } return res; }, []); } getPunctuation(_declarationKind) { return ';'; } applyVariablesWrapper(variablesBlock, operationType) { const extraType = this.config.allowUndefinedQueryVariables && operationType === 'Query' ? ' | undefined' : ''; this._needsExactUtilityType = true; return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; } collectInnerTypesRecursively({ node, usedSchemaTypes, location, }) { if (node instanceof graphql_1.GraphQLEnumType) { if (usedSchemaTypes[node.name]) { return; } usedSchemaTypes[node.name] = { type: 'GraphQLEnumType', node, tsType: this.convertName(node.name), }; return; } if (node instanceof graphql_1.GraphQLScalarType) { const scalarType = usedSchemaTypes[node.name] || { type: 'GraphQLScalarType', node, tsType: (visitor_plugin_common_1.DEFAULT_INPUT_SCALARS[node.name]?.input || this.config.scalars?.[node.name]?.input.type) ?? 'unknown', useCases: { variables: location === 'variables', input: location === 'input', output: false, }, }; if (scalarType.type !== 'GraphQLScalarType') { throw new Error(`${node.name} has been incorrectly parsed as Scalar. This should not happen.`); } // ensure scalar's useCases is updated to have `useCases.input:true` or `useCases.variables:true`, depending on the use case // this is required because if the scalar has been parsed previously, it may only have `useCases.output:true`, and not `useCases.input:true` or `useCases.variables:true` if (location === 'input') { scalarType.useCases.input = true; } if (location === 'variables') { scalarType.useCases.variables = true; } usedSchemaTypes[node.name] = scalarType; return; } // GraphQLInputObjectType if (usedSchemaTypes[node.name]) { return; } usedSchemaTypes[node.name] = { type: 'GraphQLInputObjectType', node, tsType: this.convertName(node.name), }; const fields = node.getFields(); for (const field of Object.values(fields)) { const fieldType = (0, graphql_1.getNamedType)(field.type); this.collectInnerTypesRecursively({ node: fieldType, usedSchemaTypes, location: 'input', }); } } /** * @description collects schema types used in operations: * - used Enums for Variables * - used Scalars for Variables * - used Input for Variables (recursively) * * - used Enums for Result * - used Scalars for Result */ collectUsedSchemaTypesToGenerate({ schema, documentNode, }) { const schemaTypes = schema.getTypeMap(); const usedSchemaTypes = {}; // Collect input enums and input types (0, graphql_1.visit)(documentNode, { VariableDefinition: variableDefinitionNode => { (0, graphql_1.visit)(variableDefinitionNode, { NamedType: namedTypeNode => { const foundInputType = schemaTypes[namedTypeNode.name.value]; if (foundInputType && (foundInputType instanceof graphql_1.GraphQLInputObjectType || foundInputType instanceof graphql_1.GraphQLScalarType || foundInputType instanceof graphql_1.GraphQLEnumType) && !(0, visitor_plugin_common_1.isNativeNamedType)(foundInputType)) { this.collectInnerTypesRecursively({ node: foundInputType, usedSchemaTypes, location: 'variables', }); } }, }); }, }); // Collect output enums const typeInfo = new graphql_1.TypeInfo(schema); (0, graphql_1.visit)(documentNode, // AST doesn’t include field types (they are defined in the schema) - only names. // TypeInfo is a stateful helper that tracks typing context while walking the AST // visitWithTypeInfo wires that context into a visitor. (0, graphql_1.visitWithTypeInfo)(typeInfo, { Field: () => { const fieldType = typeInfo.getType(); if (fieldType) { const namedType = (0, graphql_1.getNamedType)(fieldType); if (namedType instanceof graphql_1.GraphQLEnumType) { usedSchemaTypes[namedType.name] = { type: 'GraphQLEnumType', node: namedType, tsType: this.convertName(namedType.name), }; return; } if (namedType instanceof graphql_1.GraphQLScalarType) { const scalarType = usedSchemaTypes[namedType.name] || { type: 'GraphQLScalarType', node: namedType, tsType: this.convertName(namedType.name), useCases: { variables: false, input: false, output: true }, }; if (scalarType.type !== 'GraphQLScalarType') { throw new Error(`${namedType.name} has been incorrectly parsed as Scalar. This should not happen.`); } // ensure scalar's useCases is updated to have `useCases.output:true` // this is required because if the scalar has been parsed previously, it may only have `useCases.input:true` or `useCases.variables:true`, not `useCases.output:true` scalarType.useCases.output = true; usedSchemaTypes[namedType.name] = scalarType; } } }, })); return usedSchemaTypes; } getExactUtilityType() { if (!this.config.generateOperationTypes || // 1. If we don't generate operation types, definitely do not need `Exact` !this._needsExactUtilityType // 2. Even if we generate operation types, we may not need `Exact` if there's no operations in the documents i.e. only fragments found ) { return null; } return `${internalUtilityTypeWarning}type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };`; } getIncrementalUtilityType() { if (!this.config.generateOperationTypes) { return null; } // Note: `export` here is important for 2 reasons // 1. It is not always used in the rest of the file, so this is a safe way to avoid lint rules (in tsconfig or eslint) complaining it's not used in the current file. // 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 };`; } } exports.TypeScriptDocumentsVisitor = TypeScriptDocumentsVisitor; const internalUtilityTypeWarning = '/** Internal type. DO NOT USE DIRECTLY. */\n'; function parseOneOfInputValue({ node, schema, ancestors, }) { const realParentDef = ancestors?.[ancestors.length - 1]; if (realParentDef) { const parentType = schema.getType(realParentDef.name.value); if ((0, visitor_plugin_common_1.isOneOfInputObjectType)(parentType)) { if (node.type.kind === graphql_1.Kind.NON_NULL_TYPE) { throw new Error('Fields on an input object type can not be non-nullable. It seems like the schema was not validated.'); } return { isOneOfInputValue: true, realParentDef, parentType }; } } return { isOneOfInputValue: false }; } function collectAndFlattenTypeNodes({ currentTypeNode, isPreviousNodeNonNullable, typeNodes, }) { if (currentTypeNode.kind === graphql_1.Kind.NON_NULL_TYPE) { const nextTypeNode = currentTypeNode.type; collectAndFlattenTypeNodes({ currentTypeNode: nextTypeNode, isPreviousNodeNonNullable: true, typeNodes, }); } else if (currentTypeNode.kind === graphql_1.Kind.LIST_TYPE) { typeNodes.push({ type: 'ListType', isNonNullable: isPreviousNodeNonNullable }); const nextTypeNode = currentTypeNode.type; collectAndFlattenTypeNodes({ currentTypeNode: nextTypeNode, isPreviousNodeNonNullable: false, typeNodes, }); } else if (currentTypeNode.kind === graphql_1.Kind.NAMED_TYPE) { typeNodes.push({ type: 'NamedType', isNonNullable: isPreviousNodeNonNullable, name: currentTypeNode.name.value, }); } }