nicot 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,161 +1,814 @@
1
- # nicot
1
+ # NICOT
2
2
 
3
- Nest.js interacting with class-validator + OpenAPI + TypeORM for Nest.js Restful API development.
3
+ **NICOT** 是一个基于 NestJS + TypeORM 的后端开发框架。通过实体定义即生成:
4
+ - 数据库模型(TypeORM)
5
+ - 字段校验(class-validator)
6
+ - 请求 DTO(Create / Update / Query)
7
+ - RESTful 接口与文档(Swagger)
8
+ - 统一返回结构、查询控制、权限注入等
4
9
 
5
- ## Install
10
+ 适用于希望快速搭建标准化接口、减少重复代码的后端项目。
6
11
 
7
- In your Nest.js project, run the following command:
12
+ ---
13
+
14
+ ## 📦 安装
8
15
 
9
16
  ```bash
10
- npm install @nestjs/swagger typeorm @nestjs/typeorm class-validator class-transformer nicot
17
+ npm install nicot typeorm @nestjs/typeorm class-validator class-transformer reflect-metadata @nestjs/swagger
11
18
  ```
12
19
 
13
- ## Entity
14
-
15
- Those decorators would all decorate the following, with the SAME settings.
20
+ ---
16
21
 
17
- - TypeORM `@Entity()` settings.
18
- - class-validator validation settings.
19
- - `@nestjs/swagger` `@ApiProperty()` settings.
22
+ ## 🧱 定义实体 Entity
20
23
 
