Skip to content

Commit

Permalink
Merge pull request #341 from thib3113/metas
Browse files Browse the repository at this point in the history
allow to add metas in the schema
  • Loading branch information
icebob authored Apr 21, 2024
2 parents a6bfdc7 + 0b58a91 commit 40b64c6
Show file tree
Hide file tree
Showing 28 changed files with 838 additions and 29 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,7 @@ Name | Default text
`dateMin` | The '{field}' field must be greater than or equal to {expected}.
`dateMax` | The '{field}' field must be less than or equal to {expected}.
`forbidden` | The '{field}' field is forbidden.
‍‍`email` | The '{field}' field must be a valid e-mail.
`email` | The '{field}' field must be a valid e-mail.
`emailEmpty` | The '{field}' field must not be empty.
`emailMin` | The '{field}' field length must be greater than or equal to {expected} characters long.
`emailMax` | The '{field}' field length must be less than or equal to {expected} characters long.
Expand All @@ -1571,6 +1571,20 @@ Name | Description
`expected` | The expected value
`actual` | The actual value

# Pass custom metas
In some case, you will need to do something with the validation schema .
Like reusing the validator to pass custom settings, you can use properties starting with `$$`

````typescript
const check = v.compile({
$$name: 'Person',
$$description: 'write a description about this schema',
firstName: { type: "string" },
lastName: { type: "string" },
birthDate: { type: "date" }
});
````

# Development
```
npm run dev
Expand Down
22 changes: 14 additions & 8 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,9 +877,9 @@ export type ValidationRule =
| ValidationRuleName;

/**
* Definition for validation schema based on validation rules
*
*/
export type ValidationSchema<T = any> = {
export interface ValidationSchemaMetaKeys {
/**
* Object properties which are not specified on the schema are ignored by default.
* If you set the $$strict option to true any additional properties will result in an strictObject error.
Expand All @@ -899,12 +899,18 @@ export type ValidationSchema<T = any> = {
* @default false
*/
$$root?: boolean;
} & {
/**
* List of validation rules for each defined field
*/
[key in keyof T]: ValidationRule | undefined | any;
};
}

/**
* Definition for validation schema based on validation rules
*/
export type ValidationSchema<T = any> = ValidationSchemaMetaKeys & {
/**
* List of validation rules for each defined field
*/
[key in keyof T]: ValidationRule | undefined | any;
}


/**
* Structure with description of validation error message
Expand Down
6 changes: 3 additions & 3 deletions lib/rules/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = function ({ schema, messages }, path, context) {
sourceCode.push("var parentObj = value;");
sourceCode.push("var parentField = field;");

const keys = Object.keys(subSchema);
const keys = Object.keys(subSchema).filter(key => !this.isMetaKey(key));

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

const labelName = subSchema[property].label;
const label = labelName ? `'${escapeEvalString(labelName)}'` : undefined;

sourceCode.push(`\n// Field: ${escapeEvalString(newPath)}`);
sourceCode.push(`field = parentField ? parentField + "${safeSubName}" : "${name}";`);
sourceCode.push(`value = ${safePropName};`);
Expand All @@ -70,7 +70,7 @@ module.exports = function ({ schema, messages }, path, context) {
sourceCode.push(this.compileRule(rule, context, newPath, innerSource, safePropName));
if (this.opts.haltOnFirstError === true) {
sourceCode.push("if (errors.length) return parentObj;");
}
}
}

