create-restforge-skills 0.1.1 → 0.3.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 +158 -150
- package/package.json +1 -1
- package/skills/restforge/SKILL.md +740 -512
- package/skills/restforge/references/auth.md +10 -7
- package/skills/restforge/references/rdf-advanced.md +538 -488
- package/skills/restforge/references/udf-catalog.md +14 -6
|
@@ -1,488 +1,538 @@
|
|
|
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/`). The live platform is
|
|
5
|
-
> authoritative — when this file and the platform disagree, trust the platform,
|
|
6
|
-
> then update this file. For `fieldValidation` constraints, see
|
|
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
|
-
```json
|
|
68
|
-
"datatablesQuery": "
|
|
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
|
-
|
|
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/`). The live platform is
|
|
5
|
+
> authoritative — when this file and the platform disagree, trust the platform,
|
|
6
|
+
> then update this file. For `fieldValidation` constraints, see
|
|
7
|
+
> `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 fields (all sections below that show a `fields[]` entry) | `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
|
+
---
|
|
25
|
+
|
|
26
|
+
## Table of Contents
|
|
27
|
+
|
|
28
|
+
1. [Data Source Resolution](#data-source-resolution)
|
|
29
|
+
2. [Query File Reference](#query-file-reference)
|
|
30
|
+
3. [Field Lookup](#field-lookup)
|
|
31
|
+
4. [Default Scope](#default-scope)
|
|
32
|
+
5. [Workflow (Change-Status)](#workflow-change-status)
|
|
33
|
+
6. [Master-Detail (Composite)](#master-detail-composite)
|
|
34
|
+
7. [Aggregate Config](#aggregate-config)
|
|
35
|
+
8. [Adjust Config](#adjust-config)
|
|
36
|
+
9. [Import Config](#import-config)
|
|
37
|
+
10. [Processor](#processor)
|
|
38
|
+
11. [Kafka Event Publishing](#kafka-event-publishing)
|
|
39
|
+
12. [Components (Lifecycle Hooks)](#components-lifecycle-hooks)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Data Source Resolution
|
|
44
|
+
|
|
45
|
+
RESTForge resolves data sources per endpoint using a priority chain. Define
|
|
46
|
+
only what is needed; the platform falls back automatically.
|
|
47
|
+
|
|
48
|
+
| Endpoint | Resolution order |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `/datatables` | `datatablesQuery` → `SELECT * FROM tableName` |
|
|
51
|
+
| `/read`, `/first`, `/lookup` | `viewName` → `viewQuery` → `tableName` |
|
|
52
|
+
| `/export` | `exportQuery` → `SELECT {fields} FROM tableName` |
|
|
53
|
+
| `/read-composite` (detail) | `detailQuery` → detail `tableName` |
|
|
54
|
+
|
|
55
|
+
**`viewName`** — reference a database VIEW:
|
|
56
|
+
```json
|
|
57
|
+
"viewName": "v_order_summary"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**`viewQuery`** — inline SQL (virtual view, no DB object created):
|
|
61
|
+
```json
|
|
62
|
+
"viewQuery": "SELECT o.*, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**`datatablesQuery`** — SQL for the paginated table with `:search`, `:sort`,
|
|
66
|
+
`:limit`, `:offset` placeholders:
|
|
67
|
+
```json
|
|
68
|
+
"datatablesQuery": "SELECT o.*, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE 1=1"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Write source is always `tableName` — `viewName`/`viewQuery` are read-only.
|
|
72
|
+
|
|
73
|
+
Ground every one of these keys with `codegen_get_query_declarative_catalog`
|
|
74
|
+
before writing them, and run the SQL through `codegen_validate_sql` first: a
|
|
75
|
+
JOIN or column typo here surfaces as a runtime 500 on `/datatables` or `/export`,
|
|
76
|
+
not as a payload validation error.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Query File Reference
|
|
81
|
+
|
|
82
|
+
SQL queries can be stored in external `.sql` files using the `file:` prefix.
|
|
83
|
+
Path is relative to the payload file location.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
"datatablesQuery": "file:sql/orders-datatables.sql",
|
|
87
|
+
"exportQuery": "file:sql/orders-export.sql"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Convention for folder structure:
|
|
91
|
+
```
|
|
92
|
+
payload/
|
|
93
|
+
├── order.json
|
|
94
|
+
└── sql/
|
|
95
|
+
├── orders-datatables.sql
|
|
96
|
+
└── orders-export.sql
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
External SQL files support the same placeholders as inline queries.
|
|
100
|
+
For master-detail, each detail query is in a separate file.
|
|
101
|
+
|
|
102
|
+
The `file:` form hides the SQL from a quick payload review, which makes
|
|
103
|
+
`codegen_validate_sql` on the file content worth more here than for inline
|
|
104
|
+
queries. The placeholder rules themselves come from
|
|
105
|
+
`codegen_get_query_declarative_catalog`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Field Lookup
|
|
110
|
+
|
|
111
|
+
Configures dropdown/autocomplete data for a field. Used for foreign key fields
|
|
112
|
+
that need a human-readable label.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"fieldName": "category_id",
|
|
117
|
+
"type": "string",
|
|
118
|
+
"fieldLookup": {
|
|
119
|
+
"apiPath": "/category",
|
|
120
|
+
"id": "category_id",
|
|
121
|
+
"text": "category_name"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- `apiPath` — backend resource that provides lookup options via `/lookup` endpoint.
|
|
127
|
+
- `id` — field returned as the stored value.
|
|
128
|
+
- `text` — field returned as the display label.
|
|
129
|
+
|
|
130
|
+
`fieldLookup` sits on a field that also carries `fieldValidation`; ground those
|
|
131
|
+
constraints with `codegen_get_field_validation_catalog` rather than reusing the
|
|
132
|
+
keys shown in the examples here.
|
|
133
|
+
|
|
134
|
+
Static lookup (no API call):
|
|
135
|
+
```json
|
|
136
|
+
"fieldLookup": {
|
|
137
|
+
"type": "static",
|
|
138
|
+
"options": [
|
|
139
|
+
{ "id": "A", "text": "Option A" },
|
|
140
|
+
{ "id": "B", "text": "Option B" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Default Scope
|
|
148
|
+
|
|
149
|
+
Automatic WHERE clause injected on `/lookup` and `/read`-family endpoints.
|
|
150
|
+
Used for tenant isolation, user-scoped data, or active record filtering.
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
"defaultScope": {
|
|
154
|
+
"actions": ["lookup", "read", "datatables"],
|
|
155
|
+
"conditions": [
|
|
156
|
+
{ "key": "is_active", "value": true },
|
|
157
|
+
{ "key": "company_id", "value": ":companyId" }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `actions[]` — which endpoints apply the scope.
|
|
163
|
+
- Conditions with `:paramName` resolve from the request context (e.g., JWT claims).
|
|
164
|
+
- Combines with user-supplied WHERE via AND.
|
|
165
|
+
- The `is_active` column is auto-synced by the processor's `.active()` method.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Workflow (Change-Status)
|
|
170
|
+
|
|
171
|
+
Adds a `/change-status` endpoint with state machine validation.
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
"workflow": {
|
|
175
|
+
"statusField": "status",
|
|
176
|
+
"transitions": [
|
|
177
|
+
{
|
|
178
|
+
"from": "draft",
|
|
179
|
+
"to": "submitted",
|
|
180
|
+
"action": "submit",
|
|
181
|
+
"onBefore": "http://internal-service/validate",
|
|
182
|
+
"onAfter": "http://notification-service/notify"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"from": "submitted",
|
|
186
|
+
"to": "approved",
|
|
187
|
+
"action": "approve"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"from": ["submitted", "approved"],
|
|
191
|
+
"to": "rejected",
|
|
192
|
+
"action": "reject"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Property | Notes |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `statusField` | Field name that holds the current status |
|
|
201
|
+
| `transitions[].from` | Current status (string or array of strings) |
|
|
202
|
+
| `transitions[].to` | Target status after transition |
|
|
203
|
+
| `transitions[].action` | Action identifier in the request body |
|
|
204
|
+
| `transitions[].onBefore` | HTTP call before transition; 4xx/5xx blocks the transition (HTTP 422/502) |
|
|
205
|
+
| `transitions[].onAfter` | HTTP call after transition; failure does not roll back |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Master-Detail (Composite)
|
|
210
|
+
|
|
211
|
+
Adds `/create-composite`, `/update-composite`, and `/read-composite` endpoints.
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
"details": [
|
|
215
|
+
{
|
|
216
|
+
"tableName": "order_item",
|
|
217
|
+
"foreignKey": "order_id",
|
|
218
|
+
"primaryKey": "item_id",
|
|
219
|
+
"fields": [
|
|
220
|
+
{ "fieldName": "product_id", "type": "string" },
|
|
221
|
+
{ "fieldName": "qty", "type": "integer" },
|
|
222
|
+
{ "fieldName": "price", "type": "decimal" }
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- Multiple entries in `details[]` generate multiple detail tabs.
|
|
229
|
+
- `foreignKey` links detail rows to the master record.
|
|
230
|
+
- Detail `fields[]` follow the same validation rules as master fields.
|
|
231
|
+
- `/update-composite` supports three detail operations in one call:
|
|
232
|
+
`insert` (new rows), `update` (changed rows), `delete` (removed rows).
|
|
233
|
+
- `/read-composite` returns the master record with all detail arrays nested.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Aggregate Config
|
|
238
|
+
|
|
239
|
+
Adds an `/aggregate` endpoint for COUNT, SUM, AVG, MIN, MAX operations.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
"aggregateConfig": {
|
|
243
|
+
"joins": [
|
|
244
|
+
{
|
|
245
|
+
"type": "LEFT",
|
|
246
|
+
"table": "category",
|
|
247
|
+
"on": "product.category_id = category.category_id"
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"groupBy": ["category_name"],
|
|
251
|
+
"operations": [
|
|
252
|
+
{ "function": "COUNT", "field": "product_id", "alias": "total_products" },
|
|
253
|
+
{ "function": "SUM", "field": "stock_qty", "alias": "total_stock" },
|
|
254
|
+
{ "function": "AVG", "field": "price", "alias": "avg_price" }
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Operation | Description |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `COUNT` | Count rows or non-null values |
|
|
262
|
+
| `SUM` | Sum numeric field |
|
|
263
|
+
| `AVG` | Average numeric field |
|
|
264
|
+
| `MIN` | Minimum value |
|
|
265
|
+
| `MAX` | Maximum value |
|
|
266
|
+
|
|
267
|
+
The client sends `groupBy[]` and `having[]` in the request to filter results.
|
|
268
|
+
|
|
269
|
+
`joins[].on` is raw SQL: verify the join expression with `codegen_validate_sql`
|
|
270
|
+
(wrapped in a SELECT against the same tables) before committing it to the
|
|
271
|
+
payload.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Adjust Config
|
|
276
|
+
|
|
277
|
+
Adds an `/adjust` endpoint for atomic numeric field increments/decrements.
|
|
278
|
+
Prevents race conditions on stock, balance, and counter fields.
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
"adjustConfig": {
|
|
282
|
+
"fields": ["stock_qty", "reserved_qty"],
|
|
283
|
+
"guards": [
|
|
284
|
+
{
|
|
285
|
+
"field": "stock_qty",
|
|
286
|
+
"operator": "gte",
|
|
287
|
+
"value": 0,
|
|
288
|
+
"message": "Stock cannot be negative"
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- `fields[]` — fields that can be adjusted; must be numeric type.
|
|
295
|
+
- `guards[]` — pre-condition checks; request is rejected (HTTP 422) if any
|
|
296
|
+
guard fails after applying the adjustment.
|
|
297
|
+
- The client sends `{ "field": "stock_qty", "amount": -5 }` in the request.
|
|
298
|
+
- Adjustment is executed as an atomic SQL UPDATE with WHERE guard.
|
|
299
|
+
|
|
300
|
+
Guards are enforced at adjust time only. A numeric bound that must hold for every
|
|
301
|
+
write path belongs in `fieldValidation` as well — check what the installed
|
|
302
|
+
platform offers with `codegen_get_field_validation_catalog`.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Import Config
|
|
307
|
+
|
|
308
|
+
Adds `/import-preview` and `/import-commit` endpoints for Excel (.xlsx) imports.
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
"importConfig": {
|
|
312
|
+
"sheet": 0,
|
|
313
|
+
"startRow": 2,
|
|
314
|
+
"strategy": "upsert",
|
|
315
|
+
"upsertKey": ["sku"],
|
|
316
|
+
"columns": [
|
|
317
|
+
{ "header": "SKU", "fieldName": "sku" },
|
|
318
|
+
{ "header": "Product Name", "fieldName": "product_name" },
|
|
319
|
+
{
|
|
320
|
+
"header": "Category",
|
|
321
|
+
"fieldName": "category_id",
|
|
322
|
+
"lookup": { "apiPath": "/category", "matchField": "category_name", "returnField": "category_id" }
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Property | Notes |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `sheet` | Sheet index (0-based) or sheet name |
|
|
331
|
+
| `startRow` | First data row (1-based); default: `2` (row 1 = header) |
|
|
332
|
+
| `strategy` | `"insert"` (fail on duplicate) or `"upsert"` (update on match) |
|
|
333
|
+
| `upsertKey[]` | Fields used to identify existing records for upsert |
|
|
334
|
+
| `columns[].header` | Excel column header text |
|
|
335
|
+
| `columns[].fieldName` | RDF field to map to |
|
|
336
|
+
| `columns[].lookup` | Resolve a display value to an ID before insert |
|
|
337
|
+
|
|
338
|
+
Import is a two-step process: `/import-preview` validates and returns a diff;
|
|
339
|
+
`/import-commit` applies changes. The client uploads the Excel file to `/import-preview`
|
|
340
|
+
with a `POST multipart/form-data` request.
|
|
341
|
+
|
|
342
|
+
Imported rows are checked against the `fieldValidation` of the target fields, so
|
|
343
|
+
ground those constraints with `codegen_get_field_validation_catalog` before
|
|
344
|
+
mapping columns — a mapping that satisfies the header names but violates a
|
|
345
|
+
constraint fails at preview time, per row.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Processor
|
|
350
|
+
|
|
351
|
+
Alternative RDF structure for custom non-CRUD endpoints. A processor payload
|
|
352
|
+
does NOT have `tableName`, `fieldName`, or `action`. Each entry in `processor[]`
|
|
353
|
+
defines one endpoint. Generated with `npx restforge processor create`.
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"description": "Sales Order custom endpoints",
|
|
358
|
+
"processor": [
|
|
359
|
+
{
|
|
360
|
+
"name": "submit-order",
|
|
361
|
+
"method": "POST",
|
|
362
|
+
"description": "Submit a draft order to pending approval",
|
|
363
|
+
"sql": {
|
|
364
|
+
"query": "UPDATE sales.sales_order SET status = 'pending_approval' WHERE so_id = $1 AND status = 'draft'",
|
|
365
|
+
"params": ["so_id"]
|
|
366
|
+
},
|
|
367
|
+
"request": {
|
|
368
|
+
"body": {
|
|
369
|
+
"so_id": { "type": "uuid", "required": true },
|
|
370
|
+
"notes": { "type": "string", "required": false, "maxLength": 200 }
|
|
371
|
+
},
|
|
372
|
+
"headers": {
|
|
373
|
+
"X-App-Code": { "type": "string", "required": true, "mapTo": "app_code" }
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
"response": {
|
|
377
|
+
"message": {
|
|
378
|
+
"success": "Sales order submitted for approval.",
|
|
379
|
+
"empty": "Sales order not found or not in draft status.",
|
|
380
|
+
"error": "Failed to submit sales order."
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
| Property | Required | Notes |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `processor[].name` | Yes | Endpoint name — becomes file name and URL segment |
|
|
391
|
+
| `processor[].method` | Yes | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
392
|
+
| `processor[].sql.query` | Conditional | Inline SQL with `$1, $2, ...` placeholders. If `sql` block present, one of `query` or `file` is required |
|
|
393
|
+
| `processor[].sql.file` | Conditional | Path to external `.sql` file, relative to payload folder |
|
|
394
|
+
| `processor[].sql.params` | No | Field names bound to placeholders; resolved from body/params/query/header `mapTo`; falls back to `default` if input empty |
|
|
395
|
+
| `processor[].request.body` | No | Request body field schema |
|
|
396
|
+
| `processor[].request.params` | No | Route params; each key adds `/:key` to the path |
|
|
397
|
+
| `processor[].request.headers` | No | Header schema; use `mapTo` to rename into `input` |
|
|
398
|
+
| `processor[].request.validate` | No | Default `true`. Set `false` to opt-out router-level validation |
|
|
399
|
+
| `processor[].response.message` | No | `success`, `empty` (SQL mode only), `error` messages |
|
|
400
|
+
| `processor[].cache.enabled` | No | Default `false`. Response cache for GET processors |
|
|
401
|
+
| `processor[].cache.ttl` | No | Cache TTL in seconds; default `300` |
|
|
402
|
+
|
|
403
|
+
**Field schema properties** (apply to `request.body`, `request.params`, `request.headers`):
|
|
404
|
+
|
|
405
|
+
| Property | Notes |
|
|
406
|
+
|---|---|
|
|
407
|
+
| `type` | `string`, `number`, `integer`, `boolean`, `uuid`, `array`, `object`, `date`, `datetime` |
|
|
408
|
+
| `required` | Router rejects with HTTP 400 if absent |
|
|
409
|
+
| `format` | Regex whitelist: `email`, `url`, `phone-id`, `uuid` |
|
|
410
|
+
| `enum` | Whitelist of allowed values |
|
|
411
|
+
| `minLength` / `maxLength` | Length check for string fields |
|
|
412
|
+
| `sensitive` | `true` masks value as `***MASKED***` in router debug log |
|
|
413
|
+
| `default` | Fallback value for `sql.params` binding when input is empty |
|
|
414
|
+
| `mapTo` | (`headers` only) field name to use in `input` object |
|
|
415
|
+
|
|
416
|
+
**Generator behavior:**
|
|
417
|
+
- Router (`{endpoint}.js`) — always overwritten on re-run.
|
|
418
|
+
- Processor file (`processor/{endpoint}/{name}.js`) — skipped if already exists (safe to re-run).
|
|
419
|
+
Use `--force` to overwrite.
|
|
420
|
+
|
|
421
|
+
**Without sql block** — payload with only `name`, `method`, and `request` is valid.
|
|
422
|
+
Router registers the route with validation; processor file is generated as a manual
|
|
423
|
+
implementation scaffold. Business logic is written in the processor file.
|
|
424
|
+
|
|
425
|
+
`sql.query` is executed as written: run it through `codegen_validate_sql` first
|
|
426
|
+
when it is a SELECT, and keep in mind that `codegen_create_processor` takes the
|
|
427
|
+
payload as a bare file name (no path form), unlike the dashboard generators.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Kafka Event Publishing
|
|
432
|
+
|
|
433
|
+
Publishes events to a Kafka topic after CRUD operations. Requires
|
|
434
|
+
`KAFKA_ENABLED=true` in backend config.
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
"kafka": {
|
|
438
|
+
"events": [
|
|
439
|
+
{ "action": "create", "topic": "order.created.events" },
|
|
440
|
+
{ "action": "update", "topic": "order.updated.events" },
|
|
441
|
+
{ "action": "delete", "topic": "order.deleted.events" }
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- `action` — CRUD action that triggers the event: `create`, `update`, `delete`,
|
|
447
|
+
`change-status`, `create-composite`, `update-composite`.
|
|
448
|
+
- `topic` — Kafka topic name. Supports `{module}` and `{endpoint}` placeholders:
|
|
449
|
+
`"{module}.{endpoint}.events"`.
|
|
450
|
+
- Event payload contains the full record after the operation.
|
|
451
|
+
- Publishing is async and does not block the API response.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Components (Lifecycle Hooks)
|
|
456
|
+
|
|
457
|
+
`components` configures CRUD lifecycle hooks that execute local JavaScript handler
|
|
458
|
+
files. Added to a standard CRUD payload (one that has `tableName`).
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"components": [
|
|
463
|
+
{
|
|
464
|
+
"properties": {
|
|
465
|
+
"filename": "components/supplier-hooks.js",
|
|
466
|
+
"methods": [
|
|
467
|
+
{
|
|
468
|
+
"name": "validateSupplierCode",
|
|
469
|
+
"events": "onBeforeInsert",
|
|
470
|
+
"params": [
|
|
471
|
+
{ "value": "{requestData}" },
|
|
472
|
+
{ "value": "{user_id}" }
|
|
473
|
+
]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"name": "notifySlack",
|
|
477
|
+
"events": "onAfterInsert"
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
| Property | Required | Notes |
|
|
487
|
+
|---|---|---|
|
|
488
|
+
| `components[].properties.filename` | Yes | Path to handler file, relative to project root |
|
|
489
|
+
| `components[].properties.methods` | Yes | List of method bindings |
|
|
490
|
+
| `methods[].name` | Yes | Function name exported from the handler file |
|
|
491
|
+
| `methods[].events` | Yes | Event hook (see table below) |
|
|
492
|
+
| `methods[].params` | No | Template variables forwarded to the handler |
|
|
493
|
+
|
|
494
|
+
**Supported event hooks:**
|
|
495
|
+
|
|
496
|
+
| Event | Trigger |
|
|
497
|
+
|---|---|
|
|
498
|
+
| `onBeforeInsert`, `onAfterInsert` | `/create` endpoint |
|
|
499
|
+
| `onBeforeUpdate`, `onAfterUpdate` | `/update` endpoint |
|
|
500
|
+
| `onBeforeDelete`, `onAfterDelete` | `/delete` endpoint |
|
|
501
|
+
| `onBeforeCompositeInsert`, `onAfterCompositeInsert` | `/create-composite` endpoint |
|
|
502
|
+
| `onBeforeCompositeUpdate`, `onAfterCompositeUpdate` | `/update-composite` endpoint |
|
|
503
|
+
|
|
504
|
+
**Template variables for `params[].value`:**
|
|
505
|
+
|
|
506
|
+
| Variable | Value |
|
|
507
|
+
|---|---|
|
|
508
|
+
| `{tableName}` | Resource table name |
|
|
509
|
+
| `{requestData}` | Full request body |
|
|
510
|
+
| `{oldData}` | Data before operation (`update`, `delete`) |
|
|
511
|
+
| `{newData}` | Data after operation (`create`, `update`) |
|
|
512
|
+
| `{operation}` | Operation name: `insert` / `update` / `delete` |
|
|
513
|
+
| `{user_id}` | User ID from request context |
|
|
514
|
+
| `{timestamp}` | Execution timestamp |
|
|
515
|
+
| `{record_id}` | Primary key of the affected record |
|
|
516
|
+
|
|
517
|
+
**Handler file signature** (`src/components/handlers/`):
|
|
518
|
+
|
|
519
|
+
```javascript
|
|
520
|
+
async function handlerName(/* resolved params... */, services) {
|
|
521
|
+
const { db, logger, redis, kafka, cache } = services;
|
|
522
|
+
// business logic
|
|
523
|
+
return { success: true, message: '...' };
|
|
524
|
+
}
|
|
525
|
+
module.exports = { handlerName };
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
A lifecycle hook is not a substitute for declared validation: keep rule checks
|
|
529
|
+
that `fieldValidation` can express in the payload, grounded with
|
|
530
|
+
`codegen_get_field_validation_catalog`, and reserve `components` for logic the
|
|
531
|
+
catalog genuinely cannot express (external calls, cross-table effects,
|
|
532
|
+
notifications).
|
|
533
|
+
|
|
534
|
+
- `services` is injected automatically as the last argument; no need to declare it
|
|
535
|
+
in `params[]`.
|
|
536
|
+
- All events are **blocking** — `return { success: false }` or throwing an exception
|
|
537
|
+
rolls back the entire transaction.
|
|
538
|
+
- If `components` is absent from the payload, CRUD operates normally without hooks.
|