nicot 1.2.3 → 1.2.5

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,1102 @@ class User extends IdBase() {
43
92
  @DateColumn()
44
93
  @NotWritable()
45
94
  createdAt: Date;
95
+
96
+ isValidInCreate() {
97
+ return this.age < 18 ? 'Minors are not allowed to register' : undefined;
98
+ }
99
+
100
+ isValidInUpdate() {
101
+ return undefined;
102
+ }
46
103
  }
47
104
  ```
48
105
 
49
- ---
106
+ ### 2. Create a RestfulFactory
50
107
 
51
- ## 🧾 主键基础类:IdBase / StringIdBase
108
+ Best practice: **one factory file per entity**.
52
109
 
53
- 在定义实体时,NICOT 提供了两种基础类 `IdBase` 与 `StringIdBase`,可作为实体的继承基类,为你自动处理:
110
+ ```ts
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
+ });
116
+ ```
54
117
 
55
- - 主键字段定义(自增或字符串主键)
56
- - 主键字段的权限控制与文档注解
57
- - 默认排序逻辑(id 降序 / 升序)
58
- - 支持 queryBuilder 查询条件注入
118
+ ### 3. Service with CrudService
59
119
 
60
- ---
61
-
62
- ### 1. `IdBase()` - 数字主键(自增)
120
+ ```ts
121
+ // user.service.ts
122
+ @Injectable()
123
+ export class UserService extends UserFactory.crudService() {
124
+ constructor(@InjectRepository(User) repo: Repository<User>) {
125
+ super(repo);
126
+ }
127
+ }
128
+ ```
63
129
 
64
- 适合常见的自增整型主键使用场景。
130
+ ### 4. Controller using factory-generated decorators
65
131
 
66
132
  ```ts
67
- @Entity()
68
- class User extends IdBase() {
69
- // 继承字段:id: number (bigint unsigned, primary key, auto-increment)
133
+ // user.controller.ts
134
+ import { Controller } from '@nestjs/common';
135
+ import { PutUser } from '../auth/put-user.decorator'; // your own decorator
136
+
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 {}
141
+
142
+ @Controller('users')
143
+ export class UserController {
144
+ constructor(private readonly service: UserService) {}
145
+
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
+ }
155
+
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
+ }
165
+
166
+ @UserFactory.findOne()
167
+ async findOne(@UserFactory.idParam() id: number) {
168
+ return this.service.findOne(id);
169
+ }
170
+
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
+ }
178
+
179
+ @UserFactory.delete()
180
+ async delete(@UserFactory.idParam() id: number) {
181
+ return this.service.delete(id);
182
+ }
70
183
  }
71
184
  ```
72
185
 
73
- - 自动添加字段:`id: number`
74
- - 默认排序为 `ORDER BY id DESC`
75
- - 使用 `Generated('increment')` 作为主键生成策略
76
- - 搭配 `@IntColumn` + `@NotWritable()`,在创建 / 修改时不可写
186
+ Start the Nest app, and you get:
187
+
188
+ - `POST /users`
189
+ - `GET /users/:id`
190
+ - `GET /users`
191
+ - `PATCH /users/:id`
192
+ - `DELETE /users/:id`
193
+ - Optional `POST /users/import`
194
+
195
+ Documented in Swagger, with DTOs derived directly from your entity definition.
77
196
 
78
197
  ---
79
198
 
80
- ### 2. `StringIdBase()` - 字符串主键(手动或 UUID)
199
+ ## Base ID classes: IdBase / StringIdBase
200
+
201
+ In NICOT you usually don’t hand-roll primary key fields. Instead you **inherit** one of the base classes.
81
202
 
82
- 适合你希望使用业务主键或 UUID 作为主键的场景。传入 `uuid: true` 参数后自动生成 UUID 主键。
203
+ ### `IdBase()` numeric auto-increment primary key
83
204
 
84
205
  ```ts
85
206
  @Entity()
86
- class ApiKey extends StringIdBase({ uuid: true, description: 'API 密钥 ID' }) {
87
- // 继承字段:id: string (uuid, primary key)
207
+ export class Article extends IdBase({ description: 'Article ID' }) {
208
+ // id: number (bigint unsigned, primary, auto-increment)
88
209
  }
89
210
  ```
90
211
 
91
- - 自动添加字段:`id: string`
92
- - 默认排序为 `ORDER BY id ASC`
93
- - 支持配置长度(`length`)和描述(`description`)
94
- - `uuid: true` 时自动添加 `@Generated('uuid')`
212
+ Behavior:
95
213
 
96
- ---
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`)
97
220
 
98
- ### 3. 示例对比
221
+ ### `StringIdBase()` — string / UUID primary key
99
222
 
100
223
  ```ts
101
224
  @Entity()
102
- class Article extends IdBase({ description: '文章 ID' }) {
103
- // id: number 自动生成
104
- }
105
-
106
- @Entity()
107
- class Token extends StringIdBase({
225
+ export class ApiKey extends StringIdBase({
108
226
  uuid: true,
109
- description: '访问令牌',
227
+ description: 'API key ID',
110
228
  }) {
111
- // id: string,自动生成 UUID
229
+ // id: string (uuid, primary)
112
230
  }
113
231
  ```
114
232
 
115
- ---
233
+ Behavior:
116
234
 
117
- ### 小结
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`)
118
243
 
119
- | 基类 | 主键类型 | 排序默认 | ID 生成策略 | 使用场景 |
120
- |-----------------|------------|----------|----------------------|------------------------|
121
- | `IdBase()` | number | DESC | 自增 `Generated('increment')` | 常规实体 ID |
122
- | `StringIdBase()`| string | ASC | 可选 UUID / 手动输入 | UUID 主键、业务主键等 |
244
+ Summary:
123
245
 
124
- 建议你为每个实体都继承其中一个基类,以统一主键结构和查询逻辑。
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 |
125
250
 
126
251
  ---
127
252
 
128
- ## 🧠 字段装饰器总览
253
+ ## Column decorators overview
129
254
 
130
- NICOT 提供了一系列 `***Column()` 装饰器,统一处理字段的:
255
+ NICOT’s `***Column()` decorators combine:
131
256
 
132
- - 数据类型定义(TypeORM
133
- - 输入校验(class-validator
134
- - 文档描述(@nestjs/swagger)
257
+ - **TypeORM column definition**
258
+ - **class-validator rules**
259
+ - **Swagger `@ApiProperty()` metadata**
135
260
 
136
- ### 字段类型装饰器(`***Column()`)
261
+ Common ones:
137
262
 
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()` |
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 |
150
276
 
151
- 所有字段装饰器都支持第二个参数 `options`:
277
+ All of them accept an `options` parameter:
152
278
 
153
279
  ```ts
154
280
  @StringColumn(255, {
155
281
  required: true,
156
- description: '用户名',
282
+ description: 'Display name',
157
283
  default: 'Anonymous',
158
284
  })
159
- name: string;
285
+ displayName: string;
160
286
  ```
161
287
 
162
288
  ---
163
289
 
164
- ## 🔒 字段访问限制装饰器(行为控制)
290
+ ## Access control decorators
165
291
 
166
- NICOT 提供以下装饰器用于控制字段在不同接口中的表现:
292
+ These decorators control **where** a field appears:
167
293
 
