UNPKG

@graphql-codegen/typescript-operations

Version:

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

385 lines (384 loc) • 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeScriptDocumentsVisitor = void 0; const tslib_1 = require("tslib"); const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common"); const auto_bind_1 = tslib_1.__importDefault(require("auto-bind")); const graphql_1 = require("graphql"); const ts_operation_variables_to_object_js_1 = require("./ts-operation-variables-to-object.js"); class TypeScriptDocumentsVisitor extends visitor_plugin_common_1.BaseDocumentsVisitor { _usedNamedInputTypes = {}; _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), avoidOptionals: (0, visitor_plugin_common_1.normalizeAvoidOptionals)((0, visitor_plugin_common_1.getConfigValue)(config.avoidOptionals, 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'), enumValues: (0, visitor_plugin_common_1.parseEnumValues)({ schema, mapOrStr: config.enumValues, ignoreEnumValuesFromSchema: config.ignoreEnumValuesFromSchema, }), futureProofEnums: (0, visitor_plugin_common_1.getConfigValue)(config.futureProofEnums, false), }, schema); this._outputPath = outputPath; (0, auto_bind_1.default)(this); const defaultMaybeValue = 'T | null'; const maybeValue = (0, visitor_plugin_common_1.getConfigValue)(config.maybeValue, defaultMaybeValue); 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 || []), ]; this._usedNamedInputTypes = this.collectUsedInputTypes({ schema, documentNode }); 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 => maybeValue.replace('T', type), wrapArray: type => { const listModifier = this.config.immutableTypes ? 'ReadonlyArray' : 'Array'; return `${listModifier}<${type}>`; }, }); }, printFieldsOnNewLines: this.config.printFieldsOnNewLines, }; const processor = new visitor_plugin_common_1.PreResolveTypesProcessor(processorConfig); this.setSelectionSetHandler(new visitor_plugin_common_1.SelectionSetToObject(processor, 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(this.scalars, this.convertName.bind(this), // FIXME: this is the legacy avoidOptionals which was used to make Result fields non-optional. This use case is no longer valid. // It's also being used for Variables so people could already be using it. // Maybe it's better to deprecate and remove, to see what users think. this.config.avoidOptionals, this.config.immutableTypes, this.config.namespacedImportName, enumsNames, this.config.enumPrefix, this.config.enumSuffix, this.config.enumValues, this.config.arrayInputCoercion, undefined, undefined)); this._declarationBlockConfig = { ignoreExport: this.config.noExport, enumNameValueSeparator: ' =', }; } EnumTypeDefinition(node) { const enumName = node.name.value; if (!this._usedNamedInputTypes[enumName] || this.config.importSchemaTypesFrom) { return null; } 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, 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._usedNamedInputTypes[inputTypeName]) { return null; } if ((0, visitor_plugin_common_1.isOneOfInputObjectType)(this._schema.getType(inputTypeName))) { return new visitor_plugin_common_1.DeclarationBlock(this._declarationBlockConfig) .asKind('type') .withName(this.convertName(node)) .withComment(node.description?.value) .withContent(`\n` + (node.fields || []).join('\n |')).string; } return new visitor_plugin_common_1.DeclarationBlock(this._declarationBlockConfig) .asKind('type') .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._usedNamedInputTypes[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 (usedInputType.tsType !== 'any' && !typeNode.isNonNullable) { typePart += ' | null | undefined'; } continue; } if (typeNode.type === 'ListType') { typePart = `Array<${typePart}>`; if (!typeNode.isNonNullable) { typePart += ' | null | undefined'; } } } // TODO: eddeee888 check if we want to support `directiveArgumentAndInputFieldMappings` for operations // if (node.directives && this.config.directiveArgumentAndInputFieldMappings) { // typePart = // getDirectiveOverrideType({ // directives: node.directives, // directiveArgumentAndInputFieldMappings: this.config.directiveArgumentAndInputFieldMappings, // }) || typePart; // } 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.keys(this._usedNamedInputTypes).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, // FIXME: rebase with master for the new extension emitLegacyCommonJSImports: true, }), ]; } getPunctuation(_declarationKind) { return ';'; } applyVariablesWrapper(variablesBlock, operationType) { const extraType = this.config.allowUndefinedQueryVariables && operationType === 'Query' ? ' | undefined' : ''; return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; } collectInnerTypesRecursively(node, usedInputTypes) { if (usedInputTypes[node.name]) { return; } if (node instanceof graphql_1.GraphQLEnumType) { usedInputTypes[node.name] = { type: 'GraphQLEnumType', node, tsType: this.convertName(node.name), }; return; } if (node instanceof graphql_1.GraphQLScalarType) { usedInputTypes[node.name] = { type: 'GraphQLScalarType', node, tsType: (ts_operation_variables_to_object_js_1.SCALARS[node.name] || this.config.scalars?.[node.name]?.input.type) ?? 'any', }; return; } // GraphQLInputObjectType usedInputTypes[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(fieldType, usedInputTypes); } } collectUsedInputTypes({ schema, documentNode, }) { const schemaTypes = schema.getTypeMap(); const usedInputTypes = {}; // 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(foundInputType, usedInputTypes); } }, }); }, }); // 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 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) { usedInputTypes[namedType.name] = { type: 'GraphQLEnumType', node: namedType, tsType: this.convertName(namedType.name), }; } } }, })); return usedInputTypes; } getEnumsImports() { const usedEnumMap = {}; for (const [enumName, enumDetails] of Object.entries(this.config.enumValues)) { if (this._usedNamedInputTypes[enumName]) { usedEnumMap[enumName] = enumDetails; } } return (0, visitor_plugin_common_1.getEnumsImports)({ enumValues: usedEnumMap, useTypeImports: this.config.useTypeImports, }); } getExactUtilityType() { if (!this.config.generatesOperationTypes) { return null; } return 'type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };'; } getIncrementalUtilityType() { if (!this.config.generatesOperationTypes) { 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 "export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };"; } } exports.TypeScriptDocumentsVisitor = TypeScriptDocumentsVisitor; 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, }); } }