21
24
  ```ts
22
25
  @Entity()
23
- export class User extends IdBase() {
24
- @Index()
25
- @QueryLike() // queries as 'where name LIKE :name%'
26
- @StringColumn(5, {
26
+ class User extends IdBase() {
27
+ @QueryEqual()
28
+ @StringColumn(255, {
27
29
  required: true,
28
- description: 'User name',
30
+ description: '用户名',
29
31
  })
30
32
  name: string;
31
33
 
32
- @QueryEqual() // queries as 'where age = :age'
33
- @IntColumn('int', { unsigned: true, description: 'User age', default: 20 })
34
+ @IntColumn('int', { unsigned: true })
34
35
  age: number;
35
36
 
36
- @EnumColumn(Gender, { description: 'User gender' })
37
- gender: Gender;
37
+ @StringColumn(255)
38
+ @NotInResult()
39
+ password: string;
40
+
41
+ @DateColumn()
42
+ @NotWritable()
43
+ createdAt: Date;
44
+ }
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🧾 主键基础类:IdBase / StringIdBase
50
+
51
+ 在定义实体时,NICOT 提供了两种基础类 `IdBase` 与 `StringIdBase`,可作为实体的继承基类,为你自动处理:
52
+
53
+ - 主键字段定义(自增或字符串主键)
54
+ - 主键字段的权限控制与文档注解
55
+ - 默认排序逻辑(id 降序 / 升序)
56
+ - 支持 queryBuilder 查询条件注入
57
+
58
+ ---
59
+
60
+ ### 1. `IdBase()` - 数字主键(自增)
61
+
62
+ 适合常见的自增整型主键使用场景。
63
+
64
+ ```ts
65
+ @Entity()
66
+ class User extends IdBase() {
67
+ // 继承字段:id: number (bigint unsigned, primary key, auto-increment)
68
+ }
69
+ ```
70
+
71
+ - 自动添加字段:`id: number`
72
+ - 默认排序为 `ORDER BY id DESC`
73
+ - 使用 `Generated('increment')` 作为主键生成策略
74
+ - 搭配 `@IntColumn` + `@NotWritable()`,在创建 / 修改时不可写
75
+
76
+ ---
77
+
78
+ ### 2. `StringIdBase()` - 字符串主键(手动或 UUID)
79
+
80
+ 适合你希望使用业务主键或 UUID 作为主键的场景。传入 `uuid: true` 参数后自动生成 UUID 主键。
81
+
82
+ ```ts
83
+ @Entity()
84
+ class ApiKey extends StringIdBase({ uuid: true, description: 'API 密钥 ID' }) {
85
+ // 继承字段:id: string (uuid, primary key)
86
+ }
87
+ ```
88
+
89
+ - 自动添加字段:`id: string`
90
+ - 默认排序为 `ORDER BY id ASC`
91
+ - 支持配置长度(`length`)和描述(`description`)
92
+ - `uuid: true` 时自动添加 `@Generated('uuid')`
93
+
94
+ ---
95
+
96
+ ### 3. 示例对比
97
+
98
+ ```ts
99
+ @Entity()
100
+ class Article extends IdBase({ description: '文章 ID' }) {
101
+ // id: number 自动生成
102
+ }
103
+
104
+ @Entity()
105
+ class Token extends StringIdBase({
106
+ uuid: true,
107
+ description: '访问令牌',
108
+ }) {
109
+ // id: string,自动生成 UUID
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ### 小结
116
+
117
+ | 基类 | 主键类型 | 排序默认 | ID 生成策略 | 使用场景 |
118
+ |-----------------|------------|----------|----------------------|------------------------|
119
+ | `IdBase()` | number | DESC | 自增 `Generated('increment')` | 常规实体 ID |
120
+ | `StringIdBase()`| string | ASC | 可选 UUID / 手动输入 | UUID 主键、业务主键等 |
121
+
122
+ 建议你为每个实体都继承其中一个基类,以统一主键结构和查询逻辑。
123
+
124
+ ---
125
+
126
+ ## 🧠 字段装饰器总览
127
+
128
+ NICOT 提供了一系列 `***Column()` 装饰器,统一处理字段的:
129
+
130
+ - 数据类型定义(TypeORM)
131
+ - 输入校验(class-validator)
132
+ - 文档描述(@nestjs/swagger)
133
+
134
+ ### 字段类型装饰器(`***Column()`)
135
+
136
+ | 装饰器名 | 数据类型 | 自动添加的验证与文档 |
137
+ |----------------------|----------------|---------------------------------|
138
+ | `@StringColumn(len)` | string | `@IsString()` + `@Length()` |
139
+ | `@IntColumn(type)` | int/bigint/... | `@IsInt()` + Swagger number 类型 |
140
+ | `@FloatColumn(type)` | float/decimal | `@IsNumber()` |
141
+ | `@BoolColumn()` | boolean | `@IsBoolean()` |
142
+ | `@DateColumn()` | Date | `@IsDate()` |
143
+ | `@JsonColumn(T)` | 任意对象/数组 | `@IsObject()` / `@ValidateNested()` 等 |
144
+
145
+ 所有字段装饰器都支持第二个参数 `options`:
146
+
147
+ ```ts
148
+ @StringColumn(255, {
149
+ required: true,
150
+ description: '用户名',
151
+ default: 'Anonymous',
152
+ })
153
+ name: string;
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 🔒 字段访问限制装饰器(行为控制)
159
+
160
+ NICOT 提供以下装饰器用于控制字段在不同接口中的表现:
161
+
162
+ | 装饰器名 | 行为控制说明 |
163
+ |-----------------------|--------------------------------------------------------|
164
+ | `@NotWritable()` | 不允许在创建(POST)或修改(PATCH)时传入 |
165
+ | `@NotChangeable()` | 不允许在修改(PATCH)时更新(只可创建) |
166
+ | `@NotQueryable()` | 不允许在 GET 查询参数中使用该字段 |
167
+ | `@NotInResult()` | 不会出现在任何返回结果中(如密码字段) |
168
+ | `@NotColumn()` | 不是数据库字段(仅逻辑字段,如计算用字段) |
169
+
170
+ RestfulFactory 处理 Entity 类的时候,会以这些装饰器为依据,裁剪生成的 DTO 和查询参数。
171
+
172
+ 这些限制装饰器非常适合处理:
173
+
174
+ - 安全字段(如密码、Token)
175
+ - 系统字段(如创建时间、创建者 ID)
176
+ - 只读字段(如 auto-increment 主键)
177
+
178
+ ---
179
+
180
+ ### 示例:完整字段定义
181
+
182
+ ```ts
183
+ @StringColumn(255, {
184
+ required: true,
185
+ description: '用户昵称',
186
+ })
187
+ @NotWritable()
188
+ nickname: string;
189
+
190
+ @BoolColumn()
191
+ @QueryMatchBoolean()
192
+ isActive: boolean;
193
+ ```
194
+
195
+ ---
196
+
197
+ ## 🔍 查询装饰器总览(Query 系列)
198
+
199
+ NICOT 提供了一套查询装饰器,用于在 Entity 字段上声明支持的 GET 查询条件。它们会自动应用到 `findAll()` 中的 queryBuilder。
38
200
 
39
- @NotColumn()
40
- somethingElse: any; // Would not come from client input, and would not go into OpenAPI document.
201
+ ### ✅ 内建查询装饰器
41
202
 
42
- // possible optional override operations
203
+ | 装饰器名 | 查询效果 |
204
+ |-----------------------------|------------------------------------------------|
205
+ | `@QueryEqual()` | 精确匹配:`WHERE field = :value` |
206
+ | `@QueryLike()` | 前缀模糊匹配:`WHERE field LIKE :value%` |
207
+ | `@QuerySearch()` | 宽泛模糊搜索:`WHERE field LIKE %:value%` |
208
+ | `@QueryMatchBoolean()` | `true/false/1/0` 转换为布尔类型查询 |
209
+ | `@QueryEqualZeroNullable()` | `0 → IS NULL`,否则 `= :value`(适合 nullable)|
210
+ | `@QueryGreater()` | 大于查询:`WHERE field > :value` |
211
+ | `@QueryOrderBy()` | 排序字段控制:`ORDER BY field ASC|DESC` |
43
212
 
44
- override isValidInCreate() { // Custom before-create check.
45
- if (!this.name.length) {
46
- return 'Name cannot be empty!';
213
+ ---
214
+
215
+ ## 🛠 自定义查询装饰器:`QueryCondition()`
216
+
217
+ 如果你需要构建更复杂或专用的查询逻辑,可以使用 `QueryCondition()` 创建自己的装饰器:
218
+
219
+ ### 示例:大于查询
220
+
221
+ ```ts
222
+ export const QueryGreater = () =>
223
+ QueryCondition((dto, qb, alias, key) => {
224
+ if (dto[key] != null) {
225
+ qb.andWhere(`${alias}.${key} > :${key}`, { [key]: dto[key] });
47
226
  }
48
- }
227
+ });
228
+ ```
49
229
 
50
- override isValidInUpdate() { // Custom before-update check.
51
- if (this.name && !this.name.length) {
52
- return 'Name cannot be empty!';
230
+ ### 示例:动态排序字段(带字段名映射)
231
+
232
+ ```ts
233
+ export const QueryOrderBy = () =>
234
+ QueryCondition((dto, qb, alias, key) => {
235
+ const orderValue = dto[key];
236
+ if (orderValue) {
237
+ const originalKey = key.replace(/OrderBy$/, '');
238
+ qb.addOrderBy(`${alias}.${originalKey}`, orderValue);
53
239
  }
54
- }
240
+ });
241
+ ```
55
242
 
56
- override async beforeCreate() {
57
- this.name = this.name.toLowerCase(); // Do something before create.
58
- }
243
+ > 使用方式与普通装饰器一致,应用在实体字段上即可。
59
244
 
60
- override async afterCreate() {
61
- this.name = this.name.toUpperCase(); // Do something after create before sending to user.
62
- }
245
+ ---
63
246
 
64
- override async beforeGet() {
65
- }
247
+ ### 使用效果示例
66
248
 
67
- override async afterGet() {
68
- }
249
+ ```ts
250
+ @IntColumn('int', { unsigned: true })
251
+ @QueryGreater()
252
+ views: number;
253
+
254
+ @StringColumn(255)
255
+ @QueryLike()
256
+ title: string;
257
+
258
+ @BoolColumn()
259
+ @QueryMatchBoolean()
260
+ isPublished: boolean;
261
+
262
+ @NotWritable()
263
+ @NotInResult()
264
+ @QueryOrderBy()
265
+ @IsIn(['ASC', 'DESC'])
266
+ @ApiProperty({ enum: ['ASC', 'DESC'], description: 'Order by views' })
267
+ viewsOrderBy?: 'ASC' | 'DESC';
268
+ ```
69
269
 
70
- override async beforeUpdate() {
270
+
271
+
272
+ ---
273
+
274
+ ## 🔁 生命周期钩子
275
+
276
+ 支持在实体中定义以下方法:
277
+
278
+ ```ts
279
+ class User {
280
+ async beforeCreate() {}
281
+ async afterCreate() {}
282
+ async beforeUpdate() {}
283
+ async afterUpdate() {}
284
+ async beforeGet() {}
285
+ async afterGet() {}
286
+
287
+ isValidInCreate(): string | undefined {
288
+ return this.name ? undefined : '必须填写名称';
71
289
  }
72
290
  }
