Compare commits
No commits in common. "d61527709de0145baaee7501857be1382ad0514a" and "d5507d551041e723d3cfd268bd6a89d08871d9f1" have entirely different histories.
d61527709d
...
d5507d5510
10
.eslintrc.js
10
.eslintrc.js
@ -11,7 +11,13 @@ module.exports = {
|
|||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint', 'jest', 'prettier', 'import', 'unused-imports'],
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'jest',
|
||||||
|
'prettier',
|
||||||
|
'import',
|
||||||
|
'unused-imports',
|
||||||
|
],
|
||||||
extends: [
|
extends: [
|
||||||
// airbnb规范
|
// airbnb规范
|
||||||
// https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb
|
// https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb
|
||||||
@ -96,7 +102,7 @@ module.exports = {
|
|||||||
// https://github.com/sweepline/eslint-plugin-unused-imports
|
// https://github.com/sweepline/eslint-plugin-unused-imports
|
||||||
'unused-imports/no-unused-imports': 1,
|
'unused-imports/no-unused-imports': 1,
|
||||||
'unused-imports/no-unused-vars': [
|
'unused-imports/no-unused-vars': [
|
||||||
'warn',
|
'error',
|
||||||
{
|
{
|
||||||
vars: 'all',
|
vars: 'all',
|
||||||
args: 'none',
|
args: 'none',
|
||||||
|
16
package.json
16
package.json
@ -23,19 +23,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.0.3",
|
"@nestjs/common": "^10.0.3",
|
||||||
"@nestjs/core": "^10.0.3",
|
"@nestjs/core": "^10.0.3",
|
||||||
"@nestjs/platform-fastify": "^10.0.3",
|
"@nestjs/platform-express": "^10.0.3",
|
||||||
"@nestjs/swagger": "^7.4.2",
|
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
|
||||||
"better-sqlite3": "^11.10.0",
|
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.2",
|
|
||||||
"deepmerge": "^4.3.1",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1"
|
||||||
"sanitize-html": "^2.17.0",
|
|
||||||
"typeorm": "^0.3.24"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.3",
|
"@nestjs/cli": "^10.0.3",
|
||||||
@ -43,10 +34,9 @@
|
|||||||
"@nestjs/testing": "^10.0.3",
|
"@nestjs/testing": "^10.0.3",
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.62",
|
||||||
"@swc/core": "^1.3.66",
|
"@swc/core": "^1.3.66",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "29.5.2",
|
"@types/jest": "29.5.2",
|
||||||
"@types/lodash": "^4.17.16",
|
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
"@typescript-eslint/parser": "^5.60.0",
|
||||||
|
1125
pnpm-lock.yaml
1125
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
23
src/app.controller.spec.ts
Normal file
23
src/app.controller.spec.ts
Normal file
@ -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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
13
src/app.controller.ts
Normal file
13
src/app.controller.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { database } from './config';
|
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';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ContentModule, CoreModule.forRoot(), DatabaseModule.forRoot(database)],
|
imports: [],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
export const database = (): TypeOrmModuleOptions => ({
|
|
||||||
charset: 'utf8mb4',
|
|
||||||
logging: ['error'],
|
|
||||||
type: 'mysql',
|
|
||||||
host: '192.168.50.26',
|
|
||||||
port: 3306,
|
|
||||||
username: '3r',
|
|
||||||
password: '12345678',
|
|
||||||
database: '3r',
|
|
||||||
synchronize: true,
|
|
||||||
autoLoadEntities: true,
|
|
||||||
});
|
|
@ -1 +0,0 @@
|
|||||||
export * from './database.config';
|
|
13
src/main.ts
13
src/main.ts
@ -1,18 +1,9 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
|
||||||
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
|
const app = await NestFactory.create(AppModule);
|
||||||
cors: true,
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
logger: ['error', 'warn'],
|
|
||||||
});
|
|
||||||
app.setGlobalPrefix('api');
|
|
||||||
await app.listen(process.env.PORT ?? 3000, () => {
|
|
||||||
console.log('api: http://localhost:3000');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
export enum PostBodyType {
|
|
||||||
HTML = 'html',
|
|
||||||
MD = 'markdown',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PostOrder {
|
|
||||||
CREATED = 'createdAt',
|
|
||||||
UPDATED = 'updatedAt',
|
|
||||||
PUBLISHED = 'publishedAt',
|
|
||||||
CUSTOM = 'custom',
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
|
||||||
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
|
||||||
import { SanitizeService } from '@/modules/content/services/SanitizeService';
|
|
||||||
import { PostService } from '@/modules/content/services/post.service';
|
|
||||||
|
|
||||||
import { PostSubscriber } from '@/modules/content/subscribers/post.subscriber';
|
|
||||||
import { DatabaseModule } from '@/modules/database/database.module';
|
|
||||||
|
|
||||||
import { PostController } from './controllers/post.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([PostEntity]),
|
|
||||||
DatabaseModule.forRepository([PostRepository]),
|
|
||||||
],
|
|
||||||
controllers: [PostController],
|
|
||||||
providers: [PostService, PostSubscriber, SanitizeService],
|
|
||||||
exports: [PostService, DatabaseModule.forRepository([PostRepository])],
|
|
||||||
})
|
|
||||||
export class ContentModule {}
|
|
@ -1,50 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { PostService } from '@/modules/content/services/post.service';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
@Controller('posts')
|
|
||||||
export class PostController {
|
|
||||||
constructor(private postService: PostService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async list(@Query() options: PaginateOptions) {
|
|
||||||
return this.postService.paginate(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
async show(@Param('id', new ParseUUIDPipe()) id: string) {
|
|
||||||
return this.postService.detail(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: RecordAny,
|
|
||||||
) {
|
|
||||||
return this.postService.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
data: RecordAny,
|
|
||||||
) {
|
|
||||||
return this.postService.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
|
||||||
return this.postService.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
|
|
||||||
import {
|
|
||||||
IsBoolean,
|
|
||||||
IsDefined,
|
|
||||||
IsEnum,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
IsUUID,
|
|
||||||
MaxLength,
|
|
||||||
Min,
|
|
||||||
ValidateIf,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
import { isNil, toNumber } from 'lodash';
|
|
||||||
|
|
||||||
import { PostOrder } from '@/modules/content/constants';
|
|
||||||
import { toBoolean } from '@/modules/core/helpers';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
export class QueryPostDto implements PaginateOptions {
|
|
||||||
@Transform(({ value }) => toBoolean(value))
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
isPublished?: boolean;
|
|
||||||
|
|
||||||
@IsEnum(PostOrder, { message: `` })
|
|
||||||
@IsOptional()
|
|
||||||
orderBy: PostOrder;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
page = 1;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
limit = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CreatePostDto {
|
|
||||||
@MaxLength(255, {
|
|
||||||
always: true,
|
|
||||||
message: 'The maximum length of the article title is $constraint1',
|
|
||||||
})
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: 'The article title must be filled in.' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: 'The content of the article must be filled in.' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@MaxLength(500, {
|
|
||||||
always: true,
|
|
||||||
message: 'The maximum length of the article description is $constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
summary?: string;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toBoolean(value))
|
|
||||||
@IsBoolean({ always: true })
|
|
||||||
@ValidateIf((value) => !isNil(value.publish))
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
publish?: boolean;
|
|
||||||
|
|
||||||
@MaxLength(20, {
|
|
||||||
always: true,
|
|
||||||
each: true,
|
|
||||||
message: 'The maximum length of each keyword is $constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
keywords?: string[];
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(0, { message: 'The sorted value must be greater than 0.' })
|
|
||||||
@IsNumber(undefined, { always: true })
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
customOrder?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
|
||||||
@IsUUID(undefined, {
|
|
||||||
groups: ['update'],
|
|
||||||
message: 'The format of the article ID is incorrect.',
|
|
||||||
})
|
|
||||||
@IsDefined({ groups: ['update'], message: 'The article ID must be specified' })
|
|
||||||
id: string;
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import { Expose } from 'class-transformer';
|
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostBodyType } from '@/modules/content/constants';
|
|
||||||
|
|
||||||
@Entity('content_posts')
|
|
||||||
export class PostEntity extends BaseEntity {
|
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ comment: '文章标题' })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@Column({ comment: '文章内容', type: 'text' })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@Column({ comment: '文章描述', nullable: true })
|
|
||||||
summary?: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
|
||||||
keywords?: [];
|
|
||||||
|
|
||||||
@Column({ comment: '文章类型', type: 'enum', enum: PostBodyType })
|
|
||||||
type: PostBodyType;
|
|
||||||
|
|
||||||
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
|
||||||
publishedAt?: Date | null;
|
|
||||||
|
|
||||||
@Column({ comment: '自定义文章排序', default: 0 })
|
|
||||||
customOrder: number;
|
|
||||||
|
|
||||||
@CreateDateColumn({ comment: '创建时间' })
|
|
||||||
createdAt?: Date;
|
|
||||||
|
|
||||||
@Column({ comment: '更新时间', nullable: true })
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
|
||||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
|
||||||
|
|
||||||
@CustomRepository(PostEntity)
|
|
||||||
export class PostRepository extends Repository<PostEntity> {
|
|
||||||
buildBaseQB() {
|
|
||||||
return this.createQueryBuilder('post');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import sanitizeHtml from 'sanitize-html';
|
|
||||||
|
|
||||||
import { deepMerge } from '@/modules/core/helpers';
|
|
||||||
|
|
||||||
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'));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { isNil } from '@nestjs/common/utils/shared.utils';
|
|
||||||
|
|
||||||
import { isFunction, omit } from 'lodash';
|
|
||||||
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostOrder } from '@/modules/content/constants';
|
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
|
||||||
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
|
||||||
import { PaginateOptions, QueryHook } from '@/modules/database/types';
|
|
||||||
import { paginate } from '@/modules/database/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PostService {
|
|
||||||
constructor(protected repository: PostRepository) {}
|
|
||||||
|
|
||||||
async paginate(options: PaginateOptions, callback?: QueryHook<PostEntity>) {
|
|
||||||
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
|
|
||||||
return paginate(qb, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, `The post ${id} not exists!`);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(data: RecordAny) {
|
|
||||||
const item = await this.repository.save(data);
|
|
||||||
return this.detail(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(data: RecordAny) {
|
|
||||||
data.updatedAt = new Date();
|
|
||||||
await this.repository.update(data.id, omit(data, ['id']));
|
|
||||||
return this.detail(data.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id: string) {
|
|
||||||
const item = await this.repository.findOneByOrFail({ id });
|
|
||||||
return this.repository.remove(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async buildListQuery(
|
|
||||||
qb: SelectQueryBuilder<PostEntity>,
|
|
||||||
options: RecordAny,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected queryOrderBy(qb: SelectQueryBuilder<PostEntity>, orderBy?: PostOrder) {
|
|
||||||
switch (orderBy) {
|
|
||||||
case PostOrder.CREATED:
|
|
||||||
return qb.orderBy('post.createdAt', 'DESC');
|
|
||||||
case PostOrder.UPDATED:
|
|
||||||
return qb.orderBy('post.updatedAt', 'DESC');
|
|
||||||
case PostOrder.PUBLISHED:
|
|
||||||
return qb.orderBy('post.publishedAt', 'DESC');
|
|
||||||
case PostOrder.CUSTOM:
|
|
||||||
return qb.orderBy('post.custom', 'DESC');
|
|
||||||
default:
|
|
||||||
return qb
|
|
||||||
.orderBy('post.createdAt', 'DESC')
|
|
||||||
.addOrderBy('post.updatedAt', 'DESC')
|
|
||||||
.addOrderBy('post.publishedAt', 'DESC');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { DataSource, EventSubscriber } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostBodyType } from '@/modules/content/constants';
|
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
|
||||||
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
|
||||||
import { SanitizeService } from '@/modules/content/services/SanitizeService';
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { DynamicModule, Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class CoreModule {
|
|
||||||
static forRoot(): DynamicModule {
|
|
||||||
return {
|
|
||||||
module: CoreModule,
|
|
||||||
global: true,
|
|
||||||
providers: [],
|
|
||||||
exports: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './utils';
|
|
@ -1,34 +0,0 @@
|
|||||||
import deepmerge from 'deepmerge';
|
|
||||||
import { isNil } from 'lodash';
|
|
||||||
|
|
||||||
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 = <T, P>(
|
|
||||||
x: Partial<T>,
|
|
||||||
y: Partial<P>,
|
|
||||||
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 P extends T ? T : T & P;
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
|
@ -1,42 +0,0 @@
|
|||||||
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
|
||||||
import { getDataSourceToken, TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { DataSource, ObjectType } from 'typeorm';
|
|
||||||
|
|
||||||
import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class DatabaseModule {
|
|
||||||
static forRoot(configRegister: () => TypeOrmModuleOptions): DynamicModule {
|
|
||||||
return {
|
|
||||||
global: true,
|
|
||||||
module: DatabaseModule,
|
|
||||||
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 {
|
|
||||||
exports: providers,
|
|
||||||
module: DatabaseModule,
|
|
||||||
providers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { SetMetadata } from '@nestjs/common';
|
|
||||||
import { ObjectType } from 'typeorm';
|
|
||||||
|
|
||||||
import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants';
|
|
||||||
|
|
||||||
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
|
|
||||||
SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);
|
|
@ -1,23 +0,0 @@
|
|||||||
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 PaginateReturn<E extends ObjectLiteral> {
|
|
||||||
meta: PaginateMeta;
|
|
||||||
items: E[];
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import { isNil } from 'lodash';
|
|
||||||
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
|
||||||
|
|
||||||
import { PaginateOptions, PaginateReturn } from '@/modules/database/types';
|
|
||||||
|
|
||||||
export const paginate = async <T extends ObjectLiteral>(
|
|
||||||
qb: SelectQueryBuilder<T>,
|
|
||||||
options: PaginateOptions,
|
|
||||||
): Promise<PaginateReturn<T>> => {
|
|
||||||
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 ? limit : totalItems % limit;
|
|
||||||
const itemCount = page < totalPages ? limit : remainder;
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
meta: {
|
|
||||||
totalItems,
|
|
||||||
itemCount,
|
|
||||||
perPage: limit,
|
|
||||||
totalPages,
|
|
||||||
currentPage: page,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
33
typings/global.d.ts
vendored
33
typings/global.d.ts
vendored
@ -1,33 +0,0 @@
|
|||||||
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 ClassToPlain<T> = { [key in keyof T]: T[key] };
|
|
||||||
|
|
||||||
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];
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type ReRequired<T> = {
|
|
||||||
[P in keyof T]-?: T[P] extends (infer U)[] | undefined
|
|
||||||
? ReRequired<U>[]
|
|
||||||
: T[P] extends object | undefined
|
|
||||||
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
|
|
||||||
? T[P]
|
|
||||||
: ReRequired<T[P]>
|
|
||||||
: T[P];
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type WrapperType<T> = T;
|
|
Loading…
Reference in New Issue
Block a user