feat(leaning):完成NestJS整合TypeORM实现CRUD
- 增加CORE模块中的工具函数 - 增加数据库模块和数据库配置SQLite - 增加数据分页处理函数 - 文章模块更新提供者、实体、控制器、存储库替换为TypeORM操作 - 数据库模块增加自定义存储库动态模块
This commit is contained in:
parent
9ac17e43e3
commit
db1fcf05bc
1
back/Insomnia4.json
Normal file
1
back/Insomnia4.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"_type":"export","__export_format":4,"__export_date":"2023-09-23T09:15:41.007Z","__export_source":"insomnia.desktop.app:v2023.5.8","resources":[{"_id":"req_54139b7550f643b6845c1e051a602807","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1692768329669,"created":1673005787757,"url":"{{base_url}}/posts","name":"文章分页列表","description":"","method":"GET","body":{},"parameters":[{"id":"pair_a9c03e97e1cb404fa4502f1db7b84188","name":"page","value":"1","description":""},{"id":"pair_13e90b118d0044239ea632ef7b89c541","name":"limit","value":"3","description":"","disabled":false},{"id":"pair_909eeb5e367e4c84bfc9c1ca521a229a","name":"orderBy","value":"custom","description":"","disabled":false},{"id":"pair_16d24f39e4e8465f9d4e7047d8260455","name":"isPublished","value":"false","description":"","disabled":true}],"headers":[],"authentication":{},"metaSortKey":-1673006024968,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_cf1f617499f54fd2b42f9875c942610d","parentId":null,"modified":1695460454680,"created":1692767531537,"name":"nestapp-4","description":"","scope":"collection","_type":"workspace"},{"_id":"req_b2ca3725cf924206ae265fe04aed466e","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1695460410755,"created":1673122921310,"url":"{{base_url}}/posts/2065c805-1a6b-4e81-9dd4-80f457334b26","name":"文章详情","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1673006024955.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_54208d3f295d4bf7a6ebb6837197d409","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1695460385905,"created":1673005854149,"url":"{{base_url}}/posts","name":"新增文章","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"title\": \"测试文章7\",\n\t\"body\": \"这是第7篇文章\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1673006024943,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_c38a22de0a384cd8a98891de582c30f8","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1695460418492,"created":1673006024918,"url":"{{base_url}}/posts","name":"更新文章","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"id\": \"2065c805-1a6b-4e81-9dd4-80f457334b26\",\n\t\"customOrder\": 1,\n\t\"publishedAt\": \"{% customTimestamp 'specific', '', '', '', '', '', '', '', 'iso-8601', '', '' %}\"\n}"},"parameters":[],"headers":[{"id":"pair_d70cc4fe9f534354a3617ff43378bfdd","name":"Content-Type","value":"application/json","description":""}],"authentication":{},"metaSortKey":-1673006024918,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_39bc515120874ad6a5e0c9eb5a88479c","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1695460431300,"created":1673006224369,"url":"{{base_url}}/posts/2065c805-1a6b-4e81-9dd4-80f457334b26","name":"删除文章","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1673006024868,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_b7e12d26efb6436f9f989e6a7eeb8bec","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1673005797444,"created":1673005758611,"name":"Base Environment","data":{},"dataPropertyOrder":{},"color":null,"isPrivate":false,"metaSortKey":1673005758611,"_type":"environment"},{"_id":"jar_8716af200d694ecf9bdf74803381db0c","parentId":"wrk_cf1f617499f54fd2b42f9875c942610d","modified":1673005758611,"created":1673005758611,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"env_5e79c99a87e54a9f915296ecb8c05856","parentId":"env_b7e12d26efb6436f9f989e6a7eeb8bec","modified":1692767608192,"created":1673005798804,"name":"dev","data":{"host":"127.0.0.1:3100","base_url":"{{host}}/api"},"dataPropertyOrder":{"&":["host","base_url"]},"color":null,"isPrivate":false,"metaSortKey":1673005798804,"_type":"environment"}]}
|
1
back/Insomnia6.json
Normal file
1
back/Insomnia6.json
Normal file
File diff suppressed because one or more lines are too long
1
back/Insomnia9.json
Normal file
1
back/Insomnia9.json
Normal file
File diff suppressed because one or more lines are too long
BIN
back/database4.db
Normal file
BIN
back/database4.db
Normal file
Binary file not shown.
BIN
back/database6.db
Normal file
BIN
back/database6.db
Normal file
Binary file not shown.
BIN
back/database9.db
Normal file
BIN
back/database9.db
Normal file
Binary file not shown.
162
package.json
162
package.json
@ -1,81 +1,87 @@
|
|||||||
{
|
{
|
||||||
"name": "ink-nestjs-api",
|
"name": "ink-nestjs-api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^10.2.9",
|
|
||||||
"@nestjs/core": "^10.2.9",
|
|
||||||
"@nestjs/platform-fastify": "^10.2.9",
|
|
||||||
"@nestjs/swagger": "^7.1.16",
|
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.0",
|
|
||||||
"fastify": "^4.24.3",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"rxjs": "^7.8.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nestjs/cli": "^10.2.1",
|
|
||||||
"@nestjs/schematics": "^10.0.3",
|
|
||||||
"@nestjs/testing": "^10.2.9",
|
|
||||||
"@swc/cli": "^0.1.63",
|
|
||||||
"@swc/core": "^1.3.96",
|
|
||||||
"@types/jest": "^29.5.8",
|
|
||||||
"@types/lodash": "^4.14.201",
|
|
||||||
"@types/node": "^20.9.1",
|
|
||||||
"@types/supertest": "^2.0.16",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
|
||||||
"@typescript-eslint/parser": "^6.11.0",
|
|
||||||
"eslint": "^8.53.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
||||||
"eslint-config-prettier": "^9.0.0",
|
|
||||||
"eslint-plugin-import": "^2.29.0",
|
|
||||||
"eslint-plugin-jest": "^27.6.0",
|
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
|
||||||
"eslint-plugin-unused-imports": "^3.0.0",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"prettier": "^3.1.0",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"ts-jest": "^29.1.1",
|
|
||||||
"ts-loader": "^9.5.1",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^5.2.2"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
},
|
||||||
"collectCoverageFrom": [
|
"dependencies": {
|
||||||
"**/*.(t|j)s"
|
"@nestjs/common": "^10.2.9",
|
||||||
],
|
"@nestjs/core": "^10.2.9",
|
||||||
"coverageDirectory": "../coverage",
|
"@nestjs/platform-fastify": "^10.2.9",
|
||||||
"testEnvironment": "node"
|
"@nestjs/swagger": "^7.1.16",
|
||||||
}
|
"@nestjs/typeorm": "^10.0.1",
|
||||||
|
"better-sqlite3": "^9.0.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"fastify": "^4.24.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
|
"typeorm": "^0.3.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.2.1",
|
||||||
|
"@nestjs/schematics": "^10.0.3",
|
||||||
|
"@nestjs/testing": "^10.2.9",
|
||||||
|
"@swc/cli": "^0.1.63",
|
||||||
|
"@swc/core": "^1.3.96",
|
||||||
|
"@types/jest": "^29.5.8",
|
||||||
|
"@types/lodash": "^4.14.201",
|
||||||
|
"@types/node": "^20.9.2",
|
||||||
|
"@types/sanitize-html": "^2.9.4",
|
||||||
|
"@types/supertest": "^2.0.16",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-import": "^2.29.0",
|
||||||
|
"eslint-plugin-jest": "^27.6.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
"eslint-plugin-unused-imports": "^3.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
}
|
}
|
794
pnpm-lock.yaml
794
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { database } from '@/config';
|
||||||
import { ContentModule } from '@/modules/content/content.module';
|
import { ContentModule } from '@/modules/content/content.module';
|
||||||
|
import { CoreModule } from '@/modules/core/core.module';
|
||||||
|
import { DatabaseModule } from '@/modules/database/database.module';
|
||||||
import { WelcomeModule } from '@/modules/welcome/welcome.module';
|
import { WelcomeModule } from '@/modules/welcome/welcome.module';
|
||||||
|
|
||||||
import { CoreModule } from './modules/core/core.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [DatabaseModule.forRoot(database), ContentModule, WelcomeModule, CoreModule.forRoot()],
|
||||||
ContentModule,
|
|
||||||
WelcomeModule,
|
|
||||||
CoreModule.forRoot({
|
|
||||||
config: {
|
|
||||||
name: '欢迎访问 Ink NestJS API !',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
23
src/config/database.config.ts
Normal file
23
src/config/database.config.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库配置
|
||||||
|
*/
|
||||||
|
export const database = (): TypeOrmModuleOptions => ({
|
||||||
|
// 以下为mysql配置
|
||||||
|
// charset: 'utf8mb4',
|
||||||
|
// logging: ['error'],
|
||||||
|
// type: 'mysql',
|
||||||
|
// host: '127.0.0.1',
|
||||||
|
// port: 3306,
|
||||||
|
// username: 'root',
|
||||||
|
// password: '123456789',
|
||||||
|
// database: 'ink_apps',
|
||||||
|
// 以下为sqlite配置
|
||||||
|
type: 'better-sqlite3',
|
||||||
|
database: resolve(__dirname, '../../back/database4.db'),
|
||||||
|
synchronize: true,
|
||||||
|
autoLoadEntities: true,
|
||||||
|
});
|
1
src/config/index.ts
Normal file
1
src/config/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './database.config';
|
@ -9,8 +9,10 @@ const bootstrap = async () => {
|
|||||||
logger: ['error', 'warn'],
|
logger: ['error', 'warn'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
await app.listen(2333, () => {
|
await app.listen(2333, () => {
|
||||||
console.log('api: http://localhost:2333');
|
console.log('api: http://localhost:2333/api');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
17
src/modules/content/constants.ts
Normal file
17
src/modules/content/constants.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 文章内容类型
|
||||||
|
*/
|
||||||
|
export enum PostBodyType {
|
||||||
|
HTML = 'html',
|
||||||
|
MD = 'markdown',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章排序类型
|
||||||
|
*/
|
||||||
|
export enum PostOrderType {
|
||||||
|
CREATED = 'createdAt',
|
||||||
|
UPDATED = 'updatedAt',
|
||||||
|
PUBLISHED = 'publishedAt',
|
||||||
|
CUSTOM = 'custom',
|
||||||
|
}
|
@ -1,11 +1,22 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { PostController } from './controllers';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { PostService } from './services';
|
|
||||||
|
import { PostController } from '@/modules/content/controllers';
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
|
||||||
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
|
import { PostService, SanitizeService } from '@/modules/content/services';
|
||||||
|
import { PostSubscriber } from '@/modules/content/subscribers';
|
||||||
|
import { DatabaseModule } from '@/modules/database/database.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([PostEntity]),
|
||||||
|
DatabaseModule.forRepository([PostRepository]),
|
||||||
|
],
|
||||||
controllers: [PostController],
|
controllers: [PostController],
|
||||||
providers: [PostService],
|
providers: [PostService, PostSubscriber, SanitizeService],
|
||||||
exports: [PostService],
|
exports: [PostService, DatabaseModule.forRepository([PostRepository])],
|
||||||
})
|
})
|
||||||
export class ContentModule {}
|
export class ContentModule {}
|
||||||
|
@ -1,24 +1,38 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, ValidationPipe } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
ValidationPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { CreatePostDto, UpdatePostDto } from '../dtos';
|
import { PostService } from '@/modules/content/services';
|
||||||
import { PostService } from '../services';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文章控制器
|
* 文章控制器
|
||||||
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
||||||
*/
|
*/
|
||||||
@Controller('post')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async index() {
|
async list(
|
||||||
return this.postService.findAll();
|
@Query()
|
||||||
|
options: PaginateOptions,
|
||||||
|
) {
|
||||||
|
return this.postService.paginate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async show(@Param('id') id: number) {
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.findOne(id);
|
return this.postService.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ -32,7 +46,7 @@ export class PostController {
|
|||||||
groups: ['create'],
|
groups: ['create'],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
data: CreatePostDto,
|
data: RecordAny,
|
||||||
) {
|
) {
|
||||||
return this.postService.create(data);
|
return this.postService.create(data);
|
||||||
}
|
}
|
||||||
@ -48,13 +62,13 @@ export class PostController {
|
|||||||
groups: ['update'],
|
groups: ['update'],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
data: UpdatePostDto,
|
data: RecordAny,
|
||||||
) {
|
) {
|
||||||
return this.postService.update(data);
|
return this.postService.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async delete(@Param('id') id: number) {
|
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.delete(id);
|
return this.postService.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
src/modules/content/entities/index.ts
Normal file
1
src/modules/content/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './post.entity';
|
43
src/modules/content/entities/post.entity.ts
Normal file
43
src/modules/content/entities/post.entity.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
@CreateDateColumn({ comment: '创建时间' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ comment: '更新时间' })
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Column({ comment: '文章自定义排序', default: 0 })
|
||||||
|
customOrder: number;
|
||||||
|
}
|
1
src/modules/content/repositories/index.ts
Normal file
1
src/modules/content/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './post.repository';
|
11
src/modules/content/repositories/post.repository.ts
Normal file
11
src/modules/content/repositories/post.repository.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { CustomRepository } from '@/modules/database/decorators';
|
||||||
|
|
||||||
|
@CustomRepository(PostEntity)
|
||||||
|
export class PostRepository extends Repository<PostEntity> {
|
||||||
|
buildBaseQB() {
|
||||||
|
return this.createQueryBuilder('post');
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export * from './post.service';
|
export * from './post.service';
|
||||||
|
export * from './sanitize.service';
|
||||||
|
@ -1,64 +1,119 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { isNil } from 'lodash';
|
import { isFunction, isNil, omit } from 'lodash';
|
||||||
|
|
||||||
import { CreatePostDto, UpdatePostDto } from '../dtos';
|
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { PostEntity } from '../types';
|
import { PostOrderType } from '@/modules/content/constants';
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
|
|
||||||
|
import { paginate } from '@/modules/database/helpers';
|
||||||
|
import { PaginateOptions, QueryHook } from '@/modules/database/types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostService {
|
export class PostService {
|
||||||
protected posts: PostEntity[] = [
|
constructor(protected repository: PostRepository) {}
|
||||||
{ title: '第一篇文章标题', body: '第一篇文章内容' },
|
|
||||||
{ title: '第二篇文章标题', body: '第二篇文章内容' },
|
|
||||||
{ title: '第三篇文章标题', body: '第三篇文章内容' },
|
|
||||||
{ title: '第四篇文章标题', body: '第四篇文章内容' },
|
|
||||||
{ title: '第五篇文章标题', body: '第五篇文章内容' },
|
|
||||||
{ title: '第六篇文章标题', body: '第六篇文章内容' },
|
|
||||||
].map((v, id) => ({ ...v, id }));
|
|
||||||
|
|
||||||
async findAll() {
|
/**
|
||||||
return this.posts;
|
* 获取分页数据
|
||||||
|
* @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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
/**
|
||||||
const post = this.posts.find((item) => item.id === id);
|
* 查询单篇文章
|
||||||
|
* @param id
|
||||||
if (isNil(post)) throw new NotFoundException(`id: ${id} 文章不存在`);
|
* @param callback 添加额外的查询
|
||||||
|
*/
|
||||||
return post;
|
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: CreatePostDto) {
|
/**
|
||||||
const newPost: PostEntity = {
|
* 创建文章
|
||||||
id: Math.max(...this.posts.map(({ id }) => id + 1)),
|
* @param data
|
||||||
...data,
|
*/
|
||||||
};
|
async create(data: Record<string, any>) {
|
||||||
|
const item = await this.repository.save(data);
|
||||||
|
|
||||||
this.posts.push(newPost);
|
return this.detail(item.id);
|
||||||
|
|
||||||
return newPost;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(data: UpdatePostDto) {
|
/**
|
||||||
let toUpdate = this.posts.find((item) => item.id === data.id);
|
* 更新文章
|
||||||
|
* @param data
|
||||||
if (isNil(toUpdate)) throw new NotFoundException(`id: ${data.id} 文章不存在`);
|
*/
|
||||||
|
async update(data: Record<string, any>) {
|
||||||
toUpdate = { ...toUpdate, ...data };
|
await this.repository.update(data.id, omit(data, ['id']));
|
||||||
|
return this.detail(data.id);
|
||||||
this.posts = this.posts.map((item) => (item.id === data.id ? toUpdate : item));
|
|
||||||
|
|
||||||
return toUpdate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: number) {
|
/**
|
||||||
const toDelete = this.posts.find((item) => item.id === id);
|
* 删除文章
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
async delete(id: string) {
|
||||||
|
const item = await this.repository.findOneByOrFail({ id });
|
||||||
|
return this.repository.remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNil(toDelete)) throw new NotFoundException(`id: ${id} 文章不存在`);
|
/**
|
||||||
|
* 构建文章列表查询器
|
||||||
|
* @param qb 初始查询构造器
|
||||||
|
* @param options 排查分页选项后的查询选项
|
||||||
|
* @param callback 添加额外的查询
|
||||||
|
*/
|
||||||
|
protected async buildListQuery(
|
||||||
|
qb: SelectQueryBuilder<PostEntity>,
|
||||||
|
options: Record<string, any>,
|
||||||
|
callback?: QueryHook<PostEntity>,
|
||||||
|
) {
|
||||||
|
const { orderBy, isPublished } = options;
|
||||||
|
let newQb = qb;
|
||||||
|
if (typeof isPublished === 'boolean') {
|
||||||
|
newQb = isPublished
|
||||||
|
? newQb.where({
|
||||||
|
publishedAt: Not(IsNull()),
|
||||||
|
})
|
||||||
|
: newQb.where({
|
||||||
|
publishedAt: IsNull(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newQb = this.queryOrderBy(newQb, orderBy);
|
||||||
|
if (callback) return callback(newQb);
|
||||||
|
return newQb;
|
||||||
|
}
|
||||||
|
|
||||||
this.posts = this.posts.filter((item) => item.id !== id);
|
/**
|
||||||
|
* 对文章进行排序的Query构建
|
||||||
return toDelete;
|
* @param qb
|
||||||
|
* @param orderBy 排序方式
|
||||||
|
*/
|
||||||
|
protected queryOrderBy(qb: SelectQueryBuilder<PostEntity>, orderBy?: PostOrderType) {
|
||||||
|
switch (orderBy) {
|
||||||
|
case PostOrderType.CREATED:
|
||||||
|
return qb.orderBy('post.createdAt', 'DESC');
|
||||||
|
case PostOrderType.UPDATED:
|
||||||
|
return qb.orderBy('post.updatedAt', 'DESC');
|
||||||
|
case PostOrderType.PUBLISHED:
|
||||||
|
return qb.orderBy('post.publishedAt', 'DESC');
|
||||||
|
case PostOrderType.CUSTOM:
|
||||||
|
return qb.orderBy('customOrder', 'DESC');
|
||||||
|
default:
|
||||||
|
return qb
|
||||||
|
.orderBy('post.createdAt', 'DESC')
|
||||||
|
.addOrderBy('post.updatedAt', 'DESC')
|
||||||
|
.addOrderBy('post.publishedAt', 'DESC');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
src/modules/content/services/sanitize.service.ts
Normal file
26
src/modules/content/services/sanitize.service.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
|
import { deepMerge } from '@/modules/core/helpers';
|
||||||
|
|
||||||
|
@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'));
|
||||||
|
}
|
||||||
|
}
|
1
src/modules/content/subscribers/index.ts
Normal file
1
src/modules/content/subscribers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './post.subscriber';
|
28
src/modules/content/subscribers/post.subscriber.ts
Normal file
28
src/modules/content/subscribers/post.subscriber.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { DataSource, EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
|
import { PostBodyType } from '@/modules/content/constants';
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
|
import { SanitizeService } from '@/modules/content/services';
|
||||||
|
|
||||||
|
@EventSubscriber()
|
||||||
|
export class PostSubscriber {
|
||||||
|
constructor(
|
||||||
|
protected dataSource: DataSource,
|
||||||
|
protected sanitizeService: SanitizeService,
|
||||||
|
protected postRepository: PostRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
listenTo() {
|
||||||
|
return PostEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载文章数据的处理
|
||||||
|
*/
|
||||||
|
async afterLoad(entity: PostEntity) {
|
||||||
|
if (entity.type === PostBodyType.HTML) {
|
||||||
|
entity.body = this.sanitizeService.sanitize(entity.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
export interface PostEntity {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
summary?: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
@ -1,23 +1,11 @@
|
|||||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigService } from './services/config.service';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
static forRoot(options: { config: RecordAny }): DynamicModule {
|
static forRoot(): DynamicModule {
|
||||||
return {
|
return {
|
||||||
module: CoreModule,
|
module: CoreModule,
|
||||||
global: true,
|
global: true,
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useFactory() {
|
|
||||||
return new ConfigService(options.config);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [ConfigService],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
src/modules/core/helpers/index.ts
Normal file
1
src/modules/core/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './utils';
|
50
src/modules/core/helpers/utils.ts
Normal file
50
src/modules/core/helpers/utils.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import deepmerge from 'deepmerge';
|
||||||
|
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于请求验证中的boolean数据转义
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export const 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于请求验证中转义null
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export const toNull = (value?: string | null): string | null | undefined => {
|
||||||
|
return value === 'null' ? null : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度合并对象
|
||||||
|
* @param x 初始值
|
||||||
|
* @param y 新值
|
||||||
|
* @param arrayMode 对于数组采取的策略,`replace`为直接替换,`merge`为合并数组
|
||||||
|
*/
|
||||||
|
export const deepMerge = <T1, T2>(
|
||||||
|
x: Partial<T1>,
|
||||||
|
y: Partial<T2>,
|
||||||
|
arrayMode: 'replace' | 'merge' = 'merge',
|
||||||
|
) => {
|
||||||
|
const options: deepmerge.Options = {};
|
||||||
|
|
||||||
|
if (arrayMode === 'replace') {
|
||||||
|
options.arrayMerge = (target, source, _options) => source;
|
||||||
|
} else if (arrayMode === 'merge') {
|
||||||
|
options.arrayMerge = (target, source, _options) =>
|
||||||
|
Array.from(new Set({ ...target, ...source }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return deepmerge(x, y, options) as T2 extends T1 ? T1 : T1 & T2;
|
||||||
|
};
|
@ -1,15 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ConfigService {
|
|
||||||
protected config: RecordAny = {};
|
|
||||||
|
|
||||||
constructor(data: RecordAny) {
|
|
||||||
this.config = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
get<T>(key: string, defaultValue?: T): T | undefined {
|
|
||||||
return get(this.config, key, defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
4
src/modules/database/constants.ts
Normal file
4
src/modules/database/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* 自定义 Repository 元数据
|
||||||
|
*/
|
||||||
|
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
47
src/modules/database/database.module.ts
Normal file
47
src/modules/database/database.module.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule, TypeOrmModuleOptions, getDataSourceToken } 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 Repo of repositories) {
|
||||||
|
const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo);
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
providers.push({
|
||||||
|
inject: [getDataSourceToken(dataSourceName)],
|
||||||
|
provide: Repo,
|
||||||
|
useFactory: (dataSource: DataSource): InstanceType<typeof Repo> => {
|
||||||
|
const base = dataSource.getRepository<ObjectType<any>>(entity);
|
||||||
|
return new Repo(base.target, base.manager, base.queryRunner);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exports: providers,
|
||||||
|
module: DatabaseModule,
|
||||||
|
providers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
1
src/modules/database/decorators/index.ts
Normal file
1
src/modules/database/decorators/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './repository.decorator';
|
13
src/modules/database/decorators/repository.decorator.ts
Normal file
13
src/modules/database/decorators/repository.decorator.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ObjectType } from 'typeorm';
|
||||||
|
|
||||||
|
import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义存储库装饰器,用于将一个TypeORM实体类关联到一个自定义存储库类。
|
||||||
|
* @param {ObjectType<T>} entity - TypeORM实体类,将被关联到自定义存储库。
|
||||||
|
* @returns {ClassDecorator} - 一个类装饰器,将元数据关联到目标存储库类。
|
||||||
|
*/
|
||||||
|
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
|
||||||
|
SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);
|
46
src/modules/database/helpers.ts
Normal file
46
src/modules/database/helpers.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
import { PaginateOptions, PaginateReturn } from '@/modules/database/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页函数
|
||||||
|
* @params qb queryBuilder实例
|
||||||
|
* @params options 分页选项
|
||||||
|
*/
|
||||||
|
export const paginate = async <E extends ObjectLiteral>(
|
||||||
|
qb: SelectQueryBuilder<E>,
|
||||||
|
options: PaginateOptions,
|
||||||
|
): Promise<PaginateReturn<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 {
|
||||||
|
items,
|
||||||
|
meta: {
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
itemCount,
|
||||||
|
perPage: limit,
|
||||||
|
currentPage: page,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
70
src/modules/database/types.ts
Normal file
70
src/modules/database/types.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个查询钩子,用于修改或增强给定的查询构建器。
|
||||||
|
* @param {SelectQueryBuilder<Entity>} qb - TypeORM的查询构建器实例。
|
||||||
|
* @returns {Promise<SelectQueryBuilder<Entity>>} - 经过修改或增强的查询构建器实例。
|
||||||
|
*/
|
||||||
|
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> {
|
||||||
|
/**
|
||||||
|
* 项目列表
|
||||||
|
*/
|
||||||
|
items: E[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页信息
|
||||||
|
*/
|
||||||
|
meta: PaginateMeta;
|
||||||
|
}
|
@ -1,59 +1,12 @@
|
|||||||
import { Controller, Get, Inject } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigService } from '../core/services/config.service';
|
|
||||||
|
|
||||||
import { FifthService } from './services/fifth.service';
|
|
||||||
import { FirstService } from './services/first.service';
|
|
||||||
import { FourthService } from './services/fourth.service';
|
|
||||||
import { SecondService } from './services/second.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 欢迎访问 Ink NestJS API
|
* 欢迎访问 Ink NestJS API
|
||||||
*/
|
*/
|
||||||
@Controller()
|
@Controller()
|
||||||
export class WelcomeController {
|
export class WelcomeController {
|
||||||
constructor(
|
|
||||||
private configService: ConfigService,
|
|
||||||
private first: FirstService,
|
|
||||||
@Inject('ID-WELCOME') private idExp: FirstService,
|
|
||||||
@Inject('FACTORY-WELCOME') private ftExp: FourthService,
|
|
||||||
@Inject('ALIAS-WELCOME') private asExp: FirstService,
|
|
||||||
@Inject('ASYNC-WELCOME') private acExp: SecondService,
|
|
||||||
private fifth: FifthService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getMessage(): string {
|
getMessage(): string {
|
||||||
return this.configService.get('name');
|
return '欢迎访问 Ink NestJS API';
|
||||||
}
|
|
||||||
|
|
||||||
@Get('value')
|
|
||||||
async useValue() {
|
|
||||||
return this.first.useValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('id')
|
|
||||||
async useId() {
|
|
||||||
return this.idExp.useId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('factory')
|
|
||||||
async useFactory() {
|
|
||||||
return this.ftExp.getContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('alias')
|
|
||||||
async useAlias() {
|
|
||||||
return this.asExp.useAlias();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('async')
|
|
||||||
async useAsync() {
|
|
||||||
return this.acExp.useAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('circular')
|
|
||||||
async useCircular() {
|
|
||||||
return this.fifth.circular();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,58 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { FirstService } from './services/first.service';
|
|
||||||
import { FourthService } from './services/fourth.service';
|
|
||||||
import { SecondService } from './services/second.service';
|
|
||||||
import { ThirdService } from './services/third.service';
|
|
||||||
import { WelcomeController } from './welcome.controller';
|
import { WelcomeController } from './welcome.controller';
|
||||||
import { FifthService } from './services/fifth.service';
|
|
||||||
import { SixthService } from './services/sixth.service';
|
|
||||||
|
|
||||||
const firstObject = {
|
|
||||||
useValue: () => 'firstObject useValue 提供者',
|
|
||||||
useAlias: () => 'firstObject 别名提供者',
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstInstance = new FirstService();
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [WelcomeController],
|
controllers: [WelcomeController],
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: FirstService,
|
|
||||||
useValue: firstObject,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'ID-WELCOME',
|
|
||||||
useValue: firstInstance,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: SecondService,
|
|
||||||
useClass: ThirdService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'FACTORY-WELCOME',
|
|
||||||
useFactory(second: SecondService) {
|
|
||||||
const factory = new FourthService(second);
|
|
||||||
|
|
||||||
return factory;
|
|
||||||
},
|
|
||||||
inject: [SecondService],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'ALIAS-WELCOME',
|
|
||||||
useExisting: FirstService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'ASYNC-WELCOME',
|
|
||||||
useFactory: async () => {
|
|
||||||
const factory = new FourthService(new SecondService());
|
|
||||||
|
|
||||||
return factory.getPromise();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FifthService,
|
|
||||||
SixthService,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class WelcomeModule {}
|
export class WelcomeModule {}
|
||||||
|
Loading…
Reference in New Issue
Block a user