168
- | 装饰器名 | 行为控制说明 |
169
- |----------------------------------------|-----------------------------------------------|
170
- | `@NotWritable()` | 不允许在创建(POST)或修改(PATCH)时传入 |
171
- | `@NotChangeable()` | 不允许在修改(PATCH)时更新(只可创建) |
172
- | `@NotQueryable()` | 不允许在 GET 查询参数中使用该字段 |
173
- | `@NotInResult()` | 不会出现在任何返回结果中(如密码字段) |
174
- | `@NotColumn()` | 不是数据库字段,仅作为查询结果间接字段(在 afterGet 钩子方法赋值) |
175
- | `@QueryColumn()` | 不是数据库字段,仅作为虚拟查询字段(和 @QueryEqual() 等查询装饰器同时使用) |
176
- | `@RelationComputed(() => EntityClass)` | 标识该字段依赖关系字段推导而来(通常在 afterGet) |
294
+ - in create/update DTOs
295
+ - in query DTOs (GET)
296
+ - in response DTOs (`ResultDto`)
177
297
 
178
- RestfulFactory 处理 Entity 类的时候,会以这些装饰器为依据,裁剪生成的 DTO 和查询参数。
298
+ ### Write / read restrictions
179
299
 
180
- 这些限制装饰器非常适合处理:
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) |
181
307
 
182
- - 安全字段(如密码、Token)
183
- - 系统字段(如创建时间、创建者 ID)
184
- - 只读字段(如 auto-increment 主键)
308
+ ### Non-column & virtual fields
185
309
 
186
- ---
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 |
187
315
 
188
- ### 示例:完整字段定义
316
+ Example:
189
317
 
190
318
  ```ts
191
- @StringColumn(255, {
192
- required: true,
193
- description: '用户昵称',
194
- })
195
- @NotWritable()
196
- nickname: string;
197
-
198
- @BoolColumn()
199
- @QueryMatchBoolean()
200
- isActive: boolean;
201
- ```
319
+ @Entity()
320
+ export class User extends IdBase() {
321
+ @StringColumn(255, { required: true })
322
+ name: string;
202
323
 
203
- ---
324
+ @StringColumn(255)
325
+ @NotInResult()
326
+ password: string;
204
327
 
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,会自动建索引 |
328
+ @DateColumn()
329
+ @NotWritable()
330
+ createdAt: Date;
228
331
 
229
- ---
332
+ @NotColumn()
333
+ @RelationComputed(() => Profile)
334
+ profileSummary: ProfileSummary;
335
+ }
336
+ ```
230
337
 
231
- ### 全文搜索
338
+ ### Decorator priority (simplified)
232
339
 
233
- 利用 `@QueryFullText(options)` 装饰器,可以在 PostgreSQL 中实现全文搜索。
340
+ When NICOT generates DTOs, it applies a **whitelist/cut-down** pipeline. Roughly:
234
341
 
235
- 程序启动的时候,会自动创建索引。不需要加 `@Index()`。
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
236
361
 
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;
362
+ In short:
245
363
 
246
- @StringColumn(255)
247
- @QueryFullText({
248
- parser: 'zhparser', // 使用中文分词器。NICOT 自动管理配置。需要手动给 postgres 添加中文分词器
249
- })
250
- simpleContent: string;
251
- ```
364
+ > If you mark something as “not writable / queryable / in result”, that wins, regardless of column type or other decorators.
252
365
 
253
366
  ---
254
367
 
255
- ## 🛠 自定义查询装饰器:`QueryCondition()`
368
+ ## Query decorators & QueryCondition
256
369
 
257
- 如果你需要构建更复杂或专用的查询逻辑,可以使用 `QueryCondition()` 创建自己的装饰器:
370
+ Query decorators define **how a field is translated into SQL** in GET queries.
258
371
 
259
- ### 示例:大于查询
372
+ Internally they all use a `QueryCondition` callback:
260
373
 
261
374
  ```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
- });
375
+ export const QueryCondition = (cond: QueryCond) =>
376
+ Metadata.set(
377
+ 'queryCondition',
378
+ cond,
379
+ 'queryConditionFields',
380
+ ) as PropertyDecorator;
268
381
  ```
269
382
 
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
- });
281
- ```
383
+ ### Lifecycle in `findAll()` / `findAllCursorPaginated()`
282
384
 
283
- > 使用方式与普通装饰器一致,应用在实体字段上即可。
385
+ When you call `CrudBase.findAll(ent)`:
284
386
 
285
- ---
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`.
286
393
 
287
- ### 使用效果示例
394
+ So `@QueryXXX()` is a **declarative hook** into the query building stage.
288
395
 
289
- ```ts
290
- @IntColumn('int', { unsigned: true })
291
- @QueryGreater()
292
- views: number;
396
+ ### Built-in Query decorators
293
397
 
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';
308
- ```
398
+ Based on `QueryWrap` / `QueryCondition`:
309
399
 
310
- ---
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
311
418
 
312
- ## GetMutator
419
+ All of these are high-level wrappers over the central abstraction:
313
420
 
314
- GET 方法的参数只能是 URL 参数,因此数据类型只能是 string,具有局限性。
421
+ ```ts
422
+ export const QueryWrap = (wrapper: QueryWrapper, field?: string) =>
423
+ QueryCondition((obj, qb, entityName, key) => {
424
+ // ...convert obj[key] and call qb.andWhere(...)
425
+ });
426
+ ```
315
427
 
316
- NICOT 提供了 `GetMutator` 装饰器,允许你在实体字段上定义一个转换函数,将 URL 参数转换为正确的数据类型,并把 OpenAPI 文档中 GET 接口的数据类型改为目标类型。
428
+ ### Composing conditions: QueryAnd / QueryOr
317
429
 
318
- ### 示例
430
+ You can combine multiple `QueryCondition` implementations:
319
431
 
320
432
  ```ts
321
- @JsonColumn(SomeObject)
322
- @GetMutator((val: string) => JSON.parse(val)) // GET 参数不会体现为 SomeObject,而是 string
323
- @QueryOperator('@>') // JSONB 包含查询
324
- meta: SomeObject;
433
+ export const QueryAnd = (...decs: PropertyDecorator[]) => { /* ... */ };
434
+ export const QueryOr = (...decs: PropertyDecorator[]) => { /* ... */ };
325
435
  ```
326
436
 
327
- ### 内建 GetMutator
437
+ - `QueryAnd(A, B)` — run both conditions on the same field (AND).
438
+ - `QueryOr(A, B)` — build an `(A) OR (B)` bracket group.
328
439
 
329
- - `@GetMutatorBool()`
330
- - `@GetMutatorInt()`
331
- - `@GetMutatorFloat()`
332
- - `@GetMutatorStringSeparated(',')`
333
- - `@GetMutatorIntSeparated()`
334
- - `@GetMutatorFloatSeparated()`
335
- - `@GetMutatorJson()`
336
-
337
- > `@BoolColumn()` 已经内建了 `@GetMutatorBool()`
440
+ These are useful for e.g. multi-column search or fallback logic.
338
441
 
339
- ---
442
+ ### Full-text search: `QueryFullText`
340
443
 
341
- ## 🧩 实体关系示例
444
+ PostgreSQL-only helper:
342
445
 
343
446
  ```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
- }
447
+ @StringColumn(255)
448
+ @QueryFullText({
449
+ configuration: 'english',
450
+ tsQueryFunction: 'websearch_to_tsquery',
451
+ orderBySimilarity: true,
452
+ })
453
+ content: string;
363
454
  ```
364
455
 
365
- ---
456
+ NICOT will:
366
457
 
367
- ## 🔁 生命周期钩子
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`.
368
461
 
369
- 支持在实体中定义以下方法:
370
-
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
- ```
462
+ > **Note:** full-text features are intended for **PostgreSQL**. On other databases they are not supported.
385
463
 
386
464
  ---
387
465
 
388
- ## 🛠 使用 CrudService(服务层标准写法)
466
+ ## GetMutator & MutatorPipe
389
467
 
390
- NICOT 提供了 `CrudService(Entity, options)`,是所有资源的标准服务实现方式。
468
+ GET query params are **always strings** on the wire, but entities may want richer types (arrays, numbers, JSON objects).
391
469
 
