init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) { }
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ContentModule } from './modules/content/content.module';
|
||||
import { CoreModule } from './modules/core/core.module';
|
||||
import { DatabaseModule } from './modules/database/database.module';
|
||||
import { database } from './config';
|
||||
|
||||
@Module({
|
||||
imports: [ContentModule, CoreModule.forRoot(), DatabaseModule.forRoot(database)],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
console.log('hello leon zeng!');
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
import { resolve } from 'path';
|
||||
export const database = (): TypeOrmModuleOptions => ({
|
||||
type: 'better-sqlite3',
|
||||
database: resolve(__dirname, '../../database.db'),
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './database.config';
|
||||
@@ -0,0 +1,9 @@
|
||||
function sum(a: number, b: number): number {
|
||||
return 0;
|
||||
}
|
||||
interface SumFunction extends Function {
|
||||
(a: number, b: number): number;
|
||||
}
|
||||
const mySum = (a: number, b?: number, c?: number): number => {
|
||||
return a + b + c;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
type DecoratorFunc = (target: any, key: string, descriptor: PropertyDescriptor) => void;
|
||||
|
||||
const createDecorator = (decorator: DecoratorFunc) => (Model: any, key: string) => {
|
||||
const target = Model.prototype;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
||||
console.log(descriptor);
|
||||
decorator(target, key, descriptor);
|
||||
};
|
||||
const logger: DecoratorFunc = (target, key, descriptor) => {
|
||||
Object.defineProperty(target, key, {
|
||||
...descriptor,
|
||||
value: async (...args: any[]) => {
|
||||
try {
|
||||
console.log(descriptor, this, args);
|
||||
return descriptor.value.apply(this, args);
|
||||
} finally {
|
||||
const now = new Date().getTime();
|
||||
console.log(`lasted logged in ${now.toString()}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
class User {
|
||||
async login() {
|
||||
console.log('login start');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000);
|
||||
console.log('login ................');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const exp1 = () => {
|
||||
console.log('exp1');
|
||||
const loggerDecorator = createDecorator(logger);
|
||||
loggerDecorator(User, 'login');
|
||||
const user = new User();
|
||||
user.login();
|
||||
console.log('user logged', user);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const HelloDerorator = <T extends new (...args: any[]) => any>(constructor: T) => {
|
||||
return class extends constructor {
|
||||
newProperty = 'new property';
|
||||
hello = 'override';
|
||||
sayHello() {
|
||||
return this.hello;
|
||||
}
|
||||
};
|
||||
};
|
||||
@HelloDerorator
|
||||
export class Hello {
|
||||
[key: string]: any;
|
||||
hello: string;
|
||||
constructor() {
|
||||
this.hello = 'hello';
|
||||
}
|
||||
}
|
||||
|
||||
export const exp2 = () => {
|
||||
const hello = new Hello();
|
||||
console.log(hello.sayHello());
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const SetNameDecorator = (firstName: string, secondName: string) => {
|
||||
const name = `${firstName} ${secondName}`;
|
||||
return <T extends new (...args: any[]) => any>(target: T) => {
|
||||
return class extends target {
|
||||
_name: string = name;
|
||||
getMyname() {
|
||||
return this._name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SetNameDecorator('zeng', 'leon')
|
||||
class UserService {
|
||||
[key: string]: any;
|
||||
c() { }
|
||||
}
|
||||
|
||||
export const exp3 = () => {
|
||||
const userService = new UserService();
|
||||
console.log(userService.getMyname());
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
type UserProfile = Record<string, any> & {
|
||||
phone?: number;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
const ProfileDecorator = (profile: UserProfile) => (target: any) => {
|
||||
const Original = target;
|
||||
let userinfo = '';
|
||||
Object.keys(profile).forEach(key => {
|
||||
userinfo += `${key}: ${profile[key]}\n`;
|
||||
})
|
||||
|
||||
Original.prototype.userinfo = userinfo;
|
||||
function constructor(...args: any[]) {
|
||||
console.log('construct has been called');
|
||||
return new Original(...args);
|
||||
}
|
||||
constructor.prototype = Original.prototype;
|
||||
constructor.myinfo = `myinfo ${userinfo}`
|
||||
return constructor as typeof Original;
|
||||
}
|
||||
|
||||
@ProfileDecorator({
|
||||
phone: 1234567890,
|
||||
address: 'zhongguo'
|
||||
})
|
||||
class User { }
|
||||
|
||||
export const exp4 = () => {
|
||||
console.log("------------exp4-----------")
|
||||
const user = new User();
|
||||
console.log((user as any).userinfo);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
const RoleDecorator = (roles: string[]) => (target: any, key: string) => {
|
||||
if (!target.userRoles) {
|
||||
target.userRoles = [];
|
||||
}
|
||||
console.log("测试编译", key, roles);
|
||||
roles.forEach((role: string) => target.userRoles.push(role));
|
||||
|
||||
}
|
||||
|
||||
const SetRoleDecorator = <T extends new (...args: any[]) => any>(constructor: T) => {
|
||||
const roles = [
|
||||
{ name: 'super-admin', desc: '超级管理员' },
|
||||
{ name: 'admin', desc: '管理员' },
|
||||
{ name: 'user', desc: '普通用户' },
|
||||
]
|
||||
return class extends constructor {
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
this.roles = roles.filter((role) => this.userRoles.includes(role.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SetRoleDecorator
|
||||
class UserEntity {
|
||||
@RoleDecorator(['admin', 'user'])
|
||||
roles: string[] = [];
|
||||
}
|
||||
|
||||
export const exp5 = () => {
|
||||
const user = new UserEntity();
|
||||
console.log(user.roles);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
const loggerDecorator = () => {
|
||||
return function logMethod(target: any, propertyName: string, propertyDescriptor: PropertyDescriptor) {
|
||||
const method = propertyDescriptor.value;
|
||||
|
||||
propertyDescriptor.value = async function (...args: any[]) {
|
||||
try {
|
||||
return method.call(target, ...args);
|
||||
} finally {
|
||||
const now = Date.now();
|
||||
console.log(`lasted logged in ${now.toString()}`);
|
||||
}
|
||||
}
|
||||
return propertyDescriptor
|
||||
}
|
||||
}
|
||||
class UserService {
|
||||
@loggerDecorator()
|
||||
async login() {
|
||||
console.log('login success');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const exp6 = () => {
|
||||
|
||||
const user = new UserService();
|
||||
user.login();
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
export const exp7 = () => {
|
||||
|
||||
const userService = new UserService();
|
||||
userService.delete(1);
|
||||
console.log(userService.getUsers());
|
||||
|
||||
};
|
||||
|
||||
type NewType = {
|
||||
id: number;
|
||||
username: string;
|
||||
};
|
||||
const parseConf: ((...args: any[]) => any)[] = [];
|
||||
|
||||
export const parse =
|
||||
(parseTo: (...args: any[]) => any) =>
|
||||
(target: any, propertyName: string, index: number) => {
|
||||
parseConf[index] = parseTo;
|
||||
};
|
||||
|
||||
|
||||
class UserService {
|
||||
private users: NewType[] = [
|
||||
{ id: 1, username: 'admin' },
|
||||
{ id: 2, username: 'pincman' },
|
||||
];
|
||||
|
||||
getUsers() {
|
||||
return this.users;
|
||||
}
|
||||
|
||||
@parseDecorator
|
||||
delete(@parse((arg: any) => Number(arg)) id: number) {
|
||||
this.users = this.users.filter((userObj) => userObj.id !== id);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 在函数调用前执行格式化操作
|
||||
export const parseDecorator = (
|
||||
target: any,
|
||||
propertyName: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
): PropertyDescriptor => {
|
||||
console.log('开始格式化数据');
|
||||
return {
|
||||
...descriptor,
|
||||
value(...args: any[]) {
|
||||
// 获取格式化后的参数列表
|
||||
const newArgs = args.map((v, i) =>
|
||||
parseConf[i] ? parseConf[i](v) : v,
|
||||
);
|
||||
console.log('格式化完毕');
|
||||
return descriptor.value.apply(this, newArgs);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
export const HiddenDecorator = () => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
console.log(descriptor);
|
||||
descriptor.enumerable = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserEntity {
|
||||
private _nickname: string;
|
||||
// @ts-ignore
|
||||
private fullName: string;
|
||||
|
||||
@HiddenDecorator()
|
||||
@PrefixDecorator('jesse')
|
||||
get nickname(): string {
|
||||
return this._nickname;
|
||||
}
|
||||
|
||||
set nickname(value: string) {
|
||||
this._nickname = value;
|
||||
this.fullName = `${value}-fullname`;
|
||||
}
|
||||
}
|
||||
|
||||
export const exp8 = () => {
|
||||
const user = new UserEntity();
|
||||
user.nickname = 'leon';
|
||||
console.log(user.nickname);
|
||||
console.log(Object.keys(user));
|
||||
}
|
||||
|
||||
function PrefixDecorator(prefix: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
return {
|
||||
...descriptor,
|
||||
set(v) {
|
||||
descriptor.set.apply(this, [`${prefix}_${v}`])
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
|
||||
async function bootstrap() {
|
||||
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
|
||||
cors: true,
|
||||
logger: ['error', 'warn']
|
||||
});
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
await app.listen(3000, () => {
|
||||
console.log('Application is running on: http://localhost:3000');
|
||||
});
|
||||
}
|
||||
bootstrap();
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum PostBodyType {
|
||||
HTML = 'html',
|
||||
MD = 'markdown',
|
||||
}
|
||||
|
||||
export enum PostOrderType {
|
||||
CREATED = 'createdAt',
|
||||
UPDATED = 'updatedAt',
|
||||
PUBLISHED = 'publishedAt',
|
||||
CUSTOM = 'custom'
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PostController } from './controllers/post.controller';
|
||||
import { PostEntity } from './entities/post.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DatabaseModule } from './../database/database.module';
|
||||
import { PostService } from './services/post.service';
|
||||
import { PostRepository } from './repositories/post.repository';
|
||||
import { SanitizeService } from './services/sanitize.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PostEntity]), DatabaseModule.forRepository([PostRepository])],
|
||||
controllers: [PostController],
|
||||
providers: [SanitizeService, PostRepository, PostService],
|
||||
exports: [PostService, DatabaseModule.forRepository([PostRepository])]
|
||||
})
|
||||
export class ContentModule { }
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
|
||||
import { PostService } from '../services/post.service';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(protected service: PostService) { }
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param options
|
||||
*/
|
||||
@Get()
|
||||
async list(@Query() options: PaginateOptions) {
|
||||
return this.service.paginate(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单篇文章
|
||||
* @param id
|
||||
*/
|
||||
@Get(':id')
|
||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.detail(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增文章
|
||||
* @param data
|
||||
*/
|
||||
@Post()
|
||||
async store(@Body() data: Record<string, any>) {
|
||||
return this.service.create(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文章
|
||||
* @param data
|
||||
*/
|
||||
@Patch()
|
||||
async update(@Body() data: Record<string, any>) {
|
||||
return this.service.update(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章
|
||||
* @param id
|
||||
*/
|
||||
@Delete()
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
|
||||
|
||||
@Injectable()
|
||||
export class CreatePostDto {
|
||||
|
||||
@MaxLength(255, {
|
||||
always: true,
|
||||
message: 'Title is too long',
|
||||
})
|
||||
@IsNotEmpty({ groups: ['create'], message: '帖子标题必须填写' })
|
||||
@IsOptional({ groups: ['update'], })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ groups: ['create'], message: '帖子内容必须填写' })
|
||||
@IsOptional({ groups: ['update'] })
|
||||
body: string;
|
||||
|
||||
@MaxLength(500, {
|
||||
always: true,
|
||||
message: 'Summaries is too long',
|
||||
})
|
||||
@IsOptional({ always: true, })
|
||||
summaries?: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// src/modules/content/dtos/update-post.dto.ts
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { IsDefined, IsNumber } from 'class-validator';
|
||||
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
||||
@IsNumber(undefined, { groups: ['update'], message: '帖子ID格式错误' })
|
||||
@IsDefined({ groups: ['update'], message: '帖子ID必须指定' })
|
||||
id: number;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { PostBodyType } from './../constants';
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity('content_posts')
|
||||
export class PostEntity extends BaseEntity {
|
||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: '32' })
|
||||
id: string;
|
||||
|
||||
@Column({ comment: '文章标题' })
|
||||
title: string;
|
||||
|
||||
@Column({ comment: '文章内容', type: 'text' })
|
||||
body: string;
|
||||
|
||||
@Column({ comment: '文章描述', nullable: true })
|
||||
summary: string;
|
||||
|
||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
||||
keywords: string[];
|
||||
|
||||
@Column({ comment: '文章类型', type: 'varchar', default: PostBodyType.MD })
|
||||
type: PostBodyType;
|
||||
|
||||
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
||||
publishedAt: Date | null;
|
||||
|
||||
@Column({ comment: '自定义文章排序', default: 0 })
|
||||
customOrder: number
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@CreateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||
import { PostEntity } from './../entities/post.entity';
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
@CustomRepository(PostEntity)
|
||||
export class PostRepository extends Repository<PostEntity> {
|
||||
buildBaseQB() {
|
||||
return this.createQueryBuilder('post');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { isFunction, isNil, omit } from 'lodash';
|
||||
import { PostOrderType } from './../constants';
|
||||
import { PaginateOptions, QueryHook } from '@/modules/database/types';
|
||||
import { PostRepository } from './../repositories/post.repository';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PostEntity } from '../entities/post.entity';
|
||||
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
||||
import { paginate } from '@/modules/database/helpers';
|
||||
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
constructor(protected repository: PostRepository) { }
|
||||
/**
|
||||
* 获取分页数据
|
||||
* @param options 分页选项
|
||||
* @param callback 添加额外的查询
|
||||
*/
|
||||
async paginate(options: PaginateOptions, callback?: QueryHook<PostEntity>) {
|
||||
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
|
||||
return paginate(qb, options);
|
||||
}
|
||||
/**
|
||||
* 查询单篇文章
|
||||
* @param id 文章id
|
||||
* @param callback 额外的查询
|
||||
*/
|
||||
async detail(id: string, callback?: QueryHook<PostEntity>) {
|
||||
let qb = this.repository.buildBaseQB();
|
||||
qb.where(`post.id = :id`, { id });
|
||||
qb = !isNil(callback) && isFunction(callback) ? await callback(qb) : qb;
|
||||
const item = await qb.getOne();
|
||||
if (!item) {
|
||||
throw new EntityNotFoundError(PostEntity, `Post with id ${id} not found`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
/**
|
||||
* 创建文章
|
||||
* @param data 文章
|
||||
*/
|
||||
async create(data: Record<string, any>) {
|
||||
const item = await this.repository.save(data);
|
||||
return this.detail(item.id)
|
||||
}
|
||||
/**
|
||||
* 更新文章
|
||||
* @param data
|
||||
*/
|
||||
async update(data: Record<string, any>) {
|
||||
await this.repository.update(data.id, omit(data, ['id']));
|
||||
return this.detail(data.id);
|
||||
}
|
||||
/**
|
||||
* 删除文章
|
||||
* @param id
|
||||
*/
|
||||
async delete(id: string) {
|
||||
const item = await this.repository.findOneByOrFail({ id });
|
||||
return this.repository.remove(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建文章列表查询器
|
||||
* @param qb 初始查询构造器
|
||||
* @param options 排查分页选项后的查询选项
|
||||
* @param callback 添加额外的查询
|
||||
*/
|
||||
buildListQuery(qb: SelectQueryBuilder<PostEntity>, options: Record<string, any>, callback: QueryHook<PostEntity>) {
|
||||
const { orderBy, isPublished } = options;
|
||||
if (typeof isPublished === 'boolean') {
|
||||
isPublished ? qb.where({ publishedAt: Not(IsNull()) }) : qb.where({ publishedAt: IsNull() });
|
||||
}
|
||||
|
||||
this.queryOrderBy(qb, orderBy);
|
||||
if (callback) return callback(qb);
|
||||
return qb;
|
||||
}
|
||||
/**
|
||||
* 对文章进行排序的query构建
|
||||
* @param qb 查询构造器
|
||||
* @param orderBy 排序方式
|
||||
*/
|
||||
queryOrderBy(qb: SelectQueryBuilder<PostEntity>, orderBy?: PostOrderType) {
|
||||
switch (orderBy) {
|
||||
case PostOrderType.CREATED:
|
||||
qb.orderBy('createdAt', 'DESC');
|
||||
break;
|
||||
case PostOrderType.UPDATED:
|
||||
qb.orderBy('updatedAt', 'DESC');
|
||||
break;
|
||||
case PostOrderType.PUBLISHED:
|
||||
qb.orderBy('publishedAt', 'DESC');
|
||||
break;
|
||||
case PostOrderType.CUSTOM:
|
||||
qb.orderBy('customOrder', 'DESC');
|
||||
break;
|
||||
default:
|
||||
qb.orderBy('createdAt', 'DESC');
|
||||
qb.addOrderBy('updatedAt', 'DESC');
|
||||
qb.addOrderBy('publishedAt', 'DESC');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { deepMerge } from "@/modules/core/helpers";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
@Injectable()
|
||||
export class SanitizeService {
|
||||
protected config: sanitizeHtml.IOptions = {};
|
||||
constructor() {
|
||||
this.config = {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'code']),
|
||||
allowedAttributes: {
|
||||
...sanitizeHtml.defaults.allowedAttributes,
|
||||
'*': ['class', 'style', 'height', 'width'],
|
||||
},
|
||||
parser: {
|
||||
lowerCaseTags: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sanitize(body: string, options?: sanitizeHtml.IOptions) {
|
||||
return sanitizeHtml(body, deepMerge(this.config, options ?? {}, 'replace'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PostBodyType } from './../constants';
|
||||
import { DataSource, EventSubscriber } from "typeorm";
|
||||
import { SanitizeService } from "../services/sanitize.service";
|
||||
import { PostRepository } from "../repositories/post.repository";
|
||||
import { PostEntity } from "../entities/post.entity";
|
||||
|
||||
@EventSubscriber()
|
||||
export class PostSubscriber {
|
||||
constructor(protected dataSource: DataSource, protected sanitizeService: SanitizeService, protected postRepository: PostRepository) {
|
||||
dataSource.subscribers.push(this);
|
||||
}
|
||||
|
||||
listenTo() {
|
||||
return PostEntity;
|
||||
}
|
||||
|
||||
async afterLoad(entity: PostEntity) {
|
||||
if (entity.type === PostBodyType.HTML) {
|
||||
entity.body = this.sanitizeService.sanitize(entity.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module, DynamicModule } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class CoreModule {
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: CoreModule,
|
||||
global: true,
|
||||
providers: [],
|
||||
exports: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './utils';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { isNil } from 'lodash';
|
||||
import deepmerge from 'deepmerge'
|
||||
export function toBoolean(value?: string | boolean): boolean {
|
||||
if (isNil(value)) return false;
|
||||
if (typeof value === 'boolean') return value;
|
||||
try {
|
||||
return JSON.parse(value.toLowerCase());
|
||||
} catch (error) {
|
||||
return value as unknown as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export function toNull(value?: string | null): string | null | undefined {
|
||||
return value === 'null' ? null : value;
|
||||
}
|
||||
|
||||
export const deepMerge = <T1, T2>(x: Partial<T1>, y: Partial<T2>, arrayMode: 'replace' | 'merge' = 'merge') => {
|
||||
const options: deepmerge.Options = {};
|
||||
if (arrayMode === 'replace') {
|
||||
options.arrayMerge = (_d, s, _o) => s;
|
||||
} else if (arrayMode === 'merge') {
|
||||
options.arrayMerge = (_d, s, _o) => Array.from(new Set([..._d, ...s]));
|
||||
}
|
||||
return deepmerge(x, y, options) as T2 extends T1 ? T1 : T1 & T2;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
||||
import { TypeOrmModule, TypeOrmModuleOptions, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { CUSTOM_REPOSITORY_METADATA } from './constants';
|
||||
import { DataSource, ObjectType } from 'typeorm';
|
||||
|
||||
@Module({})
|
||||
export class DatabaseModule {
|
||||
static forRoot(configRegister: () => TypeOrmModuleOptions): DynamicModule {
|
||||
return {
|
||||
module: DatabaseModule,
|
||||
global: true,
|
||||
imports: [TypeOrmModule.forRoot(configRegister())]
|
||||
};
|
||||
}
|
||||
|
||||
static forRepository<T extends Type<any>>(
|
||||
repositories: T[], dataSourceName?: string
|
||||
): DynamicModule {
|
||||
const providers: Provider[] = [];
|
||||
|
||||
for (const Repository of repositories) {
|
||||
const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repository);
|
||||
if (!entity) {
|
||||
continue;
|
||||
}
|
||||
providers.push({
|
||||
inject: [getDataSourceToken(dataSourceName)],
|
||||
provide: Repository,
|
||||
useFactory: (dataSource: DataSource): InstanceType<typeof Repository> => {
|
||||
const base = dataSource.getRepository<ObjectType<any>>(entity);
|
||||
return new Repository(base.target, base.manager, base.queryRunner);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
module: DatabaseModule,
|
||||
global: true,
|
||||
providers
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CUSTOM_REPOSITORY_METADATA } from './../constants';
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { ObjectType } from 'typeorm';
|
||||
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
|
||||
SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isNil } from 'lodash';
|
||||
import { PaginateOptions, PaginateResult } from './types';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
export const paginate = async<E extends ObjectLiteral>(qb: SelectQueryBuilder<E>, options: PaginateOptions): Promise<PaginateResult<E>> => {
|
||||
const limit = isNil(options.limit) || options.limit < 1 ? 1 : options.limit;
|
||||
const page = isNil(options.page) || options.page < 1 ? 1 : options.page;
|
||||
const start = page >= 1 ? page - 1 : 0;
|
||||
const totalItems = await qb.getCount();
|
||||
qb.take(limit).skip(start * limit);
|
||||
const items = await qb.getMany();
|
||||
const totalPages = totalItems % limit === 0
|
||||
? Math.floor(totalItems / limit)
|
||||
: Math.floor(totalItems / limit) + 1;
|
||||
const remainder = totalItems % limit !== 0 ? totalItems % limit : limit;
|
||||
const itemCount = page < totalPages ? limit : remainder;
|
||||
return {
|
||||
data: items,
|
||||
meta: {
|
||||
totalItems,
|
||||
itemCount,
|
||||
perPage: limit,
|
||||
totalPages,
|
||||
currentPage: page,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
||||
|
||||
export type QueryHook<Entity> = (
|
||||
qb: SelectQueryBuilder<Entity>
|
||||
) => Promise<SelectQueryBuilder<Entity>>;
|
||||
|
||||
export interface PaginateMeta {
|
||||
/**
|
||||
* 当前页项目数量
|
||||
*/
|
||||
itemCount: number;
|
||||
/**
|
||||
* 项目总数量
|
||||
*/
|
||||
totalItems?: number;
|
||||
/**
|
||||
* 每页显示数量
|
||||
*/
|
||||
perPage: number;
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
totalPages?: number;
|
||||
/**
|
||||
* 当前页数
|
||||
*/
|
||||
currentPage: number;
|
||||
|
||||
}
|
||||
|
||||
export interface PaginateOptions {
|
||||
/**
|
||||
* 当前页数
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* 每页显示数量
|
||||
*/
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface PaginateResult<T extends ObjectLiteral> {
|
||||
data: T[];
|
||||
meta: PaginateMeta;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
interface SquareConfig {
|
||||
color?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function createSquare(config: SquareConfig): { color: string; area: number } {
|
||||
return {
|
||||
color: config.color || 'red',
|
||||
area: config.width ? config.width * config.width : 20
|
||||
};
|
||||
}
|
||||
let config = { colour: 'red', width: 100 };
|
||||
|
||||
let mySquare = createSquare(config);
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
declare type RecordAny = Record<string, any>;
|
||||
declare type RecordNever = Record<never, never>;
|
||||
declare type RecordAnyOrNever = RecordAny | RecordNever;
|
||||
|
||||
/**
|
||||
* 基础类型接口
|
||||
*/
|
||||
declare type BaseType = boolean | number | string | undefined | null;
|
||||
|
||||
/**
|
||||
* 环境变量类型转义函数接口
|
||||
*/
|
||||
declare type ParseType<T extends BaseType = string> = (value: string) => T;
|
||||
|
||||
/**
|
||||
* 一个类的类型
|
||||
*/
|
||||
declare type ClassType<T> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* 嵌套对象全部可选
|
||||
*/
|
||||
declare type RePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[] | undefined
|
||||
? RePartial<U>[]
|
||||
: T[P] extends object | undefined
|
||||
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
|
||||
? T[P]
|
||||
: RePartial<T[P]>
|
||||
: T[P];
|
||||
}
|
||||
|
||||
/**
|
||||
* 防止swc下循环依赖报错
|
||||
*/
|
||||
declare type WrapperType<T> = T;
|
||||
Reference in New Issue
Block a user