Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple customPartial implementation #223

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions library/src/schemas/object/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ export function object<
let issues: Issues | undefined;
const output: Record<string, any> = {};

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<string, unknown>)[key];
Expand Down Expand Up @@ -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<TObjectEntries, TObjectRest>,
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;
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion library/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ export type PipeResult<TOutput> =
/**
* Validation and transformation pipe type.
*/
export type Pipe<TValue> = ((value: TValue) => PipeResult<TValue>)[];
export type Pipe<TValue> = (((value: TValue) => PipeResult<TValue>) & {
partial?: string[];
})[];

/**
* Async validation and transformation pipe type.
Expand Down
83 changes: 83 additions & 0 deletions library/src/validations/customPartial/customPartial.test.ts
Original file line number Diff line number Diff line change
@@ -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',
}),
])
);
});
});
25 changes: 25 additions & 0 deletions library/src/validations/customPartial/customPartial.ts
Original file line number Diff line number Diff line change
@@ -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<TInput, TKeys extends keyof TInput>(
requiredFields: TKeys[],
requirement: (input: Pick<TInput, TKeys>) => boolean,
error?: ErrorMessage
// TODO: path
) {
const pipe = (input: TInput): PipeResult<TInput> =>
!requirement(input)
? getPipeIssues('customPartial', error || 'Invalid input', input)
: getOutput(input);

pipe.partial = requiredFields;
return pipe;
}
1 change: 1 addition & 0 deletions library/src/validations/customPartial/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './customPartial.ts';
Loading