Skip to content

Commit 40b64c6

Browse files
authored
Merge pull request #341 from thib3113/metas
allow to add metas in the schema
2 parents a6bfdc7 + 0b58a91 commit 40b64c6

28 files changed

+838
-29
lines changed

README.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -1547,7 +1547,7 @@ Name | Default text
15471547
`dateMin` | The '{field}' field must be greater than or equal to {expected}.
15481548
`dateMax` | The '{field}' field must be less than or equal to {expected}.
15491549
`forbidden` | The '{field}' field is forbidden.
1550-
‍‍`email` | The '{field}' field must be a valid e-mail.
1550+
`email` | The '{field}' field must be a valid e-mail.
15511551
`emailEmpty` | The '{field}' field must not be empty.
15521552
`emailMin` | The '{field}' field length must be greater than or equal to {expected} characters long.
15531553
`emailMax` | The '{field}' field length must be less than or equal to {expected} characters long.
@@ -1571,6 +1571,20 @@ Name | Description
15711571
`expected` | The expected value
15721572
`actual` | The actual value
15731573

1574+
# Pass custom metas
1575+
In some case, you will need to do something with the validation schema .
1576+
Like reusing the validator to pass custom settings, you can use properties starting with `$$`
1577+
1578+
````typescript
1579+
const check = v.compile({
1580+
$$name: 'Person',
1581+
$$description: 'write a description about this schema',
1582+
firstName: { type: "string" },
1583+
lastName: { type: "string" },
1584+
birthDate: { type: "date" }
1585+
});
1586+
````
1587+
15741588
# Development
15751589
```
15761590
npm run dev

index.d.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -877,9 +877,9 @@ export type ValidationRule =
877877
| ValidationRuleName;
878878

879879
/**
880-
* Definition for validation schema based on validation rules
880+
*
881881
*/
882-
export type ValidationSchema<T = any> = {
882+
export interface ValidationSchemaMetaKeys {
883883
/**
884884
* Object properties which are not specified on the schema are ignored by default.
885885
* If you set the $$strict option to true any additional properties will result in an strictObject error.
@@ -899,12 +899,18 @@ export type ValidationSchema<T = any> = {
899899
* @default false
900900
*/
901901
$$root?: boolean;
902-
} & {
903-
/**
904-
* List of validation rules for each defined field
905-
*/
906-
[key in keyof T]: ValidationRule | undefined | any;
907-
};
902+
}
903+
904+
/**
905+
* Definition for validation schema based on validation rules
906+
*/
907+
export type ValidationSchema<T = any> = ValidationSchemaMetaKeys & {
908+
/**
909+
* List of validation rules for each defined field
910+
*/
911+
[key in keyof T]: ValidationRule | undefined | any;
912+
}
913+
908914

909915
/**
910916
* Structure with description of validation error message

lib/rules/object.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = function ({ schema, messages }, path, context) {
4646
sourceCode.push("var parentObj = value;");
4747
sourceCode.push("var parentField = field;");
4848

49-
const keys = Object.keys(subSchema);
49+
const keys = Object.keys(subSchema).filter(key => !this.isMetaKey(key));
5050

5151
for (let i = 0; i < keys.length; i++) {
5252
const property = keys[i];
@@ -58,7 +58,7 @@ module.exports = function ({ schema, messages }, path, context) {
5858

5959
const labelName = subSchema[property].label;
6060
const label = labelName ? `'${escapeEvalString(labelName)}'` : undefined;
61-
61+
6262
sourceCode.push(`\n// Field: ${escapeEvalString(newPath)}`);
6363
sourceCode.push(`field = parentField ? parentField + "${safeSubName}" : "${name}";`);
6464
sourceCode.push(`value = ${safePropName};`);
@@ -70,7 +70,7 @@ module.exports = function ({ schema, messages }, path, context) {
7070
sourceCode.push(this.compileRule(rule, context, newPath, innerSource, safePropName));
7171
if (this.opts.haltOnFirstError === true) {
7272
sourceCode.push("if (errors.length) return parentObj;");
73-
}
73+
}
7474
}
7575

7676
// Strict handler

