nicot 1.2.2 → 1.2.4

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,41 +1,90 @@
1
1
  # NICOT
2
2
 
3
- **NICOT** 是一个基于 NestJS + TypeORM 的后端开发框架。通过实体定义即生成:
4
- - 数据库模型(TypeORM)
5
- - 字段校验(class-validator)
6
- - 请求 DTO(Create / Update / Query)
7
- - RESTful 接口与文档(Swagger)
8
- - 统一返回结构、查询控制、权限注入等
3
+ **NICOT** is an entity-driven REST framework for **NestJS + TypeORM**.
9
4
 
10
- 适用于希望快速搭建标准化接口、减少重复代码的后端项目。
5
+ You define an entity once, and NICOT generates:
6
+
7
+ - ORM columns (TypeORM)
8
+ - Validation rules (class-validator)
9
+ - Request DTOs (Create / Update / Query)
10
+ - RESTful endpoints + Swagger docs
11
+ - Unified response shape, pagination, relations loading
12
+
13
+ with **explicit, field-level control** over:
14
+
15
+ - what can be written
16
+ - what can be queried
17
+ - what can be returned
11
18
 
12
19
  ---
13
20
 
14
- ## 📦 安装
21
+ ## Name & Philosophy
22
+
23
+ **NICOT** stands for:
24
+
25
+ - **N** — NestJS
26
+ - **I** — **nesties** (the shared utility toolkit NICOT builds on)
27
+ - **C** — class-validator
28
+ - **O** — OpenAPI / Swagger
29
+ - **T** — TypeORM
30
+
31
+ The name also hints at **“nicotto”** / **“nicotine”**: something small that can be habit-forming. The idea is:
32
+
33
+ > **One entity definition becomes the contract for everything**
34
+ > (DB, validation, DTO, OpenAPI, CRUD, pagination, relations).
15
35
 
16
- 在你的 Nest.js 项目中:
36
+ NICOT’s design is:
37
+
38
+ - **Entity-driven**: metadata lives close to your domain model, not in a separate schema file.
39
+ - **Whitelist-first**: what can be queried or returned is **only** what you explicitly decorate.
40
+ - **AOP-like hooks**: lifecycle methods and query decorators let you inject logic without scattering boilerplate.
41
+
42
+ ---
43
+
44
+ ## Installation
17
45
 
18
46
  ```bash
19
47
  npm install nicot @nestjs/config typeorm @nestjs/typeorm class-validator class-transformer reflect-metadata @nestjs/swagger
20
48
  ```
21
49
 
50
+ NICOT targets:
51
+
52
+ - NestJS ^9 / ^10 / ^11
53
+ - TypeORM ^0.3.x
54
+
22
55
  ---
23
56
 
24
- ## 🧱 定义实体 Entity
57
+ ## Quick Start
58
+
59
+ ### 1. Define your entity
25
60
 
26
61
  ```ts
62
+ import { Entity } from 'typeorm';
63
+ import {
64
+ IdBase,
65
+ StringColumn,
66
+ IntColumn,
67
+ BoolColumn,
68
+ DateColumn,
69
+ NotInResult,
70
+ NotWritable,
71
+ QueryEqual,
72
+ QueryMatchBoolean,
73
+ } from 'nicot';
74
+
27
75
  @Entity()
28
- class User extends IdBase() {
76
+ export class User extends IdBase() {
77
+ @StringColumn(255, { required: true, description: 'User name' })
29
78
  @QueryEqual()
30
- @StringColumn(255, {
31
- required: true,
32
- description: '用户名',
33
- })
34
79
  name: string;
35
80
 
36
81
  @IntColumn('int', { unsigned: true })
37
82
  age: number;
38
83
 
84
+ @BoolColumn({ default: true })
85
+ @QueryMatchBoolean()
86
+ isActive: boolean;
87
+
39
88
  @StringColumn(255)
40
89
  @NotInResult()
41
90
  password: string;
@@ -43,1205 +92,844 @@ class User extends IdBase() {
43
92
  @DateColumn()
44
93
  @NotWritable()
45
94
  createdAt: Date;
46
- }
47
- ```
48
-
49
- ---
50
95
 
51
- ## 🧾 主键基础类:IdBase / StringIdBase
52
-
53
- 在定义实体时,NICOT 提供了两种基础类 `IdBase` 与 `StringIdBase`,可作为实体的继承基类,为你自动处理:
54
-
55
- - 主键字段定义(自增或字符串主键)
56
- - 主键字段的权限控制与文档注解
57
- - 默认排序逻辑(id 降序 / 升序)
58
- - 支持 queryBuilder 查询条件注入
59
-
60
- ---
61
-
62
- ### 1. `IdBase()` - 数字主键(自增)
63
-
64
- 适合常见的自增整型主键使用场景。
96
+ isValidInCreate() {
97
+ return this.age < 18 ? 'Minors are not allowed to register' : undefined;
98
+ }
65
99
 
66
- ```ts
67
- @Entity()
68
- class User extends IdBase() {
69
- // 继承字段:id: number (bigint unsigned, primary key, auto-increment)
100
+ isValidInUpdate() {
101
+ return undefined;
102
+ }
70
103
  }
71
104
  ```
72
105
 
73
- - 自动添加字段:`id: number`
74
- - 默认排序为 `ORDER BY id DESC`
75
- - 使用 `Generated('increment')` 作为主键生成策略
76
- - 搭配 `@IntColumn` + `@NotWritable()`,在创建 / 修改时不可写
106
+ ### 2. Create a RestfulFactory
77
107
 
78
- ---
79
-
80
- ### 2. `StringIdBase()` - 字符串主键(手动或 UUID)
81
-
82
- 适合你希望使用业务主键或 UUID 作为主键的场景。传入 `uuid: true` 参数后自动生成 UUID 主键。
108
+ Best practice: **one factory file per entity**.
83
109
 
84
110
  ```ts
85
- @Entity()
86
- class ApiKey extends StringIdBase({ uuid: true, description: 'API 密钥 ID' }) {
87
- // 继承字段:id: string (uuid, primary key)
88
- }
111
+ // user.factory.ts
112
+ export const UserFactory = new RestfulFactory(User, {
113
+ relations: [], // explicitly loaded relations (for DTO + queries)
114
+ skipNonQueryableFields: true, // query DTO = only fields with @QueryXXX
115
+ });
89
116
  ```
90
117
 
91
- - 自动添加字段:`id: string`
92
- - 默认排序为 `ORDER BY id ASC`
93
- - 支持配置长度(`length`)和描述(`description`)
94
- - `uuid: true` 时自动添加 `@Generated('uuid')`
95
-
96
- ---
97
-
98
- ### 3. 示例对比
118
+ ### 3. Service with CrudService
99
119
 
100
120
  ```ts
101
- @Entity()
102
- class Article extends IdBase({ description: '文章 ID' }) {
103
- // id: number 自动生成
104
- }
105
-
106
- @Entity()
107
- class Token extends StringIdBase({
108
- uuid: true,
109
- description: '访问令牌',
110
- }) {
111
- // id: string,自动生成 UUID
121
+ // user.service.ts
122
+ @Injectable()
123
+ export class UserService extends UserFactory.crudService() {
124
+ constructor(@InjectRepository(User) repo: Repository<User>) {
125
+ super(repo);
126
+ }
112
127
  }
113
128
  ```
114
129
 
115
- ---
116
-
117
- ### 小结
118
-
119
- | 基类 | 主键类型 | 排序默认 | ID 生成策略 | 使用场景 |
120
- |-----------------|------------|----------|----------------------|------------------------|
121
- | `IdBase()` | number | DESC | 自增 `Generated('increment')` | 常规实体 ID |
122
- | `StringIdBase()`| string | ASC | 可选 UUID / 手动输入 | UUID 主键、业务主键等 |
123
-
124
- 建议你为每个实体都继承其中一个基类,以统一主键结构和查询逻辑。
125
-
126
- ---
127
-
128
- ## 🧠 字段装饰器总览
129
-
130
- NICOT 提供了一系列 `***Column()` 装饰器,统一处理字段的:
131
-
132
- - 数据类型定义(TypeORM)
133
- - 输入校验(class-validator)
134
- - 文档描述(@nestjs/swagger)
135
-
136
- ### 字段类型装饰器(`***Column()`)
137
-
138
- | 装饰器名 | 数据类型 | 自动添加的验证与文档 |
139
- |------------------------|-----------------|---------------------------------------|
140
- | `@StringColumn(len)` | varchar | `@IsString()` + `@Length()` |
141
- | `@TextColumn()` | text | `@IsString()` |
142
- | `@UuidColumn()` | uuid | `@IsUUID()` |
143
- | `@IntColumn(type)` | int/bigint/... | `@IsInt()` + Swagger number 类型 |
144
- | `@FloatColumn(type)` | float/decimal | `@IsNumber()` |
145
- | `@BoolColumn()` | boolean | `@IsBoolean()` |
146
- | `@DateColumn()` | Date | `@IsDate()` |
147
- | `@JsonColumn(T)` | 任意对象/数组 (jsonb) | `@IsObject()` / `@ValidateNested()` 等 |
148
- | `@SimpleJsonColumn(T)` | 任意对象/数组 (json) | `@IsObject()` / `@ValidateNested()` 等 |
149
- | `@StringJsonColumn(T)` | 任意对象/数组 (text) | `@IsObject()` / `@ValidateNested()` 等 |
150
-
151
- 所有字段装饰器都支持第二个参数 `options`:
130
+ ### 4. Controller using factory-generated decorators
152
131
 
153
132
  ```ts
154
- @StringColumn(255, {
155
- required: true,
156
- description: '用户名',
157
- default: 'Anonymous',
158
- })
159
- name: string;
160
- ```
161
-
162
- ---
163
-
164
- ## 🔒 字段访问限制装饰器(行为控制)
165
-
166
- NICOT 提供以下装饰器用于控制字段在不同接口中的表现:
167
-
168
- | 装饰器名 | 行为控制说明 |
169
- |----------------------------------------|-----------------------------------------------|
170
- | `@NotWritable()` | 不允许在创建(POST)或修改(PATCH)时传入 |
171
- | `@NotChangeable()` | 不允许在修改(PATCH)时更新(只可创建) |
172
- | `@NotQueryable()` | 不允许在 GET 查询参数中使用该字段 |
173
- | `@NotInResult()` | 不会出现在任何返回结果中(如密码字段) |
174
- | `@NotColumn()` | 不是数据库字段,仅作为查询结果间接字段(在 afterGet 钩子方法赋值) |
175
- | `@QueryColumn()` | 不是数据库字段,仅作为虚拟查询字段(和 @QueryEqual() 等查询装饰器同时使用) |
176
- | `@RelationComputed(() => EntityClass)` | 标识该字段依赖关系字段推导而来(通常在 afterGet) |
133
+ // user.controller.ts
134
+ import { Controller } from '@nestjs/common';
135
+ import { PutUser } from '../auth/put-user.decorator'; // your own decorator
177
136
 