392
- 你只需继承它,并传入对应的实体和配置,即可拥有完整的:
393
- - 查询(支持分页、排序、过滤、关系)
394
- - 创建、更新、删除(带钩子、校验、字段控制)
395
- - 统一返回结构
470
+ NICOT uses:
396
471
 
397
- ---
472
+ - `@GetMutator(...)` metadata on the entity field
473
+ - `MutatorPipe` to apply the conversion at runtime
474
+ - `PatchColumnsInGet` to adjust Swagger docs for GET DTOs
398
475
 
399
- ### 定义 Service
476
+ ### Concept
400
477
 
401
- ```ts
402
- import { CrudService } from 'nicot';
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.
403
481
 
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
- }
482
+ ### Example
411
483
 
412
- // 可根据需要添加业务方法(非覆盖)
413
- async downloadArticle(id: number): Promise<Buffer> {
414
- const res = await this.findOne(id);
415
- return res.data.getContentAsBuffer();
416
- }
417
- }
484
+ ```ts
485
+ @JsonColumn(SomeFilterObject)
486
+ @GetMutatorJson() // parse JSON string from ?filter=...
487
+ @QueryOperator('@>') // use jsonb containment operator
488
+ filter: SomeFilterObject;
418
489
  ```
419
490
 
420
- ---
491
+ Built-in helpers include:
421
492
 
422
- ### 关于 relations
493
+ - `@GetMutatorBool()`
494
+ - `@GetMutatorInt()`
495
+ - `@GetMutatorFloat()`
496
+ - `@GetMutatorStringSeparated(',')`
497
+ - `@GetMutatorIntSeparated()`
498
+ - `@GetMutatorFloatSeparated()`
499
+ - `@GetMutatorJson()`
423
500
 
424
- `relations: string[]` `CrudService` 的核心配置项之一。它用于在查询中自动加载关联实体(即 TypeORM 的 `leftJoinAndSelect`)。
501
+ Internally, `PatchColumnsInGet` tweaks Swagger metadata so that:
425
502
 
426
- - `'user'` 表示加载 `article.user`
427
- - `'user.articles'` 表示递归加载嵌套关系
428
- - 默认使用 `LEFT JOIN`,如需 `INNER JOIN` 可通过 `Inner('user')` 指定
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).
429
505
 
430
- 这能确保你在 Controller 中无需手动构建复杂的 join 查询。
506
+ And `RestfulFactory.findAllParam()` wires everything together:
507
+
508
+ - Applies `MutatorPipe` if GetMutators exist.
509
+ - Applies `OmitPipe(fieldsInGetToOmit)` to strip non-queryable fields.
510
+ - Optionally applies `PickPipe(queryableFields)` when `skipNonQueryableFields: true`.
431
511
 
432
512
  ---
433
513
 
434
- ### 方法列表
514
+ ## `skipNonQueryableFields`: only expose explicitly declared query fields
435
515
 
436
- | 方法名 | 说明 |
437
- |------------------|----------------------------------------|
438
- | `findAll(dto, qb?)` | 查询列表(支持查询装饰器 / 分页) |
439
- | `findOne(id, qb?)` | 查单条数据,自动关联 / 过滤 / 封装 |
440
- | `create(dto)` | 创建数据,带验证、钩子处理 |
441
- | `update(id, dto, extraConditions?)` | 更新数据并支持条件限制 |
442
- | `delete(id, extraConditions?)` | 删除数据(软删) |
516
+ By default, `findAllDto` is:
443
517
 
444
- ---
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`).
445
524
 
446
- ### 示例:条件限制用户只能操作自己数据
525
+ If you want GET queries to accept **only** fields that have `@QueryEqual()` / `@QueryLike()` / `@QueryIn()` etc, use:
447
526
 
448
527
  ```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
- }
528
+ const UserFactory = new RestfulFactory(User, {
529
+ relations: [],
530
+ skipNonQueryableFields: true,
531
+ });
456
532
  ```
457
533
 
458
- ---
534
+ Effects:
459
535
 
460
- ### 建议实践
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.
461
541
 
462
- - 所有实体的服务类都应继承 `CrudService(Entity, options)`
463
- - `relations` 是推荐使用的配置方式,替代手动 join
464
- - 如果你有定制查询逻辑,建议用 `super.findAll(...)` + `.data` 进行后处理
465
- - 避免直接使用 `repo`,使用封装后的方法保持一致性与钩子逻辑生效
542
+ Mental model:
466
543
 
467
- ---
468
-
469
- ## 🧩 Controller 自动生成(RestfulFactory)
470
-
471
- NICOT 提供了 `RestfulFactory(Entity)` 工厂函数,自动为实体生成标准 RESTful Controller 接口装饰器及参数提取器。
544
+ > “If you want a field to be filterable in GET `/users`, you **must** explicitly add a `@QueryXXX()` decorator. Otherwise it’s invisible.”
472
545
 
473
- 你不再需要手动定义每个路由,只需:
546
+ Recommended:
474
547
 
475
- 1. 创建 DTO(工厂生成)
476
- 2. 使用工厂提供的装饰器
548
+ - For **admin / multi-tenant APIs** → turn `skipNonQueryableFields: true` ON.
549
+ - For **internal tools / quick debugging** → you can leave it OFF for convenience.
477
550
 
478
551
  ---
479
552
 
480
- ### 一键生成的接口说明
553
+ ## Binding Context (Data Binding & Multi-Tenant Isolation)
481
554
 
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` | 删除单条数据(软删) |
555
+ In real systems, you often need to isolate data by *context*:
489
556
 
490
- ---
557
+ - current user
558
+ - current tenant / app
559
+ - current organization / project
491
560
 
492
- ### 参数提取装饰器一览
561
+ Typical rules:
493
562
 
494
- | 装饰器 | 用途说明 |
495
- |----------------------------|-----------------------------------------|
496
- | `@factory.createParam()` | 注入 `createDto`,自动校验 body |
497
- | `@factory.updateParam()` | 注入 `updateDto`,自动校验 body |
498
- | `@factory.findAllParam()` | 注入 `queryDto`,自动校验 query |
499
- | `@factory.idParam()` | 注入路径参数中的 id |
563
+ - A user can only see their own rows.
564
+ - Updates/deletes must be scoped by ownership.
565
+ - You don’t want to copy-paste `qb.andWhere('userId = :id', ...)` everywhere.
500
566
 
501
- 这些参数装饰器全部内建了 `ValidationPipe`,支持自动转换与校验。
567
+ NICOT provides **BindingColumn / BindingValue / useBinding / beforeSuper** on top of `CrudBase` so that
568
+ *multi-tenant isolation* becomes part of the **entity contract**, not scattered per-controller logic.
502
569
 
503
570
  ---
504
571
 
505
- ### 查询能力:基于实体字段的装饰器
572
+ ### BindingColumn — declare “this field must be bound”
506
573
 
507
- `@factory.findAll()` 所生成的接口具有完整的查询能力,其行为由实体字段上的 `@QueryXXX()` 装饰器控制:
574
+ Use `@BindingColumn` on entity fields that should be filled and filtered by the backend context,
575
+ instead of coming from the client payload.
508
576
 
509
577
  ```ts
510
- @StringColumn(255)
511
- @QueryEqual()
512
- name: string;
513
-
514
- @BoolColumn()
515
- @QueryMatchBoolean()
516
- isActive: boolean;
517
- ```
518
-
519
- 则生成的 `GET /resource?name=Tom&isActive=true` 接口会自动构建对应的 SQL 条件。
520
-
521
- ---
522
-
523
- ### RestfulFactory 配置项
524
-
525
- ```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() 装饰器的字段
578
+ @Entity()
579
+ export class Article extends IdBase() {
580
+ @BindingColumn() // default bindingKey: "default"
581
+ @IntColumn('int', { unsigned: true })
582
+ userId: number;
583
+
584
+ @BindingColumn('app') // bindingKey: "app"
585
+ @IntColumn('int', { unsigned: true })
586
+ appId: number;
538
587
  }
