From 7dd6e7dcecb1a11e9397c8fe29d0ef1aba54ab21 Mon Sep 17 00:00:00 2001 From: Victor Zhivotikov Date: Fri, 1 Nov 2024 06:22:25 +0000 Subject: [PATCH] fix: merging generated and user provided schemas with intersection --- src/createValibotSchema.ts | 304 ++++++++++++++++++------------------- test/index.test.ts | 4 +- 2 files changed, 154 insertions(+), 154 deletions(-) diff --git a/src/createValibotSchema.ts b/src/createValibotSchema.ts index d66568a..b8fc5d3 100644 --- a/src/createValibotSchema.ts +++ b/src/createValibotSchema.ts @@ -1,5 +1,5 @@ -import { PothosSchemaError } from '@pothos/core' -import * as v from 'valibot' +import { PothosSchemaError } from "@pothos/core"; +import * as v from "valibot"; import type { ArrayValidationOptions, BaseValidationOptions, @@ -7,143 +7,143 @@ import type { StringValidationOptions, ValidationOptionUnion, ValiSchema, -} from './types' +} from "./types"; -const baseValidations = ['refine', 'schema', 'check'] as const +const baseValidations = ["refine", "schema", "check"] as const; const numberValidations = [ ...baseValidations, - 'integer', - 'maxValue', - 'minValue', - 'type', -] as const + "integer", + "maxValue", + "minValue", + "type", +] as const; const bigIntValidations = [ ...baseValidations, - 'maxValue', - 'minValue', - 'type', -] as const + "maxValue", + "minValue", + "type", +] as const; -const booleanValidations = [...baseValidations, 'type'] as const +const booleanValidations = [...baseValidations, "type"] as const; const dateValidations = [ ...baseValidations, - 'maxValue', - 'minValue', - 'type', -] as const + "maxValue", + "minValue", + "type", +] as const; const stringValidations = [ ...baseValidations, - 'trim', - 'nonEmpty', - 'email', - 'length', - 'maxLength', - 'minLength', - 'regex', - 'type', - 'url', - 'uuid', -] as const + "trim", + "nonEmpty", + "email", + "length", + "maxLength", + "minLength", + "regex", + "type", + "url", + "uuid", +] as const; const arrayValidations = [ ...baseValidations, - 'items', - 'nonEmpty', - 'length', - 'maxLength', - 'minLength', - 'type', -] as const + "items", + "nonEmpty", + "length", + "maxLength", + "minLength", + "type", +] as const; -const objectValidations = [...baseValidations, 'type'] as const +const objectValidations = [...baseValidations, "type"] as const; function validatorCreator>( - type: NonNullable, + type: NonNullable, validationNames: readonly (keyof T)[], - create: (options: T) => ValiSchema, + create: (options: T) => ValiSchema ) { function check(options: ValidationOptionUnion): options is T { if ( - typeof options !== 'object' || + typeof options !== "object" || (options.type && options.type !== type) ) { - return false + return false; } - const validations = Object.keys(options) + const validations = Object.keys(options); return validations.every((validation) => - validationNames.includes(validation as keyof T), - ) + validationNames.includes(validation as keyof T) + ); } return (options: ValidationOptionUnion) => { if (check(options)) { - return create(options) + return create(options); } - return null - } + return null; + }; } export function refine( originalValidator: ValiSchema, - options: ValidationOptionUnion | null | undefined, + options: ValidationOptionUnion | null | undefined ): ValiSchema { if (!options) { - return originalValidator + return originalValidator; } - let validator = originalValidator + let validator = originalValidator; if (options.schema) { - validator = v.pipeAsync(options.schema, originalValidator) + validator = v.intersectAsync([originalValidator, options.schema]); } if (options.refine) { - validator = options.refine(validator as any) + validator = options.refine(validator as any); } if (!options.check) { - return validator + return validator; } - if (typeof options.check === 'function') { - return v.pipeAsync(validator, v.checkAsync(options.check as any)) + if (typeof options.check === "function") { + return v.pipeAsync(validator, v.checkAsync(options.check as any)); } // @ts-ignore - if (typeof options.check?.[0] === 'function') { + if (typeof options.check?.[0] === "function") { const [checkFn, errorMessage] = options.check as [ () => boolean, - v.ErrorMessage, - ] - return v.pipeAsync(validator, v.checkAsync(checkFn, errorMessage)) + v.ErrorMessage + ]; + return v.pipeAsync(validator, v.checkAsync(checkFn, errorMessage)); } - const checks = options.check as [() => boolean, v.ErrorMessage][] + const checks = options.check as [() => boolean, v.ErrorMessage][]; return checks.reduce( (prev, [checkFn, errorMessage]) => v.pipeAsync(prev, v.checkAsync(checkFn, errorMessage)), - validator, - ) + validator + ); } export const createNumberValidator = validatorCreator( - 'number', + "number", numberValidations, (options: NumberValidationOptions) => { - let validator = v.number() as ValiSchema + let validator = v.number() as ValiSchema; if (options.integer) { validator = v.pipeAsync( validator, v.integer( - Array.isArray(options.integer) ? options.integer[1] : undefined, - ), - ) + Array.isArray(options.integer) ? options.integer[1] : undefined + ) + ); } if (options.minValue) { @@ -151,8 +151,8 @@ export const createNumberValidator = validatorCreator( validator, Array.isArray(options.minValue) ? v.minValue(Number(options.minValue[0]), options.minValue[1]) - : v.minValue(Number(options.minValue)), - ) + : v.minValue(Number(options.minValue)) + ); } if (options.maxValue) { @@ -160,27 +160,27 @@ export const createNumberValidator = validatorCreator( validator, Array.isArray(options.maxValue) ? v.maxValue(Number(options.maxValue[0]), options.maxValue[1]) - : v.maxValue(Number(options.maxValue)), - ) + : v.maxValue(Number(options.maxValue)) + ); } - return refine(validator, options) - }, -) + return refine(validator, options); + } +); export const createBigintValidator = validatorCreator( - 'bigint', + "bigint", bigIntValidations, (options) => { - let validator = v.bigint() as ValiSchema + let validator = v.bigint() as ValiSchema; if (options.minValue) { validator = v.pipeAsync( validator, Array.isArray(options.minValue) ? v.minValue(BigInt(options.minValue[0]), options.minValue[1]) - : v.minValue(BigInt(options.minValue)), - ) + : v.minValue(BigInt(options.minValue)) + ); } if (options.maxValue) { @@ -188,33 +188,33 @@ export const createBigintValidator = validatorCreator( validator, Array.isArray(options.maxValue) ? v.maxValue(BigInt(options.maxValue[0]), options.maxValue[1]) - : v.maxValue(BigInt(options.maxValue)), - ) + : v.maxValue(BigInt(options.maxValue)) + ); } - return refine(validator, options) - }, -) + return refine(validator, options); + } +); export const createBooleanValidator = validatorCreator( - 'boolean', + "boolean", booleanValidations, - (options) => refine(v.boolean(), options), -) + (options) => refine(v.boolean(), options) +); export const createDateValidator = validatorCreator( - 'date', + "date", dateValidations, (options) => { - let validator = v.date() as ValiSchema + let validator = v.date() as ValiSchema; if (options.minValue) { validator = v.pipeAsync( validator, Array.isArray(options.minValue) ? v.minValue(new Date(options.minValue[0]), options.minValue[1]) - : v.minValue(new Date(options.minValue)), - ) + : v.minValue(new Date(options.minValue)) + ); } if (options.maxValue) { @@ -222,35 +222,35 @@ export const createDateValidator = validatorCreator( validator, Array.isArray(options.maxValue) ? v.maxValue(new Date(options.maxValue[0]), options.maxValue[1]) - : v.maxValue(new Date(options.maxValue)), - ) + : v.maxValue(new Date(options.maxValue)) + ); } - return refine(validator, options) - }, -) + return refine(validator, options); + } +); export const createStringValidator = validatorCreator( - 'string', + "string", stringValidations, (options: StringValidationOptions) => { - let validator = v.string() as ValiSchema + let validator = v.string() as ValiSchema; if (options.trim) { - validator = v.pipeAsync(validator, v.trim()) + validator = v.pipeAsync(validator, v.trim()); } - const booleanConstraints = ['nonEmpty', 'email', 'url', 'uuid'] as const + const booleanConstraints = ["nonEmpty", "email", "url", "uuid"] as const; for (const constraint of booleanConstraints) { if (options[constraint]) { - const value = options[constraint] + const value = options[constraint]; validator = v.pipeAsync( validator, // @ts-ignore - v[constraint](Array.isArray(value) ? value[1] : undefined), - ) + v[constraint](Array.isArray(value) ? value[1] : undefined) + ); } } @@ -259,8 +259,8 @@ export const createStringValidator = validatorCreator( validator, Array.isArray(options.length) ? v.length(Number(options.length[0]), options.length[1]) - : v.length(Number(options.length)), - ) + : v.length(Number(options.length)) + ); } if (options.minLength) { @@ -268,8 +268,8 @@ export const createStringValidator = validatorCreator( validator, Array.isArray(options.minLength) ? v.minLength(Number(options.minLength[0]), options.minLength[1]) - : v.minLength(Number(options.minLength)), - ) + : v.minLength(Number(options.minLength)) + ); } if (options.maxLength) { @@ -277,8 +277,8 @@ export const createStringValidator = validatorCreator( validator, Array.isArray(options.maxLength) ? v.maxLength(Number(options.maxLength[0]), options.maxLength[1]) - : v.maxLength(Number(options.maxLength)), - ) + : v.maxLength(Number(options.maxLength)) + ); } if (options.regex) { @@ -286,44 +286,44 @@ export const createStringValidator = validatorCreator( validator, Array.isArray(options.regex) ? v.regex(options.regex[0], options.regex[1]) - : v.regex(options.regex), - ) + : v.regex(options.regex) + ); } - return refine(validator, options) - }, -) + return refine(validator, options); + } +); export function isArrayValidator( - options: ValidationOptionUnion, + options: ValidationOptionUnion ): options is ArrayValidationOptions { if ( - typeof options !== 'object' || - (options.type && options.type !== 'array') + typeof options !== "object" || + (options.type && options.type !== "array") ) { - return false + return false; } - const validations = Object.keys(options) + const validations = Object.keys(options); return validations.every((validation) => - arrayValidations.includes(validation as keyof ArrayValidationOptions), - ) + arrayValidations.includes(validation as keyof ArrayValidationOptions) + ); } export function createArrayValidator( options: ArrayValidationOptions, - items: ValiSchema, + items: ValiSchema ) { - let validator = v.arrayAsync(items) + let validator = v.arrayAsync(items); if (options.nonEmpty) { validator = v.pipeAsync( validator, v.nonEmpty( - Array.isArray(options.nonEmpty) ? options.nonEmpty[1] : undefined, - ), - ) + Array.isArray(options.nonEmpty) ? options.nonEmpty[1] : undefined + ) + ); } if (options.length !== undefined) { @@ -331,8 +331,8 @@ export function createArrayValidator( validator, Array.isArray(options.length) ? v.length(Number(options.length[0]), options.length[1]) - : v.length(Number(options.length)), - ) + : v.length(Number(options.length)) + ); } if (options.minLength) { @@ -340,8 +340,8 @@ export function createArrayValidator( validator, Array.isArray(options.minLength) ? v.minLength(Number(options.minLength[0]), options.minLength[1]) - : v.minLength(Number(options.minLength)), - ) + : v.minLength(Number(options.minLength)) + ); } if (options.maxLength) { @@ -349,18 +349,18 @@ export function createArrayValidator( validator, Array.isArray(options.maxLength) ? v.maxLength(Number(options.maxLength[0]), options.maxLength[1]) - : v.maxLength(Number(options.maxLength)), - ) + : v.maxLength(Number(options.maxLength)) + ); } - return refine(validator, options) + return refine(validator, options); } export const createObjectValidator = validatorCreator( - 'object', + "object", objectValidations, - (options) => refine(v.looseObject({}), options), -) + (options) => refine(v.looseObject({}), options) +); const validationCreators = [ createNumberValidator, @@ -369,57 +369,57 @@ const validationCreators = [ createDateValidator, createStringValidator, createObjectValidator, -] +]; export function isBaseValidator(options: ValidationOptionUnion) { - const validations = Object.keys(options) + const validations = Object.keys(options); return validations.every((validation) => baseValidations.includes( - validation as Exclude, - ), - ) + validation as Exclude + ) + ); } export function combine(validators: ValiSchema[], required: boolean) { const union = validators.length > 1 ? v.unionAsync(validators as [ValiSchema, ValiSchema]) - : validators[0]! + : validators[0]!; - return required ? union : v.nullishAsync(union) + return required ? union : v.nullishAsync(union); } export default function createValibotSchema( options: ValidationOptionUnion | null | undefined, - required = false, + required = false ): ValiSchema { if (!options) { - return v.unknown() + return v.unknown(); } if (isBaseValidator(options)) { - return combine([refine(v.unknown(), options)], required) + return combine([refine(v.unknown(), options)], required); } const typeValidators = validationCreators .map((create) => create(options)) - .filter(Boolean) as ValiSchema[] + .filter(Boolean) as ValiSchema[]; if (isArrayValidator(options)) { const items = options.items ? createValibotSchema(options.items) - : v.unknown() - typeValidators.push(createArrayValidator(options, items)) + : v.unknown(); + typeValidators.push(createArrayValidator(options, items)); } if (typeValidators.length === 0) { throw new PothosSchemaError( `No type validator can implement every constraint in (${Object.keys( - options, - )})`, - ) + options + )})` + ); } - return combine([...typeValidators], required) + return combine([...typeValidators], required); } diff --git a/test/index.test.ts b/test/index.test.ts index 096fe2f..1121ed4 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -53,7 +53,7 @@ describe("validation", () => { contactInfo: { name: "deathman" email: "Deathman@example.com" - phone: " 555-123-4567 " + phone: " 555-123-456 " aliases: ["deathman92"] } enum1: [Two, One] @@ -73,7 +73,7 @@ describe("validation", () => { "exampleField": null, }, "errors": [ - [GraphQLError: enum1: Invalid input: Received Array, recursive.float: Invalid input: Received 1, recursive.recurse.number: Invalid value: Expected <=5 but received 6, recursive.recurse.float: Invalid input: Received 1, recursive.recurse.recurse: number must not be 3, odd: number must be odd, contactInfo.name: Name should be capitalized, contactInfo.aliases: Aliases should be capitalized, contactInfo.email: no example.com email addresses, contactInfo.email: email should be lowercase, contactInfo.aliases: contactInfo should include at least 2 aliases], + [GraphQLError: enum1: Invalid input: Received Array, recursive.float: Invalid input: Received 1, recursive.recurse.number: Invalid value: Expected <=5 but received 6, recursive.recurse.float: Invalid input: Received 1, recursive.recurse.recurse: number must not be 3, odd: number must be odd, contactInfo.name: Name should be capitalized, contactInfo.aliases: Aliases should be capitalized, contactInfo.email: no example.com email addresses, contactInfo.phone: Invalid length: Expected 12 but received 11, contactInfo.phone: Invalid format: Expected /^\\d{3}-\\d{3}-\\d{4}$/u but received "555-123-456", contactInfo.email: email should be lowercase, contactInfo.aliases: contactInfo should include at least 2 aliases], ], } `);