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-CN.md
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# NICOT — Entity-Driven REST Framework for NestJS + TypeORM
|
|
2
|
+
|
|
3
|
+
**N**estJS · **I** (nesties) · **C**lass-validator · **O**penAPI · **T**ypeORM
|
|
4
|
+
(记法:nicotto / nicotine —— 用了就上头 😌)
|
|
5
|
+
|
|
6
|
+
NICOT 是一个 *Entity-Driven* 的全自动 REST 后端框架。
|
|
7
|
+
|
|
8
|
+
> **维护实体 = 自动得到 DTO、验证规则、分页、过滤器、Controller、Service、OpenAPI。**
|
|
9
|
+
|
|
10
|
+
核心理念:
|
|
11
|
+
|
|
12
|
+
- 默认关闭,一切需显式开启(whitelist-only)。
|
|
13
|
+
- 实体就是契约(Entity = Schema)。
|
|
14
|
+
- 点状扩展(AOP-like hooks),不发明 DSL。
|
|
15
|
+
- 保持 NestJS 味道,避免“被框架绑架”。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 快速示例
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
@Entity()
|
|
23
|
+
export class User extends IdBase() {
|
|
24
|
+
@StringColumn(255, { required: true })
|
|
25
|
+
@QueryEqual()
|
|
26
|
+
name: string;
|
|
27
|
+
|
|
28
|
+
@IntColumn('int')
|
|
29
|
+
age: number;
|
|
30
|
+
|
|
31
|
+
@StringColumn(255)
|
|
32
|
+
@NotInResult()
|
|
33
|
+
password: string;
|
|
34
|
+
|
|
35
|
+
@NotWritable()
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const UserFactory = new RestfulFactory(User);
|
|
42
|
+
|
|
43
|
+
@Injectable()
|
|
44
|
+
export class UserService extends UserFactory.crudService() {
|
|
45
|
+
constructor(@InjectRepository(User) repo) {
|
|
46
|
+
super(repo);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Controller('users')
|
|
51
|
+
export class UserController extends UserFactory.baseController() {
|
|
52
|
+
constructor(service: UserService) {
|
|
53
|
+
super(service);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 特性摘要
|
|
61
|
+
|
|
62
|
+
- 自动生成 DTO(Create / Update / Find / CursorFind / Result)。
|
|
63
|
+
- 自动生成 Controller + Service。
|
|
64
|
+
- 白名单式字段权限:可写、可查、可返回分别控制。
|
|
65
|
+
- 自动分页(页码 / 游标)。
|
|
66
|
+
- 轻量查询 DSL(QueryCondition)。
|
|
67
|
+
- MutatorPipe:URL 字符串 → 实际类型。
|
|
68
|
+
- 生命周期钩子(validate、beforeGet、afterCreate...)。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## IdBase / StringIdBase
|
|
73
|
+
|
|
74
|
+
### IdBase()
|
|
75
|
+
|
|
76
|
+
- bigint 自增主键(unsigned)
|
|
77
|
+
- 默认排序:id DESC
|
|
78
|
+
- 自动挂载:NotWritable、QueryEqual
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
@Entity()
|
|
82
|
+
class User extends IdBase() {}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### StringIdBase()
|
|
86
|
+
|
|
87
|
+
- 字符串主键
|
|
88
|
+
- 默认排序:id ASC
|
|
89
|
+
- 支持 uuid: true 自动生成
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
@Entity()
|
|
93
|
+
class Token extends StringIdBase({ uuid: true }) {}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 访问权限装饰器(字段级“能看 / 能写 / 能查”)
|
|
99
|
+
|
|
100
|
+
NICOT 用一组装饰器,把“这个字段在什么场景出现”讲清楚:
|
|
101
|
+
|
|
102
|
+
- 写入相关:Create / Update 请求体里有没有这个字段
|
|
103
|
+
- 查询相关:GET 查询参数里能不能用这个字段
|
|
104
|
+
- 返回相关:响应 JSON 里有没有这个字段
|
|
105
|
+
- 数据库相关:是不是实际的列
|
|
106
|
+
|
|
107
|
+
常用装饰器:
|
|
108
|
+
|
|
109
|
+
| 装饰器 | Create DTO | Update DTO | Query DTO | Result DTO | 数据库列 |
|
|
110
|
+
|---------------|-----------|-----------|----------|-----------|---------|
|
|
111
|
+
| NotWritable | ❌ | ❌ | — | ✔ / ❌ 取决于 NotInResult | ✔ |
|
|
112
|
+
| NotCreatable | ❌ | ✔ | — | ✔ / ❌ | ✔ |
|
|
113
|
+
| NotChangeable | ✔ | ❌ | — | ✔ / ❌ | ✔ |
|
|
114
|
+
| NotQueryable | ✔ | ✔ | ❌ | ✔ / ❌ | ✔ |
|
|
115
|
+
| NotInResult | ✔ | ✔ | ✔ | ❌ | ✔ |
|
|
116
|
+
| NotColumn | ❌ | ❌ | ❌ | ❌ | ✖(仅运行时字段) |
|
|
117
|
+
|
|
118
|
+
一个典型例子:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
class User extends IdBase() {
|
|
122
|
+
@StringColumn(255, { description: '登录邮箱' })
|
|
123
|
+
@NotInResult()
|
|
124
|
+
@NotWritable()
|
|
125
|
+
email: string; // 库里有,接口永不返回,也不能写
|
|
126
|
+
|
|
127
|
+
@StringColumn(255)
|
|
128
|
+
@NotInResult()
|
|
129
|
+
password: string; // 密码永远不出现在任何返回里
|
|
130
|
+
|
|
131
|
+
@DateColumn()
|
|
132
|
+
@NotWritable()
|
|
133
|
+
createdAt: Date; // 只读字段:只出现在返回,不能在 Create/Update 中写
|
|
134
|
+
|
|
135
|
+
@NotColumn()
|
|
136
|
+
profileCompleted: boolean; // 运行时计算字段(afterGet 里赋值),不落库
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
访问控制的核心思路:
|
|
141
|
+
|
|
142
|
+
- 敏感字段一开始就挂上 NotInResult + NotWritable。
|
|
143
|
+
- 不在外部写的字段,用 NotCreatable / NotChangeable 精确限制。
|
|
144
|
+
- 只在内部使用的临时字段,用 NotColumn 标记,避免误入 DTO / 返回 / 查询。
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 查询系统:QueryCondition
|
|
149
|
+
|
|
150
|
+
只要字段想被 GET 查询使用,就必须显式声明。
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
@QueryLike()
|
|
154
|
+
name: string;
|
|
155
|
+
|
|
156
|
+
@QueryIn()
|
|
157
|
+
tags: string[];
|
|
158
|
+
|
|
159
|
+
@QueryGreater()
|
|
160
|
+
age: number;
|
|
161
|
+
|
|
162
|
+
@QueryFullText({ parser: 'zhparser', orderBySimilarity: true })
|
|
163
|
+
content: string;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
常见条件:
|
|
167
|
+
|
|
168
|
+
| 装饰器 | 描述 |
|
|
169
|
+
|---------------------|-----------------------|
|
|
170
|
+
| QueryEqual | 精确匹配 |
|
|
171
|
+
| QueryLike | 前缀 LIKE |
|
|
172
|
+
| QuerySearch | 包含 LIKE |
|
|
173
|
+
| QueryGreater/Less | 数值比较 |
|
|
174
|
+
| QueryIn / QueryNotIn| IN / NOT IN |
|
|
175
|
+
| QueryMatchBoolean | 自动解析 true/false |
|
|
176
|
+
| QueryOperator | 自定义操作符 |
|
|
177
|
+
| QueryWrap | 自定义表达式 |
|
|
178
|
+
| QueryAnd / QueryOr | 条件组合 |
|
|
179
|
+
| QueryFullText | PostgreSQL 全文搜索 |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## GET Mutator(URL → 类型转换)
|
|
184
|
+
|
|
185
|
+
URL 参数永远是 string。
|
|
186
|
+
MutatorPipe 用于把字符串转换成真正的运行时类型。
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
@GetMutatorInt()
|
|
190
|
+
@QueryEqual()
|
|
191
|
+
score: number; // ?score=123 → number 123
|
|
192
|
+
|
|
193
|
+
@GetMutatorJson()
|
|
194
|
+
@QueryOperator('@>')
|
|
195
|
+
meta: SomeJSONType; // ?meta={"foo":"bar"} → 对象
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
在 OpenAPI 里,这些字段仍以 string 展示;在实际运行时,它们已经被转换为你想要的类型。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Relations 与 @RelationComputed
|
|
203
|
+
|
|
204
|
+
NICOT 的关系配置出现在两个层面,各自含义不同:
|
|
205
|
+
|
|
206
|
+
- RestfulFactory.relations:
|
|
207
|
+
控制生成的 Result DTO 中“哪些关系字段会被返回”。
|
|
208
|
+
|
|
209
|
+
- CrudService.relations:
|
|
210
|
+
控制 SQL 层面会 join 哪些关系。
|
|
211
|
+
|
|
212
|
+
推荐做法:
|
|
213
|
+
|
|
214
|
+
- 单独建一个 xxx.factory.ts,把这两个地方都统一配置好。
|
|
215
|
+
- Service 用 factory.crudService()。
|
|
216
|
+
- Controller 用 factory.baseController()。
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// user.entity.ts
|
|
220
|
+
@Entity()
|
|
221
|
+
export class User extends IdBase() {
|
|
222
|
+
@OneToMany(() => Article, article => article.user)
|
|
223
|
+
articles: Article[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// user.factory.ts
|
|
227
|
+
export const UserFactory = new RestfulFactory(User, {
|
|
228
|
+
relations: ['articles'],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// user.service.ts
|
|
232
|
+
@Injectable()
|
|
233
|
+
export class UserService extends UserFactory.crudService() {
|
|
234
|
+
constructor(@InjectRepository(User) repo) {
|
|
235
|
+
super(repo);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// user.controller.ts
|
|
240
|
+
@Controller('users')
|
|
241
|
+
export class UserController extends UserFactory.baseController() {
|
|
242
|
+
constructor(userService: UserService) {
|
|
243
|
+
super(userService);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
这样:
|
|
249
|
+
|
|
250
|
+
- DTO 中会包含 articles 字段。
|
|
251
|
+
- 查询时会自动 left join user.articles。
|
|
252
|
+
- 不需要自己维护多份 relations 配置。
|
|
253
|
+
|
|
254
|
+
### @RelationComputed:标记“由关系推导出的 NotColumn 字段”
|
|
255
|
+
|
|
256
|
+
有些字段本身不落库(NotColumn),但它是由若干关系字段组合出来的,并且你希望它可以:
|
|
257
|
+
|
|
258
|
+
- 出现在 Result DTO 中,
|
|
259
|
+
- 同时不把整棵关联树一路无限展开。
|
|
260
|
+
|
|
261
|
+
这种场景使用 @RelationComputed。
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
@Entity()
|
|
265
|
+
export class Participant extends IdBase() {
|
|
266
|
+
@OneToMany(() => Match, m => m.player1)
|
|
267
|
+
matches1: Match[];
|
|
268
|
+
|
|
269
|
+
@OneToMany(() => Match, m => m.player2)
|
|
270
|
+
matches2: Match[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@Entity()
|
|
274
|
+
export class Match extends IdBase() {
|
|
275
|
+
@ManyToOne(() => Participant, p => p.matches1)
|
|
276
|
+
player1: Participant;
|
|
277
|
+
|
|
278
|
+
@ManyToOne(() => Participant, p => p.matches2)
|
|
279
|
+
player2: Participant;
|
|
280
|
+
|
|
281
|
+
@NotColumn()
|
|
282
|
+
@RelationComputed(() => Participant)
|
|
283
|
+
players: Participant[];
|
|
284
|
+
|
|
285
|
+
async afterGet() {
|
|
286
|
+
this.players = [this.player1, this.player2].filter(Boolean);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// match.factory.ts
|
|
293
|
+
export const MatchFactory = new RestfulFactory(Match, {
|
|
294
|
+
relations: ['player1', 'player2', 'players'],
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
作用可以简单理解为:
|
|
299
|
+
|
|
300
|
+
- players 虽然是 NotColumn,但被当成“关系节点”参与 relations 剪裁。
|
|
301
|
+
- DTO 会包含 player1 / player2 / players 三个字段。
|
|
302
|
+
- 但不会因为 players 是 Participant[] 就把 participants 的所有反向关系再展开一遍。
|
|
303
|
+
|
|
304
|
+
总结一下关系相关的最佳实践:
|
|
305
|
+
|
|
306
|
+
- 真正的 @ManyToOne / @OneToMany 一律在 entity 上写清楚。
|
|
307
|
+
- 所有对外需要返回的关系字段,集中在 xxx.factory.ts 的 relations 里配置。
|
|
308
|
+
- 复杂组合 / 聚合字段(NotColumn)用 @RelationComputed 标记依赖类型,再加到 relations 里。
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## `skipNonQueryableFields`: 只暴露你显式声明的查询字段
|
|
313
|
+
|
|
314
|
+
默认情况下,`findAllDto` 会包含:
|
|
315
|
+
|
|
316
|
+
- `PageSettingsDto` 的分页字段(`pageCount`, `recordsPerPage`)
|
|
317
|
+
- 实体中**没有被** `NotQueryable` / `NotColumn` / 必须 GetMutator 但未配置的字段剔除掉的剩余字段
|
|
318
|
+
|
|
319
|
+
也就是说,只要没被标成“禁止查询”,理论上 GET DTO 里就能看到它。
|
|
320
|
+
|
|
321
|
+
如果你希望 **GET 查询参数只允许那些显式挂了 `@QueryEqual()` / `@QueryLike()` 等查询装饰器的字段**,可以开启:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const UserFactory = new RestfulFactory(User, {
|
|
325
|
+
relations: [],
|
|
326
|
+
skipNonQueryableFields: true,
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
开启后行为变成:
|
|
331
|
+
|
|
332
|
+
- `findAllDto` 中**仅保留**挂了 QueryCondition 系列装饰器的字段:
|
|
333
|
+
- `@QueryEqual`
|
|
334
|
+
- `@QueryLike`
|
|
335
|
+
- `@QueryIn`
|
|
336
|
+
- `@QueryFullText`
|
|
337
|
+
- 等所有基于 `QueryCondition` 的装饰器
|
|
338
|
+
- 其他普通字段(即使没被 `NotQueryable` 标记)**不会**出现在 GET DTO 里,也不会出现在 Swagger 的查询参数中。
|
|
339
|
+
- `findAllParam()` 在运行时会额外套一层 `PickPipe(this.queryableFields)`,把 query 里的无关字段都剔掉,达到“白名单”效果。
|
|
340
|
+
|
|
341
|
+
简单理解:
|
|
342
|
+
|
|
343
|
+
> 不挂 `@QueryXXX` 就完全不能在 GET /list 上当查询条件用,连 OpenAPI 文档都看不到。
|
|
344
|
+
|
|
345
|
+
这在下面几种场景特别好用:
|
|
346
|
+
|
|
347
|
+
- 你想让前端“按字段提示”来写查询,而不是随便往 URL 里塞东西。
|
|
348
|
+
- 实体字段特别多,只想开放少量查询条件,避免 Swagger 里出现一长串 query 参数。
|
|
349
|
+
- 把“能不能被查”这件事集中收敛到实体上的 `@QueryXXX()` 装饰器,读代码一眼就知道有哪些查询入口。
|
|
350
|
+
|
|
351
|
+
配合方式:
|
|
352
|
+
|
|
353
|
+
- 想允许查询:在字段上挂 `@QueryEqual` / `@QueryLike` / `@QueryIn` 等。
|
|
354
|
+
- 不想允许查询:什么都不挂(或者明确 `@NotQueryable`)。
|
|
355
|
+
- 想缩小 GET DTO:在对应 `RestfulFactory` 上加 `skipNonQueryableFields: true`。
|
|
356
|
+
|
|
357
|
+
推荐实践是:
|
|
358
|
+
|
|
359
|
+
- **后台管理接口**:几乎都开 `skipNonQueryableFields: true`,强制前后端只围绕“显式查询字段”合作。
|
|
360
|
+
- **内部工具 / 临时调试接口**:可以保持默认行为,不开这个选项,方便随手查数据。
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 自动生成的 DTO
|
|
366
|
+
|
|
367
|
+
通过 RestfulFactory,你可以直接拿到一堆已经裁剪好的 DTO 类型,例如:
|
|
368
|
+
|
|
369
|
+
- createDto / updateDto
|
|
370
|
+
- findAllDto(含分页字段)
|
|
371
|
+
- findAllCursorPaginatedDto(游标分页)
|
|
372
|
+
- entityResultDto(按 NotInResult / relations 剪裁字段)
|
|
373
|
+
- entityCreateResultDto(创建时返回的精简版本)
|
|
374
|
+
- entityReturnMessageDto / entityArrayReturnMessageDto / entityCursorPaginationReturnMessageDto
|
|
375
|
+
|
|
376
|
+
使用方式类似:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const UserFactory = new RestfulFactory(User, { relations: ['articles'] });
|
|
380
|
+
|
|
381
|
+
export class CreateUserDto extends UserFactory.createDto {}
|
|
382
|
+
export class UpdateUserDto extends UserFactory.updateDto {}
|
|
383
|
+
export class FindAllUserDto extends UserFactory.findAllDto {}
|
|
384
|
+
export class UserResultDto extends UserFactory.entityResultDto {}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
你可以在手写 Controller 时直接复用这些 DTO。
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 分页系统
|
|
392
|
+
|
|
393
|
+
### 页码分页(默认)
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
GET /users?pageCount=1&recordsPerPage=25
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
如需修改默认 page size,可以在实体中 override PageSettings 相关方法(例如):
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
@Entity()
|
|
403
|
+
class Log extends IdBase() {
|
|
404
|
+
override getRecordsPerPage() {
|
|
405
|
+
return this.recordsPerPage || 1000;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 游标分页
|
|
411
|
+
|
|
412
|
+
支持:
|
|
413
|
+
|
|
414
|
+
- 多字段排序
|
|
415
|
+
- next/prev 双向翻页
|
|
416
|
+
- 基于 Base64URL 的 cursor payload
|
|
417
|
+
|
|
418
|
+
算法较复杂,只在 api.md 里详细展开。
|
|
419
|
+
在 README 里你只需要记得:**这是适合时间线 / 无限滚动的分页模式**。
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 生命周期钩子
|
|
424
|
+
|
|
425
|
+
实体可以实现以下方法来参与 CRUD 生命周期:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
class User extends IdBase() {
|
|
429
|
+
async beforeCreate() {}
|
|
430
|
+
async afterCreate() {}
|
|
431
|
+
|
|
432
|
+
async beforeGet() {}
|
|
433
|
+
async afterGet() {}
|
|
434
|
+
|
|
435
|
+
async beforeUpdate() {}
|
|
436
|
+
async afterUpdate() {}
|
|
437
|
+
|
|
438
|
+
isValidInCreate(): string | undefined {
|
|
439
|
+
if (!this.name) return 'name is required';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
isValidInUpdate(): string | undefined {
|
|
443
|
+
if (this.age != null && this.age < 0) return 'age must be >= 0';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
- isValidInCreate / isValidInUpdate:返回字符串 → 400 错误。
|
|
449
|
+
- beforeXxx / afterXxx:可以做补全、审计、统计等逻辑。
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## 手写 Controller(高级用法)
|
|
454
|
+
|
|
455
|
+
“手写”不是完全放弃工厂,而是 **继续用 RestfulFactory 的装饰器和 DTO**,在方法实现里插入你自己的业务逻辑。
|
|
456
|
+
|
|
457
|
+
下面是一个示例:基于当前登录用户做数据隔离。
|
|
458
|
+
其中 `@PutUser()` 是你项目里的业务装饰器(和 NICOT 无关),负责注入当前用户。
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
// post.factory.ts
|
|
462
|
+
export const PostFactory = new RestfulFactory(Post, {
|
|
463
|
+
relations: [], // 明确这里不加载任何关系
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// post.service.ts
|
|
467
|
+
@Injectable()
|
|
468
|
+
export class PostService extends PostFactory.crudService() {
|
|
469
|
+
constructor(@InjectRepository(Post) repo: Repository<Post>) {
|
|
470
|
+
super(repo);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// post.controller.ts
|
|
475
|
+
import { Controller } from '@nestjs/common';
|
|
476
|
+
import { PutUser } from '../common/put-user.decorator';
|
|
477
|
+
|
|
478
|
+
// 在 controller 外面把 DTO 固定成具名类,方便引用 / 推导
|
|
479
|
+
export class FindAllPostDto extends PostFactory.findAllDto {}
|
|
480
|
+
export class CreatePostDto extends PostFactory.createDto {}
|
|
481
|
+
|
|
482
|
+
@Controller('posts')
|
|
483
|
+
export class PostController {
|
|
484
|
+
constructor(private readonly service: PostService) {}
|
|
485
|
+
|
|
486
|
+
@PostFactory.findAll()
|
|
487
|
+
async findAll(
|
|
488
|
+
@PostFactory.findAllParam() dto: FindAllPostDto,
|
|
489
|
+
@PutUser() user: User,
|
|
490
|
+
) {
|
|
491
|
+
return this.service.findAll(dto, qb => {
|
|
492
|
+
qb.andWhere('post.userId = :uid', { uid: user.id });
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@PostFactory.create()
|
|
497
|
+
async create(
|
|
498
|
+
@PostFactory.createParam() dto: CreatePostDto,
|
|
499
|
+
@PutUser() user: User,
|
|
500
|
+
) {
|
|
501
|
+
dto.userId = user.id;
|
|
502
|
+
return this.service.create(dto);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
要点是:
|
|
508
|
+
|
|
509
|
+
- 路由装饰器仍然来自 PostFactory(保证 DTO / Swagger / 返回结构一致)。
|
|
510
|
+
- 参数装饰器也来自 PostFactory(自动 ValidationPipe / MutatorPipe / OmitPipe 等)。
|
|
511
|
+
- 你只在方法体内做“多一步”:
|
|
512
|
+
- 把 user.id 写进 dto。
|
|
513
|
+
- 对 QueryBuilder 追加额外 where 条件。
|
|
514
|
+
|
|
515
|
+
如果你完全绕开 CrudService / RestfulFactory(例如直接 repo.find),那就等于跳出 NICOT 的生命周期系统,需要自己保证安全性与一致性。
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 装饰器行为矩阵(整体优先级视角)
|
|
520
|
+
|
|
521
|
+
| 装饰器 | Create DTO | Update DTO | Query DTO | Result DTO |
|
|
522
|
+
|---------------------|-----------|-----------|----------|-----------|
|
|
523
|
+
| NotWritable | ❌ | ❌ | — | — |
|
|
524
|
+
| NotCreatable | ❌ | ✔ | — | — |
|
|
525
|
+
| NotChangeable | ✔ | ❌ | — | — |
|
|
526
|
+
| NotQueryable | ✔ | ✔ | ❌ | ✔ |
|
|
527
|
+
| NotInResult | ✔ | ✔ | ✔ | ❌ |
|
|
528
|
+
| NotColumn | ❌ | ❌ | ❌ | ❌ |
|
|
529
|
+
| QueryCondition 系列 | — | — | ✔ | — |
|
|
530
|
+
| GetMutator | — | — | ✔(string→类型) | — |
|
|
531
|
+
|
|
532
|
+
可以把这张表理解成:
|
|
533
|
+
“如果出现冲突,以更‘收紧’的装饰器为准”。
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## 安装
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
npm install nicot typeorm @nestjs/typeorm class-validator class-transformer reflect-metadata @nestjs/swagger
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## 设计哲学(Philosophy)
|
|
546
|
+
|
|
547
|
+
### 1. Entity = Contract
|
|
548
|
+
避免重复维护 schema / DTO / API,所有行为围绕实体展开。
|
|
549
|
+
|
|
550
|
+
### 2. Whitelist-only
|
|
551
|
+
字段要能写、能查、能返回,都必须显式声明。
|
|
552
|
+
没有“默认全部暴露”的行为。
|
|
553
|
+
|
|
554
|
+
### 3. 不发明 DSL
|
|
555
|
+
依赖 TypeScript 装饰器而不是额外 DSL / YAML。
|
|
556
|
+
你看到的就是 TypeScript 代码本身。
|
|
557
|
+
|
|
558
|
+
### 4. 自动化不隐藏逻辑
|
|
559
|
+
CRUD 可以一键生成,但 QueryCondition、MutatorPipe、hooks、extraQuery 都是显式可见的扩展点。
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## LICENSE
|
|
564
|
+
|
|
565
|
+
MIT
|