lib/validator.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ class Validator {
121121
rule.schema.nullable !== false || rule.schema.type === "forbidden" :
122122
rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";
123123

124-
const ruleHasDefault = considerNullAsAValue ?
125-
rule.schema.default != undefined && rule.schema.default != null :
124+
const ruleHasDefault = considerNullAsAValue ?
125+
rule.schema.default != undefined && rule.schema.default != null :
126126
rule.schema.default != undefined;
127127

128128
if (ruleHasDefault) {
@@ -161,6 +161,30 @@ class Validator {
161161
return src.join("\n");
162162
}
163163

164+
/**
165+
* check if the key is a meta key
166+
*
167+
* @param key
168+
* @return {boolean}
169+
*/
170+
isMetaKey(key) {
171+
return key.startsWith("$$");
172+
}
173+
/**
174+
* will remove all "metas" keys (keys starting with $$)
175+
*
176+
* @param obj
177+
*/
178+
removeMetasKeys(obj) {
179+
Object.keys(obj).forEach(key => {
180+
if(!this.isMetaKey(key)) {
181+
return;
182+
}
183+
184+
delete obj[key];
185+
});
186+
}
187+
164188
/**
165189
* Compile a schema
166190
*
@@ -204,7 +228,7 @@ class Validator {
204228
properties: prevSchema
205229
};
206230

207-
delete prevSchema.$$strict;
231+
this.removeMetasKeys(prevSchema);
208232
}
209233
}
210234

test/integration.spec.js

+103
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const Validator = require("../lib/validator");
4+
const {RuleEmail} = require("../index");
45

56
describe("Test flat schema", () => {
67
const v = new Validator();
@@ -1445,3 +1446,105 @@ describe("edge cases", () => {
14451446
]);
14461447
});
14471448
});
1449+
1450+
describe("allow metas starting with $$", () => {
1451+
const v = new Validator({ useNewCustomCheckerFunction: true });
1452+
describe("test on schema", () => {
1453+
it("should not remove keys from source object", async () => {
1454+
const schema = {
1455+
$$foo: {
1456+
foo: "bar"
1457+
},
1458+
name: { type: "string" } };
1459+
const clonedSchema = {...schema};
1460+
v.compile(schema);
1461+
1462+
expect(schema).toStrictEqual(clonedSchema);
1463+
});
1464+
1465+
it("should works with $$root", () => {
1466+
const schema = {
1467+
$$foo: {
1468+
foo: "bar"
1469+
},
1470+
$$root: true,
1471+
type: "email",
1472+
empty: true
1473+
};
1474+
const clonedSchema = {...schema};
1475+
const check = v.compile(schema);
1476+
1477+
expect(check("[email protected]")).toEqual(true);
1478+
expect(check("")).toEqual(true);
1479+
expect(schema).toStrictEqual(clonedSchema);
1480+
});
1481+
1482+
it("should works with $$async", async () => {
1483+
const custom1 = jest.fn().mockResolvedValue("NAME");
1484+
const schema = {
1485+
$$foo: {
1486+
foo: "bar"
1487+
},
1488+
$$async: true,
1489+
name: { type: "string", custom: custom1 },
1490+
};
1491+
const clonedSchema = {...schema};
1492+
const check = v.compile(schema);
1493+
1494+
//check schema meta was not changed
1495+
expect(schema.$$foo).toStrictEqual(clonedSchema.$$foo);
1496+
1497+
expect(check.async).toBe(true);
1498+
1499+
let obj = {
1500+
id: 3,
1501+
name: "John",
1502+
username: " john.doe ",
1503+
age: 30
1504+
};
1505+
1506+
const res = await check(obj);
1507+
expect(res).toBe(true);
1508+
1509+
expect(custom1).toBeCalledTimes(1);
1510+
expect(custom1).toBeCalledWith("John", [], schema.name, "name", null, expect.anything());
1511+
});
1512+
});
1513+
1514+
describe("test on rule", () => {
1515+
it("should not remove keys from source object", async () => {
1516+
const schema = {
1517+
name: {
1518+
$$foo: {
1519+
foo: "bar"
1520+
},
1521+
type: "string"
1522+
}
1523+
};
1524+
const clonedSchema = {...schema};
1525+
v.compile(schema);
1526+
1527+
expect(schema).toStrictEqual(clonedSchema);
1528+
});
1529+
it("should works with $$type", async () => {
1530+
const schema = {
1531+
dot: {
1532+
$$foo: {
1533+
foo: "bar"
1534+
},
1535+
$$type: "object",
1536+
x: "number", // object props here
1537+
y: "number", // object props here
1538+
}
1539+
};
1540+
const clonedSchema = {...schema};
1541+
const check = v.compile(schema);
1542+
1543+
expect(schema).toStrictEqual(clonedSchema);
1544+
expect(check({
1545+
x: 1,
1546+
y: 1,
1547+
})).toBeTruthy();
1548+
});
1549+
});
1550+
});

test/rules/any.spec.js

+55-5
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,39 @@ describe("Test rule: any", () => {
3939
expect(check([])).toEqual(true);
4040
expect(check({})).toEqual(true);
4141
});
42+
43+
it("should allow custom metas", async () => {
44+
const schema = {
45+
$$foo: {
46+
foo: "bar"
47+
},
48+
$$root: true,
49+
type: "any",
50+
optional: true
51+
};
52+
const clonedSchema = {...schema};
53+
const check = v.compile(schema);
54+
55+
expect(schema).toStrictEqual(clonedSchema);
56+
57+
expect(check(null)).toEqual(true);
58+
expect(check(undefined)).toEqual(true);
59+
expect(check(0)).toEqual(true);
60+
expect(check(1)).toEqual(true);
61+
expect(check("")).toEqual(true);
62+
expect(check("true")).toEqual(true);
63+
expect(check("false")).toEqual(true);
64+
expect(check([])).toEqual(true);
65+
expect(check({})).toEqual(true);
66+
});
4267
});
4368

4469
describe("new case (with considerNullAsAValue flag set to true)", () => {
4570
const v = new Validator({considerNullAsAValue: true});
46-
71+
4772
it("should give back true anyway", () => {
4873
const check = v.compile({ $$root: true, type: "any" });
49-
74+
5075
expect(check(null)).toEqual(true);
5176
expect(check(undefined)).toEqual([{ type: "required", actual: undefined, message: "The '' field is required." }]);
5277
expect(check(0)).toEqual(true);
@@ -57,10 +82,35 @@ describe("Test rule: any", () => {
5782
expect(check([])).toEqual(true);
5883
expect(check({})).toEqual(true);
5984
});
60-
85+
6186
it("should give back true anyway as optional", () => {
6287
const check = v.compile({ $$root: true, type: "any", optional: true });
63-
88+
89+
expect(check(null)).toEqual(true);
90+
expect(check(undefined)).toEqual(true);
91+
expect(check(0)).toEqual(true);
92+
expect(check(1)).toEqual(true);
93+
expect(check("")).toEqual(true);
94+
expect(check("true")).toEqual(true);
95+
expect(check("false")).toEqual(true);
96+
expect(check([])).toEqual(true);
97+
expect(check({})).toEqual(true);
98+
});
99+
100+
it("should allow custom metas", async () => {
101+
const schema = {
102+
$$foo: {
103+
foo: "bar"
104+
},
105+
$$root: true,
106+
type: "any",
107+
optional: true
108+
};
109+
const clonedSchema = {...schema};
110+
const check = v.compile(schema);
111+
112+
expect(schema).toStrictEqual(clonedSchema);
113+
64114
expect(check(null)).toEqual(true);
65115
expect(check(undefined)).toEqual(true);
66116
expect(check(0)).toEqual(true);
@@ -72,4 +122,4 @@ describe("Test rule: any", () => {
72122
expect(check({})).toEqual(true);
73123
});
74124
});
75-
});
125+
});

test/rules/array.spec.js

+30
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,34 @@ describe("Test rule: array", () => {
251251
});
252252
});
253253
});
254+
255+
it("should allow custom metas", async () => {
256+
const itemSchema = {
257+
$$foo: {
258+
foo: "bar"
259+
},
260+
type: "string",
261+
};
262+
const schema = {
263+
$$foo: {
264+
foo: "bar"
265+
},
266+
$$root: true,
267+
type: "array",
268+
items: itemSchema
269+
};
270+
const clonedSchema = {...schema};
271+
const clonedItemSchema = {...itemSchema};
272+
const check = v.compile(schema);
273+
274+
expect(schema).toStrictEqual(clonedSchema);
275+
expect(itemSchema).toStrictEqual(clonedItemSchema);
276+
277+
expect(check([])).toEqual(true);
278+
expect(check(["human"])).toEqual(true);
279+
expect(check(["male", 3, "female", true])).toEqual([
280+
{ type: "string", field: "[1]", actual: 3, message: "The '[1]' field must be a string." },
281+
{ type: "string", field: "[3]", actual: true, message: "The '[3]' field must be a string." }
282+
]);
283+
});
254284
});

0 commit comments

Comments
 (0)