Compare commits
4 Commits
778248b16f
...
764fe06c50
Author | SHA1 | Date | |
---|---|---|---|
764fe06c50 | |||
116b92fe98 | |||
067ed5d40e | |||
91dcac731d |
@ -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": {
|
||||
"^@/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"testMatch": ["<rootDir>/test/**/*.test.ts"],
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*.test.ts"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { APP_PIPE } from '@nestjs/core';
|
||||
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
|
||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||
|
||||
import { database } from './config';
|
||||
|
||||
import { DEFAULT_VALIDATION_CONFIG } from './modules/content/constants';
|
||||
import { ContentModule } from './modules/content/content.module';
|
||||
import { CoreModule } from './modules/core/core.module';
|
||||
import { AppFilter } from './modules/core/providers/app.filter';
|
||||
import { AppPipe } from './modules/core/providers/app.pipe';
|
||||
import { DatabaseModule } from './modules/database/database.module';
|
||||
|
||||
@ -17,6 +20,14 @@ import { DatabaseModule } from './modules/database/database.module';
|
||||
provide: APP_PIPE,
|
||||
useValue: new AppPipe(DEFAULT_VALIDATION_CONFIG),
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AppInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AppFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@ -9,15 +9,11 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||
|
||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '../dtos/category.dto';
|
||||
import { CategoryService } from '../services';
|
||||
|
||||
@UseInterceptors(AppInterceptor)
|
||||
@Controller('category')
|
||||
export class CategoryController {
|
||||
constructor(protected service: CategoryService) {}
|
||||
|
@ -8,16 +8,12 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||
|
||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '../dtos/comment.dto';
|
||||
import { CommentService } from '../services';
|
||||
|
||||
@Controller('comment')
|
||||
@UseInterceptors(AppInterceptor)
|
||||
export class CommentController {
|
||||
constructor(protected service: CommentService) {}
|
||||
|
||||
|
@ -9,14 +9,11 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
|
||||
import { PostService } from '@/modules/content/services/post.service';
|
||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||
|
||||
@UseInterceptors(AppInterceptor)
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(private postService: PostService) {}
|
||||
|
@ -9,16 +9,12 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||
|
||||
import { CreateTagDto, QueryTagDto, UpdateTagDto } from '../dtos/tag.dto';
|
||||
import { TagService } from '../services';
|
||||
|
||||
@Controller('tag')
|
||||
@UseInterceptors(AppInterceptor)
|
||||
export class TagController {
|
||||
constructor(protected service: TagService) {}
|
||||
|
||||
|
69
src/modules/core/constraints/data.exist.constraint.ts
Normal file
69
src/modules/core/constraints/data.exist.constraint.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
ValidationOptions,
|
||||
registerDecorator,
|
||||
} from 'class-validator';
|
||||
import { ObjectType, Repository, DataSource } from 'typeorm';
|
||||
|
||||
type Condition = {
|
||||
entity: ObjectType<any>;
|
||||
map?: string;
|
||||
};
|
||||
|
||||
@ValidatorConstraint({ name: 'dataExist', async: true })
|
||||
@Injectable()
|
||||
export class DataExistConstraint implements ValidatorConstraintInterface {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async validate(value: any, validationArguments?: ValidationArguments) {
|
||||
let repo: Repository<any>;
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
let map = 'id';
|
||||
if ('entity' in validationArguments.constraints[0]) {
|
||||
map = validationArguments.constraints[0].map ?? 'id';
|
||||
repo = this.dataSource.getRepository(validationArguments.constraints[0].entitiy);
|
||||
} else {
|
||||
repo = this.dataSource.getRepository(validationArguments.constraints[0]);
|
||||
}
|
||||
const item = await repo.findOne({ where: { [map]: value } });
|
||||
return !!item;
|
||||
}
|
||||
defaultMessage?(validationArguments?: ValidationArguments): string {
|
||||
if (!validationArguments.constraints[0]) {
|
||||
return 'Model not been specified!';
|
||||
}
|
||||
return `All instance of ${validationArguments.constraints[0].name} must been exists in databse!`;
|
||||
}
|
||||
}
|
||||
|
||||
function IsDataExist(
|
||||
entity: ObjectType<any>,
|
||||
validationOptions?: ValidationOptions,
|
||||
): (object: RecordAny, propertyName: string) => void;
|
||||
|
||||
function IsDataExist(
|
||||
condition: Condition,
|
||||
validationOptions?: ValidationOptions,
|
||||
): (object: RecordAny, propertyName: string) => void;
|
||||
|
||||
function IsDataExist(
|
||||
condition: Condition | ObjectType<any>,
|
||||
validationOptions?: ValidationOptions,
|
||||
): (object: RecordAny, propertyName: string) => void {
|
||||
return (object: RecordAny, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [condition],
|
||||
validator: DataExistConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { IsDataExist };
|
38
src/modules/core/constraints/match.constraint.ts
Normal file
38
src/modules/core/constraints/match.constraint.ts
Normal file
@ -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> | 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,
|
||||
});
|
||||
};
|
||||
}
|
48
src/modules/core/constraints/password.constraint.ts
Normal file
48
src/modules/core/constraints/password.constraint.ts
Normal file
@ -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> | 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,
|
||||
});
|
||||
};
|
||||
}
|
39
src/modules/core/constraints/phone.number.constraint.ts
Normal file
39
src/modules/core/constraints/phone.number.constraint.ts
Normal file
@ -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',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
68
src/modules/core/constraints/unique.constraint.ts
Normal file
68
src/modules/core/constraints/unique.constraint.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
import { isNil, merge } from 'lodash';
|
||||
import { DataSource, ObjectType } from 'typeorm';
|
||||
|
||||
type Condition = {
|
||||
entity: ObjectType<any>;
|
||||
property?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ValidatorConstraint({ name: 'dataUnique', async: true })
|
||||
export class UniqueConstraint implements ValidatorConstraintInterface {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async validate(value: any, validationArguments?: ValidationArguments): Promise<boolean> {
|
||||
const config: Omit<Condition, 'entity'> = { property: validationArguments.property };
|
||||
const condition = ('entity' in validationArguments.constraints[0]
|
||||
? merge(config, validationArguments.constraints[0])
|
||||
: {
|
||||
...config,
|
||||
entity: validationArguments.constraints[0],
|
||||
}) as unknown as Required<Condition>;
|
||||
if (!condition.entity) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const repo = this.dataSource.getRepository(condition.entity);
|
||||
return isNil(
|
||||
await repo.findOne({ where: { [condition.property]: value }, withDeleted: true }),
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
defaultMessage?(validationArguments?: ValidationArguments): string {
|
||||
const { entity, property } = validationArguments.constraints[0];
|
||||
const queryProperty = property ?? validationArguments.property;
|
||||
if (!(validationArguments.object as any).getManager) {
|
||||
return 'getManager function not been found!';
|
||||
}
|
||||
if (!entity) {
|
||||
return 'Model not been specified!';
|
||||
}
|
||||
return `${queryProperty} of ${entity.name} must been unique!`;
|
||||
}
|
||||
}
|
||||
|
||||
export function IsUnique(
|
||||
params: ObjectType<any> | Condition,
|
||||
validationOptions: ValidationOptions,
|
||||
) {
|
||||
return (object: RecordAny, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [params],
|
||||
validator: UniqueConstraint,
|
||||
});
|
||||
};
|
||||
}
|
38
src/modules/core/providers/app.filter.ts
Normal file
38
src/modules/core/providers/app.filter.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { ArgumentsHost, HttpException, HttpStatus, Type } from '@nestjs/common';
|
||||
import { BaseExceptionFilter } from '@nestjs/core';
|
||||
import { isObject } from 'lodash';
|
||||
import { EntityNotFoundError, EntityPropertyNotFoundError, QueryFailedError } from 'typeorm';
|
||||
|
||||
export class AppFilter<T = Error> extends BaseExceptionFilter<T> {
|
||||
protected resExceptions: Array<{ class: Type<Error>; status?: number } | Type<Error>> = [
|
||||
{ class: EntityNotFoundError, status: HttpStatus.NOT_FOUND },
|
||||
{ class: QueryFailedError, status: HttpStatus.BAD_REQUEST },
|
||||
{ class: EntityPropertyNotFoundError, status: HttpStatus.BAD_REQUEST },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
catch(exception: T, host: ArgumentsHost) {
|
||||
const applicationRef =
|
||||
this.applicationRef || (this.httpAdapterHost && this.httpAdapterHost.httpAdapter)!;
|
||||
const resException = this.resExceptions.find((item) =>
|
||||
'class' in item ? exception instanceof item.class : exception instanceof item,
|
||||
);
|
||||
if (!resException && !(exception instanceof HttpException)) {
|
||||
return this.handleUnknownError(exception, host, applicationRef);
|
||||
}
|
||||
let res: string | object = '';
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
if (exception instanceof HttpException) {
|
||||
res = exception.getResponse();
|
||||
status = exception.getStatus();
|
||||
} else if (resException) {
|
||||
const e = exception as unknown as Error;
|
||||
res = e.message;
|
||||
if ('class' in resException && resException.status) {
|
||||
status = resException.status;
|
||||
}
|
||||
}
|
||||
const message = isObject(res) ? res : { statusCode: status, message: res };
|
||||
applicationRef!.reply(host.getArgByIndex(1), message, status);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user