diff --git a/.changeset/strict-visitor-plugin-common-utils.md b/.changeset/strict-visitor-plugin-common-utils.md new file mode 100644 index 00000000000..8e487cec6d9 --- /dev/null +++ b/.changeset/strict-visitor-plugin-common-utils.md @@ -0,0 +1,26 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +--- + +Fix type errors in `visitor-plugin-common` (`utils`, `imports`, `naming`, `mappers`, `types`, `variables-to-object`, `convert-schema-enum-to-declaration-block-string`, `selection-set-processor/base` and `optimize-operations`) under `strict: true`: + +- `transformComment` accepts `undefined` and `null`, which it already handles by returning an empty string. +- `block` declares its param as `string[] | null | undefined`, which it already handles by returning an empty string. +- `DeclarationBlock`'s fields are typed as `string | null` (`_name` as `string | NameNode | null`), and `withBlock` accepts `null`, which renders the same as no block. +- `DeclarationBlock` falls back to returning the block unchanged when `blockTransformer` is passed as `undefined`, instead of throwing. +- `buildScalars` accepts an `undefined` scalars mapping, which it already handles by using only the defaults and the schema. +- `buildTypeImport` accepts a nullish `identifier` and `source`, and throws a descriptive error when either is missing. +- `ParsedEnumValuesMap`'s `mappedValues`, `sourceIdentifier`, `sourceFile` and `importIdentifier` accept `null`, which `parseEnumValues` already emits. +- `transformMappers` defaults `rawMappers` to `{}`. +- `OperationVariablesToObject.getName`, `OperationVariablesToObject.transform` and `BaseSelectionSetProcessor.buildSelectionSetFromStrings` may return `null`, which they already return. +- `buildEnumValuesBlock` throws a descriptive error when an enum value is not defined on the schema's enum. +- `optimizeOperations` throws a descriptive error when a document file has no parsed document. +- `unique`'s default key uses `String(item)`. +- `convertSchemaEnumToDeclarationBlockString` treats an enum node without `values` as empty instead of throwing. +- `generateFragmentImportStatement` treats a missing `identifiers` as empty. +- `getDeprecationReason` returns the default reason for a `@deprecated` directive without arguments. +- `isOneOfInputObjectType` returns `false` instead of `undefined`. +- `buildEnumValuesBlock` treats a non-enum schema type with the enum's name as having no schema enum. +- `OperationVariablesToObject` emits `''` instead of the text `undefined`/`null` when a variable has no name or no type identifier. + +Generated output is unchanged, except in the cases above. diff --git a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts index bdbb084864d..524cfb5b276 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts @@ -731,7 +731,7 @@ export class BaseTypesVisitor< ): string { if (this.config.onlyEnums) return ''; - const comment = transformComment(node.description?.value || '', 1); + const comment = transformComment(node.description?.value, 1); const { input } = this._parsedConfig.declarationKind; // `node.type` is already printed to a string by the visitor; removing the cast needs a visited-AST type for `node` 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 1e9a5875ebe..a0fbb4bc064 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 @@ -1,8 +1,8 @@ -import type { - EnumTypeDefinitionNode, - EnumValueDefinitionNode, - GraphQLEnumType, - GraphQLSchema, +import { + isEnumType, + type EnumTypeDefinitionNode, + type EnumValueDefinitionNode, + type GraphQLSchema, } from 'graphql'; import { convertName } from './naming.js'; import type { ConvertFn, ParsedEnumValuesMap } from './types.js'; @@ -75,7 +75,7 @@ export const convertSchemaEnumToDeclarationBlockString = ({ .withName(enumTypeName) .withContent( '\n' + - node.values + (node.values || []) .map(enumOption => { const name = enumOption.name.value; const enumValue: string | number = getValueFromConfig(name) ?? name; @@ -95,7 +95,7 @@ export const convertSchemaEnumToDeclarationBlockString = ({ .withName(enumTypeName) .asKind('enum') .withBlock( - node.values + (node.values || []) .map((enumOption, i) => { const valueFromConfig = getValueFromConfig(enumOption.name.value); const enumValue: string | number = valueFromConfig ?? i; @@ -130,7 +130,7 @@ export const convertSchemaEnumToDeclarationBlockString = ({ .withName(enumTypeName) .withComment(node.description?.value) .withBlock( - node.values + (node.values || []) .map(enumOption => { const optionName = makeValidEnumIdentifier( convertName({ @@ -164,7 +164,7 @@ export const convertSchemaEnumToDeclarationBlockString = ({ .withBlock( buildEnumValuesBlock({ typeName: enumName, - values: node.values, + values: node.values || [], schema, naming, ignoreEnumValuesFromSchema, @@ -189,9 +189,8 @@ export const buildEnumValuesBlock = ({ typeName: string; values: ReadonlyArray; }): string => { - const schemaEnumType: GraphQLEnumType | undefined = schema - ? (schema.getType(typeName) as GraphQLEnumType) - : undefined; + const schemaType = schema?.getType(typeName); + const schemaEnumType = isEnumType(schemaType) ? schemaType : undefined; return values .map(enumOption => { @@ -212,10 +211,16 @@ export const buildEnumValuesBlock = ({ }), ); const comment = getNodeComment(enumOption); - const schemaEnumValue = - schemaEnumType && !ignoreEnumValuesFromSchema - ? schemaEnumType.getValue(enumOption.name.value).value - : undefined; + let schemaEnumValue; + if (schemaEnumType && !ignoreEnumValuesFromSchema) { + const schemaEnumValueDefinition = schemaEnumType.getValue(enumOption.name.value); + if (!schemaEnumValueDefinition) { + throw new Error( + `Enum value "${enumOption.name.value}" is not defined on enum "${typeName}" in the schema`, + ); + } + schemaEnumValue = schemaEnumValueDefinition.value; + } let enumValue: string | number = typeof schemaEnumValue === 'undefined' ? enumOption.name.value : schemaEnumValue; diff --git a/packages/plugins/other/visitor-plugin-common/src/imports.ts b/packages/plugins/other/visitor-plugin-common/src/imports.ts index 260c4c35364..c2faef3f346 100644 --- a/packages/plugins/other/visitor-plugin-common/src/imports.ts +++ b/packages/plugins/other/visitor-plugin-common/src/imports.ts @@ -40,7 +40,7 @@ export function generateFragmentImportStatement( const { importSource: fragmentImportSource, ...rest } = statement; const { identifiers, path, namespace } = fragmentImportSource; const importSource: ImportSource = { - identifiers: identifiers + identifiers: (identifiers || []) .filter(fragmentImport => kind === 'both' || kind === fragmentImport.kind) .map(({ name }) => name), path, @@ -126,9 +126,9 @@ export function getEnumsImports({ useTypeImports, }: { typeIdentifierConverted: string; - importIdentifier: string | null; - sourceIdentifier: string | null; - sourceFile: string | null; + importIdentifier?: string | null; + sourceIdentifier?: string | null; + sourceFile?: string | null; useTypeImports: boolean; }): string[] { if (importIdentifier !== sourceIdentifier) { @@ -196,11 +196,17 @@ export function buildTypeImport({ useTypeImports, asDefault = false, }: { - identifier: string; - source: string; + identifier: string | null | undefined; + source: string | null | undefined; useTypeImports: boolean; asDefault?: boolean; }): string { + // `== null` (not a falsy check) so only a missing identifier or source throws; an empty string is still allowed + if (identifier == null || source == null) { + throw new Error( + `Cannot build a type import without an identifier and a source (identifier: ${identifier}, source: ${source})`, + ); + } if (asDefault) { if (useTypeImports) { return `import type { default as ${identifier} } from '${source}';`; diff --git a/packages/plugins/other/visitor-plugin-common/src/mappers.ts b/packages/plugins/other/visitor-plugin-common/src/mappers.ts index cb41e5c520a..6727f2c33d8 100644 --- a/packages/plugins/other/visitor-plugin-common/src/mappers.ts +++ b/packages/plugins/other/visitor-plugin-common/src/mappers.ts @@ -194,7 +194,7 @@ export function isExternalMapper(value: string): boolean { } export function transformMappers( - rawMappers: RawResolversConfig['mappers'], + rawMappers: RawResolversConfig['mappers'] = {}, mapperTypeSuffix?: string, ): ParsedResolversConfig['mappers'] { const result: ParsedResolversConfig['mappers'] = {}; diff --git a/packages/plugins/other/visitor-plugin-common/src/naming.ts b/packages/plugins/other/visitor-plugin-common/src/naming.ts index 19b053bf217..670392d945c 100644 --- a/packages/plugins/other/visitor-plugin-common/src/naming.ts +++ b/packages/plugins/other/visitor-plugin-common/src/naming.ts @@ -16,7 +16,7 @@ function getKind(node: ASTNode | string): keyof NamingConventionMap { return 'typeNames'; } -function getName(node: ASTNode | string): string | undefined { +function getName(node: ASTNode | string | undefined): string | undefined { if (node == null) { return undefined; } @@ -100,10 +100,11 @@ export function convertFactory(config: { namingConvention?: NamingConvention }): } if (typeof config.namingConvention === 'function') { + const namingConvention = config.namingConvention; return (str: string, opts: ConvertOptions = {}) => { return convertNameParts( str, - config.namingConvention as (str: string) => string, + namingConvention, getConfigValue(opts?.transformUnderscore, false), ); }; @@ -114,11 +115,11 @@ export function convertFactory(config: { namingConvention?: NamingConvention }): } if (typeof config.namingConvention === 'object') { - if (!config.namingConvention[type]) { + const namingConvention = config.namingConvention; + if (!namingConvention[type]) { return (str: string, opts: ConvertOptions = {}) => { const transformUnderscore = - (config.namingConvention as NamingConventionMap).transformUnderscore || - opts?.transformUnderscore; + namingConvention.transformUnderscore || opts?.transformUnderscore; return convertNameParts(str, pascalCase, getConfigValue(transformUnderscore, false)); }; } @@ -126,13 +127,13 @@ export function convertFactory(config: { namingConvention?: NamingConvention }): return (str: string, opts: ConvertOptions = {}) => { return convertNameParts( str, - resolveExternalModuleAndFn(config.namingConvention[type]), + resolveExternalModuleAndFn(namingConvention[type]), getConfigValue(opts?.transformUnderscore, true), ); }; } - return config.namingConvention[type] as any; + return config.namingConvention[type]; } return (node, opts) => { diff --git a/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts b/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts index 1d357490e78..01212b9d403 100644 --- a/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts +++ b/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts @@ -9,7 +9,14 @@ export function optimizeOperations( ): Types.DocumentFile[] { const newDocuments = optimizeDocuments( schema, - documents.map(s => s.document), + documents.map(s => { + if (!s.document) { + throw new Error( + `Cannot optimize the document at "${s.location}": it has no parsed document`, + ); + } + return s.document; + }), options, ); diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts index 1735f16c742..68afc3bddd2 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/base.ts @@ -46,7 +46,7 @@ export class BaseSelectionSetProcessor = export type ParsedEnumValuesMap = { [enumName: string]: { // If values are explictly set, this will include the mapped values - mappedValues?: { [valueName: string]: string | number }; + mappedValues?: { [valueName: string]: string | number } | null; // The GraphQL enum name typeIdentifier: string; // The GraphQL enum name after namingConvention conversion typeIdentifierConverted: string; // The actual identifier that you should use in the code (original or aliased) - sourceIdentifier?: string; + sourceIdentifier?: string | null; // In case of external enum, this will contain the source file path - sourceFile?: string; + sourceFile?: string | null; // If the identifier is external (imported) - this will contain the imported expression (including alias), otherwise null - importIdentifier?: string; + importIdentifier?: string | null; // Is defualt import is used to import the enum isDefault?: boolean; }; diff --git a/packages/plugins/other/visitor-plugin-common/src/utils.ts b/packages/plugins/other/visitor-plugin-common/src/utils.ts index e9ab1eff4f1..04238741dd5 100644 --- a/packages/plugins/other/visitor-plugin-common/src/utils.ts +++ b/packages/plugins/other/visitor-plugin-common/src/utils.ts @@ -57,7 +57,7 @@ export function quoteIfNeeded(array: string[], joinWith = ' & '): string { return `(${array.join(joinWith)})`; } -export function block(array) { +export function block(array: string[] | null | undefined): string { return array && array.length !== 0 ? '{\n' + array.join('\n') + '\n}' : ''; } @@ -107,7 +107,7 @@ export interface DeclarationBlockConfig { } export function transformComment( - comment: string | StringValueNode, + comment: string | StringValueNode | undefined | null, indentLevel = 0, disabled = false, ): string { @@ -129,14 +129,14 @@ export function transformComment( } export class DeclarationBlock { - _decorator = null; + _decorator: string | null = null; _export = false; - _name = null; - _kind = null; - _methodName = null; - _content = null; - _block = null; - _nameGenerics = null; + _name: string | NameNode | null = null; + _kind: string | null = null; + _methodName: string | null = null; + _content: string | null = null; + _block: string | null = null; + _nameGenerics: string | null = null; _comment: string | null = null; _ignoreBlockWrapper = false; @@ -189,7 +189,7 @@ export class DeclarationBlock { return this; } - withBlock(block: string): DeclarationBlock { + withBlock(block: string | null): DeclarationBlock { this._block = block; return this; @@ -210,6 +210,7 @@ export class DeclarationBlock { public get string(): string { let result = ''; + const blockTransformer = this._config.blockTransformer || (block => block); if (this._decorator) { result += this._decorator + '\n'; @@ -245,14 +246,14 @@ export class DeclarationBlock { const block = [before, this._block, after].filter(val => !!val).join('\n'); if (this._methodName) { - result += `${this._methodName}(${this._config.blockTransformer(block)})`; + result += `${this._methodName}(${blockTransformer(block)})`; } else { - result += this._config.blockTransformer(block); + result += blockTransformer(block); } } else if (this._content) { result += this._content; } else if (this._kind) { - result += this._config.blockTransformer('{}'); + result += blockTransformer('{}'); } return stripTrailingSpaces( @@ -293,7 +294,7 @@ export function buildScalarsFromConfig( export function buildScalars( schema: GraphQLSchema | undefined, - scalarsMapping: ScalarsMap, + scalarsMapping: ScalarsMap | undefined, defaultScalarsMapping: NormalizedScalarsMap = DEFAULT_SCALARS, defaultScalarType: string | null = 'unknown', ): ParsedScalarsMap { @@ -641,8 +642,11 @@ export function isOneOfInputObjectType( isOneOfType = isInputObjectType(namedType) && - ((namedType as unknown as Record<'isOneOf', boolean | undefined>).isOneOf || - namedType.astNode?.directives?.some(d => d.name.value === 'oneOf')); + // `isOneOf` only exists on GraphQLInputObjectType from graphql 16.9; removing this cast needs + // dropping support for older graphql versions + (((namedType as unknown as Record<'isOneOf', boolean | undefined>).isOneOf || + namedType.astNode?.directives?.some(d => d.name.value === 'oneOf')) ?? + false); isOneOfTypeCache.set(namedType, isOneOfType); @@ -666,7 +670,7 @@ export function flatten(array: Array>): Array { export function unique( array: Array, - key: (item: T) => string | number = item => item.toString(), + key: (item: T) => string | number = item => String(item), ): Array { return Object.values(array.reduce((acc, item) => ({ [key(item)]: item, ...acc }), {})); } @@ -752,7 +756,7 @@ export const getNodeComment = ( const getDeprecationReason = (directive: DirectiveNode): string | void => { if (directive.name.value === 'deprecated') { let reason = 'Field no longer supported'; - const deprecatedReason = directive.arguments[0]; + const deprecatedReason = directive.arguments?.[0]; if (deprecatedReason && deprecatedReason.value.kind === Kind.STRING) { reason = deprecatedReason.value.value; } diff --git a/packages/plugins/other/visitor-plugin-common/src/variables-to-object.ts b/packages/plugins/other/visitor-plugin-common/src/variables-to-object.ts index 86b14db2ed4..d1a7db133d9 100644 --- a/packages/plugins/other/visitor-plugin-common/src/variables-to-object.ts +++ b/packages/plugins/other/visitor-plugin-common/src/variables-to-object.ts @@ -32,7 +32,7 @@ export class OperationVariablesToObject { autoBind(this); } - getName(node: TDefinitionType): string { + getName(node: TDefinitionType): string | null { if (node.name) { if (typeof node.name === 'string') { return node.name; @@ -49,7 +49,7 @@ export class OperationVariablesToObject { transform( variablesNode: ReadonlyArray | undefined, - ): string { + ): string | null { if (!variablesNode || variablesNode.length === 0) { return null; } @@ -110,7 +110,8 @@ export class OperationVariablesToObject { } else if (this._enumValues[typeName]?.sourceFile) { typeValue = this._enumValues[typeName].typeIdentifierConverted || - this._enumValues[typeName].sourceIdentifier; + this._enumValues[typeName].sourceIdentifier || + ''; } else { typeValue = `${prefix}${this._convertName(baseType, { useTypesPrefix: this._enumNames.includes(typeName) ? this._enumPrefix : true, @@ -119,7 +120,7 @@ export class OperationVariablesToObject { } } - const fieldName = this.getName(variable); + const fieldName = this.getName(variable) || ''; const fieldType = this.wrapAstTypeWithModifiers(typeValue, variable.type, this._applyCoercion); const hasDefaultValue =