539
588
  ```
540
589
 
541
- ---
590
+ NICOT will:
542
591
 
543
- ### 示例 Controller
592
+ - on `create`:
593
+ - write binding values into `userId` / `appId` (if provided)
594
+ - on `findAll`:
595
+ - automatically add `WHERE userId = :value` / `appId = :value`
596
+ - on `update` / `delete`:
597
+ - add the same binding conditions, preventing cross-tenant access
544
598
 
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 {}
599
+ Effectively: **binding columns are your “ownership / tenant” fields**.
550
600
 
551
- @Controller('user')
552
- export class UserController {
553
- constructor(private readonly service: UserService) {}
601
+ ---
554
602
 
555
- @factory.create()
556
- async create(@factory.createParam() dto: CreateUserDto) {
557
- return this.service.create(dto);
558
- }
603
+ ### BindingValue — where the binding values come from
559
604
 
560
- @factory.findAll()
561
- async findAll(@factory.findAllParam() dto: FindAllUserDto) {
562
- return this.service.findAll(dto);
563
- }
605
+ `@BindingValue` is placed on service properties or methods that provide the actual binding values.
564
606
 
565
- @factory.findOne()
566
- async findOne(@factory.idParam() id: number) {
567
- return this.service.findOne(id);
607
+ ```ts
608
+ @Injectable()
609
+ class ArticleService extends CrudService(Article) {
610
+ constructor(@InjectRepository(Article) repo: Repository<Article>) {
611
+ super(repo);
568
612
  }
569
-
570
- @factory.update()
571
- async update(@factory.idParam() id: number, @factory.updateParam() dto: UpdateUserDto) {
572
- return this.service.update(id, dto);
613
+
614
+ @BindingValue() // for BindingColumn()
615
+ get currentUserId() {
616
+ return this.ctx.userId;
573
617
  }
574
-
575
- @factory.delete()
576
- async delete(@factory.idParam() id: number) {
577
- return this.service.delete(id);
618
+
619
+ @BindingValue('app') // for BindingColumn('app')
620
+ get currentAppId() {
621
+ return this.ctx.appId;
578
622
  }
579
623
  }
580
624
  ```
581
625
 
582
- ---
626
+ At runtime, NICOT will:
583
627
 
584
- ### 补充说明
628
+ - collect all `BindingValue` metadata
629
+ - build a partial entity `{ userId, appId, ... }`
630
+ - use it to:
631
+ - fill fields on `create`
632
+ - add `WHERE` conditions on `findAll`, `update`, `delete`
585
633
 
586
- - 所有路由默认返回统一结构(`GenericReturnMessageDto` / `BlankReturnMessageDto`)
587
- - 所有参数自动校验,无需手动加 `ValidationPipe`
588
- - `findAll()` 自动支持分页、排序、模糊查询、布尔匹配等
589
- - 如果你使用了实体关系(relations),则 `findOne()` / `findAll()` 也自动关联查询
590
- - 所有的接口都是返回状态码 200。
591
- - OpenAPI 文档会自动生成,包含所有 DTO 类型与查询参数。
592
- - Service 需要使用 `CrudService(Entity, options)` 进行标准化实现。
634
+ If both client payload and BindingValue provide a value, **BindingValue wins** for binding columns.
635
+
636
+ > You can use:
637
+ > - properties (sync)
638
+ > - getters
639
+ > - methods (sync)
640
+ > - async methods
641
+ > NICOT will await async BindingValues when necessary.
593
642
 
594
643
  ---
595
644
 
596
- ### 导出 DTO
645
+ ### Request-scoped context provider (recommended)
597
646
 
598
- `RestfulFactory` 会自动生成以下 DTO 类:供你导出并在其他的 OpenAPI 装饰器中使用。
647
+ The “canonical” way to provide binding values in a web app is:
599
648
 
600
- ```ts
601
- const factory = new RestfulFactory(User, {
602
- relations: ['articles'],
603
- });
649
+ 1. Extract context (user, app, tenant, etc.) from the incoming request.
650
+ 2. Put it into a **request-scoped provider**.
651
+ 3. Have `@BindingValue` simply read from that provider.
604
652
 
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
- ```
653
+ This keeps:
614
654
 
615
- ---
655
+ - context lifetime = request lifetime
656
+ - services as singletons
657
+ - binding logic centralized and testable
616
658
 
617
- ### 关系定义
659
+ #### 1) Define a request-scoped binding context
618
660
 
619
- 类似于 `CrudService`,`RestfulFactory` 也需要在配置中定义关系字段。语法和 `CrudService` `relations` 参数完全一致。
661
+ Using `createProvider` from **nesties**, you can declare a strongly-typed request-scoped provider:
620
662
 
621
663
  ```ts
622
- class User extends IdBase() {
623
- @OneToMany(() => Article, article => article.user)
624
- articles: Article[];
625
-
626
- @OneToMany(() => Comment, comment => comment.user)
627
- comments: Comment[];
664
+ export const BindingContextProvider = createProvider(
665
+ {
666
+ provide: 'BindingContext',
667
+ scope: Scope.REQUEST, // ⭐ one instance per HTTP request
668
+ inject: [REQUEST, AuthService] as const,
669
+ },
670
+ async (req, auth) => {
671
+ const user = await auth.getUserFromRequest(req);
672
+ return {
673
+ userId: user.id,
674
+ appId: Number(req.headers['x-app-id']),
675
+ };
676
+ },
677
+ );
678
+ ```
628
679
 
629
- @OneToMany(() => Like, like => like.users)
630
- likes: Like[];
631
- }
680
+ Key points:
632
681
 
633
- class Article extends IdBase() {
634
- @ManyToOne(() => User, user => user.articles)
635
- user: User;
682
+ - `scope: Scope.REQUEST` each request has its own context instance.
683
+ - `inject: [REQUEST, AuthService]` you can pull anything you need to compute bindings.
684
+ - `createProvider` infers `(req, auth)` types automatically.
636
685
 
637
- @OneToMany(() => Comment, comment => comment.article)
638
- comments: Comment[];
686
+ #### 2) Inject the context into your service and expose BindingValues
639
687
 
640
- @OneToMany(() => Like, like => like.article)
641
- likes: Like[];
688
+ ```ts
689
+ @Injectable()
690
+ class ArticleService extends CrudService(Article) {
691
+ constructor(
692
+ @InjectRepository(Article) repo: Repository<Article>,
693
+ @Inject('BindingContext')
694
+ private readonly ctx: { userId: number; appId: number },
695
+ ) {
696
+ super(repo);
697
+ }
698
+
699
+ @BindingValue()
700
+ get currentUserId() {
701
+ return this.ctx.userId;
702
+ }
703
+
704
+ @BindingValue('app')
705
+ get currentAppId() {
706
+ return this.ctx.appId;
707
+ }
642
708
  }
709
+ ```
643
710
 
644
- class Like extends IdBase() {
645
- @ManyToOne(() => User, user => user.likes)
646
- user: User;
711
+ With this setup:
647
712
 
648
- @ManyToOne(() => Article, article => article.likes)
649
- article: Article;
650
- }
713
+ - each request gets its own `{ userId, appId }` context
714
+ - `@BindingValue` simply reads from that context
715
+ - `CrudBase` applies bindings for create / findAll / update / delete automatically
716
+ - controllers do **not** need to repeat `userId` conditions
651
717
 
652
- class Comment extends IdBase() {
653
- @ManyToOne(() => Article, article => article.comments)
654
- article: Article;
718
+ This is the **recommended** way to use binding in a NestJS HTTP app.
655
719
 
656
- @ManyToOne(() => User, user => user.articles)
657
- user: User;
658
- }
720
+ ---
659
721
 
660
- const factory = new RestfulFactory(User, {
661
- relations: ['comments', 'articles', 'articles.comments'], // 生成的 DTO 类中,只含有标明的关系字段,而 articles.user 不会被包含
662
- });
722
+ ### useBinding override binding per call
663
723
 
664
- class UserResultDto extends factory.entityResultDto {
665
- // 生成的 DTO 类中包含 comments, articles, articles.comments 字段
666
- // 但是不包含 likes, articles.user, articles.likes 等未声明关系字段
667
- }
668
- ```
724
+ For tests, scripts, or some internal flows, you may want to override binding values *per call*
725
+ instead of relying on `@BindingValue`.
669
726
 
670
- 如果你的配套 `CrudService` 不准备加载任何关系,那么可以传入空数组:
727
+ Use `useBinding` for this:
671
728
 
672
729
  ```ts
673
- const factory = new RestfulFactory(User, {
674
- relations: [], // DTO 不包含任何关系字段
675
- });
730
+ // create with explicit binding
731
+ const res = await articleService
732
+ .useBinding(7) // bindingKey: "default"
733
+ .useBinding(44, 'app') // bindingKey: "app"
734
+ .create({ name: 'Article 1' });
735
+
736
+ // query in the same binding scope
737
+ const list = await articleService
738
+ .useBinding(7)
739
+ .useBinding(44, 'app')
740
+ .findAll({});
676
741
  ```
677
742
 
678
- 如果不写 `relations`,则默认会尽可能加载所有非 `@NotInResult()` 的关系字段。但现在推荐显式声明需要加载的关系,以避免不必要的 OpenAPI 文档杂乱。
743
+ Key properties:
744
+
745
+ - override is **per call**, not global
746
+ - multiple concurrent calls with different `useBinding` values are isolated
747
+ - merges with `@BindingValue` (explicit `useBinding` can override default BindingValue)
679
748
 
680
- > 这是曾经版本的 nicot (<1.1.9) 的做法。
749
+ This is particularly handy in unit tests and CLI scripts.
681
750
 
682
751
  ---
683
752
 
684
- ### 依赖关系的间接字段
753
+ ### beforeSuper — safe overrides with async logic (advanced)
685
754
 
686
- 如果你有实体类,某一间接字段(`@NotColumn()`),依赖某个关系字段,那么需要显示声明这个字段。
755
+ `CrudService` subclasses are singletons, but bindings are *per call*.
687
756
 
688
- ```ts
689
- export class Participant extends IdBase() {
690
- @OneToMany(() => Match, match => match.player1)
691
- matches1: Match[];
757
+ If you override `findAll` / `update` / `delete` and add `await` **before** calling `super`,
758
+ you can accidentally mess with binding order / concurrency.
692
759
 
693
- @OneToMany(() => Match, match => match.player2)
694
- matches2: Match[];
695
- }
760
+ NICOT offers `beforeSuper` as a small helper:
696
761
 
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);
762
+ ```ts
763
+ @Injectable()
764
+ class SlowArticleService extends ArticleService {
765
+ override async findAll(
766
+ ...args: Parameters<typeof ArticleService.prototype.findAll>
767
+ ) {
768
+ await this.beforeSuper(async () => {
769
+ // any async work before delegating to CrudBase
770
+ await new Promise((resolve) => setTimeout(resolve, 100));
771
+ });
772
+ return super.findAll(...args);
709
773
  }
710
774
  }
775
+ ```
711
776
 
712
- const factory = new RestfulFactory(Match, {
713
- relations: ['player1', 'player2', 'players'],
714
- });
777
+ What `beforeSuper` ensures:
715
778
 
716
- class MatchResultDto extends factory.entityResultDto {
717
- // 包含 player1, player2, players 字段,但是不包含 player1.matches1, player1.matches2 等间接关系字段
718
- }
719
- ```
779
+ 1. capture (freeze) current binding state
780
+ 2. run your async pre-logic
781
+ 3. restore binding state
782
+ 4. continue into `CrudBase` with the correct bindings
783
+
784
+ This is an **advanced** hook; most users don’t need it. For typical per-request isolation, prefer request-scoped context + `@BindingValue`.
720
785
 
721
786
  ---
722
787
 
723
- ## 📄 分页查询(自动支持)
788
+ ### How Binding works inside CrudBase
724
789
 
725
- NICOT `findAll()` 方法默认支持分页,**无需你手动声明分页字段**,框架内部已内置分页 DTO 与逻辑。
790
+ On each CRUD operation, NICOT does roughly:
726
791
 
727
- ---
792
+ 1. collect `BindingValue` from the service (properties / getters / methods / async methods)
793
+ 2. merge with `useBinding(...)` overlays
794
+ 3. build a “binding partial entity”
795
+ 4. apply it to:
796
+ - `create`: force binding fields
797
+ - `findAll` / `update` / `delete`: add binding-based `WHERE` conditions
798
+ 5. continue with:
799
+ - `beforeGet` / `beforeUpdate` / `beforeCreate`
800
+ - query decorators (`@QueryXXX`)
801
+ - pagination
802
+ - relations
728
803
 
729
- ### 默认分页行为
804
+ You can think of Binding as **“automatic ownership filters”** configured declaratively on:
730
805
 
731
- 所有 `findAll()` 查询接口会自动识别以下 query 参数:
806
+ - entities (`@BindingColumn`)
807
+ - services (`@BindingValue`, `useBinding`, `beforeSuper`, request-scoped context)
732
808
 
733
- | 参数 | 类型 | 默认值 | 说明 |
734
- |------------------|----------|--------|---------------------------------|
735
- | `pageCount` | number | `1` | 第几页,从 1 开始 |
736
- | `recordsPerPage` | number | `25` | 每页多少条数据 |
809
+ ---
737
810
 
738
- 这些字段由框架内置的 `PageSettingsDto` 管理,自动注入到 `findAllParam()` 的 DTO 中,无需你自己定义。
811
+ ## Pagination
739
812
 
740
- 分页逻辑最终会转化为:
813
+ ### Offset pagination (default)
741
814
 
742
- ```ts
743
- qb.take(recordsPerPage).skip((pageCount - 1) * recordsPerPage);
744
- ```
815
+ Every `findAll()` uses **offset pagination** via `PageSettingsDto`:
745
816
 
746
- ---
817
+ - Query fields:
818
+ - `pageCount` (1-based)
819
+ - `recordsPerPage` (default 25)
820
+ - Internally:
821
+ - Applies `.take(recordsPerPage).skip((pageCount - 1) * recordsPerPage)`
747
822
 
748
- ### 🔧 如何更改分页行为
823
+ If your entity extends `PageSettingsDto`, it can control defaults by overriding methods like `getRecordsPerPage()`.
749
824
 
750
- 分页逻辑由实体继承类中的方法控制(如 `getRecordsPerPage()`),如果你希望关闭分页或调高上限,可以 override 这些方法:
825
+ You can also effectively “disable” pagination for specific entities by returning a large value:
751
826
 
752
827
  ```ts
753
828
  @Entity()
754
- class LogEntry extends IdBase() {
755
- // ...其他字段
829
+ export class LogEntry extends IdBase() {
830
+ // ...
756
831
 
757
- override getRecordsPerPage() {
758
- return this.recordsPerPage || 99999; // 禁用分页(或返回极大值)
832
+ getRecordsPerPage() {
833
+ return this.recordsPerPage || 99999;
759
834
  }
760
835
  }
761
836
  ```
762
837
 
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 上。
775
-
776
- ---
838
+ ### Cursor pagination
777
839
 
778
- ## 🔁 游标分页(Cursor Pagination)
840
+ NICOT also supports **cursor-based pagination** via:
779
841
 
780
- NICOT 支持游标式分页查询(Cursor-based Pagination),相比传统的页码分页,在数据量大、频繁变更或无限滚动的场景中更加稳定可靠。
842
+ - `CrudBase.findAllCursorPaginated()`
843
+ - `RestfulFactory.findAllCursorPaginatedDto`
844
+ - `entityCursorPaginationReturnMessageDto`
781
845
 
782
- ---
783
-
784
- ### ✅ 使用方式
785
-
786
- 定义查询 DTO 时继承工厂生成的游标分页基类:
846
+ Usage sketch:
787
847
 
788
848
  ```ts
789
- class FindAllUserCursorDto extends factory.findAllCursorPaginatedDto {}
790
- ```
849
+ class FindAllUserCursorDto extends UserFactory.findAllCursorPaginatedDto {}
791
850
 
792
- 在 Controller 中,使用以下工厂方法:
793
-
794
- ```ts
795
- @factory.findAllCursorPaginated()
796
- async findAll(@factory.findAllParam() dto: FindAllUserCursorDto) {
797
- return this.service.findAllCursorPaginated(dto);
851
+ @UserFactory.findAllCursorPaginated()
852
+ async findAll(
853
+ @UserFactory.findAllParam() dto: FindAllUserCursorDto,
854
+ ) {
855
+ return this.service.findAllCursorPaginated(dto);
798
856
  }
799
857
  ```
800
858
 
801
- > ⚠️ 注意:`findAll()` 与 `findAllCursorPaginated()` **不能同时使用**,因为它们会绑定到同一个 GET `/` 路由。请选择其中一种分页模式。
859
+ Notes:
860
+
861
+ - Offset vs cursor pagination share the same query decorators and entity metadata.
862
+ - You choose one mode per controller route (`paginateType: 'offset' | 'cursor' | 'none'` in `baseController()`).
863
+ - Cursor payload and multi-column sorting behavior are documented in more detail in the API reference.
802
864
 
803
865
  ---
804
866
 
805
- ### 📥 请求字段说明
867
+ ## CrudBase & CrudService
806
868
 
807
- | 字段名 | 类型 | 描述 |
808
- |--------------------|---------|------------------------------------------------|
809
- | `recordsPerPage` | number | 每页数据数量,默认 25 |
810
- | `paginationCursor` | string | 上一次请求返回的游标(`nextCursor` 或 `previousCursor`)|
869
+ `CrudBase<T>` holds the core CRUD and query logic:
811
870
 
812
- - 首次请求无需传 `paginationCursor`
813
- - 后续请求使用返回的游标即可获取上一页或下一页数据
871
+ - `create(ent, beforeCreate?)`
872
+ - `findOne(id, extraQuery?)`
873
+ - `findAll(dto?, extraQuery?)`
874
+ - `findAllCursorPaginated(dto?, extraQuery?)`
875
+ - `update(id, dto, cond?)`
876
+ - `delete(id, cond?)`
877
+ - `importEntities(entities, extraChecking?)`
878
+ - `exists(id)`
879
+ - `onModuleInit()` (full-text index loader for Postgres)
814
880
 
815
- ---
881
+ It honors:
816
882
 
817
- ### 📤 返回结构说明
883
+ - Relations configuration (`relations` → joins + DTO shape)
884
+ - `NotInResult` / `outputFieldsToOmit` in responses (`cleanEntityNotInResultFields()`)
885
+ - Lifecycle hooks on the entity:
886
+ - `beforeCreate` / `afterCreate`
887
+ - `beforeGet` / `afterGet`
888
+ - `beforeUpdate` / `afterUpdate`
889
+ - `isValidInCreate` / `isValidInUpdate` (return a string = validation error)
818
890
 
819
- 返回值格式与传统分页一致,但字段不同:
891
+ You usually don’t subclass `CrudBase` directly; instead you use:
820
892
 
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"
893
+ ```ts
894
+ export function CrudService<T extends ValidCrudEntity<T>>(
895
+ entityClass: ClassType<T>,
896
+ crudOptions: CrudOptions<T> = {},
897
+ ) {
898
+ return class CrudServiceImpl extends CrudBase<T> {
899
+ constructor(repo: Repository<T>) {
900
+ super(entityClass, repo, crudOptions);
901
+ }
902
+ };
830
903
  }
831
904
  ```
832
905
 
833
- - 游标格式为 Base64URL 编码(安全可用于 URL 参数)
834
- - `nextCursor` / `previousCursor` 是可选字段,仅在有下一页或上一页时返回
835
-
836
- ---
837
-
838
- ### 🔐 兼容性说明
906
+ And let `RestfulFactory` call this for you via `factory.crudService()`.
839
907
 
840
- - 所有字段控制装饰器(如 `@NotInResult()`, `@QueryEqual()`, `@NotQueryable()` 等)在游标分页中同样生效
841
- - 查询参数仍来自实体声明,Swagger 自动生成文档
842
- - 无需变更现有实体结构,只需更换 `findAllDto` 和分页调用方法
908
+ > 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
909
 
844
910
  ---
845
911
 
846
- ### 适用场景
912
+ ## RestfulFactory: DTO & Controller generator
847
913
 
848
- - 无限滚动分页加载(如微博、时间线)
849
- - 数据频繁变动(传统分页页数易错)
850
- - 前后端希望避免“总页数”等全表统计带来的性能消耗
914
+ `RestfulFactory<T>` is the heart of “entity → DTOs → controller decorators” mapping.
851
915
 
852
- ---
853
-
854
- ### 🧪 示例请求
916
+ ### Options
855
917
 
856
- ```http
857
- GET /user?recordsPerPage=20&paginationCursor=eyJpZCI6MTAwfQ
918
+ ```ts
919
+ interface RestfulFactoryOptions<T> {
920
+ fieldsToOmit?: (keyof T)[];
921
+ writeFieldsToOmit?: (keyof T)[];
922
+ createFieldsToOmit?: (keyof T)[];
923
+ updateFieldsToOmit?: (keyof T)[];
924
+ findAllFieldsToOmit?: (keyof T)[];
925
+ outputFieldsToOmit?: (keyof T)[];
926
+ prefix?: string;
927
+ keepEntityVersioningDates?: boolean;
928
+ entityClassName?: string;
929
+ relations?: (string | RelationDef)[];
930
+ skipNonQueryableFields?: boolean;
931
+ }
858
932
  ```
859
933
 
860
- ---
861
-
862
- ### 🛑 注意事项
934
+ Key ideas:
863
935
 
864
- - 不支持跳页(如 pageCount = 5 这种跳转)
865
- - 不再返回 `pageCount`、`totalPages` 等字段
866
- - 若你的 Controller 中已有 `@factory.findAll()`,请不要再使用游标分页版本
936
+ - **relations**: both for:
937
+ - which relations are eager-loaded and exposed in DTO,
938
+ - and which joins are added to queries.
939
+ - **outputFieldsToOmit**: extra fields to drop from response DTOs (in addition to `@NotInResult`).
940
+ - **prefix**: extra path prefix for controller decorators (e.g. `v1/users`).
941
+ - **skipNonQueryableFields**: described above.
867
942
 
868
- ---
869
-
870
- ## 一键生成 Controller
943
+ ### Auto-generated DTOs
871
944
 
872
- 在一般情况下,可以使用 `factory.baseController()` 生成 RESTful 控制器,自动处理所有 CRUD 接口。
945
+ For a factory:
873
946
 
874
947
  ```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
- }
948
+ export const UserFactory = new RestfulFactory(User, { relations: [] });
885
949
  ```
886
950
 
887
- 这样就可以自动生成所有 CRUD 接口,无需手动编写。
951
+ NICOT gives you:
888
952
 
889
- ### 选项
953
+ - `UserFactory.createDto`
954
+ - `UserFactory.updateDto`
955
+ - `UserFactory.findAllDto`
956
+ - `UserFactory.findAllCursorPaginatedDto`
957
+ - `UserFactory.entityResultDto`
958
+ - `UserFactory.entityCreateResultDto`
959
+ - `UserFactory.entityReturnMessageDto`
960
+ - `UserFactory.entityCreateReturnMessageDto`
961
+ - `UserFactory.entityArrayReturnMessageDto`
962
+ - `UserFactory.entityCursorPaginationReturnMessageDto`
963
+
964
+ Recommended usage:
890
965
 
891
966
  ```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
- }) {}
967
+ export class CreateUserDto extends UserFactory.createDto {}
968
+ export class UpdateUserDto extends UserFactory.updateDto {}
969
+ export class FindAllUserDto extends UserFactory.findAllDto {}
970
+ export class UserResultDto extends UserFactory.entityResultDto {}
905
971
  ```
906
972
 
907
- > 如果需要覆盖某个方法的实现,请在 `routes` 中设置 `enabled: false`,然后手动实现该方法。
973
+ This keeps types stable and easy to re-use in custom endpoints or guards.
908
974
 
909
- > 如果该 Controller 内任意路由写了 `enabled: true`,那么该 Controller 内只有 `enabled: true` 的路由会被生成。
975
+ ### Controller decorators & params
910
976
 
911
- ---
977
+ Each factory exposes decorators that match CRUD methods:
912
978
 
913
- ## 一键生成 CrudService
979
+ - `create()` + `createParam()`
980
+ - `findOne()` + `idParam()`
981
+ - `findAll()` / `findAllCursorPaginated()` + `findAllParam()`
982
+ - `update()` + `updateParam()`
983
+ - `delete()`
984
+ - `import()` (`POST /import`)
914
985
 
915
- 利用 `factory.crudService()` 生成标准的 CRUD 服务类,自动处理所有 CRUD 接口。效果与 `CrudService(Entity, options)` 类似。
986
+ These decorators stack:
916
987
 
917
- `relations` 的配置与 `RestfulFactory` `relations` 参数一致,保证 DTO 与查询参数的一致性。
988
+ - HTTP method + path (optionally prefixed)
989
+ - Swagger operation and response schemas (using the generated DTOs)
990
+ - Validation & transform pipes (through DataPipe / OptionalDataPipe / OmitPipe / MutatorPipe)
918
991
 
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
- ```
930
-
931
- 推荐在 Entity 文件中定义 `RestfulFactory`,然后在 Service 中使用 `factory.crudService()` 生成服务类,而在 Controller 中使用 `factory.baseController()` 生成控制器。
992
+ Example (revised):
932
993
 
