From 116b92fe9820fa0df2ca0954260ec82cba87823a Mon Sep 17 00:00:00 2001 From: liuyi Date: Fri, 23 May 2025 23:07:18 +0800 Subject: [PATCH] add constraint --- package.json | 8 +++- pnpm-lock.yaml | 14 ++++-- .../core/constraints/match.constraint.ts | 38 +++++++++++++++ .../core/constraints/password.constraint.ts | 48 +++++++++++++++++++ .../constraints/phone.number.constraint.ts | 39 +++++++++++++++ 5 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/modules/core/constraints/match.constraint.ts create mode 100644 src/modules/core/constraints/password.constraint.ts create mode 100644 src/modules/core/constraints/phone.number.constraint.ts diff --git a/package.json b/package.json index 1f15413..8088e97 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "rimraf": "^5.0.1", "rxjs": "^7.8.1", "sanitize-html": "^2.17.0", - "typeorm": "^0.3.24" + "typeorm": "^0.3.24", + "validator": "^13.15.0" }, "devDependencies": { "@nestjs/cli": "^10.0.3", @@ -48,6 +49,7 @@ "@types/node": "^20.3.1", "@types/sanitize-html": "^2.16.0", "@types/supertest": "^2.0.12", + "@types/validator": "^13.15.1", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "eslint": "^8.43.0", @@ -77,7 +79,9 @@ "moduleNameMapper": { "^@/(.*)$": "/src/$1" }, - "testMatch": ["/test/**/*.test.ts"], + "testMatch": [ + "/test/**/*.test.ts" + ], "transform": { "^.+\\.(t|j)s$": "ts-jest" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0aef06..81f3223 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: typeorm: specifier: ^0.3.24 version: 0.3.24(better-sqlite3@11.10.0)(reflect-metadata@0.1.14)(ts-node@10.9.2(@swc/core@1.11.24)(@types/node@20.17.46)(typescript@5.1.6)) + validator: + specifier: ^13.15.0 + version: 13.15.0 devDependencies: '@nestjs/cli': specifier: ^10.0.3 @@ -84,6 +87,9 @@ importers: '@types/supertest': specifier: ^2.0.12 version: 2.0.16 + '@types/validator': + specifier: ^13.15.1 + version: 13.15.1 '@typescript-eslint/eslint-plugin': specifier: ^5.60.0 version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6) @@ -860,8 +866,8 @@ packages: '@types/supertest@2.0.16': resolution: {integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==} - '@types/validator@13.15.0': - resolution: {integrity: sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==} + '@types/validator@13.15.1': + resolution: {integrity: sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -4906,7 +4912,7 @@ snapshots: dependencies: '@types/superagent': 8.1.9 - '@types/validator@13.15.0': {} + '@types/validator@13.15.1': {} '@types/yargs-parser@21.0.3': {} @@ -5486,7 +5492,7 @@ snapshots: class-validator@0.14.2: dependencies: - '@types/validator': 13.15.0 + '@types/validator': 13.15.1 libphonenumber-js: 1.12.8 validator: 13.15.0 diff --git a/src/modules/core/constraints/match.constraint.ts b/src/modules/core/constraints/match.constraint.ts new file mode 100644 index 0000000..bc47545 --- /dev/null +++ b/src/modules/core/constraints/match.constraint.ts @@ -0,0 +1,38 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ name: 'isMatch' }) +export class MatchConstraint implements ValidatorConstraintInterface { + validate(value: any, validationArguments?: ValidationArguments): Promise | boolean { + const [relatedProperty, reverse] = validationArguments.constraints; + const relatedValue = (validationArguments.object as any)[relatedProperty]; + return (value === relatedValue) !== reverse; + } + defaultMessage?(validationArguments?: ValidationArguments): string { + const [relatedProperty, reverse] = validationArguments.constraints; + return `${relatedProperty} and ${validationArguments.property} ${ + reverse ? `is` : `don't` + } match`; + } +} + +export function isMatch( + relatedProperty: string, + reverse = false, + validationOptions?: ValidationOptions, +) { + return (object: RecordAny, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [relatedProperty, reverse], + validator: MatchConstraint, + }); + }; +} diff --git a/src/modules/core/constraints/password.constraint.ts b/src/modules/core/constraints/password.constraint.ts new file mode 100644 index 0000000..b7b4ce7 --- /dev/null +++ b/src/modules/core/constraints/password.constraint.ts @@ -0,0 +1,48 @@ +import { + ValidationArguments, + ValidatorConstraintInterface, + ValidationOptions, + registerDecorator, +} from 'class-validator'; + +type ModelType = 1 | 2 | 3 | 4 | 5; + +export class PasswordConstraint implements ValidatorConstraintInterface { + validate(value: any, validationArguments?: ValidationArguments): Promise | boolean { + const validateModel: ModelType = validationArguments.constraints[0] ?? 1; + switch (validateModel) { + case 1: + return /\d/.test(value) && /[A-Za-z]/.test(value); + case 2: + return /\d/.test(value) && /[a-z]/.test(value); + case 3: + return /\d/.test(value) && /[A-Z]/.test(value); + case 4: + return /\d/.test(value) && /[A-Z]/.test(value) && /[a-z]/.test(value); + case 5: + return ( + /\d/.test(value) && + /[A-Z]/.test(value) && + /[a-z]/.test(value) && + /[!@#$%^&]/.test(value) + ); + default: + return /\d/.test(value) && /[A-Za-z]/.test(value); + } + } + defaultMessage?(validationArguments?: ValidationArguments): string { + return "($value) 's format error"; + } +} + +export function IsPassword(model?: ModelType, validationOptions?: ValidationOptions) { + return (object: RecordAny, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [model], + validator: PasswordConstraint, + }); + }; +} diff --git a/src/modules/core/constraints/phone.number.constraint.ts b/src/modules/core/constraints/phone.number.constraint.ts new file mode 100644 index 0000000..fb32af9 --- /dev/null +++ b/src/modules/core/constraints/phone.number.constraint.ts @@ -0,0 +1,39 @@ +import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { isMobilePhone, IsMobilePhoneOptions, MobilePhoneLocale } from 'validator'; + +export function isMatchPhone( + value: any, + locale: MobilePhoneLocale, + options?: IsMobilePhoneOptions, +): boolean { + if (!value) { + return false; + } + const phoneArr: string[] = value.split('.'); + if (phoneArr.length !== 2) { + return false; + } + return isMobilePhone(phoneArr.join(''), locale, options); +} + +export function IsMatchPhone( + locales?: MobilePhoneLocale | MobilePhoneLocale[], + options?: IsMobilePhoneOptions, + validationOptions?: ValidationOptions, +) { + return (object: RecordAny, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [locales || 'any', options], + validator: { + validate: (value: any, args: ValidationArguments): boolean => + isMatchPhone(value, args.constraints[0], args.constraints[1]), + defaultMessage: (_args: ValidationArguments) => + '$property must be a phone number,eg: +86.12345678901', + }, + }); + }; +}