// Strict handler
Expand Down
30 changes: 27 additions & 3 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ class Validator {
rule.schema.nullable !== false || rule.schema.type === "forbidden" :
rule.schema.optional === true || rule.schema.nullable === true || rule.schema.type === "forbidden";

const ruleHasDefault = considerNullAsAValue ?
rule.schema.default != undefined && rule.schema.default != null :
const ruleHasDefault = considerNullAsAValue ?
rule.schema.default != undefined && rule.schema.default != null :
rule.schema.default != undefined;

if (ruleHasDefault) {
Expand Down Expand Up @@ -161,6 +161,30 @@ class Validator {
return src.join("\n");
}

/**
* check if the key is a meta key
*
* @param key
* @return {boolean}
*/
isMetaKey(key) {
return key.startsWith("$$");
}
/**
* will remove all "metas" keys (keys starting with $$)
*
* @param obj
*/
removeMetasKeys(obj) {
Object.keys(obj).forEach(key => {
if(!this.isMetaKey(key)) {
return;
}

delete obj[key];
});
}

/**
* Compile a schema
*
Expand Down Expand Up @@ -204,7 +228,7 @@ class Validator {
properties: prevSchema
};

delete prevSchema.$$strict;
this.removeMetasKeys(prevSchema);
}
}

Expand Down
103 changes: 103 additions & 0 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

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

describe("Test flat schema", () => {
const v = new Validator();
Expand Down Expand Up @@ -1445,3 +1446,105 @@ describe("edge cases", () => {
]);
});
});

describe("allow metas starting with $$", () => {
const v = new Validator({ useNewCustomCheckerFunction: true });
describe("test on schema", () => {
it("should not remove keys from source object", async () => {
const schema = {
$$foo: {
foo: "bar"
},
name: { type: "string" } };
const clonedSchema = {...schema};
v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);
});

it("should works with $$root", () => {
const schema = {
$$foo: {
foo: "bar"
},
$$root: true,
type: "email",
empty: true
};
const clonedSchema = {...schema};
const check = v.compile(schema);

expect(check("[email protected]")).toEqual(true);
expect(check("")).toEqual(true);
expect(schema).toStrictEqual(clonedSchema);
});

it("should works with $$async", async () => {
const custom1 = jest.fn().mockResolvedValue("NAME");
const schema = {
$$foo: {
foo: "bar"
},
$$async: true,
name: { type: "string", custom: custom1 },
};
const clonedSchema = {...schema};
const check = v.compile(schema);

//check schema meta was not changed
expect(schema.$$foo).toStrictEqual(clonedSchema.$$foo);

expect(check.async).toBe(true);

let obj = {
id: 3,
name: "John",
username: " john.doe ",
age: 30
};

const res = await check(obj);
expect(res).toBe(true);

expect(custom1).toBeCalledTimes(1);
expect(custom1).toBeCalledWith("John", [], schema.name, "name", null, expect.anything());
});
});

describe("test on rule", () => {
it("should not remove keys from source object", async () => {
const schema = {
name: {
$$foo: {
foo: "bar"
},
type: "string"
}
};
const clonedSchema = {...schema};
v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);
});
it("should works with $$type", async () => {
const schema = {
dot: {
$$foo: {
foo: "bar"
},
$$type: "object",
x: "number", // object props here
y: "number", // object props here
}
};
const clonedSchema = {...schema};
const check = v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);
expect(check({
x: 1,
y: 1,
})).toBeTruthy();
});
});
});
60 changes: 55 additions & 5 deletions test/rules/any.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,39 @@ describe("Test rule: any", () => {
expect(check([])).toEqual(true);
expect(check({})).toEqual(true);
});

it("should allow custom metas", async () => {
const schema = {
$$foo: {
foo: "bar"
},
$$root: true,
type: "any",
optional: true
};
const clonedSchema = {...schema};
const check = v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);

expect(check(null)).toEqual(true);
expect(check(undefined)).toEqual(true);
expect(check(0)).toEqual(true);
expect(check(1)).toEqual(true);
expect(check("")).toEqual(true);
expect(check("true")).toEqual(true);
expect(check("false")).toEqual(true);
expect(check([])).toEqual(true);
expect(check({})).toEqual(true);
});
});

describe("new case (with considerNullAsAValue flag set to true)", () => {
const v = new Validator({considerNullAsAValue: true});

it("should give back true anyway", () => {
const check = v.compile({ $$root: true, type: "any" });

expect(check(null)).toEqual(true);
expect(check(undefined)).toEqual([{ type: "required", actual: undefined, message: "The '' field is required." }]);
expect(check(0)).toEqual(true);
Expand All @@ -57,10 +82,35 @@ describe("Test rule: any", () => {
expect(check([])).toEqual(true);
expect(check({})).toEqual(true);
});

it("should give back true anyway as optional", () => {
const check = v.compile({ $$root: true, type: "any", optional: true });


expect(check(null)).toEqual(true);
expect(check(undefined)).toEqual(true);
expect(check(0)).toEqual(true);
expect(check(1)).toEqual(true);
expect(check("")).toEqual(true);
expect(check("true")).toEqual(true);
expect(check("false")).toEqual(true);
expect(check([])).toEqual(true);
expect(check({})).toEqual(true);
});

it("should allow custom metas", async () => {
const schema = {
$$foo: {
foo: "bar"
},
$$root: true,
type: "any",
optional: true
};
const clonedSchema = {...schema};
const check = v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);

expect(check(null)).toEqual(true);
expect(check(undefined)).toEqual(true);
expect(check(0)).toEqual(true);
Expand All @@ -72,4 +122,4 @@ describe("Test rule: any", () => {
expect(check({})).toEqual(true);
});
});
});
});
30 changes: 30 additions & 0 deletions test/rules/array.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,34 @@ describe("Test rule: array", () => {
});
});
});

it("should allow custom metas", async () => {
const itemSchema = {
$$foo: {
foo: "bar"
},
type: "string",
};
const schema = {
$$foo: {
foo: "bar"
},
$$root: true,
type: "array",
items: itemSchema
};
const clonedSchema = {...schema};
const clonedItemSchema = {...itemSchema};
const check = v.compile(schema);

expect(schema).toStrictEqual(clonedSchema);
expect(itemSchema).toStrictEqual(clonedItemSchema);

expect(check([])).toEqual(true);
expect(check(["human"])).toEqual(true);
expect(check(["male", 3, "female", true])).toEqual([
{ type: "string", field: "[1]", actual: 3, message: "The '[1]' field must be a string." },
{ type: "string", field: "[3]", actual: true, message: "The '[3]' field must be a string." }
]);
});
});
Loading

0 comments on commit 40b64c6

Please sign in to comment.