933
994
  ```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
995
+ // post.factory.ts
996
+ export const PostFactory = new RestfulFactory(Post, {
997
+ relations: [], // no relations for this resource
942
998
  });
943
999
 
944
- // user.service.ts
1000
+ // post.service.ts
945
1001
  @Injectable()
946
- export class UserService extends UserRestfulFactory.crudService() {
947
- constructor(@InjectRepository(User) repo) {
1002
+ export class PostService extends PostFactory.crudService() {
1003
+ constructor(@InjectRepository(Post) repo: Repository<Post>) {
948
1004
  super(repo);
949
1005
  }
950
1006
  }
951
1007
 
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
- ---
1008
+ // post.controller.ts
1009
+ import { PutUser } from '../common/put-user.decorator';
964
1010
 
965
- ## 📦 统一返回结构与接口注解
966
-
967
- NICOT 默认提供统一的接口返回格式与 Swagger 自动注解能力,便于前后端标准化对接。
968
-
969
- ---
1011
+ export class FindAllPostDto extends PostFactory.findAllDto {}
1012
+ export class CreatePostDto extends PostFactory.createDto {}
970
1013
 
971
- ### ✅ 返回结构 DTO 类型(用于 Swagger 类型标注)
1014
+ @Controller('posts')
1015
+ export class PostController {
1016
+ constructor(private readonly service: PostService) {}
972
1017
 
973
- #### `ReturnMessageDto(EntityClass)`
974
-
975
- 用于生成带数据的标准返回结构类型(**不是直接返回值**,用于 `@nestjs/swagger`)。
1018
+ @PostFactory.findAll()
1019
+ async findAll(
1020
+ @PostFactory.findAllParam() dto: FindAllPostDto,
1021
+ @PutUser() user: User,
1022
+ ) {
1023
+ return this.service.findAll(dto, qb => {
1024
+ qb.andWhere('post.userId = :uid', { uid: user.id });
1025
+ });
1026
+ }
976
1027
 
977
- ```json
978
- {
979
- "statusCode": 200,
980
- "success": true,
981
- "message": "success",
982
- "timestamp": "2025-04-25T12:00:00.000Z",
983
- "data": {}
1028
+ @PostFactory.create()
1029
+ async create(
1030
+ @PostFactory.createParam() dto: CreatePostDto,
1031
+ @PutUser() user: User,
1032
+ ) {
1033
+ dto.userId = user.id;
1034
+ return this.service.create(dto);
1035
+ }
984
1036
  }