178
- RestfulFactory 处理 Entity 类的时候,会以这些装饰器为依据,裁剪生成的 DTO 和查询参数。
137
+ // Fix DTO types up front
138
+ export class CreateUserDto extends UserFactory.createDto {}
139
+ export class UpdateUserDto extends UserFactory.updateDto {}
140
+ export class FindAllUserDto extends UserFactory.findAllDto {}
179
141
 
180
- 这些限制装饰器非常适合处理:
142
+ @Controller('users')
143
+ export class UserController {
144
+ constructor(private readonly service: UserService) {}
181
145
 
182
- - 安全字段(如密码、Token)
183
- - 系统字段(如创建时间、创建者 ID)
184
- - 只读字段(如 auto-increment 主键)
146
+ @UserFactory.create()
147
+ async create(
148
+ @UserFactory.createParam() dto: CreateUserDto,
149
+ @PutUser() currentUser: User,
150
+ ) {
151
+ // business logic - attach owner
152
+ dto.ownerId = currentUser.id;
153
+ return this.service.create(dto);
154
+ }
185
155
 
186
- ---
156
+ @UserFactory.findAll()
157
+ async findAll(
158
+ @UserFactory.findAllParam() dto: FindAllUserDto,
159
+ @PutUser() currentUser: User,
160
+ ) {
161
+ return this.service.findAll(dto, qb => {
162
+ qb.andWhere('user.ownerId = :uid', { uid: currentUser.id });
163
+ });
164
+ }
187
165
 
188
- ### 示例:完整字段定义
166
+ @UserFactory.findOne()
167
+ async findOne(@UserFactory.idParam() id: number) {
168
+ return this.service.findOne(id);
169
+ }
189
170
 
190
- ```ts
191
- @StringColumn(255, {
192
- required: true,
193
- description: '用户昵称',
194
- })
195
- @NotWritable()
196
- nickname: string;
171
+ @UserFactory.update()
172
+ async update(
173
+ @UserFactory.idParam() id: number,
174
+ @UserFactory.updateParam() dto: UpdateUserDto,
175
+ ) {
176
+ return this.service.update(id, dto);
177
+ }
197
178
 
198
- @BoolColumn()
199
- @QueryMatchBoolean()
200
- isActive: boolean;
179
+ @UserFactory.delete()
180
+ async delete(@UserFactory.idParam() id: number) {
181
+ return this.service.delete(id);
182
+ }
183
+ }
201
184
  ```
202
185
 
203
- ---
204
-
205
- ## 🔍 查询装饰器总览(Query 系列)
206
-
207
- NICOT 提供了一套查询装饰器,用于在 Entity 字段上声明支持的 GET 查询条件。它们会自动应用到 `findAll()` 中的 queryBuilder。
208
-
209
- ### ✅ 内建查询装饰器
210
-
211
- | 装饰器名 | 查询效果 |
212
- |------------------------------------------------------|-------------------------------------------------|
213
- | `@QueryEqual()` | 精确匹配:`WHERE field = :value` |
214
- | `@QueryLike()` | 前缀模糊匹配:`WHERE field LIKE :value%` |
215
- | `@QuerySearch()` | 宽泛模糊搜索:`WHERE field LIKE %:value%` |
216
- | `@QueryMatchBoolean()` | `true/false/1/0` 转换为布尔类型查询 |
217
- | `@QueryEqualZeroNullable()` | `0 → IS NULL`,否则 `= :value`(适合 nullable) |
218
- | `@QueryIn()` | 包含查询:`WHERE field IN (:...value)`,value 可以数组或者逗号分隔 |
219
- | `@QueryNotIn()` | 不包含查询:`WHERE field NOT IN (:...value)` |
220
- | `@QueryGreater(field)` | 大于查询:`WHERE field > :value` |
221
- | `@QueryLess(field)` | 小于查询:`WHERE field < :value` |
222
- | `@QueryGreaterOrEqual(field)` | 大于等于查询:`WHERE field >= :value` |
223
- | `@QueryLessOrEqual(field)` | 小于等于查询:`WHERE field <= :value` |
224
- | `@QueryJsonbHas()` | JSONB 包含键查询:`WHERE field ? :value` |
225
- | `@QueryOperator('=')` | 自定义操作符查询:`WHERE field <operator> :value` |
226
- | `@QueryWrap((entExpr, valExpr) => `${entExpr} = ${valExpr}`) | 自定义查询片段 |
227
- | `@QueryFullText(options)` | 全文搜索查询,只支持 PostgreSQL,会自动建索引 |
228
-
229
- ---
230
-
231
- ### 全文搜索
186
+ Start the Nest app, and you get:
232
187
 
233
- 利用 `@QueryFullText(options)` 装饰器,可以在 PostgreSQL 中实现全文搜索。
188
+ - `POST /users`
189
+ - `GET /users/:id`
190
+ - `GET /users`
191
+ - `PATCH /users/:id`
192
+ - `DELETE /users/:id`
193
+ - Optional `POST /users/import`
234
194
 
235
- 程序启动的时候,会自动创建索引。不需要加 `@Index()`。
236
-
237
- ```ts
238
- @StringColumn(255)
239
- @QueryFullText({
240
- configuration: 'english', // 使用 postgres 搜索配置
241
- tsQueryFunction: 'websearch_to_tsquery'// 使用的 tsquery 函数。默认为 websearch_to_tsquery
242
- orderBySimilarity: true, // 使用相似度排序
243
- })
244
- englishContent: string;
245
-
246
- @StringColumn(255)
247
- @QueryFullText({
248
- parser: 'zhparser', // 使用中文分词器。NICOT 自动管理配置。需要手动给 postgres 添加中文分词器
249
- })
250
- simpleContent: string;
251
- ```
195
+ Documented in Swagger, with DTOs derived directly from your entity definition.
252
196
 
253
197
  ---
254
198
 
255
- ## 🛠 自定义查询装饰器:`QueryCondition()`
199
+ ## Base ID classes: IdBase / StringIdBase
256
200
 
257
- 如果你需要构建更复杂或专用的查询逻辑,可以使用 `QueryCondition()` 创建自己的装饰器:
201
+ In NICOT you usually don’t hand-roll primary key fields. Instead you **inherit** one of the base classes.
258
202
 
259
- ### 示例:大于查询
203
+ ### `IdBase()` — numeric auto-increment primary key
260
204
 
261
205
  ```ts
262
- export const QueryGreater = () =>
263
- QueryCondition((dto, qb, alias, key) => {
264
- if (dto[key] != null) {
265
- qb.andWhere(`${alias}.${key} > :${key}`, { [key]: dto[key] });
266
- }
267
- });
268
- ```
269
-
270
- ### 示例:动态排序字段(带字段名映射)
271
-
272
- ```ts
273
- export const QueryOrderBy = () =>
274
- QueryCondition((dto, qb, alias, key) => {
275
- const orderValue = dto[key];
276
- if (orderValue) {
277
- const originalKey = key.replace(/OrderBy$/, '');
278
- qb.addOrderBy(`${alias}.${originalKey}`, orderValue);
279
- }
280
- });
206
+ @Entity()
207
+ export class Article extends IdBase({ description: 'Article ID' }) {
208
+ // id: number (bigint unsigned, primary, auto-increment)
209
+ }
281
210
  ```
282
211
 
283
- > 使用方式与普通装饰器一致,应用在实体字段上即可。
212
+ Behavior:
284
213
 
285
- ---
214
+ - Adds `id: number` column (`bigint unsigned`, `primary: true`, `Generated('increment')`)
215
+ - Marks it as:
216
+ - `@NotWritable()` — cannot be written via create/update DTO
217
+ - `@IntColumn('bigint', { unsigned: true, ... })`
218
+ - `@QueryEqual()` — usable as `?id=...` in GET queries
219
+ - By default, adds `ORDER BY id DESC` in `applyQuery` (you can override or disable with `noOrderById: true`)
286
220
 
287
- ### 使用效果示例
221
+ ### `StringIdBase()` — string / UUID primary key
288
222
 
289
223
  ```ts
290
- @IntColumn('int', { unsigned: true })
291
- @QueryGreater()
292
- views: number;
293
-
294
- @StringColumn(255)
295
- @QueryLike()
296
- title: string;
297
-
298
- @BoolColumn()
299
- @QueryMatchBoolean()
300
- isPublished: boolean;
301
-
302
- @NotWritable()
303
- @NotInResult()
304
- @QueryOrderBy()
305
- @IsIn(['ASC', 'DESC'])
306
- @ApiProperty({ enum: ['ASC', 'DESC'], description: 'Order by views' })
307
- viewsOrderBy?: 'ASC' | 'DESC';
224
+ @Entity()
225
+ export class ApiKey extends StringIdBase({
226
+ uuid: true,
227
+ description: 'API key ID',
228
+ }) {
229
+ // id: string (uuid, primary)
230
+ }
308
231
  ```
309
232
 
310
- ---
233
+ Behavior:
311
234
 
312
- ## GetMutator
235
+ - Adds `id: string` column
236
+ - When `uuid: true`:
237
+ - `@UuidColumn({ primary: true, generated: true, ... })`
238
+ - `@NotWritable()`
239
+ - When `uuid: false` or omitted:
240
+ - `@StringColumn(length || 255, { required: true, ... })`
241
+ - `@IsNotEmpty()` + `@NotChangeable()` (writable only at create time)
242
+ - Default ordering: `ORDER BY id ASC` (can be disabled via `noOrderById`)
313
243
 
314
- GET 方法的参数只能是 URL 参数,因此数据类型只能是 string,具有局限性。
244
+ Summary:
315
245
 
316
- NICOT 提供了 `GetMutator` 装饰器,允许你在实体字段上定义一个转换函数,将 URL 参数转换为正确的数据类型,并把 OpenAPI 文档中 GET 接口的数据类型改为目标类型。
246
+ | Base class | Type | Default order | Generation strategy |
247
+ |-------------------|---------|---------------|-----------------------------|
248
+ | `IdBase()` | number | `id DESC` | auto increment (`bigint`) |
249
+ | `StringIdBase()` | string | `id ASC` | UUID or manual string |
317
250
 
318
- ### 示例
251
+ ---
319
252
 
320
- ```ts
321
- @JsonColumn(SomeObject)
322
- @GetMutator((val: string) => JSON.parse(val)) // GET 参数不会体现为 SomeObject,而是 string
323
- @QueryOperator('@>') // JSONB 包含查询
324
- meta: SomeObject;
325
- ```
253
+ ## Column decorators overview
326
254
 
327
- ### 内建 GetMutator
255
+ NICOT’s `***Column()` decorators combine:
328
256
 
329
- - `@GetMutatorBool()`
330
- - `@GetMutatorInt()`
331
- - `@GetMutatorFloat()`
332
- - `@GetMutatorStringSeparated(',')`
333
- - `@GetMutatorIntSeparated()`
334
- - `@GetMutatorFloatSeparated()`
335
- - `@GetMutatorJson()`
257
+ - **TypeORM column definition**
258
+ - **class-validator rules**
259
+ - **Swagger `@ApiProperty()` metadata**
336
260
 
337
- > `@BoolColumn()` 已经内建了 `@GetMutatorBool()`
261
+ Common ones:
338
262
 
339
- ---
263
+ | Decorator | DB type | Validation defaults |
264
+ |----------------------|--------------------|-----------------------------|
265
+ | `@StringColumn(len)` | `varchar(len)` | `@IsString()`, `@Length()` |
266
+ | `@TextColumn()` | `text` | `@IsString()` |
267
+ | `@UuidColumn()` | `uuid` | `@IsUUID()` |
268
+ | `@IntColumn(type)` | integer types | `@IsInt()` |
269
+ | `@FloatColumn(type)` | float/decimal | `@IsNumber()` |
270
+ | `@BoolColumn()` | `boolean` | `@IsBoolean()` |
271
+ | `@DateColumn()` | `timestamp`/date | `@IsDate()` |
272
+ | `@JsonColumn(T)` | `jsonb` | `@IsObject()` / nested val. |
273
+ | `@SimpleJsonColumn` | `json` | same as above |
274
+ | `@StringJsonColumn` | `text` (stringified JSON) | same as above |
275
+ | `@EnumColumn(Enum)` | enum or text | enum validation |
340
276
 
341
- ## 🧩 实体关系示例
277
+ All of them accept an `options` parameter:
342
278
 
343
279
  ```ts
344
- @Entity()
345
- class Article extends IdBase() {
346
- @QueryEqual()
347
- @IntColumn('bigint', { unsigned: true })
348
- userId: number;
349
-
350
- @ManyToOne(() => User, user => user.articles, { onDelete: 'CASCADE' })
351
- user: User;
352
- }
353
-
354
- @Entity()
355
- class User extends IdBase() {
356
- @OneToMany(() => Article, article => article.user)
357
- articles: Article[];
358
-
359
- async afterGet() {
360
- this.articleCount = this.articles.length;
361
- }
362
- }
280
+ @StringColumn(255, {
281
+ required: true,
282
+ description: 'Display name',
283
+ default: 'Anonymous',
284
+ })
285
+ displayName: string;
363
286
  ```
364
287
 
365
288
  ---
366
289
 
367
- ## 🔁 生命周期钩子
290
+ ## Access control decorators
368
291
 
369
- 支持在实体中定义以下方法:
292
+ These decorators control **where** a field appears:
370
293
 
371
- ```ts
372
- class User {
373
- async beforeCreate() {}
374
- async afterCreate() {}
375
- async beforeUpdate() {}
376
- async afterUpdate() {}
377
- async beforeGet() {}
378
- async afterGet() {}
379
-
380
- isValidInCreate(): string | undefined {
381
- return this.name ? undefined : '必须填写名称';
382
- }
383
- }
384
- ```
385
-
386
- ---
294
+ - in create/update DTOs
295
+ - in query DTOs (GET)
296
+ - in response DTOs (`ResultDto`)
387
297
 
388
- ## 🛠 使用 CrudService(服务层标准写法)
298
+ ### Write / read restrictions
389
299
 
390
- NICOT 提供了 `CrudService(Entity, options)`,是所有资源的标准服务实现方式。
300
+ | Decorator | Effect on DTOs |
301
+ |-------------------|-----------------------------------------------------------|
302
+ | `@NotWritable()` | Removed from both Create & Update DTO |
303
+ | `@NotCreatable()` | Removed from Create DTO only |
304
+ | `@NotChangeable()`| Removed from Update DTO only |
305
+ | `@NotQueryable()` | Removed from GET DTO (query params), can’t be used in filters |
306
+ | `@NotInResult()` | Removed from all response DTOs (including nested relations) |
391
307
 
392
- 你只需继承它,并传入对应的实体和配置,即可拥有完整的:
393
- - 查询(支持分页、排序、过滤、关系)
394
- - 创建、更新、删除(带钩子、校验、字段控制)
395
- - 统一返回结构
308
+ ### Non-column & virtual fields
396
309
 
397
- ---
310
+ | Decorator | Meaning |
311
+ |----------------------|-------------------------------------------------------------------------|
312
+ | `@NotColumn()` | Not mapped to DB; usually set in `afterGet()` as a computed field |
313
+ | `@QueryColumn()` | Only exists in query DTO (no DB column), used with `@QueryXXX()` |
314
+ | `@RelationComputed(() => Class)` | Virtual field that depends on relations; participates in relation pruning |
398
315
 
399
- ### 定义 Service
316
+ Example:
400
317
 
401
318
  ```ts
402
- import { CrudService } from 'nicot';
319
+ @Entity()
320
+ export class User extends IdBase() {
321
+ @StringColumn(255, { required: true })
322
+ name: string;
403
323
 
404
- @Injectable()
405
- export class ArticleService extends CrudService(Article, {
406
- relations: ['user'], // 自动关联 user 实体(LEFT JOIN)
407
- }) {
408
- constructor(@InjectRepository(Article) repo) {
409
- super(repo);
410
- }
324
+ @StringColumn(255)
325
+ @NotInResult()
326
+ password: string;
411
327
 
412
- // 可根据需要添加业务方法(非覆盖)
413
- async downloadArticle(id: number): Promise<Buffer> {
414
- const res = await this.findOne(id);
415
- return res.data.getContentAsBuffer();
416
- }
328
+ @DateColumn()
329
+ @NotWritable()
330
+ createdAt: Date;
331
+
332
+ @NotColumn()
333
+ @RelationComputed(() => Profile)
334
+ profileSummary: ProfileSummary;
417
335
  }
418
336
  ```
419
337
 
420
- ---
338
+ ### Decorator priority (simplified)
421
339
 
422
- ### 关于 relations
340
+ When NICOT generates DTOs, it applies a **whitelist/cut-down** pipeline. Roughly:
423
341
 
424
- `relations: string[]` `CrudService` 的核心配置项之一。它用于在查询中自动加载关联实体(即 TypeORM 的 `leftJoinAndSelect`)。
342
+ - **Create DTO omits**:
343
+ - `@NotColumn`
344
+ - `@NotWritable`
345
+ - `@NotCreatable`
346
+ - factory options: `fieldsToOmit`, `writeFieldsToOmit`, `createFieldsToOmit`
347
+ - relation fields (TypeORM relations are not part of simple create DTO)
348
+ - **Update DTO omits**:
349
+ - `@NotColumn`
350
+ - `@NotWritable`
351
+ - `@NotChangeable`
352
+ - factory options: `fieldsToOmit`, `writeFieldsToOmit`, `updateFieldsToOmit`
353
+ - **Query DTO (GET) omits**:
354
+ - `@NotColumn`
355
+ - `@NotQueryable`
356
+ - fields that **require a GetMutator** but do not actually have one
357
+ - **Response DTO (`ResultDto`) omits**:
358
+ - `@NotInResult`
359
+ - factory `outputFieldsToOmit`
360
+ - relation fields that are not in the current `relations` whitelist
425
361
 
426
- - `'user'` 表示加载 `article.user`
427
- - `'user.articles'` 表示递归加载嵌套关系
428
- - 默认使用 `LEFT JOIN`,如需 `INNER JOIN` 可通过 `Inner('user')` 指定
362
+ In short:
429
363
 
430
- 这能确保你在 Controller 中无需手动构建复杂的 join 查询。
364
+ > If you mark something as “not writable / queryable / in result”, that wins, regardless of column type or other decorators.
431
365
 
432
366
  ---
433
367
 
434
- ### 方法列表
435
-
436
- | 方法名 | 说明 |
437
- |------------------|----------------------------------------|
438
- | `findAll(dto, qb?)` | 查询列表(支持查询装饰器 / 分页) |
439
- | `findOne(id, qb?)` | 查单条数据,自动关联 / 过滤 / 封装 |
440
- | `create(dto)` | 创建数据,带验证、钩子处理 |
441
- | `update(id, dto, extraConditions?)` | 更新数据并支持条件限制 |
442
- | `delete(id, extraConditions?)` | 删除数据(软删) |
368
+ ## Query decorators & QueryCondition
443
369
 
444
- ---
370
+ Query decorators define **how a field is translated into SQL** in GET queries.
445
371
 
446
- ### 示例:条件限制用户只能操作自己数据
372
+ Internally they all use a `QueryCondition` callback:
447
373
 
448
374
  ```ts
449
- async findOne(id: number, user: User) {
450
- return this.service.findOne(id, qb => qb.andWhere('userId = :uid', { uid: user.id }));
451
- }
452
-
453
- async update(id: number, dto: UpdateDto, user: User) {
454
- return this.service.update(id, dto, { userId: user.id }); // 附加 where 条件
455
- }
375
+ export const QueryCondition = (cond: QueryCond) =>
376
+ Metadata.set(
377
+ 'queryCondition',
378
+ cond,
379
+ 'queryConditionFields',
380
+ ) as PropertyDecorator;
456
381
  ```
457
382
 
458
- ---
459
-
460
- ### 建议实践
461
-
462
- - 所有实体的服务类都应继承 `CrudService(Entity, options)`
463
- - `relations` 是推荐使用的配置方式,替代手动 join
464
- - 如果你有定制查询逻辑,建议用 `super.findAll(...)` + `.data` 进行后处理
465
- - 避免直接使用 `repo`,使用封装后的方法保持一致性与钩子逻辑生效
466
-
467
- ---
468
-
469
- ## 🧩 Controller 自动生成(RestfulFactory)
470
-
471
- NICOT 提供了 `RestfulFactory(Entity)` 工厂函数,自动为实体生成标准 RESTful Controller 接口装饰器及参数提取器。
472
-
473
- 你不再需要手动定义每个路由,只需:
383
+ ### Lifecycle in `findAll()` / `findAllCursorPaginated()`
474
384
 
475
- 1. 创建 DTO(工厂生成)
476
- 2. 使用工厂提供的装饰器
385
+ When you call `CrudBase.findAll(ent)`:
477
386
 
478
- ---
479
-
480
- ### 一键生成的接口说明
387
+ 1. NICOT creates a new entity instance.
388
+ 2. Copies the DTO into it.
389
+ 3. Calls `beforeGet()` (if present) — good place to adjust defaults.
390
+ 4. Calls `entity.applyQuery(qb, alias)` — from your base class (e.g. `IdBase` adds `orderBy(id desc)`).
391
+ 5. Applies relations joins (`relations` config).
392
+ 6. Iterates over all fields with `QueryCondition` metadata and runs the conditions to mutate the `SelectQueryBuilder`.
481
393
 
482
- | 方法 | 路径 | 功能说明 |
483
- |--------------------------|-------------------------|---------------------------|
484
- | `@factory.create()` | `POST /` | 创建,使用 `createDto` |
485
- | `@factory.findOne()` | `GET /:id` | 获取单条数据 |
486
- | `@factory.findAll()` | `GET /` | 查询列表,支持过滤 / 分页 |
487
- | `@factory.update()` | `PATCH /:id` | 修改单条数据 |
488
- | `@factory.delete()` | `DELETE /:id` | 删除单条数据(软删) |
489
-
490
- ---
394
+ So `@QueryXXX()` is a **declarative hook** into the query building stage.
491
395
 
492
- ### 参数提取装饰器一览
396
+ ### Built-in Query decorators
493
397
 
494
- | 装饰器 | 用途说明 |
495
- |----------------------------|-----------------------------------------|
496
- | `@factory.createParam()` | 注入 `createDto`,自动校验 body |
497
- | `@factory.updateParam()` | 注入 `updateDto`,自动校验 body |
498
- | `@factory.findAllParam()` | 注入 `queryDto`,自动校验 query |
499
- | `@factory.idParam()` | 注入路径参数中的 id |
500
-
501
- 这些参数装饰器全部内建了 `ValidationPipe`,支持自动转换与校验。
502
-
503
- ---
398
+ Based on `QueryWrap` / `QueryCondition`:
504
399
 
505
- ### 查询能力:基于实体字段的装饰器
400
+ - Simple operators:
401
+ - `@QueryEqual()`
402
+ - `@QueryGreater()`, `@QueryGreaterEqual()`
403
+ - `@QueryLess()`, `@QueryLessEqual()`
404
+ - `@QueryNotEqual()`
405
+ - `@QueryOperator('<', 'fieldName?')` for fully custom operators
406
+ - LIKE / search:
407
+ - `@QueryLike()` (prefix match `field LIKE value%`)
408
+ - `@QuerySearch()` (contains match `field LIKE %value%`)
409
+ - Boolean handling:
410
+ - `@QueryMatchBoolean()` — parses `"true" / "false" / "1" / "0"`
411
+ - Arrays / IN:
412
+ - `@QueryIn()` — `IN (...)`, supports comma-separated strings or arrays
413
+ - `@QueryNotIn()` — `NOT IN (...)`
414
+ - Null handling:
415
+ - `@QueryEqualZeroNullable()` — `0` (or `"0"`) becomes `IS NULL`, others `= :value`
416
+ - JSON:
417
+ - `@QueryJsonbHas()` — Postgres `?` operator on jsonb field
506
418
 
507
- `@factory.findAll()` 所生成的接口具有完整的查询能力,其行为由实体字段上的 `@QueryXXX()` 装饰器控制:
419
+ All of these are high-level wrappers over the central abstraction:
508
420
 
509
421
  ```ts
510
- @StringColumn(255)
511
- @QueryEqual()
512
- name: string;
513
-
514
- @BoolColumn()
515
- @QueryMatchBoolean()
516
- isActive: boolean;
422
+ export const QueryWrap = (wrapper: QueryWrapper, field?: string) =>
423
+ QueryCondition((obj, qb, entityName, key) => {
424
+ // ...convert obj[key] and call qb.andWhere(...)
425
+ });
517
426
  ```
518
427
 
519
- 则生成的 `GET /resource?name=Tom&isActive=true` 接口会自动构建对应的 SQL 条件。
428
+ ### Composing conditions: QueryAnd / QueryOr
520
429
 
521
- ---
522
-
523
- ### RestfulFactory 配置项
430
+ You can combine multiple `QueryCondition` implementations:
524
431
 
525
432
  ```ts
526
- export interface RestfulFactoryOptions<T> {
527
- fieldsToOmit?: (keyof T)[]; // 不出现在任何输入 DTO 中的字段
528
- writeFieldsToOmit?: (keyof T)[]; // 不出现在创建与更新 DTO 中的字段
529
- createFieldsToOmit?: (keyof T)[]; // 不出现在创建 DTO 中的字段
530
- updateFieldsToOmit?: (keyof T)[]; // 不出现在更新 DTO 中的字段
531
- findAllFieldsToOmit?: (keyof T)[]; // 不出现在查询 DTO 中的字段
532
- outputFieldsToOmit?: (keyof T)[]; // 不出现在任何输出 DTO 中的字段
533
- prefix?: string; // 接口的路由前缀
534
- keepEntityVersioningDates?: boolean; // 在返回结果中保留实体的 createTime / updateTime 字段
535
- entityClassName?: string; // 实体类名称,如果存在同一 Entity 的多个 RestfulFactory 实例时需要指定,避免 OpenAPI 类型冲突
536
- relations?: (string | RelationDef)[]; // 关联加载的关系字段,传给 CrudService 的 relations 参数,以及用于生成关系 DTO
537
- skipNonQueryableFields?: boolean; // 在查询 DTO 中跳过所有没有 @QueryXXX() 装饰器的字段
538
- }
433
+ export const QueryAnd = (...decs: PropertyDecorator[]) => { /* ... */ };
434
+ export const QueryOr = (...decs: PropertyDecorator[]) => { /* ... */ };
539
435
  ```
540
436
 
541
- ---
542
-
543
- ### 示例 Controller
437
+ - `QueryAnd(A, B)` — run both conditions on the same field (AND).
438
+ - `QueryOr(A, B)` — build an `(A) OR (B)` bracket group.
544
439
 
545
- ```ts
546
- const factory = new RestfulFactory(User, { relations: ['articles'] });
547
- class CreateUserDto extends factory.createDto {}
548
- class UpdateUserDto extends factory.updateDto {}
549
- class FindAllUserDto extends factory.findAllDto {}
440
+ These are useful for e.g. multi-column search or fallback logic.
550
441
 
551
- @Controller('user')
552
- export class UserController {
553
- constructor(private readonly service: UserService) {}
442
+ ### Full-text search: `QueryFullText`
554
443
 
555
- @factory.create()
556
- async create(@factory.createParam() dto: CreateUserDto) {
557
- return this.service.create(dto);
558
- }
444
+ PostgreSQL-only helper:
559
445
 
560
- @factory.findAll()
561
- async findAll(@factory.findAllParam() dto: FindAllUserDto) {
562
- return this.service.findAll(dto);
563
- }
564
-
565
- @factory.findOne()
566
- async findOne(@factory.idParam() id: number) {
567
- return this.service.findOne(id);
568
- }
569
-
570
- @factory.update()
571
- async update(@factory.idParam() id: number, @factory.updateParam() dto: UpdateUserDto) {
572
- return this.service.update(id, dto);
573
- }
574
-
575
- @factory.delete()
576
- async delete(@factory.idParam() id: number) {
577
- return this.service.delete(id);
578
- }
579
- }
446
+ ```ts
447
+ @StringColumn(255)
448
+ @QueryFullText({
449
+ configuration: 'english',
450
+ tsQueryFunction: 'websearch_to_tsquery',
451
+ orderBySimilarity: true,
452
+ })
453
+ content: string;
580
454
  ```
581
455
 
582
- ---
456
+ NICOT will:
583
457
 
584
- ### 补充说明
458
+ - On module init, create needed text search configuration & indexes.
459
+ - For queries, generate `to_tsvector(...) @@ websearch_to_tsquery(...)`.
460
+ - Optionally compute a `rank` subject and order by it when `orderBySimilarity: true`.
585
461
 
586
- - 所有路由默认返回统一结构(`GenericReturnMessageDto` / `BlankReturnMessageDto`)
587
- - 所有参数自动校验,无需手动加 `ValidationPipe`
588
- - `findAll()` 自动支持分页、排序、模糊查询、布尔匹配等
589
- - 如果你使用了实体关系(relations),则 `findOne()` / `findAll()` 也自动关联查询
590
- - 所有的接口都是返回状态码 200。
591
- - OpenAPI 文档会自动生成,包含所有 DTO 类型与查询参数。
592
- - Service 需要使用 `CrudService(Entity, options)` 进行标准化实现。
462
+ > **Note:** full-text features are intended for **PostgreSQL**. On other databases they are not supported.
593
463
 
594
464
  ---
595
465
 
596
- ### 导出 DTO
466
+ ## GetMutator & MutatorPipe
597
467
 
598
- `RestfulFactory` 会自动生成以下 DTO 类:供你导出并在其他的 OpenAPI 装饰器中使用。
468
+ GET query params are **always strings** on the wire, but entities may want richer types (arrays, numbers, JSON objects).
599
469
 
600
- ```ts
601
- const factory = new RestfulFactory(User, {
602
- relations: ['articles'],
603
- });
470
+ NICOT uses:
604
471
 
605
- class CreateUserDto extends factory.createDto {} // 创建用 DTO,在 POST /user 中使用
606
- class UpdateUserDto extends factory.updateDto {} // 更新用 DTO,在 PATCH /user/:id 中使用
607
- class FindAllUserDto extends factory.findAllDto {} // 查询用 DTO,在 GET /user 中使用
608
- class UserResultDto extends factory.entityResultDto {} // 查询结果 DTO,在 GET /user/:id 和 GET /user 中返回
609
- class UserCreateResultDto extends factory.entityCreateResultDto {} // 创建结果 DTO,在 POST /user 中返回。相比 entityResultDto 省略了间接字段和关系字段
610
- class UserReturnMessageDto extends factory.entityReturnMessageDto {} // 相当于 ReturnMessageDto(UserResultDto),在 GET /user 中返回
611
- class UserCreateReturnMessageDto extends factory.entityCreateReturnMessageDto {} // 相当于 ReturnMessageDto(UserCreateResultDto),在 POST /user 中返回
612
- class UserArrayResultDto extends factory.entityArrayResultDto {} // 相当于 PaginatedReturnMessageDto(UserResultDto),在 GET /user 中返回
613
- ```
472
+ - `@GetMutator(...)` metadata on the entity field
473
+ - `MutatorPipe` to apply the conversion at runtime
474
+ - `PatchColumnsInGet` to adjust Swagger docs for GET DTOs
614
475
 
615
- ---
476
+ ### Concept
616
477
 
617
- ### 关系定义
478
+ 1. Swagger/OpenAPI for GET shows the field as **string** (or string-based, possibly with example/enum from the mutator).
479
+ 2. At runtime, `MutatorPipe` reads the string value and calls your mutator function.
480
+ 3. The controller receives a **typed DTO** (e.g. array of numbers, parsed JSON) even though the URL carried strings.
618
481
 
619
- 类似于 `CrudService`,`RestfulFactory` 也需要在配置中定义关系字段。语法和 `CrudService` 的 `relations` 参数完全一致。
482
+ ### Example
620
483
 
621
484
  ```ts
622
- class User extends IdBase() {
623
- @OneToMany(() => Article, article => article.user)
624
- articles: Article[];
485
+ @JsonColumn(SomeFilterObject)
486
+ @GetMutatorJson() // parse JSON string from ?filter=...
487
+ @QueryOperator('@>') // use jsonb containment operator
488
+ filter: SomeFilterObject;
489
+ ```
625
490
 
626
- @OneToMany(() => Comment, comment => comment.user)
627
- comments: Comment[];
491
+ Built-in helpers include:
628
492
 
629
- @OneToMany(() => Like, like => like.users)
630
- likes: Like[];
631
- }
493
+ - `@GetMutatorBool()`
494
+ - `@GetMutatorInt()`
495
+ - `@GetMutatorFloat()`
496
+ - `@GetMutatorStringSeparated(',')`
497
+ - `@GetMutatorIntSeparated()`
498
+ - `@GetMutatorFloatSeparated()`
499
+ - `@GetMutatorJson()`
632
500
 
633
- class Article extends IdBase() {
634
- @ManyToOne(() => User, user => user.articles)
635
- user: User;
501
+ Internally, `PatchColumnsInGet` tweaks Swagger metadata so that:
636
502
 
637
- @OneToMany(() => Comment, comment => comment.article)
638
- comments: Comment[];
503
+ - Fields with GetMutator are shown as `type: string` (with `example` / `enum` if provided by the mutator metadata).
504
+ - Other queryable fields have their default value cleared (so GET docs don’t misleadingly show defaults).
639
505
 
640
- @OneToMany(() => Like, like => like.article)
641
- likes: Like[];
642
- }
506
+ And `RestfulFactory.findAllParam()` wires everything together:
643
507
 
644
- class Like extends IdBase() {
645
- @ManyToOne(() => User, user => user.likes)
646
- user: User;
508
+ - Applies `MutatorPipe` if GetMutators exist.
509
+ - Applies `OmitPipe(fieldsInGetToOmit)` to strip non-queryable fields.
510
+ - Optionally applies `PickPipe(queryableFields)` when `skipNonQueryableFields: true`.
647
511
 
648
- @ManyToOne(() => Article, article => article.likes)
649
- article: Article;
650
- }
512
+ ---
651
513
 
652
- class Comment extends IdBase() {
653
- @ManyToOne(() => Article, article => article.comments)
654
- article: Article;
514
+ ## `skipNonQueryableFields`: only expose explicitly declared query fields
655
515
 
656
- @ManyToOne(() => User, user => user.articles)
657
- user: User;
658
- }
516
+ By default, `findAllDto` is:
659
517
 
660
- const factory = new RestfulFactory(User, {
661
- relations: ['comments', 'articles', 'articles.comments'], // 生成的 DTO 类中,只含有标明的关系字段,而 articles.user 不会被包含
662
- });
518
+ - Entity fields minus:
519
+ - `@NotColumn`
520
+ - TypeORM relations
521
+ - `@NotQueryable`
522
+ - fields that require GetMutator but don’t have one
523
+ - Plus `PageSettingsDto`’s pagination fields (`pageCount`, `recordsPerPage`).
663
524
 
664
- class UserResultDto extends factory.entityResultDto {
665
- // 生成的 DTO 类中包含 comments, articles, articles.comments 字段
666
- // 但是不包含 likes, articles.user, articles.likes 等未声明关系字段
667
- }
668
- ```
669
-
670
- 如果你的配套 `CrudService` 不准备加载任何关系,那么可以传入空数组:
525
+ If you want GET queries to accept **only** fields that have `@QueryEqual()` / `@QueryLike()` / `@QueryIn()` etc, use:
671
526
 
672
527
  ```ts
673
- const factory = new RestfulFactory(User, {
674
- relations: [], // DTO 不包含任何关系字段
528
+ const UserFactory = new RestfulFactory(User, {
529
+ relations: [],
530
+ skipNonQueryableFields: true,
675
531
  });
676
532
  ```
677
533
 
678
- 如果不写 `relations`,则默认会尽可能加载所有非 `@NotInResult()` 的关系字段。但现在推荐显式声明需要加载的关系,以避免不必要的 OpenAPI 文档杂乱。
679
-
680
- > 这是曾经版本的 nicot (<1.1.9) 的做法。
681
-
682
- ---
683
-
684
- ### 依赖关系的间接字段
534
+ Effects:
685
535
 
686
- 如果你有实体类,某一间接字段(`@NotColumn()`),依赖某个关系字段,那么需要显示声明这个字段。
536
+ - `findAllDto` keeps only fields that:
537
+ - have a `QueryCondition` (i.e. some `@QueryXXX()` decorator),
538
+ - and are not in the omit list (`NotQueryable`, `NotColumn`, missing mutator).
539
+ - Swagger query params = exactly those queryable fields.
540
+ - At runtime, `findAllParam()` runs `PickPipe(queryableFields)`, so stray query params are dropped.
687
541
 
688
- ```ts
689
- export class Participant extends IdBase() {
690
- @OneToMany(() => Match, match => match.player1)
691
- matches1: Match[];
692
-
693
- @OneToMany(() => Match, match => match.player2)
694
- matches2: Match[];
695
- }
696
-
697
- export class Match extends IdBase() {
698
- @ManyToOne(() => Participant, participant => participant.matches1)
699
- player1: Participant;
700
- @ManyToOne(() => Participant, participant => participant.matches2)
701
- player2: Participant;
702
-
703
- @NotColumn()
704
- @RelationComputed(() => Participant) // 声明这个字段依赖于 player1 和 player2 生成,当作关系参与裁剪,避免被拖入 Participant 属性黑洞
705
- players: Participant[];
706
-
707
- async afterGet() {
708
- this.players = [this.player1, this.player2].filter(s => s);
709
- }
710
- }
711
-
712
- const factory = new RestfulFactory(Match, {
713
- relations: ['player1', 'player2', 'players'],
714
- });
715
-
716
- class MatchResultDto extends factory.entityResultDto {
717
- // 包含 player1, player2, players 字段,但是不包含 player1.matches1, player1.matches2 等间接关系字段
718
- }
719
- ```
542
+ Mental model:
720
543
 
721
- ---
544
+ > “If you want a field to be filterable in GET `/users`, you **must** explicitly add a `@QueryXXX()` decorator. Otherwise it’s invisible.”
722
545
 
723
- ## 📄 分页查询(自动支持)
546
+ Recommended:
724
547
 
725
- NICOT `findAll()` 方法默认支持分页,**无需你手动声明分页字段**,框架内部已内置分页 DTO 与逻辑。
548
+ - For **admin / multi-tenant APIs** → turn `skipNonQueryableFields: true` ON.
549
+ - For **internal tools / quick debugging** → you can leave it OFF for convenience.
726
550
 
727
551
  ---
728
552
 
729
- ### ✅ 默认分页行为
730
-
731
- 所有 `findAll()` 查询接口会自动识别以下 query 参数:
553
+ ## Pagination
732
554
 
733
- | 参数 | 类型 | 默认值 | 说明 |
734
- |------------------|----------|--------|---------------------------------|
735
- | `pageCount` | number | `1` | 第几页,从 1 开始 |
736
- | `recordsPerPage` | number | `25` | 每页多少条数据 |
555
+ ### Offset pagination (default)
737
556
 
738
- 这些字段由框架内置的 `PageSettingsDto` 管理,自动注入到 `findAllParam()` DTO 中,无需你自己定义。
557
+ Every `findAll()` uses **offset pagination** via `PageSettingsDto`:
739
558
 
740
- 分页逻辑最终会转化为:
741
-
742
- ```ts
743
- qb.take(recordsPerPage).skip((pageCount - 1) * recordsPerPage);
744
- ```
745
-
746
- ---
559
+ - Query fields:
560
+ - `pageCount` (1-based)
561
+ - `recordsPerPage` (default 25)
562
+ - Internally:
563
+ - Applies `.take(recordsPerPage).skip((pageCount - 1) * recordsPerPage)`
747
564
 
748
- ### 🔧 如何更改分页行为
565
+ If your entity extends `PageSettingsDto`, it can control defaults by overriding methods like `getRecordsPerPage()`.
749
566
 
750
- 分页逻辑由实体继承类中的方法控制(如 `getRecordsPerPage()`),如果你希望关闭分页或调高上限,可以 override 这些方法:
567
+ You can also effectively “disable” pagination for specific entities by returning a large value:
751
568
 
752
569
  ```ts
753
570
  @Entity()
754
- class LogEntry extends IdBase() {
755
- // ...其他字段
571
+ export class LogEntry extends IdBase() {
572
+ // ...
756
573
 
757
- override getRecordsPerPage() {
758
- return this.recordsPerPage || 99999; // 禁用分页(或返回极大值)
574
+ getRecordsPerPage() {
575
+ return this.recordsPerPage || 99999;
759
576
  }
760
577
  }
761
578
  ```
762
579
 
763
- 这样处理后,该实体的 `findAll()` 查询将默认返回所有数据。
764
-
765
- ---
766
-
767
- ### 示例:分页 + 条件查询
768
-
769
- ```
770
- GET /user?name=Tom&pageCount=2&recordsPerPage=10
771
- // 查询第 2 页,每页 10 条,筛选 name = Tom 的用户
772
- ```
773
-
774
- 你可以在 Controller 中完全不关心这些字段,它们已由 NICOT 自动注入、处理并应用在 QueryBuilder 上。
580
+ ### Cursor pagination
775
581
 
776
- ---
777
-
778
- ## 🔁 游标分页(Cursor Pagination)
582
+ NICOT also supports **cursor-based pagination** via:
779
583
 
780
- NICOT 支持游标式分页查询(Cursor-based Pagination),相比传统的页码分页,在数据量大、频繁变更或无限滚动的场景中更加稳定可靠。
781
-
782
- ---
584
+ - `CrudBase.findAllCursorPaginated()`
585
+ - `RestfulFactory.findAllCursorPaginatedDto`
586
+ - `entityCursorPaginationReturnMessageDto`
783
587
 
784
- ### ✅ 使用方式
785
-
786
- 定义查询 DTO 时继承工厂生成的游标分页基类:
588
+ Usage sketch:
787
589
 
788
590
  ```ts
789
- class FindAllUserCursorDto extends factory.findAllCursorPaginatedDto {}
790
- ```
791
-
792
- 在 Controller 中,使用以下工厂方法:
591
+ class FindAllUserCursorDto extends UserFactory.findAllCursorPaginatedDto {}
793
592
 
794
- ```ts
795
- @factory.findAllCursorPaginated()
796
- async findAll(@factory.findAllParam() dto: FindAllUserCursorDto) {
797
- return this.service.findAllCursorPaginated(dto);
593
+ @UserFactory.findAllCursorPaginated()
594
+ async findAll(
595
+ @UserFactory.findAllParam() dto: FindAllUserCursorDto,
596
+ ) {
597
+ return this.service.findAllCursorPaginated(dto);
798
598
  }
799
599
  ```
800
600
 
801
- > ⚠️ 注意:`findAll()` 与 `findAllCursorPaginated()` **不能同时使用**,因为它们会绑定到同一个 GET `/` 路由。请选择其中一种分页模式。
601
+ Notes:
602
+
603
+ - Offset vs cursor pagination share the same query decorators and entity metadata.
604
+ - You choose one mode per controller route (`paginateType: 'offset' | 'cursor' | 'none'` in `baseController()`).
605
+ - Cursor payload and multi-column sorting behavior are documented in more detail in the API reference.
802
606
 
803
607
  ---
804
608
 
805
- ### 📥 请求字段说明
609
+ ## CrudBase & CrudService
806
610
 
807
- | 字段名 | 类型 | 描述 |
808
- |--------------------|---------|------------------------------------------------|
809
- | `recordsPerPage` | number | 每页数据数量,默认 25 |
810
- | `paginationCursor` | string | 上一次请求返回的游标(`nextCursor` 或 `previousCursor`)|
611
+ `CrudBase<T>` holds the core CRUD and query logic:
811
612
 
812
- - 首次请求无需传 `paginationCursor`
813
- - 后续请求使用返回的游标即可获取上一页或下一页数据
613
+ - `create(ent, beforeCreate?)`
614
+ - `findOne(id, extraQuery?)`
615
+ - `findAll(dto?, extraQuery?)`
616
+ - `findAllCursorPaginated(dto?, extraQuery?)`
617
+ - `update(id, dto, cond?)`
618
+ - `delete(id, cond?)`
619
+ - `importEntities(entities, extraChecking?)`
620
+ - `exists(id)`
621
+ - `onModuleInit()` (full-text index loader for Postgres)
814
622
 
815
- ---
623
+ It honors:
816
624
 
817
- ### 📤 返回结构说明
625
+ - Relations configuration (`relations` → joins + DTO shape)
626
+ - `NotInResult` / `outputFieldsToOmit` in responses (`cleanEntityNotInResultFields()`)
627
+ - Lifecycle hooks on the entity:
628
+ - `beforeCreate` / `afterCreate`
629
+ - `beforeGet` / `afterGet`
630
+ - `beforeUpdate` / `afterUpdate`
631
+ - `isValidInCreate` / `isValidInUpdate` (return a string = validation error)
818
632
 
819
- 返回值格式与传统分页一致,但字段不同:
633
+ You usually don’t subclass `CrudBase` directly; instead you use:
820
634
 
821
- ```json
822
- {
823
- "statusCode": 200,
824
- "success": true,
825
- "message": "success",
826
- "timestamp": "2025-04-25T12:00:00.000Z",
827
- "data": [{}],
828
- "nextCursor": "eyJpZCI6MTAwfQ",
829
- "previousCursor": "eyJpZCI6NDB9"
635
+ ```ts
636
+ export function CrudService<T extends ValidCrudEntity<T>>(
637
+ entityClass: ClassType<T>,
638
+ crudOptions: CrudOptions<T> = {},
639
+ ) {
640
+ return class CrudServiceImpl extends CrudBase<T> {
641
+ constructor(repo: Repository<T>) {
642
+ super(entityClass, repo, crudOptions);
643
+ }
644
+ };
830
645
  }
831
646
  ```
832
647
 
833
- - 游标格式为 Base64URL 编码(安全可用于 URL 参数)
834
- - `nextCursor` / `previousCursor` 是可选字段,仅在有下一页或上一页时返回
648
+ And let `RestfulFactory` call this for you via `factory.crudService()`.
835
649
 
836
- ---
837
-
838
- ### 🔐 兼容性说明
839
-
840
- - 所有字段控制装饰器(如 `@NotInResult()`, `@QueryEqual()`, `@NotQueryable()` 等)在游标分页中同样生效
841
- - 查询参数仍来自实体声明,Swagger 自动生成文档
842
- - 无需变更现有实体结构,只需更换 `findAllDto` 和分页调用方法
650
+ > You can still use TypeORM’s repository methods directly in **custom business methods**, but when you do, entity lifecycle hooks (`beforeGet()`, `afterGet()`, etc.) are not automatically applied. For NICOT-managed resources, prefer going through `CrudBase` when you want its behavior.
843
651
 
844
652
  ---
845
653
 
846
- ### 适用场景
654
+ ## RestfulFactory: DTO & Controller generator
847
655
 
848
- - 无限滚动分页加载(如微博、时间线)
849
- - 数据频繁变动(传统分页页数易错)
850
- - 前后端希望避免“总页数”等全表统计带来的性能消耗
851
-
852
- ---
656
+ `RestfulFactory<T>` is the heart of “entity → DTOs → controller decorators” mapping.
853
657
 
854
- ### 🧪 示例请求
658
+ ### Options
855
659
 
856
- ```http
857
- GET /user?recordsPerPage=20&paginationCursor=eyJpZCI6MTAwfQ
660
+ ```ts
661
+ interface RestfulFactoryOptions<T> {
662
+ fieldsToOmit?: (keyof T)[];
663
+ writeFieldsToOmit?: (keyof T)[];
664
+ createFieldsToOmit?: (keyof T)[];
665
+ updateFieldsToOmit?: (keyof T)[];
666
+ findAllFieldsToOmit?: (keyof T)[];
667
+ outputFieldsToOmit?: (keyof T)[];
668
+ prefix?: string;
669
+ keepEntityVersioningDates?: boolean;
670
+ entityClassName?: string;
671
+ relations?: (string | RelationDef)[];
672
+ skipNonQueryableFields?: boolean;
673
+ }
858
674
  ```
859
675
 
860
- ---
861
-
862
- ### 🛑 注意事项
863
-
864
- - 不支持跳页(如 pageCount = 5 这种跳转)
865
- - 不再返回 `pageCount`、`totalPages` 等字段
866
- - 若你的 Controller 中已有 `@factory.findAll()`,请不要再使用游标分页版本
676
+ Key ideas:
867
677
 
868
- ---
678
+ - **relations**: both for:
679
+ - which relations are eager-loaded and exposed in DTO,
680
+ - and which joins are added to queries.
681
+ - **outputFieldsToOmit**: extra fields to drop from response DTOs (in addition to `@NotInResult`).
682
+ - **prefix**: extra path prefix for controller decorators (e.g. `v1/users`).
683
+ - **skipNonQueryableFields**: described above.
869
684
 
870
- ## 一键生成 Controller
685
+ ### Auto-generated DTOs
871
686
 
872
- 在一般情况下,可以使用 `factory.baseController()` 生成 RESTful 控制器,自动处理所有 CRUD 接口。
687
+ For a factory:
873
688
 
874
689
  ```ts
875
- const factory = new RestfulFactory(User, {
876
- relations: ['articles'],
877
- });
878
-
879
- @Controller('user')
880
- class UserController extends factory.baseController() {
881
- constructor(userService: UserService) {
882
- super(userService)
883
- }
884
- }
690
+ export const UserFactory = new RestfulFactory(User, { relations: [] });
885
691
  ```
886
692
 
887
- 这样就可以自动生成所有 CRUD 接口,无需手动编写。
693
+ NICOT gives you:
694
+
695
+ - `UserFactory.createDto`
696
+ - `UserFactory.updateDto`
697
+ - `UserFactory.findAllDto`
698
+ - `UserFactory.findAllCursorPaginatedDto`
699
+ - `UserFactory.entityResultDto`
700
+ - `UserFactory.entityCreateResultDto`
701
+ - `UserFactory.entityReturnMessageDto`
702
+ - `UserFactory.entityCreateReturnMessageDto`
703
+ - `UserFactory.entityArrayReturnMessageDto`
704
+ - `UserFactory.entityCursorPaginationReturnMessageDto`
888
705
 
889
- ### 选项
706
+ Recommended usage:
890
707
 
891
708
  ```ts
892
- class UserController extends factory.baseController({
893
- pagination: 'offset' // findAll 的分页模式。可以是 'offset', 'cursor', 'none'。默认为 'offset'
894
- globalMethodDecorators: [ApiError(404, 'Error')] // 每个方法都添加的装饰器
895
- routes: {
896
- findOne: {
897
- methodDecorators: [] // 本方法的装饰器
898
- },
899
- import: {
900
- enabled: false // 禁用该路由
901
- },
902
- // ...
903
- }
904
- }) {}
709
+ export class CreateUserDto extends UserFactory.createDto {}
710
+ export class UpdateUserDto extends UserFactory.updateDto {}
711
+ export class FindAllUserDto extends UserFactory.findAllDto {}
712
+ export class UserResultDto extends UserFactory.entityResultDto {}
905
713
  ```
906
714
 
907
- > 如果需要覆盖某个方法的实现,请在 `routes` 中设置 `enabled: false`,然后手动实现该方法。
715
+ This keeps types stable and easy to re-use in custom endpoints or guards.
908
716
 
909
- > 如果该 Controller 内任意路由写了 `enabled: true`,那么该 Controller 内只有 `enabled: true` 的路由会被生成。
717
+ ### Controller decorators & params
910
718
 
911
- ---
912
-
913
- ## 一键生成 CrudService
719
+ Each factory exposes decorators that match CRUD methods:
914
720
 
915
- 利用 `factory.crudService()` 生成标准的 CRUD 服务类,自动处理所有 CRUD 接口。效果与 `CrudService(Entity, options)` 类似。
721
+ - `create()` + `createParam()`
722
+ - `findOne()` + `idParam()`
723
+ - `findAll()` / `findAllCursorPaginated()` + `findAllParam()`
724
+ - `update()` + `updateParam()`
725
+ - `delete()`
726
+ - `import()` (`POST /import`)
916
727
 
917
- `relations` 的配置与 `RestfulFactory` 的 `relations` 参数一致,保证 DTO 与查询参数的一致性。
728
+ These decorators stack:
918
729
 
919
- ```ts
920
- const factory = new RestfulFactory(User, {
921
- relations: ['articles'],
922
- });
923
-
924
- class UserService extends factory.crudService() {
925
- constructor(@InjectRepository(User) repo) {
926
- super(repo);
927
- }
928
- }
929
- ```
730
+ - HTTP method + path (optionally prefixed)
731
+ - Swagger operation and response schemas (using the generated DTOs)
732
+ - Validation & transform pipes (through DataPipe / OptionalDataPipe / OmitPipe / MutatorPipe)
930
733
 
931
- 推荐在 Entity 文件中定义 `RestfulFactory`,然后在 Service 中使用 `factory.crudService()` 生成服务类,而在 Controller 中使用 `factory.baseController()` 生成控制器。
734
+ Example (revised):
932
735
 
933
736
  ```ts
934
- // user.entity.ts
935
- @Entity()
936
- export class User extends IdBase() {
937
- //
938
- }
939
-
940
- export const UserRestfulFactory = new RestfulFactory(User, {
941
- relations: ['articles'], // 自动代入 UserService 和 UserController 的 relations
737
+ // post.factory.ts
738
+ export const PostFactory = new RestfulFactory(Post, {
739
+ relations: [], // no relations for this resource
942
740
  });
943
741
 
944
- // user.service.ts
742
+ // post.service.ts
945
743
  @Injectable()
946
- export class UserService extends UserRestfulFactory.crudService() {
947
- constructor(@InjectRepository(User) repo) {
744
+ export class PostService extends PostFactory.crudService() {
745
+ constructor(@InjectRepository(Post) repo: Repository<Post>) {
948
746
  super(repo);
949
747
  }
950
748
  }
951
749
 
952
- // user.controller.ts
953
- @Controller('user')
954
- export class UserController extends UserRestfulFactory.baseController() {
955
- constructor(userService: UserService) {
956
- super(userService);
957
- }
958
- }
959
- ```
960
-
961
- 这么做可以真正实现『一处定义,处处使用』,避免了 DTO 与查询参数的重复定义。
962
-
963
- ---
964
-
965
- ## 📦 统一返回结构与接口注解
750
+ // post.controller.ts
751
+ import { PutUser } from '../common/put-user.decorator';
966
752
 
967
- NICOT 默认提供统一的接口返回格式与 Swagger 自动注解能力,便于前后端标准化对接。
753
+ export class FindAllPostDto extends PostFactory.findAllDto {}
754
+ export class CreatePostDto extends PostFactory.createDto {}
968
755
 
969
- ---
970
-
971
- ### 返回结构 DTO 类型(用于 Swagger 类型标注)
756
+ @Controller('posts')
757
+ export class PostController {
758
+ constructor(private readonly service: PostService) {}
972
759
 
973
- #### `ReturnMessageDto(EntityClass)`
974
-
975
- 用于生成带数据的标准返回结构类型(**不是直接返回值**,用于 `@nestjs/swagger`)。
760
+ @PostFactory.findAll()
761
+ async findAll(
762
+ @PostFactory.findAllParam() dto: FindAllPostDto,
763
+ @PutUser() user: User,
764
+ ) {
765
+ return this.service.findAll(dto, qb => {
766
+ qb.andWhere('post.userId = :uid', { uid: user.id });
767
+ });
768
+ }
976
769
 
977
- ```json
978
- {
979
- "statusCode": 200,
980
- "success": true,
981
- "message": "success",
982
- "timestamp": "2025-04-25T12:00:00.000Z",
983
- "data": {}
770
+ @PostFactory.create()
771
+ async create(
772
+ @PostFactory.createParam() dto: CreatePostDto,
773
+ @PutUser() user: User,
774
+ ) {
775
+ dto.userId = user.id;
776
+ return this.service.create(dto);
777
+ }
984
778
  }
985
779
  ```
986
780
 
987
- #### `BlankReturnMessageDto`
781
+ ### `baseController()` shortcut
988
782
 
989
- 无数据返回结构的类型(用于 DELETE、UPDATE 等空响应)。
783
+ If you don’t have extra logic, you can generate a full controller class:
990
784
 
991
- ```json
992
- {
993
- "statusCode": 200,
994
- "success": true,
995
- "message": "success"
785
+ ```ts
786
+ @Controller('users')
787
+ export class UserController extends UserFactory.baseController({
788
+ paginateType: 'offset', // 'offset' | 'cursor' | 'none'
789
+ globalMethodDecorators: [],
790
+ routes: {
791
+ import: { enabled: false }, // disable /import
792
+ },
793
+ }) {
794
+ constructor(service: UserService) {
795
+ super(service);
796
+ }
996
797
  }
997
798
  ```
998
799
 
999
- #### `PaginatedReturnMessageDto(EntityClass)`
1000
-
1001
- 带有分页信息的返回结构类型。
800
+ - If **any** route in `routes` has `enabled: true`, then **only** explicitly enabled routes are generated.
801
+ - Otherwise, all routes are generated except ones marked `enabled: false`.
1002
802
 
1003
- > EntityClass 会自动变成数组类型。
1004
-
1005
- ```json
1006
- {
1007
- "statusCode": 200,
1008
- "success": true,
1009
- "message": "success",
1010
- "timestamp": "2025-04-25T12:00:00.000Z",
1011
- "data": [{}],
1012
- "total": 100,
1013
- "totalPages": 4,
1014
- "pageCount": 1,
1015
- "recordsPerPage": 25
1016
- }
1017
- ```
803
+ This is useful for quickly bootstrapping admin APIs, then selectively disabling / overriding certain endpoints.
1018
804
 
1019
805
  ---
1020
806
 
1021
- ### 📊 实际返回结构
807
+ ## Relations & RelationComputed
1022
808
 
1023
- - **返回数据:**
809
+ Relations are controlled by:
1024
810
 
1025
- ```ts
1026
- import { GenericReturnMessageDto } from 'nicot';
811
+ - TypeORM decorators on the entity: `@ManyToOne`, `@OneToMany`, etc.
812
+ - NICOT’s `relations` whitelist in:
813
+ - `RestfulFactory` options
814
+ - `CrudOptions` for `CrudService` / `CrudBase`
1027
815
 
1028
- return new GenericReturnMessageDto(200, '操作成功', data);
1029
- ```
1030
-
1031
- - **返回空结构:**
816
+ Example:
1032
817
 
1033
818
  ```ts
1034
- import { BlankReturnMessageDto } from 'nicot';
819
+ @Entity()
820
+ export class User extends IdBase() {
821
+ @OneToMany(() => Article, article => article.user)
822
+ articles: Article[];
823
+ }
1035
824
 
1036
- return new BlankReturnMessageDto(204, '删除成功');
825
+ @Entity()
826
+ export class Article extends IdBase() {
827
+ @ManyToOne(() => User, user => user.articles)
828
+ user: User;
829
+ }
1037
830
  ```
1038
831
 
1039
- - **抛出异常结构:**
832
+ If you configure:
1040
833
 
1041
834
  ```ts
1042
- throw new BlankReturnMessageDto(404, '未找到资源').toException();
835
+ export const UserFactory = new RestfulFactory(User, {
836
+ relations: ['articles'],
837
+ });
1043
838
  ```
1044
839
 
1045
- ---
840
+ Then:
1046
841
 
1047
- ### 📚 Swagger 注解装饰器
842
+ - `UserResultDto` includes `articles` but not `articles.user` (no recursive explosion).
843
+ - Query joins `user.articles` when using `findOne` / `findAll`.
1048
844
 
1049
- NICOT 提供以下装饰器帮助你自动声明接口返回结构,无需手动写复杂的 `@ApiResponse(...)`:
845
+ ### Virtual relation: `RelationComputed`
1050
846
 
1051
- #### `@ApiTypeResponse(EntityClass)`
847
+ Sometimes you want a **computed field** that conceptually depends on relations, but is not itself a DB column.
1052
848
 
1053
- 等价于:
849
+ Example:
1054
850
 
1055
851
  ```ts
1056
- @ApiOkResponse({
1057
- type: ReturnMessageDto(EntityClass),
1058
- description: '成功响应结构',
1059
- })
1060
- ```
1061
-
1062
- #### `@ApiError(code, message)`
1063
-
1064
- 等价于:
1065
-
1066
- ```ts
1067
- @ApiResponse({
1068
- status: code,
1069
- description: message,
1070
- type: BlankReturnMessageDto,
1071
- })
1072
- ```
852
+ @Entity()
853
+ export class Match extends IdBase() {
854
+ @ManyToOne(() => Participant, p => p.matches1)
855
+ player1: Participant;
1073
856
 
1074
- ---
857
+ @ManyToOne(() => Participant, p => p.matches2)
858
+ player2: Participant;
1075
859
 
1076
- ### 示例用法
860
+ @NotColumn()
861
+ @RelationComputed(() => Participant)
862
+ players: Participant[];
1077
863
 
1078
- ```ts
1079
- @Get()
1080
- @ApiTypeResponse(User)
1081
- @ApiError(404, '未找到用户')
1082
- async findOne(@Query() dto: SearchDto) {
1083
- const user = await this.service.findOne(dto);
1084
- if (!user) {
1085
- throw new BlankReturnMessageDto(404, '未找到用户').toException();
864
+ async afterGet() {
865
+ this.players = [this.player1, this.player2].filter(Boolean);
1086
866
  }
1087
- return new GenericReturnMessageDto(200, '成功', user);
1088
867
  }
1089
- ```
1090
-
1091
- ---
1092
868
 
1093
- ## 📥 参数解析 + 验证(DataQuery / DataBody)
869
+ export const MatchFactory = new RestfulFactory(Match, {
870
+ relations: ['player1', 'player2', 'players'],
871
+ });
872
+ ```
1094
873
 
1095
- NICOT 提供便捷装饰器 `@DataQuery()` 与 `@DataBody()`,用于自动完成:
874
+ NICOT will:
1096
875
 
1097
- - 参数绑定(从 query body)
1098
- - 数据校验(class-validator)
1099
- - 类型转换(`transform: true`)
1100
- - 避免重复书写 ValidationPipe
876
+ - Treat `players` as a “computed relation” for pruning rules.
877
+ - Include `players` in the result DTO, but **not** recursively include all fields from `Participant.matches1`/`matches2` etc.
878
+ - This keeps DTOs from blowing up due to cyclic relations.
1101
879
 
1102
880
  ---
1103
881
 
1104
- ### 装饰器对照说明
882
+ ## Unified response shape
1105
883
 
1106
- | 装饰器 | 等价于标准写法 |
1107
- |----------------|-------------------------------------------------------------------------------|
1108
- | `@DataQuery()` | `@Query(new ValidationPipe({ transform: true }))` |
1109
- | `@DataBody()` | `@Body(new ValidationPipe({ transform: true }))` |
1110
-
1111
- 这些装饰器默认启用了:
1112
- - 自动类型转换(如 query string 转 number)
1113
- - 自动剔除未声明字段(`whitelist: true`)
1114
- - 自动抛出校验异常(422)
1115
-
1116
- ---
1117
-
1118
- ### 示例用法
884
+ NICOT uses a uniform wrapper for all responses:
1119
885
 
1120
886
  ```ts
1121
- @Get()
1122
- async findAll(@DataQuery() dto: SearchUserDto) {
1123
- return this.service.findAll(dto);
1124
- }
1125
-
1126
- @Post()
1127
- async create(@DataBody() dto: CreateUserDto) {
1128
- return this.service.create(dto);
887
+ {
888
+ statusCode: number;
889
+ success: boolean;
890
+ message: string;
891
+ timestamp?: string;
892
+ data?: any;
1129
893
  }
1130
894
  ```
1131
895
 
1132
- 你无需手动加 `ValidationPipe`,也无需手动处理转换错误或格式校验,NICOT 帮你做好了这一切。
896
+ Types are built via generics:
1133
897
 
1134
- ---
1135
-
1136
- ## 📊 和同类框架的对比
898
+ - `ReturnMessageDto(Entity)` — single payload
899
+ - `PaginatedReturnMessageDto(Entity)` — with `total`, `totalPages`, etc.
900
+ - `CursorPaginationReturnMessageDto(Entity)` — with `nextCursor`, `previousCursor`
901
+ - `BlankReturnMessageDto` — for responses with no data
1137
902
 
1138
- 在实际开发中,很多框架也提供了 CRUD 接口构建能力,但存在不同程度的痛点。NICOT 从底层设计上解决了这些问题,适合长期维护的中大型后端项目。
1139
-
1140
- ---
903
+ And correspondingly in `RestfulFactory`:
1141
904
 
1142
- ### ✅ FastAPI / SQLModel(Python)
905
+ - `entityReturnMessageDto`
906
+ - `entityCreateReturnMessageDto`
907
+ - `entityArrayReturnMessageDto`
908
+ - `entityCursorPaginationReturnMessageDto`
1143
909
 
1144
- - 代码简洁,自动生成 OpenAPI 文档
1145
- - ❌ 无字段权限控制(不能区分不可写/不可查)
1146
- - ❌ 查询能力不够细致,字段粒度控制弱
1147
- - ❌ DTO 拆分需手动处理,复杂模型重复多
1148
-
1149
- 🔹 **NICOT 优势:**
1150
- - 字段级别控制查询/写入/输出行为
1151
- - 自动生成 DTO + 查询 + OpenAPI + 验证
1152
- - 生命周期钩子和逻辑注入更灵活
910
+ You can still build custom endpoints and return these wrappers manually if needed.
1153
911
 
1154
912
  ---
1155
913
 
1156
- ### @nestjsx/crud(NestJS)
1157
-
1158
- - ✅ 快速生成接口
1159
- - ❌ 安全性差:字段查询/排序过于开放
1160
- - ❌ 控制力弱:很难注入逻辑或自定义查询
1161
- - ❌ Swagger 文档支持不完整
914
+ ## Best practices
1162
915
 
1163
- 🔹 **NICOT 优势:**
1164
- - 每个字段查询能力需显式声明(不开放默认)
1165
- - 完全类型安全 + 文档自动生成
1166
- - 逻辑钩子、权限注入、返回结构标准化
916
+ - **One factory per entity**, in its own `*.factory.ts` file.
917
+ - Keeps entity, factory, service, controller decoupled but aligned.
918
+ - Let **entities own the contract**:
919
+ - Column types
920
+ - Validation
921
+ - Access control (`@NotWritable`, `@NotInResult`, `@NotQueryable`)
922
+ - Query capabilities (`@QueryXXX`)
923
+ - For list APIs, strongly consider:
924
+ - `skipNonQueryableFields: true`
925
+ - `@QueryXXX` only on fields you really want public filtering on.
926
+ - Prefer `CrudService` / `CrudBase` for NICOT-managed resources, so:
927
+ - lifecycle hooks are honored,
928
+ - relations + “not in result” logic stay consistent.
929
+ - Use raw TypeORM repository methods only for clearly separated custom flows, and treat them as “outside NICOT”.
1167
930
 
1168
931
  ---
1169
932
 
1170
- ### ✅ nestjs-query
1171
-
1172
- - ✅ 支持 GraphQL / REST,类型安全强
1173
- - ❌ 学习曲线陡峭,文档不友好
1174
- - ❌ 查询逻辑复杂,难以上手
1175
- - ❌ 重度依赖 GraphQL 思维模式
1176
-
1177
- 🔹 **NICOT 优势:**
1178
- - 更贴合 REST 直觉思维
1179
- - 默认封装,低学习成本
1180
- - 保留足够扩展点,轻松注入业务逻辑
1181
-
1182
- ---
1183
-
1184
- ### ✅ GraphQL
1185
-
1186
- - ✅ 查询自由,前端控制力强
1187
- - ❌ 后端控制弱,权限处理复杂
1188
- - ❌ 易产生过度查询,性能不稳定
1189
- - ❌ 每个字段都必须写解析器,开发成本高
1190
-
1191
- 🔹 **NICOT 优势:**
1192
- - 后端主导接口结构,前端只调 REST
1193
- - 查询能力与字段权限完全可控
1194
- - 无需额外解析器,开发更快速
1195
-
1196
- ---
1197
-
1198
- ### ✅ MyBatis-Plus / Hibernate(Java)
1199
-
1200
- - ✅ 成熟,生态强,Java 企业常用
1201
- - ❌ 配置繁杂,样板代码多
1202
- - ❌ 缺乏统一的返回结构与接口注解
1203
- - ❌ 参数校验 / DTO 拆分手动重复
1204
-
1205
- 🔹 **NICOT 优势:**
1206
- - 一套装饰器统一字段校验 + ORM + 文档
1207
- - 自动 DTO 拆分,减少重复代码
1208
- - 全自动接口 + 验证 + 注解集成
1209
-
1210
- ---
1211
-
1212
- ### 🏆 框架能力矩阵对比
1213
-
1214
- | 框架 | 自动接口 | 安全性 | 文档支持 | 类型安全 | 查询控制 | 关系联查支持 | 开发效率 |
1215
- |-----------------------------|----------------|----------------|----------------|----------------|------------------|------------------|----------------|
1216
- | **NICOT** | ✅ 全自动 | ✅ 字段级控制 | ✅ 实体即文档 | ✅ 完整类型推导 | ✅ 装饰器精细控制 | ✅ 自动 relations | ✅ 极高 |
1217
- | FastAPI + SQLModel | ✅ 模型映射生成 | ❌ 缺乏限制 | ✅ 自动生成 | ❌ 运行时类型 | ❌ 查询不受控 | 🟡 手写关系加载 | ✅ 高 |
1218
- | @nestjsx/crud | ✅ 快速注册 | ❌ 默认全暴露 | ❌ Swagger 不完整 | ✅ Nest 类型系统 | ❌ 全字段可查 | 🟡 需手动配置 | ✅ 快速上手 |
1219
- | nestjs-query | ✅ 自动暴露接口 | 🟡 DTO 控权限 | 🟡 手动标注文档 | ✅ 强类型推导 | 🟡 灵活但复杂 | ✅ 关系抽象良好 | ❌ 配置繁琐 |
1220
- | GraphQL(code-first) | ❌ Resolver 必写| ❌ 查询不受控 | ✅ 类型强大 | ✅ 静态推导 | ❌ 查询过度灵活 | ✅ 查询关系强 | ❌ 繁琐/易错 |
1221
- | Hibernate(Java) | ❌ 需配 Service | 🟡 靠注解控制 | ❌ 文档需插件 | 🟡 Java 泛型弱 | 🟡 XML/HQL 控制 | ✅ JPA 级联支持 | ❌ 模板代码多 |
1222
- | MyBatis-Plus(Java) | ✅ 注解生成 | ✅ 手写控制 | ❌ 文档缺失 | ❌ 运行期校验 | ❌ 手写 SQL | ❌ 需 JOIN SQL | ❌ 重复手写多 |
1223
- | NestJS + TypeORM + 手动 DTO | ❌ 全手写 | ✅ 自由控制 | ✅ 自己写 | ✅ 类型安全 | 🟡 逻辑自己处理 | 🟡 手写 relations | ❌ 重复代码多 |
1224
-
1225
- ---
1226
-
1227
- NICOT 作为一个 “Entity 驱动” 的框架,在开发体验、安全性、自动化程度之间找到了平衡,真正做到:
1228
-
1229
- > 一份实体定义 → 自动生成完整、安全、文档完备的接口系统
1230
-
1231
-
1232
- ---
1233
-
1234
- ## ✅ 总结
1235
-
1236
- **NICOT = Entity 驱动 + 自动生成的一体化后端框架**,涵盖:
1237
-
1238
- - 实体建模 → 校验规则 → DTO → OpenAPI
1239
- - 自动生成 Controller / Service
1240
- - 灵活字段控制、查询扩展、用户注入、生命周期钩子
1241
- - 内建返回结构、Swagger 注解、守卫装饰器等功能
1242
-
1243
- 是构建 NestJS 标准化、低重复、文档完善的后端服务的理想选择。
1244
-
1245
- ## LICENSE
933
+ ## License
1246
934
 
1247
935
  MIT