create-restforge-skills 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -150
- package/package.json +33 -30
- package/skills/restforge/SKILL.md +832 -559
- package/skills/restforge/references/auth.md +2 -2
- package/skills/restforge/references/config-schema.md +238 -173
- package/skills/restforge/references/dbschema-catalog.md +245 -238
- package/skills/restforge/references/design-to-sdf.md +621 -618
- package/skills/restforge/references/field-validation.md +247 -173
- package/skills/restforge/references/rdf-advanced.md +695 -488
- package/skills/restforge/references/udf-catalog.md +623 -496
|
@@ -1,488 +1,695 @@
|
|
|
1
|
-
# Reference: RDF Advanced Features
|
|
2
|
-
|
|
3
|
-
> **Offline mirror.** This file mirrors the RDF catalog of the installed
|
|
4
|
-
> RESTForge platform (`restforge-handbook/catalogs/rdf/`)
|
|
5
|
-
>
|
|
6
|
-
> then update this file. For
|
|
7
|
-
> `references/field-validation.md`.
|
|
8
|
-
|
|
9
|
-
This reference covers advanced RDF payload features beyond standard CRUD fields.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
|
175
|
-
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
##
|
|
211
|
-
|
|
212
|
-
Adds
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
"
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
|
297
|
-
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
"
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
]
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
|
455
|
-
|
|
456
|
-
| `
|
|
457
|
-
| `
|
|
458
|
-
| `
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1
|
+
# Reference: RDF Advanced Features
|
|
2
|
+
|
|
3
|
+
> **Offline mirror.** This file mirrors the RDF catalog of the installed
|
|
4
|
+
> RESTForge platform (`restforge-handbook/catalogs/rdf/`) and the key shapes the
|
|
5
|
+
> runtime actually reads. The live platform is authoritative — when this file
|
|
6
|
+
> and the platform disagree, trust the platform, then update this file. For
|
|
7
|
+
> `fieldValidation` constraints, see `references/field-validation.md`.
|
|
8
|
+
|
|
9
|
+
This reference covers advanced RDF payload features beyond standard CRUD fields.
|
|
10
|
+
|
|
11
|
+
**Grounding tools for this file.** Reading this reference is not grounding — it
|
|
12
|
+
explains the shapes, the tools return what the *installed* platform accepts:
|
|
13
|
+
|
|
14
|
+
| Editing | Call first |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `fieldValidation` on master or detail columns | `codegen_get_field_validation_catalog` |
|
|
17
|
+
| `datatablesQuery`, `viewQuery`, `viewName`, `exportQuery`, `detailQuery`, and `file:` query references | `codegen_get_query_declarative_catalog` |
|
|
18
|
+
| Any SELECT / WITH statement before it is pasted into the payload or a `.sql` file | `codegen_validate_sql` (live EXPLAIN, executes no rows) |
|
|
19
|
+
|
|
20
|
+
Whatever the section, the finished payload goes through `codegen_validate_payload`
|
|
21
|
+
before `codegen_create_endpoint` — a processor payload through
|
|
22
|
+
`codegen_create_processor`.
|
|
23
|
+
|
|
24
|
+
**Start from a generated payload.** `codegen_generate_payload` writes `tableName`,
|
|
25
|
+
`primaryKey`, `fieldName`, `action`, `fieldValidation`, `uniqueConstraints`,
|
|
26
|
+
`dateTimeFields`, `deleteReferences`, `softDelete`, and (for a table with an
|
|
27
|
+
`is_active` column) `defaultScope`. Edit that file; do not write those keys by
|
|
28
|
+
hand. Later `codegen_generate_payload` / `codegen_sync_payload` runs keep the
|
|
29
|
+
customisations made to those keys (see SKILL.md § RDF Payload).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Table of Contents
|
|
34
|
+
|
|
35
|
+
1. [The `action` Block](#the-action-block)
|
|
36
|
+
2. [Data Source Resolution](#data-source-resolution)
|
|
37
|
+
3. [Query File Reference](#query-file-reference)
|
|
38
|
+
4. [Field Lookup](#field-lookup)
|
|
39
|
+
5. [Default Scope](#default-scope)
|
|
40
|
+
6. [Workflow (Change-Status)](#workflow-change-status)
|
|
41
|
+
7. [Master-Detail (Composite)](#master-detail-composite)
|
|
42
|
+
8. [Aggregate Config](#aggregate-config)
|
|
43
|
+
9. [Adjust Config](#adjust-config)
|
|
44
|
+
10. [Import Config](#import-config)
|
|
45
|
+
11. [Processor](#processor)
|
|
46
|
+
12. [Kafka Event Publishing](#kafka-event-publishing)
|
|
47
|
+
13. [Components (Lifecycle Hooks)](#components-lifecycle-hooks)
|
|
48
|
+
14. [Other RDF Blocks](#other-rdf-blocks)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## The `action` Block
|
|
53
|
+
|
|
54
|
+
`action` is required. Every key is a boolean flag; an unknown key produces a
|
|
55
|
+
warning, a non-boolean value is an error. A feature block alone does not create
|
|
56
|
+
its endpoint — the matching flag must be `true`.
|
|
57
|
+
|
|
58
|
+
| Flag | Endpoint(s) | Also needs |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `datatables` | `POST /datatables` | — |
|
|
61
|
+
| `create`, `update`, `delete` | `POST /create`, `/update`, `/delete` | — |
|
|
62
|
+
| `first`, `read`, `lookup` | `POST /first`, `/read`, `GET`/`POST /lookup` | — |
|
|
63
|
+
| `export` | `/export` (Excel) | — |
|
|
64
|
+
| `import` | `/import-upload`, `/import-preview`, `/import-commit`, `/import-status` | `importConfig` with `enabled: true` |
|
|
65
|
+
| `upload` | file upload routes | `uploadConfig` |
|
|
66
|
+
| `adjust` | `POST /adjust` | `adjustConfig` |
|
|
67
|
+
| `aggregate` | `POST /aggregate` | `aggregateConfig` only when JOINs are needed |
|
|
68
|
+
| `workflow` | `POST /change-status` | `workflow` block |
|
|
69
|
+
| `createComposite`, `updateComposite`, `readComposite` | `/create-composite`, `/update-composite`, `/read-composite` | `masterDetail` block |
|
|
70
|
+
| `restore` | `/restore` | `softDelete.enabled: true` (schema-derived) |
|
|
71
|
+
|
|
72
|
+
`/export` is registered for every CRUD module whether or not the flag is set;
|
|
73
|
+
the flag only shows up in `GET /info`. `/import-*` is registered only when
|
|
74
|
+
`importConfig.enabled` is `true`; set `action.import: true` as well so the RDF
|
|
75
|
+
and `/info` describe the endpoints that actually exist.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Data Source Resolution
|
|
80
|
+
|
|
81
|
+
RESTForge resolves data sources per endpoint using a priority chain. Define
|
|
82
|
+
only what is needed; the platform falls back automatically.
|
|
83
|
+
|
|
84
|
+
| Endpoint | Resolution order |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `/datatables` | `datatablesQuery` → `SELECT * FROM` (`viewName` or `tableName`) |
|
|
87
|
+
| `/read`, `/first`, `/lookup` | `viewName` → `viewQuery` → `tableName` |
|
|
88
|
+
| `/export` | `exportQuery` → `SELECT {fieldName} FROM {tableName}` |
|
|
89
|
+
| `/read-composite` (detail) | `masterDetail.detailConfig.detailQuery` → `SELECT * FROM {detailTable} WHERE {foreignKey} = ? ORDER BY {line_number or detail primaryKey}` |
|
|
90
|
+
|
|
91
|
+
**`viewName`** — reference a database VIEW:
|
|
92
|
+
```json
|
|
93
|
+
"viewName": "v_order_summary"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**`viewQuery`** — inline SQL (virtual view, no DB object created). It is wrapped
|
|
97
|
+
as a subquery, so JOINed columns can be filtered in WHERE:
|
|
98
|
+
```json
|
|
99
|
+
"viewQuery": "SELECT o.*, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**`datatablesQuery`** — the base SELECT for the paginated list. The runtime wraps
|
|
103
|
+
it with search, sort, and paging; do not add those clauses yourself:
|
|
104
|
+
```json
|
|
105
|
+
"datatablesQuery": "SELECT o.*, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**`datatablesWhere`** — the whitelist of columns the `/datatables` `searchBy`
|
|
109
|
+
parameter may target; add `"all"` to allow cross-column search. Entries use the
|
|
110
|
+
column name as it appears in the SELECT result, never with a table alias
|
|
111
|
+
(`a.supplier_code` is rejected). Every entry other than `all` must exist in the
|
|
112
|
+
columns `/datatables` returns; `codegen_create_endpoint` checks this. A
|
|
113
|
+
`searchBy` value outside the list is rejected with HTTP 400.
|
|
114
|
+
```json
|
|
115
|
+
"datatablesWhere": ["supplier_code", "supplier_name", "all"]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Write source is always `tableName` — `viewName`/`viewQuery` are read-only.
|
|
119
|
+
|
|
120
|
+
**JOIN columns in the list.** To show columns of referenced tables
|
|
121
|
+
(`supplier_name` next to `supplier_id`), prefer `codegen_sync_payload` with
|
|
122
|
+
`expandFk` over a hand-written JOIN: it writes `query/<table>-join.sql` and
|
|
123
|
+
points `datatablesQuery` (and, with `expandFk: "both"`, `viewQuery`) at it.
|
|
124
|
+
|
|
125
|
+
Ground every one of these keys with `codegen_get_query_declarative_catalog`
|
|
126
|
+
before writing them, and run the SQL through `codegen_validate_sql` first: a
|
|
127
|
+
JOIN or column typo here surfaces as a runtime 500 on `/datatables` or `/export`,
|
|
128
|
+
not as a payload validation error.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Query File Reference
|
|
133
|
+
|
|
134
|
+
SQL queries can be stored in external `.sql` files using the `file:` prefix.
|
|
135
|
+
The path is relative to the `payload/` folder.
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
"datatablesQuery": "file:query/orders-datatables.sql",
|
|
139
|
+
"exportQuery": "file:query/orders-export.sql"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Convention for folder structure:
|
|
143
|
+
```
|
|
144
|
+
payload/
|
|
145
|
+
├── order.json
|
|
146
|
+
└── query/
|
|
147
|
+
├── orders-datatables.sql
|
|
148
|
+
└── orders-export.sql
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`viewName` does not accept `file:` because it names a database object. For
|
|
152
|
+
master-detail, `detailQuery` lives under `masterDetail.detailConfig` and may use
|
|
153
|
+
`file:` too.
|
|
154
|
+
|
|
155
|
+
The `file:` form hides the SQL from a quick payload review, which makes
|
|
156
|
+
`codegen_validate_sql` on the file content worth more here than for inline
|
|
157
|
+
queries.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Field Lookup
|
|
162
|
+
|
|
163
|
+
`fieldNameLookup` (root level, optional) selects which columns `/lookup` returns
|
|
164
|
+
as `id` and `text`. There is no per-field lookup key in RDF; a dropdown on the
|
|
165
|
+
frontend is a UDF `dataSource` that calls this endpoint.
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"fieldNameLookup": {
|
|
169
|
+
"id": "category_id",
|
|
170
|
+
"text": "category_code||' - '||category_name as display_text"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Key | Notes |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `id` | Column returned as `id` |
|
|
177
|
+
| `text` | Column **or SQL expression** returned as `text`. PostgreSQL `||` concatenation is translated to `CONCAT()` for MySQL automatically |
|
|
178
|
+
|
|
179
|
+
- Both keys are required once the block exists.
|
|
180
|
+
- Without the block, the text column is auto-detected by name (`name`, `code`,
|
|
181
|
+
`title`, `text`, `label`, `tag`).
|
|
182
|
+
- With the block, `/lookup` search runs on the columns extracted from `text`.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Default Scope
|
|
187
|
+
|
|
188
|
+
Automatic WHERE filter injected on the `lookup` and `read` actions only.
|
|
189
|
+
`/datatables` and `/first` are not affected.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
"defaultScope": {
|
|
193
|
+
"lookup": { "is_active": true },
|
|
194
|
+
"read": { "is_active": true }
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- The keys are action names: only `lookup` (`GET` and `POST /lookup`) and `read`
|
|
199
|
+
(`POST /read`). Any other key produces a warning and is ignored.
|
|
200
|
+
- Each action maps `column: value`. The column must be in `fieldName`; the value
|
|
201
|
+
must be a boolean, string, or number literal. Values are not taken from the
|
|
202
|
+
request or the JWT.
|
|
203
|
+
- The filter is combined with the user-supplied WHERE via AND.
|
|
204
|
+
- For an `is_active` column, `codegen_generate_payload` writes the block
|
|
205
|
+
automatically and `codegen_sync_payload` keeps only the `is_active` key in
|
|
206
|
+
step with the table; custom keys (e.g. `show_in_store`) are left alone.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Workflow (Change-Status)
|
|
211
|
+
|
|
212
|
+
Adds a `POST /change-status` endpoint with state machine validation. Needs both
|
|
213
|
+
`action.workflow: true` and the `workflow` block.
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
"action": { "workflow": true },
|
|
217
|
+
"workflow": {
|
|
218
|
+
"statusField": "status",
|
|
219
|
+
"transitions": {
|
|
220
|
+
"draft": ["confirmed", "cancelled"],
|
|
221
|
+
"confirmed": ["closed", "cancelled"],
|
|
222
|
+
"closed": [],
|
|
223
|
+
"cancelled": []
|
|
224
|
+
},
|
|
225
|
+
"hooks": {
|
|
226
|
+
"confirmed": {
|
|
227
|
+
"onBefore": [],
|
|
228
|
+
"onAfter": [
|
|
229
|
+
{
|
|
230
|
+
"type": "api",
|
|
231
|
+
"method": "POST",
|
|
232
|
+
"url": "/stock-management/process-inbound",
|
|
233
|
+
"body": { "stock_inbound_id": "{{id}}", "status": "{{newStatus}}", "previous_status": "{{oldStatus}}" },
|
|
234
|
+
"blocking": true,
|
|
235
|
+
"timeout": 10000
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Property | Notes |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `statusField` | Column that holds the status (default `"status"`); must be in `fieldName` |
|
|
246
|
+
| `transitions` | Map `current status → [allowed target statuses]`. `null`/absent = any transition allowed. A target outside the list → HTTP 422 `Status transition not allowed` |
|
|
247
|
+
| `hooks.<target status>` | Keyed by the **target** status, not by an action name. Holds `onBefore[]` and/or `onAfter[]` |
|
|
248
|
+
| hook `type` | Only `"api"` |
|
|
249
|
+
| hook `method` | `POST` (default), `PUT`, or `PATCH` |
|
|
250
|
+
| hook `url` | Full URL, absolute path (`/api/...`), or short path (`/{endpoint}/{action}`) |
|
|
251
|
+
| hook `body` / `headers` | Template variables `{{id}}`, `{{newStatus}}`, `{{oldStatus}}`, `{{record.<field>}}` |
|
|
252
|
+
| hook `blocking` | `true` = a failed call rolls back the transaction and the request is answered HTTP 502; `false` (default) = fire-and-forget |
|
|
253
|
+
| hook `timeout` | Milliseconds, default `10000` |
|
|
254
|
+
|
|
255
|
+
The request body carries the primary key (or `id`) and the target `status`.
|
|
256
|
+
`onBefore` runs before the UPDATE, `onAfter` after it but before COMMIT. Local
|
|
257
|
+
JavaScript hooks for the same operation use the `onBeforeWorkflow` /
|
|
258
|
+
`onAfterWorkflow` component events (see Components). The frontend buttons are
|
|
259
|
+
UDF `workflowActions`, a separate file — never put `workflowActions` in the RDF.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Master-Detail (Composite)
|
|
264
|
+
|
|
265
|
+
Adds `/create-composite`, `/update-composite`, and `/read-composite`. Needs the
|
|
266
|
+
three composite flags in `action` and a `masterDetail` block. Generate the block
|
|
267
|
+
instead of writing it: `codegen_generate_payload` with `detail: "<detail table>"`
|
|
268
|
+
fills `masterDetail` and `detailConfig` (including `fieldValidation` and
|
|
269
|
+
`foreignKeys`) from the database and writes the detail query file.
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
"action": { "createComposite": true, "updateComposite": true, "readComposite": true },
|
|
273
|
+
"masterDetail": {
|
|
274
|
+
"enabled": true,
|
|
275
|
+
"detailTable": "stock_inbound_item",
|
|
276
|
+
"foreignKey": "stock_inbound_id",
|
|
277
|
+
"cascadeDelete": true,
|
|
278
|
+
"transactionMode": "required",
|
|
279
|
+
"detailConfig": {
|
|
280
|
+
"tableName": "stock_inbound_item",
|
|
281
|
+
"primaryKey": "stock_inbound_item_id",
|
|
282
|
+
"fieldName": ["stock_inbound_item_id", "stock_inbound_id", "line_number", "item_product_id", "qty_received", "unit_price", "total_amount"],
|
|
283
|
+
"detailQuery": "file:query/stock-inbound-detail.sql",
|
|
284
|
+
"requiredFields": ["line_number", "item_product_id", "qty_received", "unit_price"],
|
|
285
|
+
"autoCalculateFields": {
|
|
286
|
+
"total_amount": { "type": "calculated", "formula": "qty_received * unit_price" }
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
"headerCalculations": {
|
|
290
|
+
"total_items": { "type": "count", "source": "items.length" },
|
|
291
|
+
"total_amount": { "type": "sum", "source": "items.total_amount" }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Property | Notes |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `enabled` | `true` activates the feature |
|
|
299
|
+
| `detailTable` / `foreignKey` | Detail table and its FK column to the header |
|
|
300
|
+
| `cascadeDelete` | `true` = deleting the header first deletes its detail rows in the same transaction (all four dialects; no `ON DELETE CASCADE` needed) |
|
|
301
|
+
| `transactionMode` | Only `"required"` |
|
|
302
|
+
| `detailConfig.tableName`, `primaryKey`, `fieldName` | Required. Every listed column must exist in the detail table; `codegen_create_endpoint` checks this against the database |
|
|
303
|
+
| `detailConfig.detailQuery` | Optional SELECT for detail rows; inline or `file:` |
|
|
304
|
+
| `detailConfig.requiredFields` | Columns required on each detail insert |
|
|
305
|
+
| `detailConfig.autoCalculateFields` | Per-row values: `calculated` (formula `colA * colB`, computed by the app) or `generated` (DB GENERATED column, excluded from SQL) |
|
|
306
|
+
| `detailConfig.fieldValidation`, `foreignKeys` | Filled by generate; consumed by `codegen_migrate_payload` to build the UDF `details[]` |
|
|
307
|
+
| `headerCalculations` | Header columns computed from detail rows: `type` = `count`, `sum`, `avg`, `min`, `max`; `source` = `items.length` for count, `items.<detail column>` otherwise. The key must be a header column in root `fieldName` |
|
|
308
|
+
|
|
309
|
+
Fill `headerCalculations` and `calculated` formulas by hand after generating —
|
|
310
|
+
they are business decisions. `autoCalculateFields` goes **inside**
|
|
311
|
+
`detailConfig`; placing it next to `detailConfig` is rejected. The `_manualStub`
|
|
312
|
+
key written by generate is documentation only and can be removed.
|
|
313
|
+
|
|
314
|
+
At runtime a detail row field outside the detail table columns is rejected with
|
|
315
|
+
400 and the whole request is rolled back. `/update-composite` with only detail
|
|
316
|
+
operations is valid and recomputes `headerCalculations`.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Aggregate Config
|
|
321
|
+
|
|
322
|
+
`/aggregate` runs COUNT, SUM, AVG, MIN, and MAX over the resource. It needs
|
|
323
|
+
`action.aggregate: true`. The **request body** carries the operations; the RDF
|
|
324
|
+
only declares the JOINs a request may use, so clients cannot join arbitrary
|
|
325
|
+
tables.
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
"action": { "aggregate": true },
|
|
329
|
+
"aggregateConfig": {
|
|
330
|
+
"joins": {
|
|
331
|
+
"warehouse": {
|
|
332
|
+
"tableName": "warehouse",
|
|
333
|
+
"joinType": "LEFT",
|
|
334
|
+
"sourceField": "warehouse_id",
|
|
335
|
+
"targetField": "warehouse_id",
|
|
336
|
+
"fields": ["warehouse_code", "warehouse_name"]
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
| `joins.<name>` key | Notes |
|
|
343
|
+
|---|---|
|
|
344
|
+
| *(the key)* | Join name referenced by the request (`"joins": ["warehouse"]`) |
|
|
345
|
+
| `tableName` | Joined table |
|
|
346
|
+
| `joinType` | `INNER`, `LEFT`, or `RIGHT` (the runtime rejects `FULL`) |
|
|
347
|
+
| `sourceField` / `targetField` | Column on the main table / on the joined table |
|
|
348
|
+
| `fields` | Joined-table columns the request may use in `group_by` and `where` |
|
|
349
|
+
|
|
350
|
+
Request body:
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"joins": ["warehouse"],
|
|
355
|
+
"operations": [
|
|
356
|
+
{ "function": "sum", "field": "stock_qty", "alias": "total_stock" },
|
|
357
|
+
{ "function": "count", "field": "*", "alias": "total_items" }
|
|
358
|
+
],
|
|
359
|
+
"group_by": ["warehouse_name"],
|
|
360
|
+
"where": { "logic": "AND", "conditions": [{ "key": "is_active", "operator": "=", "value": true }] },
|
|
361
|
+
"having": [{ "function": "sum", "field": "stock_qty", "operator": ">", "value": 0 }]
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- Functions: `count`, `sum`, `avg`, `min`, `max` (case-insensitive). `field: "*"`
|
|
366
|
+
is allowed for `count` only.
|
|
367
|
+
- `where` uses the same structure as `/read`: `{ logic, conditions: [{ key,
|
|
368
|
+
operator, value }] }`, nested groups allowed; a key outside the readable and
|
|
369
|
+
join fields is rejected with 400. `having` operators: `=`, `<>`, `!=`, `>`,
|
|
370
|
+
`<`, `>=`, `<=`.
|
|
371
|
+
- Main-table fields must be readable fields of the resource; an alias is
|
|
372
|
+
alphanumeric with underscores.
|
|
373
|
+
- Without `operations` the endpoint returns `{ "count": N }`.
|
|
374
|
+
- Without `group_by` the response data is one object; with `group_by` it is an
|
|
375
|
+
array of rows.
|
|
376
|
+
|
|
377
|
+
For a chart or KPI built from several tables, a backend dashboard
|
|
378
|
+
(`codegen_create_dashboard`, SQL widgets) is usually the better fit than
|
|
379
|
+
`/aggregate` per resource.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Adjust Config
|
|
384
|
+
|
|
385
|
+
Adds `POST /adjust` for atomic increments/decrements on numeric columns
|
|
386
|
+
(stock, balance, counters). Needs `action.adjust: true` and `adjustConfig`.
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
"action": { "adjust": true },
|
|
390
|
+
"adjustConfig": {
|
|
391
|
+
"fields": {
|
|
392
|
+
"stock": { "type": "number", "min": 0, "allowNegativeResult": false }
|
|
393
|
+
},
|
|
394
|
+
"reasonRequired": true
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
| Property | Notes |
|
|
399
|
+
|---|---|
|
|
400
|
+
| `fields.<column>` | Column that may be adjusted. Must be a physical column of the table |
|
|
401
|
+
| `fields.<column>.type` | Must be `"number"` (default) |
|
|
402
|
+
| `fields.<column>.min` | Lower bound after the adjustment, default `0` |
|
|
403
|
+
| `fields.<column>.allowNegativeResult` | `false` = the UPDATE carries a guard `column + value >= min`; a violation is answered HTTP 409. Default `true` (no guard) |
|
|
404
|
+
| `reasonRequired` | `true` = the request must include a non-empty `reason` |
|
|
405
|
+
|
|
406
|
+
Request body:
|
|
407
|
+
|
|
408
|
+
```json
|
|
409
|
+
{
|
|
410
|
+
"product_id": "018f...",
|
|
411
|
+
"adjustments": [{ "field": "stock", "value": -5 }],
|
|
412
|
+
"reason": "Damaged goods"
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`value` is a non-zero number. A column not listed in `fields`, a missing primary
|
|
417
|
+
key, or an empty `adjustments` array is answered HTTP 400. The adjust call fires
|
|
418
|
+
the `onBeforeAdjust` / `onAfterAdjust` component events.
|
|
419
|
+
|
|
420
|
+
The guard is enforced at adjust time only. A bound that must hold for every
|
|
421
|
+
write path belongs in `fieldValidation` as well — check what the installed
|
|
422
|
+
platform offers with `codegen_get_field_validation_catalog`.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Import Config
|
|
427
|
+
|
|
428
|
+
Excel (.xlsx) import through four endpoints: `POST /import-upload`,
|
|
429
|
+
`POST /import-preview`, `POST /import-commit`, `GET /import-status`. The routes
|
|
430
|
+
exist only when `importConfig.enabled` is `true`; set `action.import: true` too.
|
|
431
|
+
|
|
432
|
+
```json
|
|
433
|
+
"action": { "import": true },
|
|
434
|
+
"importConfig": {
|
|
435
|
+
"enabled": true,
|
|
436
|
+
"upsertKeys": ["supplier_code"],
|
|
437
|
+
"upsertStrategy": "update_existing",
|
|
438
|
+
"requiredFields": ["supplier_code", "supplier_name"],
|
|
439
|
+
"maxFileSize": "10MB",
|
|
440
|
+
"allowedFormats": ["xlsx"],
|
|
441
|
+
"chunkSize": 100,
|
|
442
|
+
"lookupFields": {
|
|
443
|
+
"city_id": {
|
|
444
|
+
"targetField": "city_id",
|
|
445
|
+
"lookupTable": "city",
|
|
446
|
+
"lookupColumn": "city_name",
|
|
447
|
+
"lookupIdColumn": "city_id",
|
|
448
|
+
"required": true
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
| Property | Notes |
|
|
455
|
+
|---|---|
|
|
456
|
+
| `enabled` | Must be `true` to register the routes |
|
|
457
|
+
| `upsertKeys` | Columns that identify an existing row (default: the primary key) |
|
|
458
|
+
| `upsertStrategy` | `update_existing` (default), `insert_only`, or `skip_existing` |
|
|
459
|
+
| `requiredFields` | Columns that must be filled in every imported row |
|
|
460
|
+
| `maxFileSize` | Upload size limit, e.g. `"10MB"` |
|
|
461
|
+
| `allowedFormats` | Default `["xlsx"]` |
|
|
462
|
+
| `chunkSize` | Rows per INSERT/UPDATE batch, default `100` |
|
|
463
|
+
| `lookupFields.<name>` | Resolve a display value in the sheet to an ID: `targetField` (resource column), `lookupTable`, `lookupColumn` (value shown in Excel), `lookupIdColumn` (value stored), `required` (`true` = reject the row when no match) |
|
|
464
|
+
|
|
465
|
+
Column headers and formats follow `columnFormats` and the field labels of the
|
|
466
|
+
module, the same source `/export` uses, so an exported file can be re-imported.
|
|
467
|
+
`fieldValidation` applies to `/create` and `/update` only, not to import; use
|
|
468
|
+
`requiredFields` and `lookupFields.required` for import-time checks.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Processor
|
|
473
|
+
|
|
474
|
+
Alternative RDF structure for custom non-CRUD endpoints. A processor payload
|
|
475
|
+
does NOT have `tableName`, `fieldName`, or `action`. Each entry in `processor[]`
|
|
476
|
+
defines one endpoint. Generated with `npx restforge processor create`.
|
|
477
|
+
|
|
478
|
+
```json
|
|
479
|
+
{
|
|
480
|
+
"description": "Sales Order custom endpoints",
|
|
481
|
+
"processor": [
|
|
482
|
+
{
|
|
483
|
+
"name": "submit-order",
|
|
484
|
+
"method": "POST",
|
|
485
|
+
"description": "Submit a draft order to pending approval",
|
|
486
|
+
"sql": {
|
|
487
|
+
"query": "UPDATE sales.sales_order SET status = 'pending_approval' WHERE so_id = $1 AND status = 'draft'",
|
|
488
|
+
"params": ["so_id"]
|
|
489
|
+
},
|
|
490
|
+
"request": {
|
|
491
|
+
"body": {
|
|
492
|
+
"so_id": { "type": "uuid", "required": true },
|
|
493
|
+
"notes": { "type": "string", "required": false, "maxLength": 200 }
|
|
494
|
+
},
|
|
495
|
+
"headers": {
|
|
496
|
+
"X-App-Code": { "type": "string", "required": true, "mapTo": "app_code" }
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"response": {
|
|
500
|
+
"message": {
|
|
501
|
+
"success": "Sales order submitted for approval.",
|
|
502
|
+
"empty": "Sales order not found or not in draft status.",
|
|
503
|
+
"error": "Failed to submit sales order."
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
| Property | Required | Notes |
|
|
512
|
+
|---|---|---|
|
|
513
|
+
| `processor[].name` | Yes | Endpoint name — becomes file name and URL segment |
|
|
514
|
+
| `processor[].method` | Yes | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
515
|
+
| `processor[].sql.query` | Conditional | Inline SQL with `$1, $2, ...` placeholders. If `sql` block present, one of `query` or `file` is required |
|
|
516
|
+
| `processor[].sql.file` | Conditional | Path to external `.sql` file, relative to payload folder |
|
|
517
|
+
| `processor[].sql.params` | No | Field names bound to placeholders; resolved from body/params/query/header `mapTo`; falls back to `default` if input empty |
|
|
518
|
+
| `processor[].request.body` | No | Request body field schema |
|
|
519
|
+
| `processor[].request.params` | No | Route params; each key adds `/:key` to the path |
|
|
520
|
+
| `processor[].request.headers` | No | Header schema; use `mapTo` to rename into `input` |
|
|
521
|
+
| `processor[].request.validate` | No | Default `true`. Set `false` to opt-out router-level validation |
|
|
522
|
+
| `processor[].response.message` | No | `success`, `empty` (SQL mode only), `error` messages |
|
|
523
|
+
| `processor[].cache.enabled` | No | Default `false`. Response cache for GET processors |
|
|
524
|
+
| `processor[].cache.ttl` | No | Cache TTL in seconds; default `300` |
|
|
525
|
+
|
|
526
|
+
**Field schema properties** (apply to `request.body`, `request.params`, `request.headers`):
|
|
527
|
+
|
|
528
|
+
| Property | Notes |
|
|
529
|
+
|---|---|
|
|
530
|
+
| `type` | `string`, `number`, `integer`, `boolean`, `uuid`, `array`, `object`, `date`, `datetime` |
|
|
531
|
+
| `required` | Router rejects with HTTP 400 if absent |
|
|
532
|
+
| `format` | Regex whitelist: `email`, `url`, `phone-id`, `uuid` |
|
|
533
|
+
| `enum` | Whitelist of allowed values |
|
|
534
|
+
| `minLength` / `maxLength` | Length check for string fields |
|
|
535
|
+
| `sensitive` | `true` masks value as `***MASKED***` in router debug log |
|
|
536
|
+
| `default` | Fallback value for `sql.params` binding when input is empty |
|
|
537
|
+
| `mapTo` | (`headers` only) field name to use in `input` object |
|
|
538
|
+
|
|
539
|
+
**Generator behavior:**
|
|
540
|
+
- Router (`{endpoint}.js`) — always overwritten on re-run.
|
|
541
|
+
- Processor file (`processor/{endpoint}/{name}.js`) — skipped if already exists (safe to re-run).
|
|
542
|
+
Use `--force` to overwrite.
|
|
543
|
+
|
|
544
|
+
**Without sql block** — payload with only `name`, `method`, and `request` is valid.
|
|
545
|
+
Router registers the route with validation; processor file is generated as a manual
|
|
546
|
+
implementation scaffold. Business logic is written in the processor file.
|
|
547
|
+
|
|
548
|
+
`sql.query` is executed as written: run it through `codegen_validate_sql` first
|
|
549
|
+
when it is a SELECT, and keep in mind that `codegen_create_processor` takes the
|
|
550
|
+
payload as a bare file name (no path form), unlike the dashboard generators.
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Kafka Event Publishing
|
|
555
|
+
|
|
556
|
+
Publishes an event to one Kafka topic after insert, update, or delete. Requires
|
|
557
|
+
`KAFKA_ENABLED=true` in the backend config as well.
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
"kafka": {
|
|
561
|
+
"enabled": true,
|
|
562
|
+
"topic": "inventory.stock_inbound",
|
|
563
|
+
"keyField": "stock_inbound_id",
|
|
564
|
+
"publishOn": { "insert": true, "update": true, "delete": false }
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
| Property | Notes |
|
|
569
|
+
|---|---|
|
|
570
|
+
| `enabled` | `true` activates publishing for this resource |
|
|
571
|
+
| `topic` | Target topic name |
|
|
572
|
+
| `keyField` | Record field used as the message key (default: the primary key) |
|
|
573
|
+
| `publishOn.insert` / `update` / `delete` | Which operations publish |
|
|
574
|
+
|
|
575
|
+
There is one topic per resource and no per-action topic list. To consume events,
|
|
576
|
+
generate a consumer with `codegen_create_kafka_consumer`.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Components (Lifecycle Hooks)
|
|
581
|
+
|
|
582
|
+
`components` binds CRUD lifecycle events to functions in local JavaScript
|
|
583
|
+
handler files. Added to a standard CRUD payload (one that has `tableName`).
|
|
584
|
+
|
|
585
|
+
```json
|
|
586
|
+
{
|
|
587
|
+
"components": [
|
|
588
|
+
{
|
|
589
|
+
"properties": {
|
|
590
|
+
"filename": "components/supplier-hooks.js",
|
|
591
|
+
"methods": [
|
|
592
|
+
{
|
|
593
|
+
"name": "validateSupplierCode",
|
|
594
|
+
"events": "onBeforeInsert",
|
|
595
|
+
"params": [
|
|
596
|
+
{ "value": "{requestData}" },
|
|
597
|
+
{ "value": "{user_id}" }
|
|
598
|
+
]
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"name": "notifySlack",
|
|
602
|
+
"events": "onAfterInsert",
|
|
603
|
+
"params": [{ "value": "{newData}" }]
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
]
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
| Property | Required | Notes |
|
|
613
|
+
|---|---|---|
|
|
614
|
+
| `components[].properties.filename` | Yes | Handler path **relative to `src/`**: `components/supplier-hooks.js` loads `src/components/supplier-hooks.js`. `..`, absolute paths, and characters outside `[a-zA-Z0-9._/-]` are rejected |
|
|
615
|
+
| `components[].properties.methods` | Yes | List of method bindings |
|
|
616
|
+
| `methods[].name` | Yes | Function name exported from the handler file |
|
|
617
|
+
| `methods[].events` | Yes | Event hook (see table below) |
|
|
618
|
+
| `methods[].params` | Yes in practice | Template variables forwarded to the handler, in order. Always write the array (use `[]` for none): a binding without `params` fails when the event fires |
|
|
619
|
+
|
|
620
|
+
**Supported event hooks:**
|
|
621
|
+
|
|
622
|
+
| Event | Trigger |
|
|
623
|
+
|---|---|
|
|
624
|
+
| `onBeforeInsert`, `onAfterInsert` | `/create` endpoint |
|
|
625
|
+
| `onBeforeUpdate`, `onAfterUpdate` | `/update` endpoint |
|
|
626
|
+
| `onBeforeDelete`, `onAfterDelete` | `/delete` endpoint |
|
|
627
|
+
| `onBeforeCompositeInsert`, `onAfterCompositeInsert` | `/create-composite` endpoint |
|
|
628
|
+
| `onBeforeCompositeUpdate`, `onAfterCompositeUpdate` | `/update-composite` endpoint |
|
|
629
|
+
| `onBeforeAdjust`, `onAfterAdjust` | `/adjust` endpoint |
|
|
630
|
+
| `onBeforeWorkflow`, `onAfterWorkflow` | `/change-status` endpoint |
|
|
631
|
+
|
|
632
|
+
**Template variables for `params[].value`:**
|
|
633
|
+
|
|
634
|
+
| Variable | Value |
|
|
635
|
+
|---|---|
|
|
636
|
+
| `{tableName}` | Resource table name |
|
|
637
|
+
| `{requestData}` | Full request body |
|
|
638
|
+
| `{oldData}` | Data before operation (`update`, `delete`) |
|
|
639
|
+
| `{newData}` | Data after operation (`create`, `update`) |
|
|
640
|
+
| `{operation}` | Operation name: `insert` / `update` / `delete` |
|
|
641
|
+
| `{user_id}` | User ID from request context |
|
|
642
|
+
| `{timestamp}` | Execution timestamp |
|
|
643
|
+
| `{record_id}` | Primary key of the affected record |
|
|
644
|
+
|
|
645
|
+
A value that is exactly one variable passes the raw value (object, array);
|
|
646
|
+
a string mixing text and variables is interpolated.
|
|
647
|
+
|
|
648
|
+
**Handler file signature** (e.g. `src/components/supplier-hooks.js`):
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
async function validateSupplierCode(requestData, userId, services) {
|
|
652
|
+
const { db, logger, redis, kafka, cache } = services;
|
|
653
|
+
if (!/^[A-Z]{3}\d{3}$/.test(requestData.supplier_code)) {
|
|
654
|
+
return { success: false, message: 'Supplier code must look like ABC123' };
|
|
655
|
+
}
|
|
656
|
+
return { success: true };
|
|
657
|
+
}
|
|
658
|
+
module.exports = { validateSupplierCode };
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
- `services` is injected automatically as the last argument; no need to declare
|
|
662
|
+
it in `params[]`. It also carries `idempotency`, `idgen`, `storage`,
|
|
663
|
+
`websocket`, `createResponse`, `createError`, and `createValidationError`.
|
|
664
|
+
- All events are **blocking**, including the `onAfter*` ones. Returning
|
|
665
|
+
`{ success: false, message }` or throwing rolls back the whole transaction.
|
|
666
|
+
- A rejection is answered **HTTP 400** with the hook's own `message` only; the
|
|
667
|
+
function name and file path are written to the server log, not to the client.
|
|
668
|
+
- If `components` is absent from the payload, CRUD operates normally without hooks.
|
|
669
|
+
|
|
670
|
+
A lifecycle hook is not a substitute for declared validation: keep rule checks
|
|
671
|
+
that `fieldValidation` can express in the payload, grounded with
|
|
672
|
+
`codegen_get_field_validation_catalog`, and reserve `components` for logic the
|
|
673
|
+
catalog genuinely cannot express (external calls, cross-table effects,
|
|
674
|
+
notifications, status locks).
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Other RDF Blocks
|
|
679
|
+
|
|
680
|
+
These blocks are read by the payload validator or the generator. Most are
|
|
681
|
+
schema-derived and written by `codegen_generate_payload`; the handbook pages
|
|
682
|
+
under `restforge-handbook/catalogs/rdf/` hold the full rules.
|
|
683
|
+
|
|
684
|
+
| Block | Purpose | Written by |
|
|
685
|
+
|---|---|---|
|
|
686
|
+
| `dateTimeFields` | Per-column `type` (`date`, `timestamp`, `timestamptz`, `time`) so the runtime normalises input and formats output. `format` is allowed for `time` only; `date`/`timestamp` always follow `DATEFORMAT`/`DATETIMEFORMAT` | generate |
|
|
687
|
+
| `uniqueConstraints` | `{name, fields}` from the database; names the conflicting field in a 409 response | generate / sync |
|
|
688
|
+
| `deleteReferences` | Child tables that block a delete (`{table, column, references}`); used in the 409 response of `/delete` | generate |
|
|
689
|
+
| `softDelete` (+ `softDeleteFk*` metadata) | Soft-delete behaviour derived from the SDF. Only `visibility` (`active_only`, `deleted_only`, `include_deleted`) is meant to be edited | generate |
|
|
690
|
+
| `auditColumns` | `false`/`null` disables, object renames the four audit columns | manual override |
|
|
691
|
+
| `concurrency` | Optimistic concurrency for `update`/`update-composite`: `{versionColumn, compare}`; `compare` = `version` (integer column, default) or `timestamp` (PostgreSQL only). Pairs with UDF `versionField` | manual |
|
|
692
|
+
| `fieldPolicy` | Per-column `strategies`: `lock` (SELECT ... FOR UPDATE) and/or `audit` (writes `<table>_audit`); `"*": {"strategies": ["audit"]}` audits every column. Replaces `fieldProtection` | manual |
|
|
693
|
+
| `uploadConfig` | File fields (JSON columns) with `maxFiles`, `maxFileSize`, `allowedTypes`, `allowedMimeTypes`, `storagePrefix`; needs `action.upload: true` | manual |
|
|
694
|
+
| `authGuard` | `{enabled, appCode, publicPaths}`: JWT verification and per-endpoint permission; generates `src/plugins/<project>-auth-guard.js` | manual |
|
|
695
|
+
| `columnFormats` | Excel column formats shared by `/export` and import | manual |
|