985
1037
  ```
986
1038
 
987
- #### `BlankReturnMessageDto`
1039
+ ### `baseController()` shortcut
988
1040
 
989
- 无数据返回结构的类型(用于 DELETE、UPDATE 等空响应)。
1041
+ If you don’t have extra logic, you can generate a full controller class:
990
1042
 
991
- ```json
992
- {
993
- "statusCode": 200,
994
- "success": true,
995
- "message": "success"
1043
+ ```ts
1044
+ @Controller('users')
1045
+ export class UserController extends UserFactory.baseController({
1046
+ paginateType: 'offset', // 'offset' | 'cursor' | 'none'
1047
+ globalMethodDecorators: [],
1048
+ routes: {
1049
+ import: { enabled: false }, // disable /import
1050
+ },
1051
+ }) {
1052
+ constructor(service: UserService) {
1053
+ super(service);
1054
+ }
996
1055
  }
997
1056
  ```
998
1057
 
999
- #### `PaginatedReturnMessageDto(EntityClass)`
1058
+ - If **any** route in `routes` has `enabled: true`, then **only** explicitly enabled routes are generated.
1059
+ - Otherwise, all routes are generated except ones marked `enabled: false`.
1000
1060
 
1001
- 带有分页信息的返回结构类型。
1002
-
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
- ```
1061
+ This is useful for quickly bootstrapping admin APIs, then selectively disabling / overriding certain endpoints.
1018
1062
 
1019
1063
  ---
1020
1064
 
1021
- ### 📊 实际返回结构
1022
-
1023
- - **返回数据:**
1065
+ ## Relations & RelationComputed
1024
1066
 
1025
- ```ts
1026
- import { GenericReturnMessageDto } from 'nicot';
1067
+ Relations are controlled by:
1027
1068
 
1028
- return new GenericReturnMessageDto(200, '操作成功', data);
1029
- ```
1069
+ - TypeORM decorators on the entity: `@ManyToOne`, `@OneToMany`, etc.
1070
+ - NICOT’s `relations` whitelist in:
1071
+ - `RestfulFactory` options
1072
+ - `CrudOptions` for `CrudService` / `CrudBase`
1030
1073
 
1031
- - **返回空结构:**
1074
+ Example:
1032
1075
 
1033
1076
  ```ts
1034
- import { BlankReturnMessageDto } from 'nicot';
1077
+ @Entity()
1078
+ export class User extends IdBase() {
1079
+ @OneToMany(() => Article, article => article.user)
1080
+ articles: Article[];
1081
+ }
1035
1082
 
1036
- return new BlankReturnMessageDto(204, '删除成功');
1083
+ @Entity()
1084
+ export class Article extends IdBase() {
1085
+ @ManyToOne(() => User, user => user.articles)
1086
+ user: User;
1087
+ }
1037
1088
  ```
1038
1089
 
1039
- - **抛出异常结构:**
1090
+ If you configure:
1040
1091
 
1041
1092
  ```ts
1042
- throw new BlankReturnMessageDto(404, '未找到资源').toException();
1093
+ export const UserFactory = new RestfulFactory(User, {
1094
+ relations: ['articles'],
1095
+ });
1043
1096
  ```
1044
1097
 
1045
- ---
1098
+ Then:
1046
1099
 
1047
- ### 📚 Swagger 注解装饰器
1100
+ - `UserResultDto` includes `articles` but not `articles.user` (no recursive explosion).
1101
+ - Query joins `user.articles` when using `findOne` / `findAll`.
1048
1102
 
1049
- NICOT 提供以下装饰器帮助你自动声明接口返回结构,无需手动写复杂的 `@ApiResponse(...)`:
1103
+ ### Virtual relation: `RelationComputed`
1050
1104
 
1051
- #### `@ApiTypeResponse(EntityClass)`
1105
+ Sometimes you want a **computed field** that conceptually depends on relations, but is not itself a DB column.
1052
1106
 
1053
- 等价于:
1107
+ Example:
1054
1108
 
1055
1109
  ```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
- ```
1110
+ @Entity()
1111
+ export class Match extends IdBase() {
1112
+ @ManyToOne(() => Participant, p => p.matches1)
1113
+ player1: Participant;
1073
1114
 
1074
- ---
1115
+ @ManyToOne(() => Participant, p => p.matches2)
1116
+ player2: Participant;
1075
1117
 
1076
- ### 示例用法
1118
+ @NotColumn()
1119
+ @RelationComputed(() => Participant)
1120
+ players: Participant[];
1077
1121
 
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();
1122
+ async afterGet() {
1123
+ this.players = [this.player1, this.player2].filter(Boolean);
1086
1124
  }
1087
- return new GenericReturnMessageDto(200, '成功', user);
1088
1125
  }
1089
- ```
1090
1126
 
1091
- ---
1092
-
1093
- ## 📥 参数解析 + 验证(DataQuery / DataBody)
1127
+ export const MatchFactory = new RestfulFactory(Match, {
1128
+ relations: ['player1', 'player2', 'players'],
1129
+ });
1130
+ ```
1094
1131
 
1095
- NICOT 提供便捷装饰器 `@DataQuery()` 与 `@DataBody()`,用于自动完成:
1132
+ NICOT will:
1096
1133
 
1097
- - 参数绑定(从 query body)
1098
- - 数据校验(class-validator)
1099
- - 类型转换(`transform: true`)
1100
- - 避免重复书写 ValidationPipe
1134
+ - Treat `players` as a “computed relation” for pruning rules.
1135
+ - Include `players` in the result DTO, but **not** recursively include all fields from `Participant.matches1`/`matches2` etc.
1136
+ - This keeps DTOs from blowing up due to cyclic relations.
1101
1137
 
1102
1138
  ---
1103
1139
 
1104
- ### 装饰器对照说明
1105
-
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
- ---
1140
+ ## Unified response shape
1117
1141
 
1118
- ### 示例用法
1142
+ NICOT uses a uniform wrapper for all responses:
1119
1143
 
1120
1144
  ```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);
