diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index 3cf69198f..abc542c53 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -155,6 +155,9 @@ export function object< let issues: Issues | undefined; const output: Record = {}; + const partialPipes = pipe?.filter((p) => p.partial != null); + const validFields: string[] = []; + // Parse schema of each key for (const [key, schema] of cachedEntries) { const value = (input as Record)[key]; @@ -191,6 +194,49 @@ export function object< // Otherwise, add value to object } else if (result.output !== undefined || key in input) { output[key] = result.output; + validFields.push(key); + } + + // TODO: Better logic to no do checks twich + const pipesToRun = partialPipes?.filter((p) => + p.partial!.every((requiredField) => + validFields.includes(requiredField) + ) + ); + const pipeResult = executePipe( + output as ObjectOutput, + pipesToRun, + info, + 'object' + ); + + if (pipeResult.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + schema: 'object', + input, + // TODO: Custom key from pipe + key, + value, + }; + + // Add modified result issues to issues + for (const issue of pipeResult.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + issue.path = [pathItem]; + } + issues?.push(issue); + } + if (!issues) { + issues = result.issues; + } + + // If necessary, abort early + if (info?.abortEarly) { + break; + } } } diff --git a/library/src/types.ts b/library/src/types.ts index e0214c291..d7e8a23f8 100644 --- a/library/src/types.ts +++ b/library/src/types.ts @@ -141,7 +141,9 @@ export type PipeResult = /** * Validation and transformation pipe type. */ -export type Pipe = ((value: TValue) => PipeResult)[]; +export type Pipe = (((value: TValue) => PipeResult) & { + partial?: string[]; +})[]; /** * Async validation and transformation pipe type. diff --git a/library/src/validations/customPartial/customPartial.test.ts b/library/src/validations/customPartial/customPartial.test.ts new file mode 100644 index 000000000..6acc8931d --- /dev/null +++ b/library/src/validations/customPartial/customPartial.test.ts @@ -0,0 +1,83 @@ +import { assert, describe, expect, test } from 'vitest'; +import { customPartial } from './customPartial.js'; +import { object, string } from '../../schemas/index.js'; +import { safeParse } from '../../methods/index.js'; +import { length } from '../length/length.js'; + +describe('customPartial', () => { + test('should run before entire object is valid', () => { + const schema = object( + { + username: string(), + password: string(), + passwordConfirmation: string(), + }, + [ + customPartial( + ['password', 'passwordConfirmation'], + (input) => input.password === input.passwordConfirmation + ), + ] + ); + + const result = safeParse(schema, { + username: 5, + password: 'password', + passwordConfirmation: 'wrong password', + }); + + assert(!result.success); + expect(result.issues.length).toEqual(2); + expect(result.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + validation: 'string', + path: expect.arrayContaining([ + expect.objectContaining({ + key: 'username', + }), + ]), + }), + expect.objectContaining({ + validation: 'customPartial', + path: expect.arrayContaining([ + expect.objectContaining({ + key: 'passwordConfirmation', + }), + ]), + }), + ]) + ); + }); + + test('should only run when required fields are valid', () => { + const schema = object( + { + username: string(), + password: string([length(100)]), + passwordConfirmation: string(), + }, + [ + customPartial( + ['password', 'passwordConfirmation'], + (input) => input.password === input.passwordConfirmation + ), + ] + ); + + const result = safeParse(schema, { + username: 5, + password: 'password', + passwordConfirmation: 'wrong password', + }); + + assert(!result.success); + expect(result.issues).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + validation: 'customPartial', + }), + ]) + ); + }); +}); diff --git a/library/src/validations/customPartial/customPartial.ts b/library/src/validations/customPartial/customPartial.ts new file mode 100644 index 000000000..921e513c3 --- /dev/null +++ b/library/src/validations/customPartial/customPartial.ts @@ -0,0 +1,25 @@ +import type { ErrorMessage, PipeResult } from '../../types.js'; +import { getOutput, getPipeIssues } from '../../utils/index.js'; + +/** + * Creates a custom validation function that validates the part object schema. + * + * @param requirement The validation function. + * @param error The error message. + * + * @returns A validation function. + */ +export function customPartial( + requiredFields: TKeys[], + requirement: (input: Pick) => boolean, + error?: ErrorMessage + // TODO: path +) { + const pipe = (input: TInput): PipeResult => + !requirement(input) + ? getPipeIssues('customPartial', error || 'Invalid input', input) + : getOutput(input); + + pipe.partial = requiredFields; + return pipe; +} diff --git a/library/src/validations/customPartial/index.ts b/library/src/validations/customPartial/index.ts new file mode 100644 index 000000000..d26564c49 --- /dev/null +++ b/library/src/validations/customPartial/index.ts @@ -0,0 +1 @@ +export * from './customPartial.ts';