Skip to content

Commit

Permalink
feat(oneOf): add new error types (#1022)
Browse files Browse the repository at this point in the history
  • Loading branch information
fedeci authored and gustavohenke committed Mar 11, 2023
1 parent 7360662 commit ac65aed
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 39 deletions.
11 changes: 8 additions & 3 deletions docs/api-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,22 @@ Same as `check([fields, message])`, but only checking `req.query`.

> _Returns:_ an array of validation chains and `{ run: (req) => Promise<unknown[]> }`
## `oneOf(validationChains[, message])`
## `oneOf(validationChains[, options])`

- `validationChains`: an array of [validation chains](api-validation-chain.md) created with `check()` or any of its variations,
or an array of arrays containing validation chains.
- `message` _(optional)_: an error message to use when all chains failed. Defaults to `Invalid value(s)`; see also [Dynamic Messages](feature-error-messages.md#dynamic-messages).
- `options` _(optional)_:
- `message` _(optional)_: an error message to use when all chains failed. Defaults to `Invalid value(s)`; see also [Dynamic Messages](feature-error-messages.md#dynamic-messages).
- `errorType` _(optional)_:
- `grouped` _(default)_: groups the errors according to the original validation chain groups;
- `flat`: joins the errors from different chain groups together;
- `leastErroredOnly`: returns only the errors of the least errored chain group.

> _Returns:_ a middleware instance and `{ run: (req) => Promise<Result> }`
Creates a middleware instance that will ensure at least one of the given chains passes the validation.
If none of the given chains passes, an error will be pushed to the `_error` pseudo-field,
using the given `message`, and the errors of each chain will be made available under a key `nestedErrors`.
using the given `message`. The errors, formatted according to the `errorType` option, will be made available under the `nestedErrors` key.

Example:

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
testRegex: 'src/.*\\.spec\\.ts',
testRegex: 'src/.*\\.spec\\.ts$',
testEnvironment: 'node',
preset: 'ts-jest',
// TS takes precedence as we want to avoid build artifacts from being required instead of up-to-date .ts file.
Expand Down
147 changes: 147 additions & 0 deletions src/middlewares/__snapshots__/one-of.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`error message can be the return of a function 1`] = `
Array [
Object {
"msg": "keep trying",
"nestedErrors": Array [
Array [
Object {
"location": "body",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
],
],
"param": "_error",
},
]
`;

exports[`should let the user to choose between multiple error types flat error type 1`] = `
Array [
Object {
"msg": "Invalid value(s)",
"nestedErrors": Array [
Object {
"location": "body",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
Object {
"location": "body",
"msg": "Invalid value",
"param": "bar",
"value": undefined,
},
],
"param": "_error",
},
]
`;

exports[`should let the user to choose between multiple error types grouped error type 1`] = `
Array [
Object {
"msg": "Invalid value(s)",
"nestedErrors": Array [
Array [
Object {
"location": "body",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
],
Array [
Object {
"location": "body",
"msg": "Invalid value",
"param": "bar",
"value": undefined,
},
],
],
"param": "_error",
},
]
`;

exports[`should let the user to choose between multiple error types leastErroredOnly error type 1`] = `
Array [
Object {
"msg": "Invalid value(s)",
"nestedErrors": Array [
Object {
"location": "body",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
],
"param": "_error",
},
]
`;

exports[`with a list of chain groups sets a single error for the _error key 1`] = `
Array [
Object {
"msg": "Invalid value(s)",
"nestedErrors": Array [
Array [
Object {
"location": "cookies",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
Object {
"location": "cookies",
"msg": "Invalid value",
"param": "bar",
"value": "def",
},
],
Array [
Object {
"location": "cookies",
"msg": "Invalid value",
"param": "baz",
"value": 123,
},
],
],
"param": "_error",
},
]
`;

exports[`with a list of chains sets a single error for the _error key 1`] = `
Array [
Object {
"msg": "Invalid value(s)",
"nestedErrors": Array [
Array [
Object {
"location": "cookies",
"msg": "Invalid value",
"param": "foo",
"value": true,
},
],
Array [
Object {
"location": "cookies",
"msg": "Invalid value",
"param": "bar",
"value": "def",
},
],
],
"param": "_error",
},
]
`;
94 changes: 67 additions & 27 deletions src/middlewares/one-of.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { InternalRequest, contextsKey } from '../base';
import { ContextRunnerImpl } from '../chain/context-runner-impl';
import { Result } from '../validation-result';
import { check } from './validation-chain-builders';
import { oneOf } from './one-of';
import { OneOfErrorType, OneOfOptions, oneOf } from './one-of';

const getOneOfContext = (req: InternalRequest) => {
const contexts = req[contextsKey] || [];
Expand Down Expand Up @@ -56,17 +56,7 @@ describe('with a list of chains', () => {

oneOf([check('foo').isInt(), check('bar').isInt()])(req, {}, () => {
const context = getOneOfContext(req);
expect(context.errors).toHaveLength(1);
expect(context.errors).toContainEqual(
expect.objectContaining({
param: '_error',
nestedErrors: expect.arrayContaining([
expect.objectContaining({ param: 'foo' }),
expect.objectContaining({ param: 'bar' }),
]),
}),
);

expect(context.errors).toMatchSnapshot();
done();
});
});
Expand All @@ -92,18 +82,7 @@ describe('with a list of chain groups', () => {

oneOf([[check('foo').isInt(), check('bar').isInt()], check('baz').isAlpha()])(req, {}, () => {
const context = getOneOfContext(req);
expect(context.errors).toHaveLength(1);
expect(context.errors).toContainEqual(
expect.objectContaining({
param: '_error',
nestedErrors: expect.arrayContaining([
expect.objectContaining({ param: 'foo' }),
expect.objectContaining({ param: 'bar' }),
expect.objectContaining({ param: 'baz' }),
]),
}),
);

expect(context.errors).toMatchSnapshot();
done();
});
});
Expand Down Expand Up @@ -139,7 +118,7 @@ describe('error message', () => {
body: { foo: true },
};

oneOf([check('foo').isInt()], 'not today')(req, {}, () => {
oneOf([check('foo').isInt()], { message: 'not today' })(req, {}, () => {
const context = getOneOfContext(req);
expect(context.errors[0]).toHaveProperty('msg', 'not today');
done();
Expand All @@ -152,15 +131,76 @@ describe('error message', () => {
};

const message = jest.fn(() => 'keep trying');
oneOf([check('foo').isInt()], message)(req, {}, () => {
oneOf([check('foo').isInt()], { message })(req, {}, () => {
const context = getOneOfContext(req);
expect(context.errors[0]).toHaveProperty('msg', 'keep trying');
expect(context.errors).toMatchSnapshot();
expect(message).toHaveBeenCalledWith({ req });
done();
});
});
});

describe('should let the user to choose between multiple error types', () => {
const errors: OneOfErrorType[] = ['grouped', 'flat'];
it.each(errors)(`%s error type`, async errorType => {
const req: InternalRequest = {
body: { foo: true },
};
const options: OneOfOptions = {
errorType,
};

await oneOf([check('foo').isString(), check('bar').isFloat()], options).run(req);
const context = getOneOfContext(req);
expect(context.errors).toMatchSnapshot();
});

it('leastErroredOnly error type', done => {
const req: InternalRequest = {
body: { foo: true, bar: 'bar' },
};
const options: OneOfOptions = {
errorType: 'leastErroredOnly',
};

oneOf(
[
[check('foo').isFloat(), check('bar').isInt()],
[check('foo').isString(), check('bar').isString()], // least errored
[check('foo').isFloat(), check('bar').isBoolean()],
],
options,
)(req, {}, () => {
const context = getOneOfContext(req);
expect(context.errors).toMatchSnapshot();
done();
});
});
});

describe('should default to grouped errorType', () => {
it('when no options are provided', async () => {
const req: InternalRequest = {
body: { foo: true },
};
await oneOf([check('foo').isString(), check('bar').isFloat()]).run(req);
const context = getOneOfContext(req);
expect(context.errors[0]?.nestedErrors?.length).toEqual(2);
});

it('when invalid error type is provided', async () => {
const req: InternalRequest = {
body: { foo: true },
};
await oneOf([check('foo').isString(), check('bar').isFloat()], {
// @ts-ignore
errorType: 'invalid error type',
}).run(req);
const context = getOneOfContext(req);
expect(context.errors[0]?.nestedErrors?.length).toEqual(2);
});
});

describe('imperatively run oneOf', () => {
it('sets errors in context when validation fails', async () => {
const req: InternalRequest = {
Expand Down

0 comments on commit ac65aed

Please sign in to comment.