1145
+ {
1146
+ statusCode: number;
1147
+ success: boolean;
1148
+ message: string;
1149
+ timestamp?: string;
1150
+ data?: any;
1129
1151
  }
1130
1152
  ```
1131
1153
 
1132
- 你无需手动加 `ValidationPipe`,也无需手动处理转换错误或格式校验,NICOT 帮你做好了这一切。
1133
-
1134
- ---
1135
-
1136
- ## 📊 和同类框架的对比
1137
-
1138
- 在实际开发中,很多框架也提供了 CRUD 接口构建能力,但存在不同程度的痛点。NICOT 从底层设计上解决了这些问题,适合长期维护的中大型后端项目。
1139
-
1140
- ---
1141
-
1142
- ### ✅ FastAPI / SQLModel(Python)
1143
-
1144
- - ✅ 代码简洁,自动生成 OpenAPI 文档
1145
- - ❌ 无字段权限控制(不能区分不可写/不可查)
1146
- - ❌ 查询能力不够细致,字段粒度控制弱
1147
- - ❌ DTO 拆分需手动处理,复杂模型重复多
1148
-
1149
- 🔹 **NICOT 优势:**
1150
- - 字段级别控制查询/写入/输出行为
1151
- - 自动生成 DTO + 查询 + OpenAPI + 验证
1152
- - 生命周期钩子和逻辑注入更灵活
1154
+ Types are built via generics:
1153
1155
 
1154
- ---
1156
+ - `ReturnMessageDto(Entity)` — single payload
1157
+ - `PaginatedReturnMessageDto(Entity)` — with `total`, `totalPages`, etc.
1158
+ - `CursorPaginationReturnMessageDto(Entity)` — with `nextCursor`, `previousCursor`
1159
+ - `BlankReturnMessageDto` — for responses with no data
1155
1160
 
1156
- ### @nestjsx/crud(NestJS)
1161
+ And correspondingly in `RestfulFactory`:
1157
1162
 
1158
- - ✅ 快速生成接口
1159
- - ❌ 安全性差:字段查询/排序过于开放
1160
- - ❌ 控制力弱:很难注入逻辑或自定义查询
1161
- - ❌ Swagger 文档支持不完整
1163
+ - `entityReturnMessageDto`
1164
+ - `entityCreateReturnMessageDto`
1165
+ - `entityArrayReturnMessageDto`
1166
+ - `entityCursorPaginationReturnMessageDto`
1162
1167
 
1163
- 🔹 **NICOT 优势:**
1164
- - 每个字段查询能力需显式声明(不开放默认)
1165
- - 完全类型安全 + 文档自动生成
1166
- - 逻辑钩子、权限注入、返回结构标准化
1168
+ You can still build custom endpoints and return these wrappers manually if needed.
1167
1169
 
1168
1170
  ---
1169
1171
 
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
- - ❌ 每个字段都必须写解析器,开发成本高
1172
+ ## Best practices
1190
1173
 
1191
- 🔹 **NICOT 优势:**
1192
- - 后端主导接口结构,前端只调 REST
1193
- - 查询能力与字段权限完全可控
1194
- - 无需额外解析器,开发更快速
1174
+ - **One factory per entity**, in its own `*.factory.ts` file.
1175
+ - Keeps entity, factory, service, controller decoupled but aligned.
1176
+ - Let **entities own the contract**:
1177
+ - Column types
1178
+ - Validation
1179
+ - Access control (`@NotWritable`, `@NotInResult`, `@NotQueryable`)
1180
+ - Query capabilities (`@QueryXXX`)
1181
+ - For list APIs, strongly consider:
1182
+ - `skipNonQueryableFields: true`
1183
+ - `@QueryXXX` only on fields you really want public filtering on.
1184
+ - Prefer `CrudService` / `CrudBase` for NICOT-managed resources, so:
1185
+ - lifecycle hooks are honored,
1186
+ - relations + “not in result” logic stay consistent.
1187
+ - Use raw TypeORM repository methods only for clearly separated custom flows, and treat them as “outside NICOT”.
1195
1188
 
1196
1189
  ---
1197
1190
 
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
1191
+ ## License
1246
1192
 
1247
1193
  MIT