73
291
  ```
74
292
 
75
- There are also other following decorators to control accessibility:
293
+ ---
294
+
295
+ ## 🛠 使用 CrudService(服务层标准写法)
296
+
297
+ NICOT 提供了 `CrudService(Entity, options)`,是所有资源的标准服务实现方式。
76
298
 
77
- - `@NotWritable()` Can only come from GET requests.
78
- - `@NotChangeable()` Cannot be changed by PATCH requests.
299
+ 你只需继承它,并传入对应的实体和配置,即可拥有完整的:
300
+ - 查询(支持分页、排序、过滤、关系)
301
+ - 创建、更新、删除(带钩子、校验、字段控制)
302
+ - 统一返回结构
79
303
 
80
- ## CrudService
304
+ ---
81
305
 
82
- Creates a service for database operation in one word.
306
+ ### 定义 Service
83
307
 
84
308
  ```ts
309
+ import { CrudService } from 'nicot';
310
+
85
311
  @Injectable()
86
- export class UserService extends CrudService(User) {
87
- constructor(@InjectDataSource() db: DataSource) {
88
- super(db.getRepository(User));
312
+ export class ArticleService extends CrudService(Article, {
313
+ relations: ['user'], // 自动关联 user 实体(LEFT JOIN)
314
+ }) {
315
+ constructor(@InjectRepository(Article) repo) {
316
+ super(repo);
317
+ }
318
+
319
+ // 可根据需要添加业务方法(非覆盖)
320
+ async downloadArticle(id: number): Promise<Buffer> {
321
+ const res = await this.findOne(id);
322
+ return res.data.getContentAsBuffer();
89
323
  }
90
324
  }
91
325
  ```
92
326
 
93
- ## Controller decorators
327
+ ---
328
+
329
+ ### 关于 relations
330
+
331
+ `relations: string[]` 是 `CrudService` 的核心配置项之一。它用于在查询中自动加载关联实体(即 TypeORM 的 `leftJoinAndSelect`)。
332
+
333
+ - `'user'` 表示加载 `article.user`
334
+ - `'user.articles'` 表示递归加载嵌套关系
335
+ - 默认使用 `LEFT JOIN`,如需 `INNER JOIN` 可通过 `Inner('user')` 指定
336
+
337
+ 这能确保你在 Controller 中无需手动构建复杂的 join 查询。
338
+
339
+ ---
340
+
341
+ ### 方法列表
342
+
343
+ | 方法名 | 说明 |
344
+ |------------------|----------------------------------------|
345
+ | `findAll(dto, qb?)` | 查询列表(支持查询装饰器 / 分页) |
346
+ | `findOne(id, qb?)` | 查单条数据,自动关联 / 过滤 / 封装 |
347
+ | `create(dto)` | 创建数据,带验证、钩子处理 |
348
+ | `update(id, dto, extraConditions?)` | 更新数据并支持条件限制 |
349
+ | `delete(id, extraConditions?)` | 删除数据(软删) |
350
+
351
+ ---
352
+
353
+ ### 示例:条件限制用户只能操作自己数据
354
+
355
+ ```ts
356
+ async findOne(id: number, user: User) {
357
+ return this.service.findOne(id, qb => qb.andWhere('userId = :uid', { uid: user.id }));
358
+ }
359
+
360
+ async update(id: number, dto: UpdateDto, user: User) {
361
+ return this.service.update(id, dto, { userId: user.id }); // 附加 where 条件
362
+ }
363
+ ```
364
+
365
+ ---
366
+
367
+ ### 建议实践
368
+
369
+ - 所有实体的服务类都应继承 `CrudService(Entity, options)`
370
+ - `relations` 是推荐使用的配置方式,替代手动 join
371
+ - 如果你有定制查询逻辑,建议用 `super.findAll(...)` + `.data` 进行后处理
372
+ - 避免直接使用 `repo`,使用封装后的方法保持一致性与钩子逻辑生效
373
+
374
+ ---
375
+
376
+ ## 🧩 Controller 自动生成(RestfulFactory)
377
+
378
+ NICOT 提供了 `RestfulFactory(Entity)` 工厂函数,自动为实体生成标准 RESTful Controller 接口装饰器及参数提取器。
379
+
380
+ 你不再需要手动定义每个路由,只需:
381
+
382
+ 1. 创建 DTO(工厂生成)
383
+ 2. 使用工厂提供的装饰器
384
+
385
+ ---
386
+
387
+ ### 一键生成的接口说明
388
+
389
+ | 方法 | 路径 | 功能说明 |
390
+ |--------------------------|-------------------------|---------------------------|
391
+ | `@factory.create()` | `POST /` | 创建,使用 `createDto` |
392
+ | `@factory.findOne()` | `GET /:id` | 获取单条数据 |
393
+ | `@factory.findAll()` | `GET /` | 查询列表,支持过滤 / 分页 |
394
+ | `@factory.update()` | `PATCH /:id` | 修改单条数据 |
395
+ | `@factory.delete()` | `DELETE /:id` | 删除单条数据(软删) |
396
+
397
+ ---
398
+
399
+ ### 参数提取装饰器一览
400
+
401
+ | 装饰器 | 用途说明 |
402
+ |----------------------------|-----------------------------------------|
403
+ | `@factory.createParam()` | 注入 `createDto`,自动校验 body |
404
+ | `@factory.updateParam()` | 注入 `updateDto`,自动校验 body |
405
+ | `@factory.findAllParam()` | 注入 `queryDto`,自动校验 query |
406
+ | `@factory.idParam()` | 注入路径参数中的 id |
407
+
408
+ 这些参数装饰器全部内建了 `ValidationPipe`,支持自动转换与校验。
409
+
410
+ ---
411
+
412
+ ### 查询能力:基于实体字段的装饰器
413
+
414
+ `@factory.findAll()` 所生成的接口具有完整的查询能力,其行为由实体字段上的 `@QueryXXX()` 装饰器控制:
415
+
416
+ ```ts
417
+ @StringColumn(255)
418
+ @QueryEqual()
419
+ name: string;
420
+
421
+ @BoolColumn()
422
+ @QueryMatchBoolean()
423
+ isActive: boolean;
424
+ ```
425
+
426
+ 则生成的 `GET /resource?name=Tom&isActive=true` 接口会自动构建对应的 SQL 条件。
427
+
428
+ ---
94
429
 
95
- Would also register proper OpenAPI documentation for the controller.
430
+ ### 示例 Controller
96
431
 
97
432
  ```ts
98
- const dec = new RestfulFactory(User);
99
- class FindAllUsersDto extends dec.findAllDto {} // to extract type and class
100
- class UpdateUserDto extends dec.updateDto {}
433
+ const factory = new RestfulFactory(User);
434
+ class CreateUserDto extends factory.createDto {}
435
+ class UpdateUserDto extends factory.updateDto {}
436
+ class FindAllUserDto extends factory.findAllDto {}
101
437
 
102
438
  @Controller('user')
103
439
  export class UserController {
104
- constructor(private userService: UserService) {}
440
+ constructor(private readonly service: UserService) {}
105
441
 
106
- @dec.create() // POST /
107
- create(@dec.createParam() user: User) {
108
- return this.userService.create(user);
442
+ @factory.create()
443
+ async create(@factory.createParam() dto: CreateUserDto) {
444
+ return this.service.create(dto);
109
445
  }
110
446
 
111
- @dec.findOne() // GET /:id
112
- findOne(@dec.idParam() id: number) {
113
- return this.userService.findOne(id);
447
+ @factory.findAll()
448
+ async findAll(@factory.findAllParam() dto: FindAllUserDto) {
449
+ return this.service.findAll(dto);
114
450
  }
115
451
 
116
- @dec.findAll() // GET /
117
- findAll(@dec.findAllParam() user: FindAllUsersDto) {
118
- return this.userService.findAll(user);
452
+ @factory.findOne()
453
+ async findOne(@factory.idParam() id: number) {
454
+ return this.service.findOne(id);
119
455
  }
120
456
 
121
- @dec.update() // PATCH /:id
122
- update(@dec.idParam() id: number, @dec.updateParam() user: UpdateUserDto) {
123
- return this.userService.update(id, user);
457
+ @factory.update()
458
+ async update(@factory.idParam() id: number, @factory.updateParam() dto: UpdateUserDto) {
459
+ return this.service.update(id, dto);
124
460
  }
125
461
 
126
- @dec.delete() // DELETE /:id
127
- delete(@dec.idParam() id: number) {
128
- return this.userService.delete(id);
462
+ @factory.delete()
463
+ async delete(@factory.idParam() id: number) {
464
+ return this.service.delete(id);
129
465
  }
130
466
  }
131
467
  ```
132
468
 
133
- ## Return message
469
+ ---
470
+
471
+ ### 补充说明
472
+
473
+ - 所有路由默认返回统一结构(`GenericReturnMessageDto` / `BlankReturnMessageDto`)
474
+ - 所有参数自动校验,无需手动加 `ValidationPipe`
475
+ - `findAll()` 自动支持分页、排序、模糊查询、布尔匹配等
476
+ - 如果你使用了实体关系(relations),则 `findOne()` / `findAll()` 也自动关联查询
477
+ - 所有的接口都是返回状态码 200。
478
+ - OpenAPI 文档会自动生成,包含所有 DTO 类型与查询参数。
479
+ - Service 需要使用 `CrudService(Entity, options)` 进行标准化实现。
480
+
481
+ ---
482
+
483
+ ## 📄 分页查询(自动支持)
484
+
485
+ NICOT 的 `findAll()` 方法默认支持分页,**无需你手动声明分页字段**,框架内部已内置分页 DTO 与逻辑。
486
+
487
+ ---
488
+
489
+ ### ✅ 默认分页行为
134
490
 
135
- Return data of all APIs are in the following format, with proper OpenAPI documentation:
491
+ 所有 `findAll()` 查询接口会自动识别以下 query 参数:
492
+
493
+ | 参数 | 类型 | 默认值 | 说明 |
494
+ |------------------|----------|--------|---------------------------------|
495
+ | `pageCount` | number | `1` | 第几页,从 1 开始 |
496
+ | `recordsPerPage` | number | `25` | 每页多少条数据 |
497
+
498
+ 这些字段由框架内置的 `PageSettingsDto` 管理,自动注入到 `findAllParam()` 的 DTO 中,无需你自己定义。
499
+
500
+ 分页逻辑最终会转化为:
136
501
 
137
502
  ```ts
138
- export interface ReturnMessage<T> {
139
- statusCode: number;
140
- message: string;
141
- success: boolean;
142
- data: T;
503
+ qb.take(recordsPerPage).skip((pageCount - 1) * recordsPerPage);
504
+ ```
505
+
506
+ ---
507
+
508
+ ### 🔧 如何更改分页行为
509
+
510
+ 分页逻辑由实体继承类中的方法控制(如 `getRecordsPerPage()`),如果你希望关闭分页或调高上限,可以 override 这些方法:
511
+
512
+ ```ts
513
+ @Entity()
514
+ class LogEntry extends IdBase() {
515
+ // ...其他字段
516
+
517
+ override getRecordsPerPage() {
518
+ return this.recordsPerPage || 99999; // 禁用分页(或返回极大值)
519
+ }
520
+ }
521
+ ```
522
+
523
+ 这样处理后,该实体的 `findAll()` 查询将默认返回所有数据。
524
+
525
+ ---
526
+
527
+ ### 示例:分页 + 条件查询
528
+
529
+ ```ts
530
+ GET /user?name=Tom&pageCount=2&recordsPerPage=10
531
+ // 查询第 2 页,每页 10 条,筛选 name = Tom 的用户
532
+ ```
533
+
534
+ 你可以在 Controller 中完全不关心这些字段,它们已由 NICOT 自动注入、处理并应用在 QueryBuilder 上。
535
+
536
+ ---
537
+
538
+ ## 📦 统一返回结构与接口注解
539
+
540
+ NICOT 默认提供统一的接口返回格式与 Swagger 自动注解能力,便于前后端标准化对接。
541
+
542
+ ---
543
+
544
+ ### ✅ 返回结构 DTO 类型(用于 Swagger 类型标注)
545
+
546
+ #### `ReturnMessageDto(EntityClass)`
547
+ 用于生成带数据的标准返回结构类型(**不是直接返回值**,用于 `@nestjs/swagger`)。
548
+
549
+ 返回结构样式:
550
+
551
+ ```json
552
+ {
553
+ "statusCode": 200,
554
+ "success": true,
555
+ "message": "success",
556
+ "timestamp": "2025-04-25T12:00:00.000Z",
557
+ "data": { ... }
143
558
  }
144
559
  ```
145
560
 
146
- You may also create a Dto class like this by the following way:
561
+ #### `BlankReturnMessageDto`
562
+ 无数据返回结构的类型(用于 DELETE、UPDATE 等空响应)。
563
+
564
+ ---
565
+
566
+ ### 📊 实际返回结构
567
+
568
+ - **返回数据:**
147
569
 
148
570
  ```ts
149
- export class UserReturnMessage extends ReturnMessageDto(User) {}
571
+ import { GenericReturnMessageDto } from 'nicot';
572
+
573
+ return new GenericReturnMessageDto(200, '操作成功', data);
150
574
  ```
151
575
 
152
- With result into the following class, also with proper OpenAPI documentation:
576
+ - **返回空结构:**
153
577
 
154
578
  ```ts
155
- export class UserReturnMessage {
156
- statusCode: number;
157
- message: string;
158
- success: boolean;
159
- data: User;
579
+ import { BlankReturnMessageDto } from 'nicot';
580
+
581
+ return new BlankReturnMessageDto(204, '删除成功');
582
+ ```
583
+
584
+ - **抛出异常结构:**
585
+
586
+ ```ts
587
+ throw new BlankReturnMessageDto(404, '未找到资源').toException();
588
+ ```
589
+
590
+ ---
591
+
592
+ ### 📚 Swagger 注解装饰器
593
+
594
+ NICOT 提供以下装饰器帮助你自动声明接口返回结构,无需手动写复杂的 `@ApiResponse(...)`:
595
+
596
+ #### `@ApiTypeResponse(EntityClass)`
597
+
598
+ 等价于:
599
+
600
+ ```ts
601
+ @ApiOkResponse({
602
+ type: ReturnMessageDto(EntityClass),
603
+ description: '成功响应结构',
604
+ })
605
+ ```
606
+
607
+ #### `@ApiError(code, message)`
608
+
609
+ 等价于:
610
+
611
+ ```ts
612
+ @ApiResponse({
613
+ status: code,
614
+ description: message,
615
+ type: BlankReturnMessageDto,
616
+ })
617
+ ```
618
+
619
+ ---
620
+
621
+ ### 示例用法
622
+
623
+ ```ts
624
+ @Get()
625
+ @ApiTypeResponse(User)
626
+ @ApiError(404, '未找到用户')
627
+ async findOne(@Query() dto: SearchDto) {
628
+ const user = await this.service.findOne(dto);
629
+ if (!user) {
630
+ throw new BlankReturnMessageDto(404, '未找到用户').toException();
631
+ }
632
+ return new GenericReturnMessageDto(200, '成功', user);
160
633
  }
161
634
  ```
635
+
636
+ ---
637
+
638
+ ## 📥 参数解析 + 验证(DataQuery / DataBody)
639
+
640
+ NICOT 提供便捷装饰器 `@DataQuery()` 与 `@DataBody()`,用于自动完成:
641
+
642
+ - 参数绑定(从 query 或 body)
643
+ - 数据校验(class-validator)
644
+ - 类型转换(`transform: true`)
645
+ - 避免重复书写 ValidationPipe
646
+
647
+ ---
648
+
649
+ ### ✅ 装饰器对照说明
650
+
651
+ | 装饰器 | 等价于标准写法 |
652
+ |----------------|-------------------------------------------------------------------------------|
653
+ | `@DataQuery()` | `@Query(new ValidationPipe({ transform: true }))` |
654
+ | `@DataBody()` | `@Body(new ValidationPipe({ transform: true }))` |
655
+
656
+ 这些装饰器默认启用了:
657
+ - 自动类型转换(如 query string 转 number)
658
+ - 自动剔除未声明字段(`whitelist: true`)
659
+ - 自动抛出校验异常(422)
660
+
661
+ ---
662
+
663
+ ### 示例用法
664
+
665
+ ```ts
666
+ @Get()
667
+ async findAll(@DataQuery() dto: SearchUserDto) {
668
+ return this.service.findAll(dto);
669
+ }
670
+
671
+ @Post()
672
+ async create(@DataBody() dto: CreateUserDto) {
673
+ return this.service.create(dto);
674
+ }
675
+ ```
676
+
677
+ 你无需手动加 `ValidationPipe`,也无需手动处理转换错误或格式校验,NICOT 帮你做好了这一切。
678
+
679
+ ---
680
+
681
+ ## 🧩 实体关系示例
682
+
683
+ ```ts
684
+ @Entity()
685
+ class Article extends IdBase() {
686
+ @QueryEqual()
687
+ @IntColumn('bigint', { unsigned: true })
688
+ userId: number;
689
+
690
+ @ManyToOne(() => User, user => user.articles, { onDelete: 'CASCADE' })
691
+ user: User;
692
+ }
693
+
694
+ @Entity()
695
+ class User extends IdBase() {
696
+ @OneToMany(() => Article, article => article.user)
697
+ articles: Article[];
698
+
699
+ async afterGet() {
700
+ this.articleCount = this.articles.length;
701
+ }
702
+ }
703
+ ```
704
+
705
+ ---
706
+
707
+ ## 📊 和同类框架的对比
708
+
709
+ 在实际开发中,很多框架也提供了 CRUD 接口构建能力,但存在不同程度的痛点。NICOT 从底层设计上解决了这些问题,适合长期维护的中大型后端项目。
710
+
711
+ ---
712
+
713
+ ### ✅ FastAPI / SQLModel(Python)
714
+
715
+ - ✅ 代码简洁,自动生成 OpenAPI 文档
716
+ - ❌ 无字段权限控制(不能区分不可写/不可查)
717
+ - ❌ 查询能力不够细致,字段粒度控制弱
718
+ - ❌ DTO 拆分需手动处理,复杂模型重复多
719
+
720
+ 🔹 **NICOT 优势:**
721
+ - 字段级别控制查询/写入/输出行为
722
+ - 自动生成 DTO + 查询 + OpenAPI + 验证
723
+ - 生命周期钩子和逻辑注入更灵活
724
+
725
+ ---
726
+
727
+ ### ✅ @nestjsx/crud(NestJS)
728
+
729
+ - ✅ 快速生成接口
730
+ - ❌ 安全性差:字段查询/排序过于开放
731
+ - ❌ 控制力弱:很难注入逻辑或自定义查询
732
+ - ❌ Swagger 文档支持不完整
733
+
734
+ 🔹 **NICOT 优势:**
735
+ - 每个字段查询能力需显式声明(不开放默认)
736
+ - 完全类型安全 + 文档自动生成
737
+ - 逻辑钩子、权限注入、返回结构标准化
738
+
739
+ ---
740
+
741
+ ### ✅ nestjs-query
742
+
743
+ - ✅ 支持 GraphQL / REST,类型安全强
744
+ - ❌ 学习曲线陡峭,文档不友好
745
+ - ❌ 查询逻辑复杂,难以上手
746
+ - ❌ 重度依赖 GraphQL 思维模式
747
+
748
+ 🔹 **NICOT 优势:**
749
+ - 更贴合 REST 直觉思维
750
+ - 默认封装,低学习成本
751
+ - 保留足够扩展点,轻松注入业务逻辑
752
+
753
+ ---
754
+
755
+ ### ✅ GraphQL
756
+
757
+ - ✅ 查询自由,前端控制力强
758
+ - ❌ 后端控制弱,权限处理复杂
759
+ - ❌ 易产生过度查询,性能不稳定
760
+ - ❌ 每个字段都必须写解析器,开发成本高
761
+
762
+ 🔹 **NICOT 优势:**
763
+ - 后端主导接口结构,前端只调 REST
764
+ - 查询能力与字段权限完全可控
765
+ - 无需额外解析器,开发更快速
766
+
767
+ ---
768
+
769
+ ### ✅ MyBatis-Plus / Hibernate(Java)
770
+
771
+ - ✅ 成熟,生态强,Java 企业常用
772
+ - ❌ 配置繁杂,样板代码多
773
+ - ❌ 缺乏统一的返回结构与接口注解
774
+ - ❌ 参数校验 / DTO 拆分手动重复
775
+
776
+ 🔹 **NICOT 优势:**
777
+ - 一套装饰器统一字段校验 + ORM + 文档
778
+ - 自动 DTO 拆分,减少重复代码
779
+ - 全自动接口 + 验证 + 注解集成
780
+
781
+ ---
782
+
783
+ ### 🏆 框架能力矩阵对比
784
+
785
+ | 框架 | 自动接口 | 安全性 | 文档支持 | 类型安全 | 查询控制 | 关系联查支持 | 开发效率 |
786
+ |-----------------------------|----------------|----------------|----------------|----------------|------------------|------------------|----------------|
787
+ | **NICOT** | ✅ 全自动 | ✅ 字段级控制 | ✅ 实体即文档 | ✅ 完整类型推导 | ✅ 装饰器精细控制 | ✅ 自动 relations | ✅ 极高 |
788
+ | FastAPI + SQLModel | ✅ 模型映射生成 | ❌ 缺乏限制 | ✅ 自动生成 | ❌ 运行时类型 | ❌ 查询不受控 | 🟡 手写关系加载 | ✅ 高 |
789
+ | @nestjsx/crud | ✅ 快速注册 | ❌ 默认全暴露 | ❌ Swagger 不完整 | ✅ Nest 类型系统 | ❌ 全字段可查 | 🟡 需手动配置 | ✅ 快速上手 |
790
+ | nestjs-query | ✅ 自动暴露接口 | 🟡 DTO 控权限 | 🟡 手动标注文档 | ✅ 强类型推导 | 🟡 灵活但复杂 | ✅ 关系抽象良好 | ❌ 配置繁琐 |
791
+ | GraphQL(code-first) | ❌ Resolver 必写| ❌ 查询不受控 | ✅ 类型强大 | ✅ 静态推导 | ❌ 查询过度灵活 | ✅ 查询关系强 | ❌ 繁琐/易错 |
792
+ | Hibernate(Java) | ❌ 需配 Service | 🟡 靠注解控制 | ❌ 文档需插件 | 🟡 Java 泛型弱 | 🟡 XML/HQL 控制 | ✅ JPA 级联支持 | ❌ 模板代码多 |
793
+ | MyBatis-Plus(Java) | ✅ 注解生成 | ✅ 手写控制 | ❌ 文档缺失 | ❌ 运行期校验 | ❌ 手写 SQL | ❌ 需 JOIN SQL | ❌ 重复手写多 |
794
+ | NestJS + TypeORM + 手动 DTO | ❌ 全手写 | ✅ 自由控制 | ✅ 自己写 | ✅ 类型安全 | 🟡 逻辑自己处理 | 🟡 手写 relations | ❌ 重复代码多 |
795
+
796
+ ---
797
+
798
+ NICOT 作为一个 “Entity 驱动” 的框架,在开发体验、安全性、自动化程度之间找到了平衡,真正做到:
799
+
800
+ > 一份实体定义 → 自动生成完整、安全、文档完备的接口系统
801
+
802
+
803
+ ---
804
+
805
+ ## ✅ 总结
806
+
807
+ **NICOT = Entity 驱动 + 自动生成的一体化后端框架**,涵盖:
808
+
809
+ - 实体建模 → 校验规则 → DTO → OpenAPI
810
+ - 自动生成 Controller / Service
811
+ - 灵活字段控制、查询扩展、用户注入、生命周期钩子
812
+ - 内建返回结构、Swagger 注解、守卫装饰器等功能
813
+
814
+ 是构建 NestJS 标准化、低重复、文档完善的后端服务的理想选择。
@@ -44,14 +44,9 @@ function StringIdBase(idOptions) {
44
44
  columnExtras: { primary: true, nullable: false },
45
45
  }),
46
46
  Reflect.metadata('design:type', String),
47
- ...(idOptions.uuid ? [
48
- (0, typeorm_1.Generated)('uuid'),
49
- (0, decorators_1.NotWritable)(),
50
- ] : [
51
- (0, class_validator_1.IsString)(),
52
- (0, class_validator_1.IsNotEmpty)(),
53
- (0, decorators_1.NotChangeable)(),
54
- ])
47
+ ...(idOptions.uuid
48
+ ? [(0, typeorm_1.Generated)('uuid'), (0, decorators_1.NotWritable)()]
49
+ : [(0, class_validator_1.IsString)(), (0, class_validator_1.IsNotEmpty)(), (0, decorators_1.NotChangeable)()]),
55
50
  ];
56
51
  const dec = (0, nesties_1.MergePropertyDecorators)(decs);
57
52
  dec(cl.prototype, 'id');
@@ -1 +1 @@
1
- {"version":3,"file":"id-base.js","sourceRoot":"","sources":["../../../src/bases/id-base.ts"],"names":[],"mappings":";;;AAAA,2CAAuC;AACvC,qCAAwD;AACxD,wCAAgD;AAChD,8CAKuB;AACvB,qDAAuD;AACvD,qCAAkD;AAMlD,SAAgB,MAAM,CAAC,YAAuB,EAAE;IAC9C,MAAM,EAAE,GAAG,MAAM,MAAO,SAAQ,oBAAQ;QAE7B,UAAU,CAAC,EAA8B,EAAE,UAAkB;YACpE,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YACjC,EAAE,CAAC,OAAO,CAAC,GAAG,UAAU,KAAK,EAAE,MAAM,CAAC,CAAC;YACvC,IAAA,4BAAkB,EAAC,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;KACF,CAAC;IACF,MAAM,GAAG,GAAG,IAAA,iCAAuB,EAAC;QAClC,IAAA,wBAAW,GAAE;QACb,IAAA,sBAAS,EAAC,QAAQ,EAAE;YAClB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,YAAY,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;SACjD,CAAC;QACF,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QACvC,IAAA,mBAAS,EAAC,WAAW,CAAC;KACvB,CAAC,CAAC;IACH,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC;AACZ,CAAC;AArBD,wBAqBC;AAOD,SAAgB,YAAY,CAAC,SAA0B;IACrD,MAAM,EAAE,GAAG,MAAM,YAAa,SAAQ,oBAAQ;QAGnC,UAAU,CACjB,EAAoC,EACpC,UAAkB;YAElB,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YACjC,EAAE,CAAC,OAAO,CAAC,GAAG,UAAU,KAAK,EAAE,KAAK,CAAC,CAAC;YACtC,IAAA,4BAAkB,EAAC,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;KACF,CAAC;IACF,MAAM,IAAI,GAAG;QACX,IAAA,yBAAY,EAAC,SAAS,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;YAC5D,QAAQ,EAAE,CAAC,SAAS,CAAC,IAAI;YACzB,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE;SACjD,CAAC;QACF,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QACvC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YACnB,IAAA,mBAAS,EAAC,MAAM,CAAC;YACjB,IAAA,wBAAW,GAAE;SACd,CAAC,CAAC,CAAC;YACF,IAAA,0BAAQ,GAAE;YACV,IAAA,4BAAU,GAAE;YACZ,IAAA,0BAAa,GAAE;SAChB,CAAC;KACH,CAAC;IACF,MAAM,GAAG,GAAG,IAAA,iCAAuB,EAAC,IAAI,CAAC,CAAC;IAC1C,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC;AACZ,CAAC;AAhCD,oCAgCC"}
1
+ {"version":3,"file":"id-base.js","sourceRoot":"","sources":["../../../src/bases/id-base.ts"],"names":[],"mappings":";;;AAAA,2CAAuC;AACvC,qCAAwD;AACxD,wCAAgD;AAChD,8CAKuB;AACvB,qDAAuD;AACvD,qCAAkD;AAMlD,SAAgB,MAAM,CAAC,YAAuB,EAAE;IAC9C,MAAM,EAAE,GAAG,MAAM,MAAO,SAAQ,oBAAQ;QAE7B,UAAU,CAAC,EAA8B,EAAE,UAAkB;YACpE,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YACjC,EAAE,CAAC,OAAO,CAAC,GAAG,UAAU,KAAK,EAAE,MAAM,CAAC,CAAC;YACvC,IAAA,4BAAkB,EAAC,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;KACF,CAAC;IACF,MAAM,GAAG,GAAG,IAAA,iCAAuB,EAAC;QAClC,IAAA,wBAAW,GAAE;QACb,IAAA,sBAAS,EAAC,QAAQ,EAAE;YAClB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,YAAY,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;SACjD,CAAC;QACF,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QACvC,IAAA,mBAAS,EAAC,WAAW,CAAC;KACvB,CAAC,CAAC;IACH,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC;AACZ,CAAC;AArBD,wBAqBC;AAOD,SAAgB,YAAY,CAAC,SAA0B;IACrD,MAAM,EAAE,GAAG,MAAM,YAAa,SAAQ,oBAAQ;QAGnC,UAAU,CACjB,EAAoC,EACpC,UAAkB;YAElB,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YACjC,EAAE,CAAC,OAAO,CAAC,GAAG,UAAU,KAAK,EAAE,KAAK,CAAC,CAAC;YACtC,IAAA,4BAAkB,EAAC,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;KACF,CAAC;IACF,MAAM,IAAI,GAAG;QACX,IAAA,yBAAY,EAAC,SAAS,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;YAC5D,QAAQ,EAAE,CAAC,SAAS,CAAC,IAAI;YACzB,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE;SACjD,CAAC;QACF,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QACvC,GAAG,CAAC,SAAS,CAAC,IAAI;YAChB,CAAC,CAAC,CAAC,IAAA,mBAAS,EAAC,MAAM,CAAC,EAAE,IAAA,wBAAW,GAAE,CAAC;YACpC,CAAC,CAAC,CAAC,IAAA,0BAAQ,GAAE,EAAE,IAAA,4BAAU,GAAE,EAAE,IAAA,0BAAa,GAAE,CAAC,CAAC;KACjD,CAAC;IACF,MAAM,GAAG,GAAG,IAAA,iCAAuB,EAAC,IAAI,CAAC,CAAC;IAC1C,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC;AACZ,CAAC;AA3BD,oCA2BC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nicot",
3
3
  "description": "Nest.js interacting with class-validator + OpenAPI + TypeORM for Nest.js Restful API development.",
4
- "version": "1.1.7",
4
+ "version": "1.1.8",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {