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-CN.md +565 -0
- package/README.md +626 -938
- package/api-cn.md +749 -0
- package/api.md +767 -0
- package/dist/index.cjs +14 -5
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +14 -5
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,90 @@
|
|
|
1
1
|
# NICOT
|
|
2
2
|
|
|
3
|
-
**NICOT**
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// 继承字段:id: number (bigint unsigned, primary key, auto-increment)
|
|
100
|
+
isValidInUpdate() {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
70
103
|
}
|
|
71
104
|
```
|
|
72
105
|
|
|
73
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
@
|
|
199
|
-
@
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
199
|
+
## Base ID classes: IdBase / StringIdBase
|
|
256
200
|
|
|
257
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
@
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
Summary:
|
|
315
245
|
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
NICOT’s `***Column()` decorators combine:
|
|
328
256
|
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
- `@
|
|
332
|
-
- `@GetMutatorStringSeparated(',')`
|
|
333
|
-
- `@GetMutatorIntSeparated()`
|
|
334
|
-
- `@GetMutatorFloatSeparated()`
|
|
335
|
-
- `@GetMutatorJson()`
|
|
257
|
+
- **TypeORM column definition**
|
|
258
|
+
- **class-validator rules**
|
|
259
|
+
- **Swagger `@ApiProperty()` metadata**
|
|
336
260
|
|
|
337
|
-
|
|
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
|
-
@
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
298
|
+
### Write / read restrictions
|
|
389
299
|
|
|
390
|
-
|
|
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
|
-
|
|
316
|
+
Example:
|
|
400
317
|
|
|
401
318
|
```ts
|
|
402
|
-
|
|
319
|
+
@Entity()
|
|
320
|
+
export class User extends IdBase() {
|
|
321
|
+
@StringColumn(255, { required: true })
|
|
322
|
+
name: string;
|
|
403
323
|
|
|
404
|
-
@
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}) {
|
|
408
|
-
constructor(@InjectRepository(Article) repo) {
|
|
409
|
-
super(repo);
|
|
410
|
-
}
|
|
324
|
+
@StringColumn(255)
|
|
325
|
+
@NotInResult()
|
|
326
|
+
password: string;
|
|
411
327
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
340
|
+
When NICOT generates DTOs, it applies a **whitelist/cut-down** pipeline. Roughly:
|
|
423
341
|
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
- `'user.articles'` 表示递归加载嵌套关系
|
|
428
|
-
- 默认使用 `LEFT JOIN`,如需 `INNER JOIN` 可通过 `Inner('user')` 指定
|
|
362
|
+
In short:
|
|
429
363
|
|
|
430
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
+
All of these are high-level wrappers over the central abstraction:
|
|
508
420
|
|
|
509
421
|
```ts
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
export class UserController {
|
|
553
|
-
constructor(private readonly service: UserService) {}
|
|
442
|
+
### Full-text search: `QueryFullText`
|
|
554
443
|
|
|
555
|
-
|
|
556
|
-
async create(@factory.createParam() dto: CreateUserDto) {
|
|
557
|
-
return this.service.create(dto);
|
|
558
|
-
}
|
|
444
|
+
PostgreSQL-only helper:
|
|
559
445
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
466
|
+
## GetMutator & MutatorPipe
|
|
597
467
|
|
|
598
|
-
|
|
468
|
+
GET query params are **always strings** on the wire, but entities may want richer types (arrays, numbers, JSON objects).
|
|
599
469
|
|
|
600
|
-
|
|
601
|
-
const factory = new RestfulFactory(User, {
|
|
602
|
-
relations: ['articles'],
|
|
603
|
-
});
|
|
470
|
+
NICOT uses:
|
|
604
471
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
482
|
+
### Example
|
|
620
483
|
|
|
621
484
|
```ts
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
485
|
+
@JsonColumn(SomeFilterObject)
|
|
486
|
+
@GetMutatorJson() // parse JSON string from ?filter=...
|
|
487
|
+
@QueryOperator('@>') // use jsonb containment operator
|
|
488
|
+
filter: SomeFilterObject;
|
|
489
|
+
```
|
|
625
490
|
|
|
626
|
-
|
|
627
|
-
comments: Comment[];
|
|
491
|
+
Built-in helpers include:
|
|
628
492
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
493
|
+
- `@GetMutatorBool()`
|
|
494
|
+
- `@GetMutatorInt()`
|
|
495
|
+
- `@GetMutatorFloat()`
|
|
496
|
+
- `@GetMutatorStringSeparated(',')`
|
|
497
|
+
- `@GetMutatorIntSeparated()`
|
|
498
|
+
- `@GetMutatorFloatSeparated()`
|
|
499
|
+
- `@GetMutatorJson()`
|
|
632
500
|
|
|
633
|
-
|
|
634
|
-
@ManyToOne(() => User, user => user.articles)
|
|
635
|
-
user: User;
|
|
501
|
+
Internally, `PatchColumnsInGet` tweaks Swagger metadata so that:
|
|
636
502
|
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
641
|
-
likes: Like[];
|
|
642
|
-
}
|
|
506
|
+
And `RestfulFactory.findAllParam()` wires everything together:
|
|
643
507
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
649
|
-
article: Article;
|
|
650
|
-
}
|
|
512
|
+
---
|
|
651
513
|
|
|
652
|
-
|
|
653
|
-
@ManyToOne(() => Article, article => article.comments)
|
|
654
|
-
article: Article;
|
|
514
|
+
## `skipNonQueryableFields`: only expose explicitly declared query fields
|
|
655
515
|
|
|
656
|
-
|
|
657
|
-
user: User;
|
|
658
|
-
}
|
|
516
|
+
By default, `findAllDto` is:
|
|
659
517
|
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
|
674
|
-
relations: [],
|
|
528
|
+
const UserFactory = new RestfulFactory(User, {
|
|
529
|
+
relations: [],
|
|
530
|
+
skipNonQueryableFields: true,
|
|
675
531
|
});
|
|
676
532
|
```
|
|
677
533
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
> 这是曾经版本的 nicot (<1.1.9) 的做法。
|
|
681
|
-
|
|
682
|
-
---
|
|
683
|
-
|
|
684
|
-
### 依赖关系的间接字段
|
|
534
|
+
Effects:
|
|
685
535
|
|
|
686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
+
Every `findAll()` uses **offset pagination** via `PageSettingsDto`:
|
|
739
558
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
return this.recordsPerPage || 99999;
|
|
574
|
+
getRecordsPerPage() {
|
|
575
|
+
return this.recordsPerPage || 99999;
|
|
759
576
|
}
|
|
760
577
|
}
|
|
761
578
|
```
|
|
762
579
|
|
|
763
|
-
|
|
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
|
-
|
|
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
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
在 Controller 中,使用以下工厂方法:
|
|
591
|
+
class FindAllUserCursorDto extends UserFactory.findAllCursorPaginatedDto {}
|
|
793
592
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
```
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
857
|
-
|
|
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
|
-
|
|
685
|
+
### Auto-generated DTOs
|
|
871
686
|
|
|
872
|
-
|
|
687
|
+
For a factory:
|
|
873
688
|
|
|
874
689
|
```ts
|
|
875
|
-
const
|
|
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
|
-
|
|
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
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
715
|
+
This keeps types stable and easy to re-use in custom endpoints or guards.
|
|
908
716
|
|
|
909
|
-
|
|
717
|
+
### Controller decorators & params
|
|
910
718
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
## 一键生成 CrudService
|
|
719
|
+
Each factory exposes decorators that match CRUD methods:
|
|
914
720
|
|
|
915
|
-
|
|
721
|
+
- `create()` + `createParam()`
|
|
722
|
+
- `findOne()` + `idParam()`
|
|
723
|
+
- `findAll()` / `findAllCursorPaginated()` + `findAllParam()`
|
|
724
|
+
- `update()` + `updateParam()`
|
|
725
|
+
- `delete()`
|
|
726
|
+
- `import()` (`POST /import`)
|
|
916
727
|
|
|
917
|
-
|
|
728
|
+
These decorators stack:
|
|
918
729
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
734
|
+
Example (revised):
|
|
932
735
|
|
|
933
736
|
```ts
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
//
|
|
742
|
+
// post.service.ts
|
|
945
743
|
@Injectable()
|
|
946
|
-
export class
|
|
947
|
-
constructor(@InjectRepository(
|
|
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
|
-
//
|
|
953
|
-
|
|
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
|
-
|
|
753
|
+
export class FindAllPostDto extends PostFactory.findAllDto {}
|
|
754
|
+
export class CreatePostDto extends PostFactory.createDto {}
|
|
968
755
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
756
|
+
@Controller('posts')
|
|
757
|
+
export class PostController {
|
|
758
|
+
constructor(private readonly service: PostService) {}
|
|
972
759
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
781
|
+
### `baseController()` shortcut
|
|
988
782
|
|
|
989
|
-
|
|
783
|
+
If you don’t have extra logic, you can generate a full controller class:
|
|
990
784
|
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1029
|
-
```
|
|
1030
|
-
|
|
1031
|
-
- **返回空结构:**
|
|
816
|
+
Example:
|
|
1032
817
|
|
|
1033
818
|
```ts
|
|
1034
|
-
|
|
819
|
+
@Entity()
|
|
820
|
+
export class User extends IdBase() {
|
|
821
|
+
@OneToMany(() => Article, article => article.user)
|
|
822
|
+
articles: Article[];
|
|
823
|
+
}
|
|
1035
824
|
|
|
1036
|
-
|
|
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
|
-
|
|
835
|
+
export const UserFactory = new RestfulFactory(User, {
|
|
836
|
+
relations: ['articles'],
|
|
837
|
+
});
|
|
1043
838
|
```
|
|
1044
839
|
|
|
1045
|
-
|
|
840
|
+
Then:
|
|
1046
841
|
|
|
1047
|
-
|
|
842
|
+
- `UserResultDto` includes `articles` but not `articles.user` (no recursive explosion).
|
|
843
|
+
- Query joins `user.articles` when using `findOne` / `findAll`.
|
|
1048
844
|
|
|
1049
|
-
|
|
845
|
+
### Virtual relation: `RelationComputed`
|
|
1050
846
|
|
|
1051
|
-
|
|
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
|
-
@
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
869
|
+
export const MatchFactory = new RestfulFactory(Match, {
|
|
870
|
+
relations: ['player1', 'player2', 'players'],
|
|
871
|
+
});
|
|
872
|
+
```
|
|
1094
873
|
|
|
1095
|
-
NICOT
|
|
874
|
+
NICOT will:
|
|
1096
875
|
|
|
1097
|
-
-
|
|
1098
|
-
-
|
|
1099
|
-
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
1140
|
-
---
|
|
903
|
+
And correspondingly in `RestfulFactory`:
|
|
1141
904
|
|
|
1142
|
-
|
|
905
|
+
- `entityReturnMessageDto`
|
|
906
|
+
- `entityCreateReturnMessageDto`
|
|
907
|
+
- `entityArrayReturnMessageDto`
|
|
908
|
+
- `entityCursorPaginationReturnMessageDto`
|
|
1143
909
|
|
|
1144
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
- ✅ 快速生成接口
|
|
1159
|
-
- ❌ 安全性差:字段查询/排序过于开放
|
|
1160
|
-
- ❌ 控制力弱:很难注入逻辑或自定义查询
|
|
1161
|
-
- ❌ Swagger 文档支持不完整
|
|
914
|
+
## Best practices
|
|
1162
915
|
|
|
1163
|
-
|
|
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
|
-
|
|
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
|