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-CN.md +799 -0
- package/README.md +816 -870
- package/api-cn.md +749 -0
- package/api.md +767 -0
- package/dist/index.cjs +307 -26
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +304 -26
- package/dist/index.mjs.map +3 -3
- package/dist/src/crud-base.d.ts +31 -0
- package/dist/src/decorators/binding.d.ts +7 -0
- package/dist/src/decorators/index.d.ts +1 -0
- package/dist/src/restful.d.ts +30 -1
- package/dist/src/utility/metadata.d.ts +3 -0
- 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,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
|
-
|
|
108
|
+
Best practice: **one factory file per entity**.
|
|
52
109
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
### `IdBase()` — numeric auto-increment primary key
|
|
83
204
|
|
|
84
205
|
```ts
|
|
85
206
|
@Entity()
|
|
86
|
-
class
|
|
87
|
-
//
|
|
207
|
+
export class Article extends IdBase({ description: 'Article ID' }) {
|
|
208
|
+
// id: number (bigint unsigned, primary, auto-increment)
|
|
88
209
|
}
|
|
89
210
|
```
|
|
90
211
|
|
|
91
|
-
|
|
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
|
-
###
|
|
221
|
+
### `StringIdBase()` — string / UUID primary key
|
|
99
222
|
|
|
100
223
|
```ts
|
|
101
224
|
@Entity()
|
|
102
|
-
class
|
|
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
|
|
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
|
-
|
|
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
|
|
255
|
+
NICOT’s `***Column()` decorators combine:
|
|
131
256
|
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
257
|
+
- **TypeORM column definition**
|
|
258
|
+
- **class-validator rules**
|
|
259
|
+
- **Swagger `@ApiProperty()` metadata**
|
|
135
260
|
|
|
136
|
-
|
|
261
|
+
Common ones:
|
|
137
262
|
|
|
138
|
-
|
|
|
139
|
-
|
|
140
|
-
| `@StringColumn(len)`
|
|
141
|
-
| `@TextColumn()`
|
|
142
|
-
| `@UuidColumn()`
|
|
143
|
-
| `@IntColumn(type)`
|
|
144
|
-
| `@FloatColumn(type)`
|
|
145
|
-
| `@BoolColumn()`
|
|
146
|
-
| `@DateColumn()`
|
|
147
|
-
| `@JsonColumn(T)`
|
|
148
|
-
| `@SimpleJsonColumn
|
|
149
|
-
| `@StringJsonColumn
|
|
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
|
-
|
|
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
|
-
|
|
285
|
+
displayName: string;
|
|
160
286
|
```
|
|
161
287
|
|
|
162
288
|
---
|
|
163
289
|
|
|
164
|
-
##
|
|
290
|
+
## Access control decorators
|
|
165
291
|
|
|
166
|
-
|
|
292
|
+
These decorators control **where** a field appears:
|
|
167
293
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
@
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
340
|
+
When NICOT generates DTOs, it applies a **whitelist/cut-down** pipeline. Roughly:
|
|
234
341
|
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
368
|
+
## Query decorators & QueryCondition
|
|
256
369
|
|
|
257
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
290
|
-
@IntColumn('int', { unsigned: true })
|
|
291
|
-
@QueryGreater()
|
|
292
|
-
views: number;
|
|
396
|
+
### Built-in Query decorators
|
|
293
397
|
|
|
294
|
-
|
|
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
|
-
|
|
419
|
+
All of these are high-level wrappers over the central abstraction:
|
|
313
420
|
|
|
314
|
-
|
|
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
|
-
|
|
428
|
+
### Composing conditions: QueryAnd / QueryOr
|
|
317
429
|
|
|
318
|
-
|
|
430
|
+
You can combine multiple `QueryCondition` implementations:
|
|
319
431
|
|
|
320
432
|
```ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
@QueryOperator('@>') // JSONB 包含查询
|
|
324
|
-
meta: SomeObject;
|
|
433
|
+
export const QueryAnd = (...decs: PropertyDecorator[]) => { /* ... */ };
|
|
434
|
+
export const QueryOr = (...decs: PropertyDecorator[]) => { /* ... */ };
|
|
325
435
|
```
|
|
326
436
|
|
|
327
|
-
|
|
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
|
-
-
|
|
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
|
-
@
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
##
|
|
466
|
+
## GetMutator & MutatorPipe
|
|
389
467
|
|
|
390
|
-
|
|
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
|
-
###
|
|
476
|
+
### Concept
|
|
400
477
|
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
493
|
+
- `@GetMutatorBool()`
|
|
494
|
+
- `@GetMutatorInt()`
|
|
495
|
+
- `@GetMutatorFloat()`
|
|
496
|
+
- `@GetMutatorStringSeparated(',')`
|
|
497
|
+
- `@GetMutatorIntSeparated()`
|
|
498
|
+
- `@GetMutatorFloatSeparated()`
|
|
499
|
+
- `@GetMutatorJson()`
|
|
423
500
|
|
|
424
|
-
|
|
501
|
+
Internally, `PatchColumnsInGet` tweaks Swagger metadata so that:
|
|
425
502
|
|
|
426
|
-
- `
|
|
427
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`@
|
|
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
|
-
@
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
export class UserController {
|
|
553
|
-
constructor(private readonly service: UserService) {}
|
|
601
|
+
---
|
|
554
602
|
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
607
|
+
```ts
|
|
608
|
+
@Injectable()
|
|
609
|
+
class ArticleService extends CrudService(Article) {
|
|
610
|
+
constructor(@InjectRepository(Article) repo: Repository<Article>) {
|
|
611
|
+
super(repo);
|
|
568
612
|
}
|
|
569
|
-
|
|
570
|
-
@
|
|
571
|
-
|
|
572
|
-
return this.
|
|
613
|
+
|
|
614
|
+
@BindingValue() // for BindingColumn()
|
|
615
|
+
get currentUserId() {
|
|
616
|
+
return this.ctx.userId;
|
|
573
617
|
}
|
|
574
|
-
|
|
575
|
-
@
|
|
576
|
-
|
|
577
|
-
return this.
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
-
|
|
590
|
-
-
|
|
591
|
-
-
|
|
592
|
-
-
|
|
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
|
-
###
|
|
645
|
+
### Request-scoped context provider (recommended)
|
|
597
646
|
|
|
598
|
-
|
|
647
|
+
The “canonical” way to provide binding values in a web app is:
|
|
599
648
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
+
Using `createProvider` from **nesties**, you can declare a strongly-typed request-scoped provider:
|
|
620
662
|
|
|
621
663
|
```ts
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
630
|
-
likes: Like[];
|
|
631
|
-
}
|
|
680
|
+
Key points:
|
|
632
681
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
638
|
-
comments: Comment[];
|
|
686
|
+
#### 2) Inject the context into your service and expose BindingValues
|
|
639
687
|
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
645
|
-
@ManyToOne(() => User, user => user.likes)
|
|
646
|
-
user: User;
|
|
711
|
+
With this setup:
|
|
647
712
|
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
user: User;
|
|
658
|
-
}
|
|
720
|
+
---
|
|
659
721
|
|
|
660
|
-
|
|
661
|
-
relations: ['comments', 'articles', 'articles.comments'], // 生成的 DTO 类中,只含有标明的关系字段,而 articles.user 不会被包含
|
|
662
|
-
});
|
|
722
|
+
### useBinding — override binding per call
|
|
663
723
|
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
727
|
+
Use `useBinding` for this:
|
|
671
728
|
|
|
672
729
|
```ts
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
+
`CrudService` subclasses are singletons, but bindings are *per call*.
|
|
687
756
|
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
694
|
-
matches2: Match[];
|
|
695
|
-
}
|
|
760
|
+
NICOT offers `beforeSuper` as a small helper:
|
|
696
761
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
713
|
-
relations: ['player1', 'player2', 'players'],
|
|
714
|
-
});
|
|
777
|
+
What `beforeSuper` ensures:
|
|
715
778
|
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
811
|
+
## Pagination
|
|
739
812
|
|
|
740
|
-
|
|
813
|
+
### Offset pagination (default)
|
|
741
814
|
|
|
742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
return this.recordsPerPage || 99999;
|
|
832
|
+
getRecordsPerPage() {
|
|
833
|
+
return this.recordsPerPage || 99999;
|
|
759
834
|
}
|
|
760
835
|
}
|
|
761
836
|
```
|
|
762
837
|
|
|
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 上。
|
|
775
|
-
|
|
776
|
-
---
|
|
838
|
+
### Cursor pagination
|
|
777
839
|
|
|
778
|
-
|
|
840
|
+
NICOT also supports **cursor-based pagination** via:
|
|
779
841
|
|
|
780
|
-
|
|
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
|
|
790
|
-
```
|
|
849
|
+
class FindAllUserCursorDto extends UserFactory.findAllCursorPaginatedDto {}
|
|
791
850
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
```
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
834
|
-
- `nextCursor` / `previousCursor` 是可选字段,仅在有下一页或上一页时返回
|
|
835
|
-
|
|
836
|
-
---
|
|
837
|
-
|
|
838
|
-
### 🔐 兼容性说明
|
|
906
|
+
And let `RestfulFactory` call this for you via `factory.crudService()`.
|
|
839
907
|
|
|
840
|
-
|
|
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
|
-
```
|
|
857
|
-
|
|
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
|
-
-
|
|
865
|
-
-
|
|
866
|
-
-
|
|
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
|
-
|
|
945
|
+
For a factory:
|
|
873
946
|
|
|
874
947
|
```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
|
-
}
|
|
948
|
+
export const UserFactory = new RestfulFactory(User, { relations: [] });
|
|
885
949
|
```
|
|
886
950
|
|
|
887
|
-
|
|
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
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
973
|
+
This keeps types stable and easy to re-use in custom endpoints or guards.
|
|
908
974
|
|
|
909
|
-
|
|
975
|
+
### Controller decorators & params
|
|
910
976
|
|
|
911
|
-
|
|
977
|
+
Each factory exposes decorators that match CRUD methods:
|
|
912
978
|
|
|
913
|
-
|
|
979
|
+
- `create()` + `createParam()`
|
|
980
|
+
- `findOne()` + `idParam()`
|
|
981
|
+
- `findAll()` / `findAllCursorPaginated()` + `findAllParam()`
|
|
982
|
+
- `update()` + `updateParam()`
|
|
983
|
+
- `delete()`
|
|
984
|
+
- `import()` (`POST /import`)
|
|
914
985
|
|
|
915
|
-
|
|
986
|
+
These decorators stack:
|
|
916
987
|
|
|
917
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
//
|
|
1000
|
+
// post.service.ts
|
|
945
1001
|
@Injectable()
|
|
946
|
-
export class
|
|
947
|
-
constructor(@InjectRepository(
|
|
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
|
-
//
|
|
953
|
-
|
|
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
|
-
|
|
1014
|
+
@Controller('posts')
|
|
1015
|
+
export class PostController {
|
|
1016
|
+
constructor(private readonly service: PostService) {}
|
|
972
1017
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1039
|
+
### `baseController()` shortcut
|
|
988
1040
|
|
|
989
|
-
|
|
1041
|
+
If you don’t have extra logic, you can generate a full controller class:
|
|
990
1042
|
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
import { GenericReturnMessageDto } from 'nicot';
|
|
1067
|
+
Relations are controlled by:
|
|
1027
1068
|
|
|
1028
|
-
|
|
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
|
-
|
|
1077
|
+
@Entity()
|
|
1078
|
+
export class User extends IdBase() {
|
|
1079
|
+
@OneToMany(() => Article, article => article.user)
|
|
1080
|
+
articles: Article[];
|
|
1081
|
+
}
|
|
1035
1082
|
|
|
1036
|
-
|
|
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
|
-
|
|
1093
|
+
export const UserFactory = new RestfulFactory(User, {
|
|
1094
|
+
relations: ['articles'],
|
|
1095
|
+
});
|
|
1043
1096
|
```
|
|
1044
1097
|
|
|
1045
|
-
|
|
1098
|
+
Then:
|
|
1046
1099
|
|
|
1047
|
-
|
|
1100
|
+
- `UserResultDto` includes `articles` but not `articles.user` (no recursive explosion).
|
|
1101
|
+
- Query joins `user.articles` when using `findOne` / `findAll`.
|
|
1048
1102
|
|
|
1049
|
-
|
|
1103
|
+
### Virtual relation: `RelationComputed`
|
|
1050
1104
|
|
|
1051
|
-
|
|
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
|
-
@
|
|
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
|
-
```
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
1127
|
+
export const MatchFactory = new RestfulFactory(Match, {
|
|
1128
|
+
relations: ['player1', 'player2', 'players'],
|
|
1129
|
+
});
|
|
1130
|
+
```
|
|
1094
1131
|
|
|
1095
|
-
NICOT
|
|
1132
|
+
NICOT will:
|
|
1096
1133
|
|
|
1097
|
-
-
|
|
1098
|
-
-
|
|
1099
|
-
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
+
And correspondingly in `RestfulFactory`:
|
|
1157
1162
|
|
|
1158
|
-
-
|
|
1159
|
-
-
|
|
1160
|
-
-
|
|
1161
|
-
-
|
|
1163
|
+
- `entityReturnMessageDto`
|
|
1164
|
+
- `entityCreateReturnMessageDto`
|
|
1165
|
+
- `entityArrayReturnMessageDto`
|
|
1166
|
+
- `entityCursorPaginationReturnMessageDto`
|
|
1162
1167
|
|
|
1163
|
-
|
|
1164
|
-
- 每个字段查询能力需显式声明(不开放默认)
|
|
1165
|
-
- 完全类型安全 + 文档自动生成
|
|
1166
|
-
- 逻辑钩子、权限注入、返回结构标准化
|
|
1168
|
+
You can still build custom endpoints and return these wrappers manually if needed.
|
|
1167
1169
|
|
|
1168
1170
|
---
|
|
1169
1171
|
|
|
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
|
-
- ❌ 每个字段都必须写解析器,开发成本高
|
|
1172
|
+
## Best practices
|
|
1190
1173
|
|
|
1191
|
-
|
|
1192
|
-
-
|
|
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
|
-
|
|
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
|