├── .eslintignore
├── .gitignore
├── .eslintrc.json
├── .adonisrc.json
├── adonis-typings
├── index.ts
├── validator.ts
├── requests.ts
└── shared.ts
├── japaFile.ts
├── .editorconfig
├── tsconfig.json
├── test
├── messages.spec.ts
├── classValidate.spec.ts
├── cases
│ └── classes.ts
└── validation.spec.ts
├── src
├── index.ts
├── decorators.ts
└── utils.ts
├── package.json
├── providers
└── ClassValidatorProvider.ts
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | app
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | app
4 | .vscode
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:adonis/typescriptPackage", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.adonisrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript": true,
3 | "providers": [
4 | "@adonisjs/core"
5 | ]
6 | }
--------------------------------------------------------------------------------
/adonis-typings/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/japaFile.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import { configure } from "japa";
3 |
4 | /**
5 | * Configure test runner
6 | */
7 | configure({
8 | files: ["test/**/*.spec.ts"],
9 | });
10 |
--------------------------------------------------------------------------------
/adonis-typings/validator.ts:
--------------------------------------------------------------------------------
1 | declare module "@ioc:Adonis/ClassValidator" {
2 | import { ValidateDecorator } from "@ioc:Adonis/ClassValidator/Shared";
3 | import {
4 | rules,
5 | schema,
6 | validator,
7 | ValidationException,
8 | } from "@ioc:Adonis/Core/Validator";
9 |
10 | const validate: ValidateDecorator;
11 |
12 | export { validate, schema, rules, validator, ValidationException };
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.json]
12 | insert_final_newline = ignore
13 |
14 | [**.min.js]
15 | indent_style = ignore
16 | insert_final_newline = ignore
17 |
18 | [MakeFile]
19 | indent_style = space
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig",
3 | "compilerOptions": {
4 | "resolveJsonModule": true,
5 | "target": "es6",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "experimentalDecorators": true,
10 | "emitDecoratorMetadata": true,
11 | "noUnusedLocals": true,
12 | "noImplicitAny": false,
13 | "pretty": true,
14 | "lib": ["esnext"],
15 | "types": ["@types/node", "@adonisjs/core"]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/adonis-typings/requests.ts:
--------------------------------------------------------------------------------
1 | declare module "@ioc:Adonis/Core/Request" {
2 | import { ClassValidatorArg, Class } from "@ioc:Adonis/ClassValidator/Shared";
3 |
4 | interface RequestContract {
5 | /**
6 | * Validate current request using a schema class. The data is
7 | * optional here, since request can pre-fill it for us.
8 | *
9 | * @param validatorClass Class to use foe validation.
10 | * @param args Custom config.
11 | */
12 | classValidate(
13 | validatorClass: Class,
14 | args?: ClassValidatorArg
15 | ): Promise;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/test/messages.spec.ts:
--------------------------------------------------------------------------------
1 | import test from "japa";
2 | import { Assert } from "japa/build/src/Assert";
3 | import { User, NoSchema } from "./cases/classes";
4 | import { getValidatorBag } from "../src/utils";
5 |
6 | test.group("Validation Message", () => {
7 | test("doesn't get message on empty schema", (assert: Assert) => {
8 | assert.deepEqual(getValidatorBag(NoSchema).messages, {});
9 | });
10 |
11 | test("validation messages are generated accurately for nested class messages", (assert: Assert) => {
12 | assert.deepEqual(getValidatorBag(User).messages, {
13 | "profile.url": "Invalid URL specified.",
14 | "addresses.*.point.required": "Field is required.",
15 | "addresses.minLength": "Length must be at least 2.",
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { validate } from "./decorators";
2 | export { getValidatorBag } from "./utils";
3 | import { getValidatorBag } from "./utils";
4 | import { plainToClass } from "class-transformer";
5 | import { schema } from "@adonisjs/validator/build/src/Schema";
6 | import { validator } from "@adonisjs/validator/build/src/Validator";
7 | import { Class, ClassValidatorArg } from "@ioc:Adonis/ClassValidator/Shared";
8 |
9 | export class ClassValidator {
10 | /**
11 | * Validate data using a class schema.
12 | * @param validatorClass Validator class.
13 | * @param data Data to validate.
14 | * @param args Validator config.
15 | * @returns Validated data.
16 | */
17 | public static async validate(
18 | validatorClass: Class,
19 | data: any,
20 | args?: Omit
21 | ): Promise {
22 | const validatorBag = getValidatorBag(validatorClass);
23 |
24 | const validatedData = await validator.validate({
25 | schema: schema.create(validatorBag.schema),
26 | cacheKey: validatorBag.key,
27 | data,
28 | ...args,
29 | });
30 |
31 | return plainToClass(validatorClass, validatedData);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adonis-class-validator",
3 | "version": "0.2.0",
4 | "author": "Tochukwu Nkemdilim",
5 | "license": "MIT",
6 | "description": "Class validator for adonis framework",
7 | "main": "build/providers/ClassValidatorProvider.js",
8 | "keywords": [
9 | "adonis",
10 | "validation",
11 | "schema",
12 | "macros"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/tnkemdilim/adonis-class-validator.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/tnkemdilim/adonis-prometheus/issues"
20 | },
21 | "homepage": "https://github.com/tnkemdilim/adonis-class-validator",
22 | "types": "./build/adonis-typings/index.d.ts",
23 | "files": [
24 | "build/adonis-typings",
25 | "build/providers",
26 | "build/src"
27 | ],
28 | "scripts": {
29 | "pretest": "npm run lint && tsc --noEmit",
30 | "test": "node -r @adonisjs/assembler/build/register japaFile.ts",
31 | "mrm": "mrm --preset=@adonisjs/mrm-preset",
32 | "lint": "eslint . --ext=.ts",
33 | "clean": "rm -rf build",
34 | "compile": "npm run lint && npm run clean && tsc",
35 | "build": "npm run compile",
36 | "prepublishOnly": "npm run test && npm run build"
37 | },
38 | "adonisjs": {
39 | "types": "adonis-class-validator",
40 | "providers": [
41 | "@adonisjs/validator",
42 | "adonis-class-validator"
43 | ]
44 | },
45 | "dependencies": {
46 | "class-transformer": "^0.4.0",
47 | "reflect-metadata": "^0.1.13"
48 | },
49 | "peerDependencies": {
50 | "@adonisjs/validator": "^11.0.1"
51 | },
52 | "devDependencies": {
53 | "@adonisjs/assembler": "^5.3.5",
54 | "@adonisjs/core": "^5.1.6",
55 | "@adonisjs/http-server": "^5.0.0",
56 | "@adonisjs/mrm-preset": "^3.0.0",
57 | "@adonisjs/require-ts": "^2.0.4",
58 | "@adonisjs/validator": "^11.0.4",
59 | "eslint": "^7.26.0",
60 | "eslint-config-prettier": "^8.3.0",
61 | "eslint-plugin-adonis": "^1.3.1",
62 | "japa": "^3.1.1",
63 | "luxon": "^1.27.0",
64 | "mrm": "^3.0.1",
65 | "typescript": "^4.2.4"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/classValidate.spec.ts:
--------------------------------------------------------------------------------
1 | import test from "japa";
2 | import { DateTime } from "luxon";
3 | import { ClassValidator } from "../src";
4 | import { validate } from "../src/decorators";
5 | import { Assert } from "japa/build/src/Assert";
6 | import { AddressPoint, NoSchema } from "./cases/classes";
7 | import { schema } from "@adonisjs/validator/build/src/Schema";
8 | import { ValidationException } from "@adonisjs/validator/build/src/ValidationException";
9 |
10 | test.group("ClassValidator", () => {
11 | test("ClassValidator.validate(...) formats and transforms correctly", async (assert: Assert) => {
12 | const dob = "2001-02-12";
13 | const format = "yyyy-MM-dd";
14 |
15 | class DateSchema {
16 | @validate(schema.date({ format }))
17 | public dob!: DateTime;
18 | }
19 |
20 | const actual = await ClassValidator.validate(DateSchema, { dob });
21 | assert.deepEqual(actual, { dob: DateTime.fromFormat(dob, format) });
22 | });
23 |
24 | test("ClassValidator.validate(...) validates correctly", async (assert: Assert) => {
25 | const expected: AddressPoint = { uniqueId: "hello world" };
26 | const actual = await ClassValidator.validate(AddressPoint, expected);
27 | assert.deepEqual(actual, expected);
28 | });
29 |
30 | test("ClassValidator.validate(...) fails on invalid input validation", async (assert: Assert) => {
31 | assert.plan(1);
32 |
33 | try {
34 | const expected = { uniqueId: 2 };
35 | await ClassValidator.validate(AddressPoint, expected);
36 | } catch (err) {
37 | assert.instanceOf(err, ValidationException);
38 | }
39 | });
40 |
41 | test("ClassValidator.validate(...) fails on empty input validation", async (assert: Assert) => {
42 | assert.plan(1);
43 |
44 | try {
45 | await ClassValidator.validate(AddressPoint, {});
46 | } catch (err) {
47 | assert.instanceOf(err, ValidationException);
48 | }
49 | });
50 |
51 | test("ClassValidator.validate(...) succeeds with empty validation schema", async (assert: Assert) => {
52 | const actual = await ClassValidator.validate(NoSchema, { hello: "hi" });
53 | assert.deepEqual(actual as any, {});
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/adonis-typings/shared.ts:
--------------------------------------------------------------------------------
1 | declare module "@ioc:Adonis/ClassValidator/Shared" {
2 | import {
3 | ArrayType,
4 | BooleanType,
5 | CustomMessages,
6 | DateType,
7 | EnumSetType,
8 | EnumType,
9 | FileType,
10 | NumberType,
11 | ObjectType,
12 | SchemaArray,
13 | SchemaLiteral,
14 | SchemaObject,
15 | StringType,
16 | TypedSchema,
17 | ValidatorNode,
18 | } from "@ioc:Adonis/Core/Validator";
19 |
20 | /**
21 | * Class constructor.
22 | */
23 | export type Class = new (...args: any) => T;
24 |
25 | /**
26 | * Nested schema function.
27 | */
28 | export type NestedSchemaTypeFn = (nested: TypedSchema) => SchemaType;
29 |
30 | /**
31 | * Decorator function.
32 | */
33 | export type DecoratorFn = (target: any, propertyKey: string) => void;
34 |
35 | /**
36 | * Class validator schema.
37 | */
38 | export type ClassValidatorBag = {
39 | messages: CustomMessages;
40 | schema: TypedSchema;
41 | key: string;
42 | };
43 |
44 | /**
45 | * Types that have members.
46 | */
47 | export type MemberType = {
48 | t?: { [x: string]: any } | undefined;
49 | getTree(): SchemaLiteral | SchemaArray | SchemaObject;
50 | };
51 |
52 | /**
53 | * Supported class validation base schemas (leverages default Adonis types).
54 | */
55 | export type SchemaType =
56 | | ReturnType
57 | | ReturnType
58 | | ReturnType
59 | | ReturnType
60 | | ReturnType
61 | | ReturnType
62 | | ReturnType
63 | | ReturnType
64 | | ReturnType
65 | | MemberType;
66 |
67 | /**
68 | * Append an optional field to the type.
69 | */
70 | type WithOptional = Omit & Partial>;
71 |
72 | /**
73 | * Class validator argument
74 | */
75 | export type ClassValidatorArg = Omit<
76 | WithOptional, "data">,
77 | "schema"
78 | >;
79 |
80 | /**
81 | * Validate decorator.
82 | */
83 | export type ValidateDecorator = ((
84 | schema: SchemaType,
85 | messages?: CustomMessages
86 | ) => DecoratorFn) & {
87 | nested: ValidateNestedDecorator;
88 | };
89 |
90 | /**
91 | * Nested validate decorator.
92 | */
93 | export type ValidateNestedDecorator = (
94 | nested: Class,
95 | schema: NestedSchemaTypeFn,
96 | messages?: CustomMessages
97 | ) => DecoratorFn;
98 | }
99 |
--------------------------------------------------------------------------------
/test/cases/classes.ts:
--------------------------------------------------------------------------------
1 | import { validate } from "../../src";
2 | import { rules } from "@adonisjs/validator/build/src/Rules";
3 | import { schema } from "@adonisjs/validator/build/src/Schema";
4 |
5 | export class NoSchema {
6 | public name!: String;
7 | }
8 |
9 | export class AddressPoint {
10 | @validate(schema.string({}, [rules.required()]))
11 | public uniqueId!: String;
12 | }
13 |
14 | export class GrandParentClass {
15 | @validate(schema.number())
16 | public totalSiblings!: number;
17 | }
18 |
19 | export class ParentClass extends GrandParentClass {
20 | @validate(schema.number())
21 | public id!: number;
22 |
23 | @validate(schema.string())
24 | public firstName!: string;
25 |
26 | @validate(schema.string())
27 | public lastName!: string;
28 | }
29 |
30 | export class ChildA extends ParentClass {
31 | @validate(schema.string(), { hello: "required" })
32 | public alias!: string;
33 | }
34 |
35 | export class ChildB extends ParentClass {
36 | @validate(schema.string())
37 | public status!: string;
38 | }
39 |
40 | export class ChildC extends ParentClass {
41 | @validate(schema.string())
42 | public signature!: string;
43 | }
44 |
45 | class Address {
46 | @validate(
47 | schema.string({ escape: true }, [rules.required(), rules.minLength(10)])
48 | )
49 | public street!: String;
50 |
51 | @validate(schema.enum(["nigeria", "france", "canada"], [rules.required()]))
52 | public country: String | undefined;
53 |
54 | @validate(schema.number([rules.required()]))
55 | public zipcode!: String;
56 |
57 | @validate.nested(
58 | AddressPoint,
59 | (addressPoint) => schema.object([rules.required()]).members(addressPoint),
60 | { required: "Field is required." }
61 | )
62 | public point!: AddressPoint;
63 |
64 | @validate.nested(AddressPoint, (addressPoint) =>
65 | schema.object.optional().members(addressPoint)
66 | )
67 | public optionalPoint!: AddressPoint;
68 | }
69 |
70 | export class User {
71 | @validate(schema.string())
72 | public username!: string;
73 |
74 | @validate(schema.string({}, [rules.url()]), { url: "Invalid URL specified." })
75 | public profile!: string;
76 |
77 | @validate.nested(
78 | Address,
79 | (address) =>
80 | schema
81 | .array([rules.minLength(2)])
82 | .members(schema.object().members(address)),
83 | { minLength: "Length must be at least 2." }
84 | )
85 | public addresses!: Address[];
86 |
87 | @validate(schema.date({ format: "yyyy-MM-dd HH:mm:ss" }))
88 | public date!: Date[];
89 | }
90 |
--------------------------------------------------------------------------------
/providers/ClassValidatorProvider.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import { plainToClass } from "class-transformer";
3 | import { ApplicationContract } from "@ioc:Adonis/Core/Application";
4 | import { RequestConstructorContract } from "@ioc:Adonis/Core/Request";
5 | import { Class, ClassValidatorArg } from "@ioc:Adonis/ClassValidator/Shared";
6 |
7 | /*
8 | |--------------------------------------------------------------------------
9 | | Provider
10 | |--------------------------------------------------------------------------
11 | |
12 | | Your application is not ready when this file is loaded by the framework.
13 | | Hence, the top level imports relying on the IoC container will not work.
14 | | You must import them inside the life-cycle methods defined inside
15 | | the provider class.
16 | |
17 | | @example:
18 | |
19 | | public async ready () {
20 | | const Database = (await import('@ioc:Adonis/Lucid/Database')).default
21 | | const Event = (await import('@ioc:Adonis/Core/Event')).default
22 | | Event.on('db:query', Database.prettyPrint)
23 | | }
24 | |
25 | */
26 | export default class ClassValidatorProvider {
27 | public static needsApplication = true;
28 | constructor(protected app: ApplicationContract) {}
29 |
30 | public async boot() {
31 | this.bindClassValidator();
32 | this.registerRequestMacro();
33 | }
34 |
35 | /**
36 | * Bind the class validator to the IOC.
37 | */
38 | private bindClassValidator() {
39 | const adonisValidator = this.app.container.use("Adonis/Core/Validator");
40 |
41 | this.app.container.singleton("Adonis/ClassValidator", () => {
42 | return {
43 | validate: require("../src").validate,
44 | ...adonisValidator,
45 | };
46 | });
47 | }
48 |
49 | /**
50 | * Register the `classValidate(...)` macros to the Request instance.
51 | */
52 | private registerRequestMacro() {
53 | const { schema } = this.app.container.use("Adonis/Core/Validator");
54 |
55 | this.app.container.withBindings(
56 | ["Adonis/Core/Request"],
57 | (request: RequestConstructorContract) => {
58 | const { getValidatorBag } = require("../src");
59 |
60 | request.macro("classValidate", async function classValidate<
61 | T
62 | >(this: any, validatorClass: Class, args?: ClassValidatorArg): Promise {
63 | const validatorBag = getValidatorBag(validatorClass);
64 | const data = await this.validate({
65 | schema: schema.create(validatorBag.schema),
66 | cacheKey: validatorBag.key,
67 | ...args,
68 | });
69 |
70 | return plainToClass(validatorClass, data);
71 | });
72 | }
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/decorators.ts:
--------------------------------------------------------------------------------
1 | import {
2 | nested,
3 | schemaIsDate,
4 | schemaIsArray,
5 | getValidatorBag,
6 | transformMessages,
7 | } from "./utils";
8 | import {
9 | Type as typeAnnotate,
10 | Transform as transformAnnotate,
11 | } from "class-transformer";
12 | import {
13 | Class,
14 | SchemaType,
15 | ValidateDecorator,
16 | NestedSchemaTypeFn,
17 | } from "@ioc:Adonis/ClassValidator/Shared";
18 | import { DateTime } from "luxon";
19 | import { CustomMessages } from "@ioc:Adonis/Core/Validator";
20 |
21 | /**
22 | * Validate a decorated field in a class.
23 | * @param schema Adonis validation schema for field.
24 | * @param messages Custom messages for field
25 | * @returns void
26 | */
27 | export const validate: ValidateDecorator = (
28 | schema: SchemaType,
29 | messages?: CustomMessages
30 | ) => {
31 | return function (target: any, propertyKey: string) {
32 | const validatorBag = getValidatorBag(target);
33 | validatorBag.schema[propertyKey] = schema as any;
34 |
35 | validatorBag.messages = {
36 | ...validatorBag.messages,
37 | ...(messages ? transformMessages(propertyKey, messages) : {}),
38 | };
39 |
40 | if (schemaIsDate(schema)) {
41 | typeAnnotate(() => String)(target, propertyKey);
42 | transformAnnotate(({ value }) => DateTime.fromISO(value))(
43 | target,
44 | propertyKey
45 | );
46 | }
47 | };
48 | };
49 |
50 | /**
51 | * Validated a field with complex type using a custom validator class.
52 | * @param validatorClass Validator class to use for field.
53 | * @param schema Adonis validation schema
54 | * @param messages Custom messages for field.
55 | * @returns void
56 | */
57 | validate.nested = (
58 | validatorClass: Class,
59 | schema: NestedSchemaTypeFn,
60 | messages?: CustomMessages
61 | ) => {
62 | return function (target: any, propertyKey: string) {
63 | const validatorBag = getValidatorBag(target);
64 | const validatorSchema = schema(nested(validatorClass));
65 | validatorBag.schema[propertyKey] = validatorSchema as any;
66 |
67 | const isArray = schemaIsArray(validatorSchema);
68 | const validatorClassMessages = getValidatorBag(validatorClass).messages;
69 |
70 | validatorBag.messages = {
71 | ...validatorBag.messages,
72 | // Messages added directly on field.
73 | ...(messages ? transformMessages(propertyKey, messages) : {}),
74 | // Messages added on nested field with reference to current field.
75 | ...transformMessages(propertyKey, validatorClassMessages, isArray),
76 | };
77 |
78 | // Initialize nested property for type casting to a class.
79 | typeAnnotate(() => validatorClass)(target, propertyKey);
80 | };
81 | };
82 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClassValidatorBag,
3 | Class,
4 | SchemaType,
5 | } from "@ioc:Adonis/ClassValidator/Shared";
6 | import { TypedSchema, CustomMessages } from "@ioc:Adonis/Core/Validator";
7 |
8 | /**
9 | * Get the typed schema of a nested validator class.
10 | * @param nestedClass Validator class
11 | * @returns Typed schema of validator class.
12 | */
13 | export const nested = (nestedClass: Class): TypedSchema =>
14 | getValidatorBag(nestedClass).schema;
15 |
16 | /**
17 | * Get the validation bag of a target.
18 | *
19 | * If none exists, target is initialized with an empty bag.
20 | * @param target Target
21 | * @returns Validation Schema.
22 | */
23 | export const getValidatorBag = (target: any): ClassValidatorBag => {
24 | if (target.constructor.name === "Object") {
25 | return { key: "", messages: {}, schema: {} };
26 | }
27 |
28 | const prototype = target?.prototype || target;
29 | const metadataKey = `@${prototype.constructor.name}.classValidatorBag`;
30 | const metadata = Reflect.getMetadata(metadataKey, prototype);
31 | if (metadata) return metadata;
32 |
33 | const key = `${nonce()}.${prototype.constructor.name}`;
34 | const parentBag = getValidatorBag(Object.getPrototypeOf(target));
35 | const validatorBag = {
36 | schema: Object.assign({}, parentBag.schema),
37 | messages: Object.assign({}, parentBag.messages),
38 | key,
39 | };
40 |
41 | Reflect.defineMetadata(metadataKey, validatorBag, prototype);
42 | return Reflect.getMetadata(metadataKey, prototype);
43 | };
44 |
45 | /**
46 | * Converts messages field name into a format compatible with Adonis.
47 | * @param fieldName Field name
48 | * @param messages Messages rules for the field specified
49 | * @returns Transformed messages
50 | */
51 | export const transformMessages = (
52 | fieldName: string,
53 | messages: CustomMessages,
54 | isArray = false
55 | ) =>
56 | Object.entries(messages).reduce((prev, [key, value]) => {
57 | prev[`${fieldName}${isArray ? ".*." : "."}${key}`] = value;
58 | return prev;
59 | }, {});
60 |
61 | /**
62 | * Check if a schema is an array schema.
63 | * @param schema Schema to check.
64 | * @returns If schema is an array.
65 | */
66 | export const schemaIsArray = (schema: SchemaType) =>
67 | "getTree" in schema && (schema as any).getTree().type === "array";
68 |
69 | /**
70 | * Check if a schema is a date schema.
71 | * @param schema Schema to check.
72 | * @returns If schema is date.
73 | */
74 | export const schemaIsDate = (schema: SchemaType) => {
75 | return "getTree" in schema && (schema as any).getTree().subtype === "date";
76 | };
77 |
78 | /**
79 | * Generate a unique and ever increasing nonce of length 15.
80 | * @returns Unique number
81 | */
82 | export const nonce = ((length: number = 15) => {
83 | let [last, repeat] = [Date.now(), 0];
84 |
85 | return () => {
86 | const now = Math.pow(10, 2) * +new Date();
87 | if (now === last) repeat++;
88 | else [last, repeat] = [now, 0];
89 |
90 | const s = (now + repeat).toString();
91 | return +s.substr(s.length - length);
92 | };
93 | })();
94 |
--------------------------------------------------------------------------------
/test/validation.spec.ts:
--------------------------------------------------------------------------------
1 | import test from "japa";
2 | import { Assert } from "japa/build/src/Assert";
3 | import { getValidatorBag } from "../src/utils";
4 | import { rules } from "@adonisjs/validator/build/src/Rules";
5 | import { schema } from "@adonisjs/validator/build/src/Schema";
6 | import {
7 | User,
8 | NoSchema,
9 | ChildA,
10 | ChildB,
11 | ChildC,
12 | ParentClass,
13 | GrandParentClass,
14 | } from "./cases/classes";
15 |
16 | test.group("Class Validation", () => {
17 | test("doesn't validate on empty schema", (assert: Assert) => {
18 | assert.deepEqual(
19 | schema.create(getValidatorBag(NoSchema).schema),
20 | schema.create({})
21 | );
22 | });
23 |
24 | test("validation rules are correct for inherited classes", (assert: Assert) => {
25 | const grandParentRules = {
26 | totalSiblings: schema.number(),
27 | };
28 |
29 | const parentRules = {
30 | ...grandParentRules,
31 | id: schema.number(),
32 | firstName: schema.string(),
33 | lastName: schema.string(),
34 | };
35 |
36 | assert.deepEqual(
37 | schema.create(getValidatorBag(GrandParentClass).schema),
38 | schema.create(grandParentRules)
39 | );
40 |
41 | assert.deepEqual(
42 | schema.create(getValidatorBag(ParentClass).schema),
43 | schema.create(parentRules)
44 | );
45 |
46 | assert.deepEqual(
47 | schema.create(getValidatorBag(ChildA).schema),
48 | schema.create({
49 | ...parentRules,
50 | alias: schema.string(),
51 | })
52 | );
53 |
54 | assert.deepEqual(
55 | schema.create(getValidatorBag(ChildB).schema),
56 | schema.create({
57 | ...parentRules,
58 | status: schema.string(),
59 | })
60 | );
61 |
62 | assert.deepEqual(
63 | schema.create(getValidatorBag(ChildC).schema),
64 | schema.create({
65 | ...parentRules,
66 | signature: schema.string(),
67 | })
68 | );
69 | });
70 |
71 | test("validation rules are correct for nested class rules", (assert: Assert) => {
72 | assert.deepEqual(
73 | schema.create(getValidatorBag(User).schema),
74 | schema.create({
75 | username: schema.string(),
76 | profile: schema.string({}, [rules.url()]),
77 | addresses: schema.array([rules.minLength(2)]).members(
78 | schema.object().members({
79 | street: schema.string({ escape: true }, [
80 | rules.required(),
81 | rules.minLength(10),
82 | ]),
83 | country: schema.enum(
84 | ["nigeria", "france", "canada"],
85 | [rules.required()]
86 | ),
87 | zipcode: schema.number([rules.required()]),
88 | point: schema.object([rules.required()]).members({
89 | uniqueId: schema.string({}, [rules.required()]),
90 | }),
91 | optionalPoint: schema.object.optional().members({
92 | uniqueId: schema.string({}, [rules.required()]),
93 | }),
94 | })
95 | ),
96 | date: schema.date({ format: "yyyy-MM-dd HH:mm:ss" }),
97 | })
98 | );
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Adonis Class Validator
2 |
3 | Adonis Class Validator provides a means to validate a request data using a class schema.
4 |
5 | On successful validation, the data returned from validation is an instance of the class schema used to validate the request.
6 |
7 | ## 🎁 Features
8 |
9 | - Convenient nesting of class rules.
10 | - Easy declaration of custom messages.
11 | - In-built caching of class schema.
12 | - Validate with existing V5 validator.
13 | - Support for all V5 validator features (`custom messages`, `creating custom rules`, `profiling`, `reporting` etc).
14 |
15 | ## 📦 Installing
16 |
17 | Simply run the following commands on your shell
18 |
19 | ```bash
20 | npm install adonis-class-validator
21 | node ace invoke adonis-class-validator
22 | ```
23 |
24 | ## 📌 Example
25 |
26 | > We're making use of all the schemas and rules baked into Adonis. 😃
27 |
28 | ```ts
29 | // SignupPayload.ts
30 | import { validate, schema, rules } from "@ioc:Adonis/ClassValidator";
31 |
32 | class SignupPayload {
33 | @validate(schema.string({}, [rules.required(), rules.email()]), {
34 | required: "Field {{name}} is required.",
35 | email: "Invalid email address",
36 | })
37 | public email!: string;
38 | }
39 | ```
40 |
41 | ```ts
42 | // SignupController.ts
43 | import { SignupPayload } from "App/Validators";
44 | import { HttpContextContract } from "@ioc:Adonis/Core/HttpContext";
45 |
46 | class SignupController {
47 | public async index({ request }: HttpContextContract) {
48 | const payload = await request.classValidate(SignupPayload);
49 | console.log(payload instanceof SignupPayload); // true
50 | }
51 | }
52 | ```
53 |
54 | > For more examples, check [here](./test/cases/classes.ts)
55 |
56 | ## Standalone Validator
57 |
58 | If you want to make use of the validator class schema to validate any form of data (outside the controller), you can easily rely on the standalone `ClassValidator.validate(...)` helper function.
59 |
60 | > NOTE: If the validation fails, an instance of ValidationException is thrown.
61 |
62 | ```ts
63 | import {
64 | ClassValidator,
65 | ValidationException,
66 | } from "@ioc:Adonis/ClassValidator";
67 |
68 | async function sendEmail() {
69 | try {
70 | const payload = await ClassValidator.validate(SignupPayload, {
71 | email: "hello@stallsone.com",
72 | });
73 | } catch (err) {
74 | // if validation error occurs, `err` is an instance of `ValidationException`
75 | }
76 | }
77 | ```
78 |
79 | ## ⚓️ Going Deeper
80 |
81 | There are currently 2 decorators supported for validation. They include:
82 |
83 | - `@validate()` : To validate primitive schemas such as `string`, `boolean`, `number`, `date`, `enum/enumSet`, `file`, `array([string|boolean|number|date|enum|file])`.
84 | - `@validate.nested()`: To nest class validator schemas through `array` and `object`.
85 |
86 | ### Nested Validation
87 |
88 | To nest a class validator schema, simply rely on the `@validate.nested()` decorator. It requires:
89 |
90 | - The `class validator schema` of the nested field.
91 | - A `callback` that whose:
92 | - `parameter`: is a adonis member equivalent of the validator schema.
93 | - `return type`: is the adonis schema to use to validate the nested field (which is either an `array` or `object`).
94 | - An `optional` custom message object.
95 |
96 | > Custom messages also support interpolation e.g. `Field {{name}} is required`.
97 |
98 | ```ts
99 | import { validate } from "@ioc:Adonis/ClassValidator";
100 |
101 | class Address {
102 | @validate(schema.number([
103 | rules.unique({ table: "users", column: "email" })
104 | ]),
105 | { unique: 'Field must be unique.' })
106 | public id!: number;
107 | }
108 |
109 | class User {
110 | @validate.nested(
111 | Address,
112 | (address) => schema
113 | .array([rules.minLength(2)])
114 | .members(schema.object().members(address)),
115 | { minLength: "Field {{name}} must contain at least 2 addresses." }
116 | )
117 | public addresses!: Address[];
118 | ```
119 |
120 | #### Custom Messages
121 |
122 | When `request.classValidate(...)` is called against the `User` schema [above](#nested-validation), the custom message generated and used for the failed validation will be:
123 |
124 | ```json
125 | {
126 | "addresses.minLength": "Field addresses must contain at least 2 addresses.",
127 | "addresses.*.unique": "Field must be unique."
128 | }
129 | ```
130 |
131 | > As far as the decorated field schema is a `schema.array()` with a `.members(...) of nested validation class`, it infers that it as the deep matching (`.*.`) patter matcher.
132 |
133 | ### Empty Classes
134 |
135 | If no property in a class was decorated with `validate()`, an empty data will be returned (where each field will be undefined).
136 |
137 | ```ts
138 | // Notice there's no schema rule.
139 | class UserPayload {
140 | public firstname!: string;
141 | }
142 |
143 | // UserController.ts
144 | export default class UsersController {
145 | public async index({ request }: HttpContextContract) {
146 | const data = await request.classValidate(UserPayload);
147 |
148 | /**
149 | * Payload wasn't validated because the class doesn't
150 | * have a property decorated with a schema.
151 | */
152 | console.log(data instanceof SignupPayload); // true
153 |
154 | /**
155 | * Data is empty because no property has a validator schema decorator.
156 | */
157 | console.log(payload); // {}
158 | }
159 | }
160 | ```
161 |
162 | ## 📝 Contributing
163 |
164 | If you find any issue, bug or missing feature, please kindly create an issue or submit a pull request.
165 |
166 | ## 🔖 License
167 |
168 | Adonis Class Validator is open-sourced software under MIT license.
169 |
--------------------------------------------------------------------------------