nicot 1.2.3 → 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 +13 -4
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +13 -4
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
package/api.md
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
# NICOT API Reference
|
|
2
|
+
|
|
3
|
+
> This document is a **high-level API / behavioral reference** for NICOT.
|
|
4
|
+
> It assumes you’ve already skimmed the main README and want more “how it really behaves” details.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. Terminology
|
|
9
|
+
|
|
10
|
+
- **Entity / model**: TypeORM entity class. In NICOT this is the *single source of truth* (fields, validation, query options are all declared here).
|
|
11
|
+
- **Factory**: An instance of `new RestfulFactory(Entity, options)`.
|
|
12
|
+
- **CrudService**: A service class created by `Factory.crudService()`.
|
|
13
|
+
- **BaseRestfulController**: The base controller used by `Factory.baseController()`.
|
|
14
|
+
- **Query DTO**: `findAllDto` / `findAllCursorPaginatedDto`.
|
|
15
|
+
- **Result DTO**: `entityResultDto` / `entityCreateResultDto`.
|
|
16
|
+
|
|
17
|
+
We use `T` below to represent the TypeScript type of an entity.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Base ID classes & entity hooks
|
|
22
|
+
|
|
23
|
+
### 1.1 IdBase / StringIdBase behavior
|
|
24
|
+
|
|
25
|
+
**IdBase(idOptions?: { description?: string; noOrderById?: boolean; })**
|
|
26
|
+
|
|
27
|
+
- Field:
|
|
28
|
+
- `id: number`
|
|
29
|
+
- DB column: `bigint unsigned`, primary, auto-increment
|
|
30
|
+
- Default ordering:
|
|
31
|
+
- If `noOrderById !== true`, `applyQuery()` will append:
|
|
32
|
+
`ORDER BY <alias>.id DESC`
|
|
33
|
+
- Validation / query:
|
|
34
|
+
- Treated as “has default” (you don’t need to send it in Create);
|
|
35
|
+
- Supports `?id=123` as an equality filter;
|
|
36
|
+
- Not writable in Create/Update DTOs.
|
|
37
|
+
|
|
38
|
+
**StringIdBase(options: { length?: number; uuid?: boolean; noOrderById?: boolean; … })**
|
|
39
|
+
|
|
40
|
+
- Field:
|
|
41
|
+
- `id: string` (primary)
|
|
42
|
+
- Behavior:
|
|
43
|
+
- `uuid: true`:
|
|
44
|
+
- Database generates UUID; Create/Update DTOs do **not** allow writing `id`.
|
|
45
|
+
- `uuid: false / omitted`:
|
|
46
|
+
- Fixed-length `varchar`, required on create, and **not changeable** later.
|
|
47
|
+
- Default ordering:
|
|
48
|
+
- If `noOrderById !== true`, append:
|
|
49
|
+
`ORDER BY <alias>.id ASC`.
|
|
50
|
+
|
|
51
|
+
You can think of it as:
|
|
52
|
+
|
|
53
|
+
- **IdBase**: Auto-increment numeric primary key, newest first.
|
|
54
|
+
- **StringIdBase(uuid)**: String/UUID primary key, ordered lexicographically by default.
|
|
55
|
+
|
|
56
|
+
### 1.2 Entity hook lifecycle
|
|
57
|
+
|
|
58
|
+
An entity class may implement the following “convention-based” hooks, which NICOT’s CrudService will call:
|
|
59
|
+
|
|
60
|
+
- Validation:
|
|
61
|
+
- `isValidInCreate(): string | undefined`
|
|
62
|
+
- `isValidInUpdate(): string | undefined`
|
|
63
|
+
- If they return a non-empty string → treated as error message and converted to a `400` response.
|
|
64
|
+
- Lifecycle:
|
|
65
|
+
- `beforeCreate()`, `afterCreate()`
|
|
66
|
+
- `beforeUpdate()`, `afterUpdate()`
|
|
67
|
+
- `beforeGet()`, `afterGet()`
|
|
68
|
+
- Query extension:
|
|
69
|
+
- `applyQuery(qb, entityAliasName)`
|
|
70
|
+
(IdBase / TimeBase use this to inject default ordering, etc. You can override it to customize.)
|
|
71
|
+
|
|
72
|
+
Rough order when doing a GET query:
|
|
73
|
+
|
|
74
|
+
1. Create instance `ent = new EntityClass()`
|
|
75
|
+
2. Assign Query DTO properties into `ent`
|
|
76
|
+
3. Call `ent.beforeGet?.()`
|
|
77
|
+
4. Call `ent.applyQuery(qb, alias)` (default ordering)
|
|
78
|
+
5. Apply relations (see section 7)
|
|
79
|
+
6. Apply field-level Query decorators
|
|
80
|
+
7. Apply pagination (offset or cursor)
|
|
81
|
+
8. Run SQL and get results
|
|
82
|
+
9. For each record, call `afterGet?.()`
|
|
83
|
+
10. Strip fields that should not appear in output (see section 2)
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Access control decorators & field pruning
|
|
88
|
+
|
|
89
|
+
### 2.1 Access decorators overview
|
|
90
|
+
|
|
91
|
+
NICOT centralizes “where/when a field is visible or writable” using decorators plus Factory config.
|
|
92
|
+
|
|
93
|
+
Typical decorators:
|
|
94
|
+
|
|
95
|
+
| Decorator | Affects stage(s) | Behavior |
|
|
96
|
+
|------------------------|-------------------------------------------|--------------------------------------------------------------------------|
|
|
97
|
+
| `@NotWritable()` | Create / Update | Field never appears in input DTOs |
|
|
98
|
+
| `@NotCreatable()` | Create | Field is excluded from Create DTO only |
|
|
99
|
+
| `@NotChangeable()` | Update | Field is excluded from Update DTO only |
|
|
100
|
+
| `@NotQueryable()` | GET Query DTO | Field is removed from query DTO (cannot be used as filter) |
|
|
101
|
+
| `@NotInResult()` | Result DTO | Field is removed from all response data (including nested relations) |
|
|
102
|
+
| `@NotColumn()` | DB mapping | Field is not mapped to a DB column, typically “computed” in `afterGet` |
|
|
103
|
+
| `@QueryColumn()` | GET Query DTO | Declares a “query-only” field (no DB column); usually combined with Query decorators |
|
|
104
|
+
| `@RelationComputed()` | Result DTO / relation pruning | Marks “computed from relations” fields to be pruned consistently with relations config |
|
|
105
|
+
|
|
106
|
+
NICOT reads these metadata in Factory / CrudService to decide:
|
|
107
|
+
|
|
108
|
+
- Which fields go into Create DTO
|
|
109
|
+
- Which fields go into Update DTO
|
|
110
|
+
- Which fields go into GET Query DTO
|
|
111
|
+
- Which fields are stripped from Result DTO
|
|
112
|
+
|
|
113
|
+
### 2.2 DTO pruning rules (priority)
|
|
114
|
+
|
|
115
|
+
Conceptually:
|
|
116
|
+
|
|
117
|
+
- **Create DTO**:
|
|
118
|
+
- Remove:
|
|
119
|
+
- All `NotColumn` fields
|
|
120
|
+
- All relations
|
|
121
|
+
- `NotWritable`, `NotCreatable`
|
|
122
|
+
- Any fields explicitly omitted by Factory options
|
|
123
|
+
- **Update DTO**:
|
|
124
|
+
- Remove:
|
|
125
|
+
- All `NotColumn`
|
|
126
|
+
- All relations
|
|
127
|
+
- `NotWritable`, `NotChangeable`
|
|
128
|
+
- Factory-level omissions
|
|
129
|
+
- **FindAll Query DTO**:
|
|
130
|
+
- Remove:
|
|
131
|
+
- All `NotColumn`
|
|
132
|
+
- All relations
|
|
133
|
+
- `NotQueryable`
|
|
134
|
+
- Fields that declare “require mutator” but don’t actually have one
|
|
135
|
+
- Factory-level omissions
|
|
136
|
+
- **Result DTO**:
|
|
137
|
+
- Remove:
|
|
138
|
+
- All “should not appear in result” fields (including some timestamp/version fields)
|
|
139
|
+
- Factory `outputFieldsToOmit`
|
|
140
|
+
- Relation fields that are not in the configured relations whitelist (see section 7)
|
|
141
|
+
|
|
142
|
+
A simple mental model:
|
|
143
|
+
|
|
144
|
+
> **For each stage (Create / Update / Query / Result), a field must be allowed by both decorators *and* Factory config to appear.**
|
|
145
|
+
|
|
146
|
+
As a user, you only need to:
|
|
147
|
+
|
|
148
|
+
- Put proper access decorators on each field;
|
|
149
|
+
- Optionally use Factory options (`fieldsToOmit`, `outputFieldsToOmit`, …) to fine-tune globally.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 3. Query system: QueryCondition & QueryXXX
|
|
154
|
+
|
|
155
|
+
### 3.1 What QueryCondition does
|
|
156
|
+
|
|
157
|
+
`QueryCondition` is the underlying mechanism of all “Query” decorators. It describes:
|
|
158
|
+
|
|
159
|
+
> “When this field is present in the query DTO, how do we map it into SQL WHERE conditions?”
|
|
160
|
+
|
|
161
|
+
NICOT will, for each GET request:
|
|
162
|
+
|
|
163
|
+
1. Collect all fields in the entity that have QueryCondition metadata;
|
|
164
|
+
2. If the query DTO has a value for that field;
|
|
165
|
+
3. Run the associated condition logic and append to the `SelectQueryBuilder`.
|
|
166
|
+
|
|
167
|
+
It only affects **GET**:
|
|
168
|
+
|
|
169
|
+
- No impact on Create / Update / Delete.
|
|
170
|
+
|
|
171
|
+
### 3.2 Built-in wrappers
|
|
172
|
+
|
|
173
|
+
NICOT provides common “query templates” as decorators. A few examples:
|
|
174
|
+
|
|
175
|
+
- `QueryEqual()`:
|
|
176
|
+
- `?status=1` → `status = :status`.
|
|
177
|
+
- `QueryLike()`:
|
|
178
|
+
- `?name=ab` → `name LIKE 'ab%'`.
|
|
179
|
+
- `QuerySearch()`:
|
|
180
|
+
- `?name=ab` → `name LIKE '%ab%'`.
|
|
181
|
+
- `QueryIn()`:
|
|
182
|
+
- `?ids=1,2,3` or `?ids[]=1&ids[]=2` → `id IN (:...ids)`.
|
|
183
|
+
- `QueryNotIn()`:
|
|
184
|
+
- Same as above, but `NOT IN`.
|
|
185
|
+
- `QueryMatchBoolean()`:
|
|
186
|
+
- Interprets `true/false/1/0/'true'/'false'` as booleans, generates `= TRUE/FALSE`.
|
|
187
|
+
- `QueryEqualZeroNullable()`:
|
|
188
|
+
- `?foo=0` or `?foo=0,0` → treat 0 as *NULL* and generate `IS NULL`; otherwise `=`.
|
|
189
|
+
|
|
190
|
+
All of these are “SQL expression templates” tied to decorators on fields.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
class User {
|
|
194
|
+
@QueryEqual()
|
|
195
|
+
status: number;
|
|
196
|
+
|
|
197
|
+
@QueryLike()
|
|
198
|
+
name: string;
|
|
199
|
+
|
|
200
|
+
@QueryIn()
|
|
201
|
+
ids: number[];
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Whenever these fields appear in the query DTO, NICOT translates them into the corresponding WHERE clauses.
|
|
206
|
+
|
|
207
|
+
### 3.3 Composition: QueryAnd / QueryOr
|
|
208
|
+
|
|
209
|
+
Sometimes you want **multiple** query behaviors on the same field, e.g.:
|
|
210
|
+
|
|
211
|
+
- A complex search that combines several conditions;
|
|
212
|
+
- A field that has more than one “matching strategy”.
|
|
213
|
+
|
|
214
|
+
NICOT gives you:
|
|
215
|
+
|
|
216
|
+
- `QueryAnd(A, B, C...)`:
|
|
217
|
+
- Runs A, B, C in sequence and ANDs all their expressions.
|
|
218
|
+
- `QueryOr(A, B, C...)`:
|
|
219
|
+
- Keeps internal AND-structure of each condition, and ORs them together.
|
|
220
|
+
|
|
221
|
+
Example (pseudo-code):
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
class Article {
|
|
225
|
+
// Same field, two query behaviors. The final expression is A OR B.
|
|
226
|
+
@QueryOr(QueryLike(), QueryEqual())
|
|
227
|
+
title: string;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Think of them as:
|
|
232
|
+
|
|
233
|
+
- A / B are extracting logic from some existing Query decorators;
|
|
234
|
+
- `QueryAnd/QueryOr` just describe how we logically combine them.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 4. PostgreSQL Full-Text Search (QueryFullText)
|
|
239
|
+
|
|
240
|
+
### 4.1 Use cases
|
|
241
|
+
|
|
242
|
+
`QueryFullText` is a specialized query decorator for **PostgreSQL**. It’s designed for:
|
|
243
|
+
|
|
244
|
+
- Marking certain text columns as “full-text searchable”;
|
|
245
|
+
- Letting the Query DTO support things like `?q=keywords`;
|
|
246
|
+
- Optionally sorting by `ts_rank` (relevance).
|
|
247
|
+
|
|
248
|
+
Capabilities:
|
|
249
|
+
|
|
250
|
+
- Automatically create full-text indexes (GIN + `to_tsvector(...)`);
|
|
251
|
+
- Automatically create/configure text search configurations (when a parser is specified, e.g. for Chinese);
|
|
252
|
+
- Query-side: generate `to_tsvector(...) @@ websearch_to_tsquery(...)` expressions;
|
|
253
|
+
- Optionally push a “relevance” virtual column to the front of the ORDER BY chain.
|
|
254
|
+
|
|
255
|
+
### 4.2 Configuration
|
|
256
|
+
|
|
257
|
+
Decorator shape (simplified):
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
class Article {
|
|
261
|
+
@QueryFullText({
|
|
262
|
+
// parser / configuration:
|
|
263
|
+
// - `parser`: e.g. 'zhparser', NICOT will create a dedicated configuration
|
|
264
|
+
// - or specify `configuration` directly, e.g. 'english'
|
|
265
|
+
parser?: string;
|
|
266
|
+
configuration?: string;
|
|
267
|
+
|
|
268
|
+
// tsQuery function, default: 'websearch_to_tsquery'
|
|
269
|
+
tsQueryFunction?: string;
|
|
270
|
+
|
|
271
|
+
// Whether to order by similarity (ts_rank)
|
|
272
|
+
orderBySimilarity?: boolean;
|
|
273
|
+
})
|
|
274
|
+
content: string;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Behavior overview:
|
|
279
|
+
|
|
280
|
+
1. **Module init**:
|
|
281
|
+
- NICOT scans entities for fields annotated with `QueryFullText`;
|
|
282
|
+
- For each such field:
|
|
283
|
+
- Ensures required extension + configuration exist;
|
|
284
|
+
- Creates appropriate GIN index on the table.
|
|
285
|
+
2. **GET query**:
|
|
286
|
+
- If that field is present in the query DTO:
|
|
287
|
+
- Adds a full-text condition;
|
|
288
|
+
- If `orderBySimilarity: true`, inserts a virtual “rank” column at the front of ORDER BY to sort by relevance.
|
|
289
|
+
|
|
290
|
+
> This feature is intended for **PostgreSQL only**.
|
|
291
|
+
> On other databases it is not guaranteed to work as expected.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 5. GetMutator: wire-format to typed DTO
|
|
296
|
+
|
|
297
|
+
`GetMutator` (and related utilities) let you define a “wire-format to real type” conversion for GET query fields. Typical use cases:
|
|
298
|
+
|
|
299
|
+
- Frontend always sends parameters as strings (`?tags=1,2,3`);
|
|
300
|
+
- In controller, you want strongly-typed structures (`number[]`, custom objects, enums);
|
|
301
|
+
- In OpenAPI, the field is documented as `string`, matching the URL format.
|
|
302
|
+
|
|
303
|
+
### 5.1 Summary of behavior
|
|
304
|
+
|
|
305
|
+
If a field has registered “mutator metadata”, NICOT will:
|
|
306
|
+
|
|
307
|
+
1. **During GET Query DTO generation**:
|
|
308
|
+
- Mark this field as `string` in OpenAPI;
|
|
309
|
+
- Optionally provide `example`, `enum`, etc.;
|
|
310
|
+
- Remove default values (to avoid Swagger auto-filling filters).
|
|
311
|
+
2. **Before entering the controller**:
|
|
312
|
+
- Shallow-copy the query DTO;
|
|
313
|
+
- For every field configured with a mutator:
|
|
314
|
+
- If its value is non-null, call the mutator to convert the string into target structure;
|
|
315
|
+
- The controller then receives the query object where that field is already converted to its “logical type”.
|
|
316
|
+
|
|
317
|
+
In other words:
|
|
318
|
+
|
|
319
|
+
> “GET query parameters always arrive as strings at the HTTP level, but NICOT runs a conversion layer before your controller, and your TypeScript DTO can safely use the converted type. OpenAPI still reflects the string wire format.”
|
|
320
|
+
|
|
321
|
+
### 5.2 Usage suggestions
|
|
322
|
+
|
|
323
|
+
Great for:
|
|
324
|
+
|
|
325
|
+
- `?ids=1,2,3` → `number[]`
|
|
326
|
+
- `?range=2024-01-01,2024-02-01` → `{ from: Date; to: Date }`
|
|
327
|
+
- `?country=us` → some enum type
|
|
328
|
+
|
|
329
|
+
Once a field is configured with a mutator:
|
|
330
|
+
|
|
331
|
+
- Define the DTO type as the *converted* type (e.g. `number[]`, not `string`);
|
|
332
|
+
- Do not manually parse string in the controller—let NICOT do it.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 6. `skipNonQueryableFields`: strict filter white-list
|
|
337
|
+
|
|
338
|
+
When constructing a `RestfulFactory`, you may pass:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
new RestfulFactory(Entity, {
|
|
342
|
+
skipNonQueryableFields: true,
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Effect:
|
|
347
|
+
|
|
348
|
+
- In GET Query DTOs (`findAllDto`, `findAllCursorPaginatedDto`):
|
|
349
|
+
- **Only fields that have QueryCondition metadata are included**;
|
|
350
|
+
- All other entity fields (even if they exist in the entity) are stripped from the query DTO.
|
|
351
|
+
- When parsing query parameters:
|
|
352
|
+
- Any query param not present in the DTO will be silently dropped.
|
|
353
|
+
|
|
354
|
+
Where it shines:
|
|
355
|
+
|
|
356
|
+
- Public APIs / multi-tenant environments:
|
|
357
|
+
- Turn this on, so your filterable fields are explicit whitelist.
|
|
358
|
+
- Internal tools:
|
|
359
|
+
- You might leave it off for flexibility (while still using Query decorators to control behavior).
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 7. Relations & @RelationComputed
|
|
364
|
+
|
|
365
|
+
### 7.1 Dual role of `relations`
|
|
366
|
+
|
|
367
|
+
“Should we join this relation?” is controlled in two places:
|
|
368
|
+
|
|
369
|
+
1. **Factory / CrudService `relations` option**:
|
|
370
|
+
- Controls which relations are joined in the SQL query;
|
|
371
|
+
- Also controls which relations appear in `entityResultDto` / `entityCreateResultDto`.
|
|
372
|
+
2. **`@RelationComputed()` on entity fields**:
|
|
373
|
+
- Declares fields that are derived *from* relations;
|
|
374
|
+
- When the relation chain is pruned, NICOT can also consistently prune these computed fields.
|
|
375
|
+
|
|
376
|
+
Default behavior:
|
|
377
|
+
|
|
378
|
+
- If `relations` is **not** specified:
|
|
379
|
+
- Service does not auto-join any relation (only the main table is queried);
|
|
380
|
+
- Result DTO also drops all relation fields.
|
|
381
|
+
- If `relations` is specified:
|
|
382
|
+
- Example: `['user', 'user.profile']`:
|
|
383
|
+
- Service will join exactly those relation paths;
|
|
384
|
+
- Result DTO includes relation fields along these paths only, others are pruned.
|
|
385
|
+
|
|
386
|
+
`@RelationComputed()` is useful when:
|
|
387
|
+
|
|
388
|
+
- A field depends on multiple relation chains:
|
|
389
|
+
- for example, `post.user.profile.nickname`;
|
|
390
|
+
- You want NICOT to treat this field consistently with relation pruning:
|
|
391
|
+
- If the underlying relations are not in the whitelist, this computed field should also be removed from Result DTO.
|
|
392
|
+
|
|
393
|
+
### 7.2 Service vs Factory `relations`
|
|
394
|
+
|
|
395
|
+
- **Service-level `relations` (CrudOptions)**:
|
|
396
|
+
- Control actual SQL joins and selected columns.
|
|
397
|
+
- **Factory-level `relations`**:
|
|
398
|
+
- Influence both query behavior and DTO structure / Swagger.
|
|
399
|
+
|
|
400
|
+
**Recommended pattern**:
|
|
401
|
+
|
|
402
|
+
- Don’t manually maintain a separate `relations` list only at Service level.
|
|
403
|
+
- Prefer to define `relations` on the Factory, and then derive Service via `Factory.crudService()`:
|
|
404
|
+
- Ensures Service / Controller / DTO are aligned in:
|
|
405
|
+
- Which relations are joined;
|
|
406
|
+
- Which fields appear in responses.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 8. CrudService options & import behavior
|
|
411
|
+
|
|
412
|
+
### 8.1 CrudOptions overview
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
interface CrudOptions<T> {
|
|
416
|
+
relations?: (string | RelationDef)[];
|
|
417
|
+
extraGetQuery?: (qb: SelectQueryBuilder<T>) => void;
|
|
418
|
+
hardDelete?: boolean;
|
|
419
|
+
createOrUpdate?: boolean;
|
|
420
|
+
keepEntityVersioningDates?: boolean;
|
|
421
|
+
outputFieldsToOmit?: (keyof T)[];
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Highlights:
|
|
426
|
+
|
|
427
|
+
- `relations`:
|
|
428
|
+
- Same semantics as in section 7.
|
|
429
|
+
- `extraGetQuery`:
|
|
430
|
+
- Applied on top of NICOT’s internal query logic for all GET operations (findOne / findAll / findAllCursorPaginated).
|
|
431
|
+
- Ideal place to enforce “tenantId constraint”, “only active records”, etc.
|
|
432
|
+
- `hardDelete`:
|
|
433
|
+
- Default: if the entity has a `deleteDateColumn`, soft delete is used; otherwise hard delete.
|
|
434
|
+
- `hardDelete: true` forces `DELETE` even if soft delete is configured.
|
|
435
|
+
- `createOrUpdate`:
|
|
436
|
+
- For `create()` and import:
|
|
437
|
+
- If id does not exist → insert;
|
|
438
|
+
- If id exists and not soft-deleted → update;
|
|
439
|
+
- If id exists but soft-deleted → delete old row then insert new row.
|
|
440
|
+
- `keepEntityVersioningDates`:
|
|
441
|
+
- Controls whether to keep some version/timestamp fields in Result DTO.
|
|
442
|
+
- `outputFieldsToOmit`:
|
|
443
|
+
- Further excludes some fields from Result DTO, on top of `NotInResult` decorators.
|
|
444
|
+
|
|
445
|
+
### 8.2 Import behavior (`importEntities`)
|
|
446
|
+
|
|
447
|
+
High-level logic of `CrudBase.importEntities(...)`:
|
|
448
|
+
|
|
449
|
+
1. Normalize raw objects into entity instances (ignoring relation fields).
|
|
450
|
+
2. For each entity:
|
|
451
|
+
- Call `isValidInCreate()`:
|
|
452
|
+
- If it returns a message → record as invalid;
|
|
453
|
+
- If provided, call `extraChecking(ent)`:
|
|
454
|
+
- Use this for cross-row or external system validation.
|
|
455
|
+
3. Filter out invalid entities; keep errors in a list.
|
|
456
|
+
4. For remaining entities:
|
|
457
|
+
- Call `beforeCreate()` on each;
|
|
458
|
+
- Run a batch create (honoring `createOrUpdate`);
|
|
459
|
+
- After saving, call `afterCreate()` on each.
|
|
460
|
+
5. Build ImportEntry DTO list:
|
|
461
|
+
- Each record includes `entry` + `result` (“OK” or error message);
|
|
462
|
+
- `entry` fields are cleaned via NICOT’s result-field pruning rules;
|
|
463
|
+
- All entries are wrapped in a standard ReturnMessage response.
|
|
464
|
+
|
|
465
|
+
Import is “partially successful”: some entries may fail while others succeed, rather than “all-or-nothing”.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 9. RestfulFactory API
|
|
470
|
+
|
|
471
|
+
### 9.1 Options recap
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
interface RestfulFactoryOptions<T, O, W, C, U, F, R> {
|
|
475
|
+
fieldsToOmit?: O[];
|
|
476
|
+
writeFieldsToOmit?: W[];
|
|
477
|
+
createFieldsToOmit?: C[];
|
|
478
|
+
updateFieldsToOmit?: U[];
|
|
479
|
+
findAllFieldsToOmit?: F[];
|
|
480
|
+
outputFieldsToOmit?: R[];
|
|
481
|
+
prefix?: string;
|
|
482
|
+
keepEntityVersioningDates?: boolean;
|
|
483
|
+
entityClassName?: string;
|
|
484
|
+
relations?: (string | RelationDef)[];
|
|
485
|
+
skipNonQueryableFields?: boolean;
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Key points:
|
|
490
|
+
|
|
491
|
+
- `entityClassName`:
|
|
492
|
+
- Used to rename DTO classes; helps avoid name collisions when multiple Factories share one entity.
|
|
493
|
+
- `prefix`:
|
|
494
|
+
- Affects all auto-generated routes:
|
|
495
|
+
- e.g. `prefix: 'admin'` → `GET /admin`, `GET /admin/:id`, etc.
|
|
496
|
+
- All other fields hook into the pruning/relations behavior described in sections 2, 6, 7.
|
|
497
|
+
|
|
498
|
+
### 9.2 Generated DTOs
|
|
499
|
+
|
|
500
|
+
For a `Post` entity, a Factory will generate:
|
|
501
|
+
|
|
502
|
+
- `PostFactory.createDto`:
|
|
503
|
+
- `CreatePostDto`, used for `POST`.
|
|
504
|
+
- `PostFactory.updateDto`:
|
|
505
|
+
- `UpdatePostDto`, used for `PATCH`.
|
|
506
|
+
- `PostFactory.findAllDto`:
|
|
507
|
+
- `FindPostDto`, used for offset-based GET pagination.
|
|
508
|
+
- `PostFactory.findAllCursorPaginatedDto`:
|
|
509
|
+
- `FindPostCursorPaginatedDto`, used for cursor-based pagination.
|
|
510
|
+
- `PostFactory.entityResultDto`:
|
|
511
|
+
- `PostResultDto`, full result structure (including allowed relations).
|
|
512
|
+
- `PostFactory.entityCreateResultDto`:
|
|
513
|
+
- `PostCreateResultDto`, used for responses from `POST`:
|
|
514
|
+
- Typically omits relations and certain computed fields.
|
|
515
|
+
|
|
516
|
+
In actual controllers, it’s recommended to define explicit classes extending these types, for clarity and stronger typings:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
// post.factory.ts
|
|
520
|
+
export const PostFactory = new RestfulFactory(Post, {
|
|
521
|
+
relations: ['user', 'comments'],
|
|
522
|
+
skipNonQueryableFields: true,
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
// post.controller.ts
|
|
528
|
+
class FindAllPostDto extends PostFactory.findAllDto {}
|
|
529
|
+
|
|
530
|
+
@Controller('posts')
|
|
531
|
+
export class PostController {
|
|
532
|
+
constructor(private readonly service: PostService) {}
|
|
533
|
+
|
|
534
|
+
@PostFactory.findAll({ summary: 'List posts of current user' })
|
|
535
|
+
async findAll(
|
|
536
|
+
@PostFactory.findAllParam() dto: FindAllPostDto,
|
|
537
|
+
@PutUser() user: User, // business decorator, not from NICOT
|
|
538
|
+
) {
|
|
539
|
+
return this.service.findAll(dto, qb =>
|
|
540
|
+
qb.andWhere('post.userId = :uid', { uid: user.id }),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
> Best practice: put each Factory in its own `*.factory.ts` file, separate from entity and controller, for reuse and readability.
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 10. Cursor pagination: contract & boundaries
|
|
551
|
+
|
|
552
|
+
Cursor pagination in NICOT is implemented internally, but from a user’s perspective it is:
|
|
553
|
+
|
|
554
|
+
- **Input**: an extra `paginationCursor` field in the Query DTO;
|
|
555
|
+
- **Output**: a `CursorPaginationReturnMessageDto` wrapping the data and cursor tokens.
|
|
556
|
+
|
|
557
|
+
### 10.1 Usage & response schema
|
|
558
|
+
|
|
559
|
+
Using Factory:
|
|
560
|
+
|
|
561
|
+
- Use `Factory.findAllCursorPaginated()` as method decorator;
|
|
562
|
+
- Use `Factory.findAllParam()` to get the combined Query DTO (including cursor).
|
|
563
|
+
|
|
564
|
+
Response (simplified):
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
{
|
|
568
|
+
statusCode: 200,
|
|
569
|
+
success: true,
|
|
570
|
+
message: 'success',
|
|
571
|
+
data: T[], // current page
|
|
572
|
+
pagination: {
|
|
573
|
+
nextCursor?: string;
|
|
574
|
+
previousCursor?: string;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Contract:
|
|
580
|
+
|
|
581
|
+
- If `nextCursor` is present:
|
|
582
|
+
- Send it in `paginationCursor` to get the next page.
|
|
583
|
+
- If `previousCursor` is present:
|
|
584
|
+
- Send it in `paginationCursor` to get the previous page.
|
|
585
|
+
|
|
586
|
+
### 10.2 Source of ORDER BY and constraints
|
|
587
|
+
|
|
588
|
+
Cursor pagination **requires a stable ordering**.
|
|
589
|
+
|
|
590
|
+
Sorting can come from:
|
|
591
|
+
|
|
592
|
+
1. Entity’s `applyQuery()` (e.g. default `id` ordering from IdBase);
|
|
593
|
+
2. `extraGetQuery(qb)` from CrudOptions;
|
|
594
|
+
3. Controller’s `extraQuery(qb)` in each Service call.
|
|
595
|
+
|
|
596
|
+
Important constraints:
|
|
597
|
+
|
|
598
|
+
- Within the same cursor chain, all of these ordering sources must remain consistent:
|
|
599
|
+
- If subsequent pages use a different set of ORDER BY fields, the cursor’s “boundary values” will not match the current query.
|
|
600
|
+
- NICOT will try to ignore fields that no longer exist in ORDER BY, but the result degrades into less-stable pagination (duplicates / gaps may appear).
|
|
601
|
+
- You are free to use `extraGetQuery / extraQuery` to change the default ordering, but:
|
|
602
|
+
- Avoid changing ordering mid-session while reusing cursors;
|
|
603
|
+
- If you need a new ordering, treat it as a fresh pagination and ignore old cursors.
|
|
604
|
+
|
|
605
|
+
### 10.3 Multi-column ordering (conceptual)
|
|
606
|
+
|
|
607
|
+
Internally NICOT uses the current ordered columns as an ordered list:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
orderKeys = [
|
|
611
|
+
'"post"."createdAt"',
|
|
612
|
+
'"post"."id"',
|
|
613
|
+
// possibly other fields, e.g. full-text similarity
|
|
614
|
+
];
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
The cursor stores something conceptually like:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
{
|
|
621
|
+
type: 'next' | 'prev',
|
|
622
|
+
payload: {
|
|
623
|
+
'"post"."createdAt"': '2024-01-01T00:00:00.000Z',
|
|
624
|
+
'"post"."id"': 123,
|
|
625
|
+
// ...
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
On the next request:
|
|
631
|
+
|
|
632
|
+
- NICOT reconstructs a WHERE clause:
|
|
633
|
+
- Roughly equivalent to:
|
|
634
|
+
|
|
635
|
+
```sql
|
|
636
|
+
(
|
|
637
|
+
(createdAt > :createdAt)
|
|
638
|
+
OR (createdAt = :createdAt AND id > :id)
|
|
639
|
+
-- ...
|
|
640
|
+
)
|
|
641
|
+
```
|
|
642
|
+
- It respects:
|
|
643
|
+
- ASC / DESC;
|
|
644
|
+
- `NULLS FIRST` / `NULLS LAST`;
|
|
645
|
+
- And tries to handle `NULL` in a way that avoids infinite loops or broken boundaries.
|
|
646
|
+
|
|
647
|
+
You never need to manipulate the payload directly—treat the cursor as an opaque string.
|
|
648
|
+
|
|
649
|
+
### 10.4 Out-of-range & data changes
|
|
650
|
+
|
|
651
|
+
**Q1: What if I use an “out-of-range” cursor string?**
|
|
652
|
+
|
|
653
|
+
- NICOT does not restrict which string you send as `paginationCursor`.
|
|
654
|
+
- If the new WHERE conditions are stricter than before:
|
|
655
|
+
- You may simply get an empty page (you “went past the end”).
|
|
656
|
+
- If they are looser:
|
|
657
|
+
- You may see data that “wasn’t in the original window” appear in the middle;
|
|
658
|
+
- Or see some rows repeated.
|
|
659
|
+
|
|
660
|
+
**Q2: What if data changes while I paginate?**
|
|
661
|
+
|
|
662
|
+
- NICOT does not create a snapshot of data.
|
|
663
|
+
- In a dynamic DB:
|
|
664
|
+
- Inserts/deletes/updates during pagination are reflected immediately;
|
|
665
|
+
- No guarantee of “no duplicates / no gaps”.
|
|
666
|
+
- Practically this is the same as many production APIs:
|
|
667
|
+
- Cursor is a best-effort “position marker”, not a strong consistency guarantee.
|
|
668
|
+
|
|
669
|
+
If you require strongly consistent pagination:
|
|
670
|
+
|
|
671
|
+
- Consider:
|
|
672
|
+
- Snapshots at the business-layer,
|
|
673
|
+
- Or embedding extra version/timestamp into your own cursor scheme and rejecting cross-version cursors on the server.
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## 11. PostgreSQL-specific features summary
|
|
678
|
+
|
|
679
|
+
NICOT has some features that are intentionally designed around **PostgreSQL**:
|
|
680
|
+
|
|
681
|
+
- `QueryFullText`:
|
|
682
|
+
- Uses PG full-text tools (`to_tsvector`, `websearch_to_tsquery`, etc.);
|
|
683
|
+
- Maintains GIN indexes & text search configurations.
|
|
684
|
+
- JSONB-related query decorators:
|
|
685
|
+
- For example, those which rely on `jsonb` operators like `?`.
|
|
686
|
+
|
|
687
|
+
On MySQL / SQLite / other DBs:
|
|
688
|
+
|
|
689
|
+
- These decorators may not work at all or degrade to a simpler behavior.
|
|
690
|
+
- It’s a good idea for your project docs to clearly state:
|
|
691
|
+
- **These features are PG-only**.
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## 12. Controllers, CrudService, and custom logic
|
|
696
|
+
|
|
697
|
+
### 12.1 Recommended pattern: Factory + CrudService
|
|
698
|
+
|
|
699
|
+
Typical usage:
|
|
700
|
+
|
|
701
|
+
1. Factory in its own file:
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
// post.factory.ts
|
|
705
|
+
export const PostFactory = new RestfulFactory(Post, {
|
|
706
|
+
relations: ['user', 'comments'],
|
|
707
|
+
skipNonQueryableFields: true,
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
2. Service derived from Factory:
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
// post.service.ts
|
|
715
|
+
export class PostService extends PostFactory.crudService() {}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
3. Controller using Factory decorators + CrudService:
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
// post.controller.ts
|
|
722
|
+
class FindAllPostDto extends PostFactory.findAllDto {}
|
|
723
|
+
|
|
724
|
+
@Controller('posts')
|
|
725
|
+
export class PostController {
|
|
726
|
+
constructor(private readonly service: PostService) {}
|
|
727
|
+
|
|
728
|
+
@PostFactory.findAll({ summary: 'List posts of current user' })
|
|
729
|
+
async findAll(
|
|
730
|
+
@PostFactory.findAllParam() dto: FindAllPostDto,
|
|
731
|
+
@PutUser() user: User, // business logic decorator, not from NICOT
|
|
732
|
+
) {
|
|
733
|
+
return this.service.findAll(dto, qb =>
|
|
734
|
+
qb.andWhere('post.userId = :uid', { uid: user.id }),
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
This ensures:
|
|
741
|
+
|
|
742
|
+
- DTOs are generated automatically (Query and Result);
|
|
743
|
+
- Hooks, access control decorators, and relation pruning all apply;
|
|
744
|
+
- Swagger and actual runtime behavior stay in sync.
|
|
745
|
+
|
|
746
|
+
### 12.2 Direct TypeORM usage notes
|
|
747
|
+
|
|
748
|
+
You can still inject and use a raw TypeORM `Repository<T>` for:
|
|
749
|
+
|
|
750
|
+
- Complex reports / analytics;
|
|
751
|
+
- Highly specialized queries / performance tuning.
|
|
752
|
+
|
|
753
|
+
But keep in mind:
|
|
754
|
+
|
|
755
|
+
- These calls will **not** run NICOT’s entity hooks automatically;
|
|
756
|
+
- They will **not** automatically honor `NotInResult` or other pruning rules;
|
|
757
|
+
- They will not use Factory’s relations whitelist or cursor pagination logic.
|
|
758
|
+
|
|
759
|
+
If you want these ad-hoc queries to be NICOT-aligned, you can:
|
|
760
|
+
|
|
761
|
+
- Post-process them with the same cleaning logic as CrudService (e.g. strip sensitive fields);
|
|
762
|
+
- Or explicitly separate them as internal endpoints, and keep NICOT’s CRUD as the public/supported API surface.
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
That’s the high-level NICOT API & behavior guide.
|
|
767
|
+
For conceptual overview and examples, see `README.md`.
|