Skip to content
Merged
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
4 changes: 4 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -3234,6 +3234,10 @@ Where:
- `errors`: Validation error(s) generated by `$_createError()` or `helpers.error()`.

If `errors` is defined, validation will abort regardless of `abortEarly`. Refer to the validation process above for further information.

You can also pass an object where:
- `method`: The function described above.
- `undefined`: When `true`, `method` is also called when the input value is `undefined`, otherwise it is skipped. Defaults to `false`. Return nothing to leave the value `undefined` and let the next extensions prepare it, returning `{ value: undefined }` stops the chain.
- `coerce`: A function with signature `function (value, helpers) {}` that coerces the input value where:
- `value`: The input value.
- `helpers`: [Validation helpers](#validation-helpers)
Expand Down
10 changes: 10 additions & 0 deletions lib/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ exports.type = function (from, options) {

// Prepare

if (def.prepare) {
const { method, undefined: acceptsUndefined } = typeof def.prepare === 'function' ? { method: def.prepare } : def.prepare;
def.prepare = acceptsUndefined ? method : (value, helpers) => {

if (value !== undefined) {
return method(value, helpers);
}
};
}

def.prepare = internals.prepare(def.prepare, parent.prepare);

// Coerce
Expand Down
9 changes: 8 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2222,6 +2222,13 @@ declare namespace Joi {
from?: string | string[];
}

type PrepareFunction = (value: any, helpers: CustomHelpers) => any;

interface PrepareObject {
method: PrepareFunction;
undefined?: boolean;
}

interface ExtensionFlag {
setter?: string;
default?: any;
Expand Down Expand Up @@ -2252,7 +2259,7 @@ declare namespace Joi {
messages?: LanguageMessages | string;
modifiers?: Record<string, (rule: any, enabled?: boolean) => any>;
overrides?: Record<string, (value: any) => Schema>;
prepare?(value: any, helpers: CustomHelpers): any;
prepare?: PrepareFunction | PrepareObject;
rebuild?(schema: ExtensionBoundSchema): void;
rules?: Record<string, ExtensionRule & ThisType<SchemaInternals>>;
terms?: Record<string, ExtensionTerm>;
Expand Down
14 changes: 10 additions & 4 deletions lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ exports.preferences = Joi.object({
internals.nameRx = /^[a-zA-Z0-9]\w*$/;


internals.hook = Joi.function().maxArity(3);


internals.rule = Joi.object({
alias: Joi.array().items(Joi.string().pattern(internals.nameRx)).single(),
args: Joi.array().items(
Expand Down Expand Up @@ -99,8 +102,8 @@ exports.extension = Joi.object({
base: Joi.object().schema()
.when('type', { is: Joi.object().regex(), then: Joi.forbidden() }),
coerce: [
Joi.function().maxArity(3),
Joi.object({ method: Joi.function().maxArity(3).required(), from: Joi.array().items(Joi.string()).single() })
internals.hook,
Joi.object({ method: internals.hook.required(), from: Joi.array().items(Joi.string()).single() })
],
flags: Joi.object().pattern(internals.nameRx, Joi.object({
setter: Joi.string(),
Expand All @@ -112,7 +115,10 @@ exports.extension = Joi.object({
messages: [Joi.object(), Joi.string()],
modifiers: Joi.object().pattern(internals.nameRx, Joi.function().minArity(1).maxArity(2)),
overrides: Joi.object().pattern(internals.nameRx, Joi.function()),
prepare: Joi.function().maxArity(3),
prepare: [
internals.hook,
Joi.object({ method: internals.hook.required(), undefined: Joi.boolean() })
],
rebuild: Joi.function().arity(1),
rules: Joi.object().pattern(internals.nameRx, internals.rule),
jsonSchema: Joi.function(),
Expand All @@ -129,7 +135,7 @@ exports.extension = Joi.object({
})
])
})),
validate: Joi.function().maxArity(3)
validate: internals.hook
})
.strict();

Expand Down
1 change: 0 additions & 1 deletion lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ exports.validate = function (value, schema, state, prefs, overrides = {}) {

const def = schema._definition;
if (def.prepare &&
value !== undefined &&
prefs.convert) {

const prepared = def.prepare(value, helpers);
Expand Down
94 changes: 94 additions & 0 deletions test/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,100 @@ describe('extension', () => {
expect(custom.number().validate(0)).to.equal({ value: undefined });
});

it('extends any to prepare undefined values', () => {

const source = { a: '1' };
const custom = Joi.extend({
type: /^.*$/,
prepare: {
undefined: true,
method(value, { schema }) {

const key = schema.$_getFlag('source');
if (value === undefined &&
key !== undefined &&
source[key] !== undefined) {

return { value: source[key] };
}
}
},
rules: {
source: {
method(key) {

return this.$_setFlag('source', key);
}
}
}
});

const schema = custom.object({
a: custom.number().source('a').default(2),
b: custom.number().source('b').default(3),
c: custom.string().source('c').required()
});

expect(schema.validate({}).error).to.be.an.error('"c" is required');
expect(schema.validate({ a: 4, c: 'x' })).to.equal({ value: { a: 4, b: 3, c: 'x' } });
expect(schema.validate({}, { convert: false }).error).to.be.an.error('"c" is required');
expect(custom.number().source('a').validate(undefined)).to.equal({ value: 1 });
});

it('extends any with an explicit prepare object', () => {

const custom = Joi.extend((joi) => ({
type: 'number',
base: joi.number(),
prepare: {
method(value) {

return { value: value.replace(',', '.') };
}
}
}));

expect(custom.number().validate('2,0')).to.equal({ value: 2.0 });
expect(custom.number().validate(undefined)).to.equal({ value: undefined });
});

it('chains prepare methods that accept undefined with those that don\'t', () => {

const supply = (joi) => ({
type: 'number',
base: joi.number(),
prepare: {
undefined: true,
method(value) {

if (value === undefined) {
return { value: '1,5' };
}
}
}
});

const comma = (joi) => ({
type: 'number',
base: joi.number(),
prepare(value) {

if (typeof value === 'string') {
return { value: value.replace(',', '.') };
}
}
});

// Later extensions prepare first, comma is skipped on undefined and runs before supply
const supplyFirst = Joi.extend(supply, comma);
expect(supplyFirst.number().validate(undefined).error).to.be.an.error('"value" must be a number');
expect(supplyFirst.number().validate('2,5')).to.equal({ value: 2.5 });

const commaFirst = Joi.extend(comma, supply);
expect(commaFirst.number().validate(undefined)).to.equal({ value: 1.5 });
expect(commaFirst.number().validate('2,5')).to.equal({ value: 2.5 });
});

it('extends object to support string coerce', () => {

const custom = Joi.extend({
Expand Down
21 changes: 21 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,27 @@ description = schema.describe();

const Joi2 = Joi.extend({ type: 'test1', base: schema });

Joi.extend((joi) => ({
type: 'number',
base: joi.number(),
prepare(value, helpers) {
return { value };
},
}));

Joi.extend((joi) => ({
type: 'number',
base: joi.number(),
prepare: {
undefined: true,
method(value, helpers) {
return { value };
},
},
}));

expect.error(Joi.extend((joi) => ({ type: 'number', base: joi.number(), prepare: { undefined: true } })));

const Joi3 = Joi.extend({
type: 'string',
base: Joi.string(),
Expand Down
Loading