UNPKG

@pothos/plugin-zod

Version:

A Pothos plugin for adding argument validation

200 lines (161 loc) 5.84 kB
import './global-types'; import SchemaBuilder, { BasePlugin, type InputTypeFieldsMapping, mapInputFields, type PothosInputFieldConfig, type PothosInputFieldType, type PothosOutputFieldConfig, PothosSchemaError, PothosValidationError, resolveInputTypeConfig, type SchemaTypes, } from '@pothos/core'; import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql'; import * as zod from 'zod'; import createZodSchema, { combine, createArrayValidator, isArrayValidator, refine, } from './createZodSchema'; import type { RefineConstraint, ValidationOptions, ValidationOptionUnion } from './types'; export * from './types'; const pluginName = 'zod'; export class PothosZodPlugin<Types extends SchemaTypes> extends BasePlugin<Types> { inputFieldValidators = new Map<string, Record<string, zod.ZodType<unknown>>>(); override onInputFieldConfig( fieldConfig: PothosInputFieldConfig<Types>, ): PothosInputFieldConfig<Types> { const fieldType = resolveInputTypeConfig(fieldConfig.type, this.buildCache); const validationOptions = fieldConfig.pothosOptions.validate as | ValidationOptionUnion | undefined; if (!validationOptions && fieldType.kind !== 'InputObject') { return fieldConfig; } const fieldName = fieldConfig.kind === 'Arg' ? `${fieldConfig.parentType}.${fieldConfig.parentField}(${fieldConfig.name})` : `${fieldConfig.parentType}.${fieldConfig.name}`; const validator = this.createValidator(validationOptions, fieldConfig.type, fieldName); if (fieldConfig.kind === 'Arg') { return { ...fieldConfig, extensions: { ...fieldConfig.extensions, validator, }, }; } this.inputFieldValidators.set(fieldConfig.parentType, { ...this.inputFieldValidators.get(fieldConfig.parentType), [fieldConfig.name]: validator, }); return fieldConfig; } private mappingCache = new Map<string, InputTypeFieldsMapping<Types, zod.ZodType<unknown>>>(); override wrapResolve( resolver: GraphQLFieldResolver<unknown, Types['Context'], object>, fieldConfig: PothosOutputFieldConfig<Types>, ): GraphQLFieldResolver<unknown, Types['Context'], object> { // Only used to check if validation is required const argMap = mapInputFields( fieldConfig.args, this.buildCache, (field) => field.extensions?.validator ?? null, this.mappingCache, ); if (!argMap && !fieldConfig.pothosOptions.validate) { return resolver; } const args: Record<string, zod.ZodType<unknown>> = {}; for (const [argName, arg] of Object.entries(fieldConfig.args)) { const validator = arg.extensions?.validator as zod.ZodType<unknown> | undefined; if (validator) { args[argName] = validator; } } let validator: zod.ZodType = zod.looseObject(args); if (fieldConfig.pothosOptions.validate) { validator = refine(validator, fieldConfig.pothosOptions.validate as ValidationOptionUnion); } const validationError = this.builder.options.zod?.validationError; const validatorWithErrorHandling = validationError && async function validate(value: unknown, ctx: object, info: GraphQLResolveInfo) { try { const result: unknown = await validator.parseAsync(value); return result; } catch (error: unknown) { const errorOrMessage = validationError( error as zod.ZodError, value as Record<string, unknown>, ctx, info, ); if (typeof errorOrMessage === 'string') { throw new PothosValidationError(errorOrMessage); } throw errorOrMessage; } }; return async (parent, rawArgs, context, info) => resolver( parent, (await (validatorWithErrorHandling ? validatorWithErrorHandling(rawArgs, context, info) : validator.parseAsync(rawArgs))) as object, context, info, ); } createValidator( optionsOrConstraint: RefineConstraint | ValidationOptionUnion | undefined, type: PothosInputFieldType<Types> | null, fieldName: string, ): zod.ZodType { const options: ValidationOptionUnion | undefined = Array.isArray(optionsOrConstraint) || typeof optionsOrConstraint === 'function' ? { refine: optionsOrConstraint } : optionsOrConstraint; if (type?.kind === 'InputObject') { const typeConfig = this.buildCache.getTypeConfig(type.ref, 'InputObject'); let fieldValidator = refine( zod.lazy(() => zod.looseObject(this.inputFieldValidators.get(typeConfig.name) ?? {})), options, ); if (typeConfig.pothosOptions.validate) { fieldValidator = refine( fieldValidator, typeConfig.pothosOptions.validate as ValidationOptions<unknown>, ); } return combine([fieldValidator], type.required); } if (type?.kind === 'List') { if (options && !isArrayValidator(options)) { throw new PothosSchemaError(`Expected valid array validator for ${fieldName}`); } const items = this.createValidator(options?.items, type.type, fieldName); if (options) { return combine([createArrayValidator(options, items)], type.required); } return combine([items.array()], type.required); } if (!options) { return zod.unknown(); } return createZodSchema(options, !type || type.required); } } SchemaBuilder.registerPlugin(pluginName, PothosZodPlugin, { v3(options) { return { validationOptions: undefined, zod: options.validationOptions, }; }, }); export default pluginName; export { default as createZodSchema } from './createZodSchema';