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

allow to add metas in the schema #341

Merged
merged 8 commits into from
Apr 21, 2024
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
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 @@ -837,9 +837,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 @@ -859,12 +859,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
Loading