create-restforge-skills 0.3.0 → 1.0.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 +91 -54
- package/cli/codex.js +42 -0
- package/cli/index.js +23 -8
- package/package.json +38 -30
- package/skills/restforge/SKILL.md +148 -54
- package/skills/restforge/agents/openai.yaml +4 -0
- 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 +368 -211
- package/skills/restforge/references/udf-catalog.md +623 -504
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Reference: RDF Advanced Features
|
|
2
2
|
|
|
3
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`.
|
|
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
8
|
|
|
9
9
|
This reference covers advanced RDF payload features beyond standard CRUD fields.
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ explains the shapes, the tools return what the *installed* platform accepts:
|
|
|
13
13
|
|
|
14
14
|
| Editing | Call first |
|
|
15
15
|
|---|---|
|
|
16
|
-
| `fieldValidation` on master or detail
|
|
16
|
+
| `fieldValidation` on master or detail columns | `codegen_get_field_validation_catalog` |
|
|
17
17
|
| `datatablesQuery`, `viewQuery`, `viewName`, `exportQuery`, `detailQuery`, and `file:` query references | `codegen_get_query_declarative_catalog` |
|
|
18
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
19
|
|
|
@@ -21,22 +21,58 @@ Whatever the section, the finished payload goes through `codegen_validate_payloa
|
|
|
21
21
|
before `codegen_create_endpoint` — a processor payload through
|
|
22
22
|
`codegen_create_processor`.
|
|
23
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
|
+
|
|
24
31
|
---
|
|
25
32
|
|
|
26
33
|
## Table of Contents
|
|
27
34
|
|
|
28
|
-
1. [
|
|
29
|
-
2. [
|
|
30
|
-
3. [
|
|
31
|
-
4. [
|
|
32
|
-
5. [
|
|
33
|
-
6. [
|
|
34
|
-
7. [
|
|
35
|
-
8. [
|
|
36
|
-
9. [
|
|
37
|
-
10. [
|
|
38
|
-
11. [
|
|
39
|
-
12. [
|
|
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.
|
|
40
76
|
|
|
41
77
|
---
|
|
42
78
|
|
|
@@ -47,29 +83,45 @@ only what is needed; the platform falls back automatically.
|
|
|
47
83
|
|
|
48
84
|
| Endpoint | Resolution order |
|
|
49
85
|
|---|---|
|
|
50
|
-
| `/datatables` | `datatablesQuery` → `SELECT * FROM tableName` |
|
|
86
|
+
| `/datatables` | `datatablesQuery` → `SELECT * FROM` (`viewName` or `tableName`) |
|
|
51
87
|
| `/read`, `/first`, `/lookup` | `viewName` → `viewQuery` → `tableName` |
|
|
52
|
-
| `/export` | `exportQuery` → `SELECT {
|
|
53
|
-
| `/read-composite` (detail) | `detailQuery` → detail `
|
|
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}` |
|
|
54
90
|
|
|
55
91
|
**`viewName`** — reference a database VIEW:
|
|
56
92
|
```json
|
|
57
93
|
"viewName": "v_order_summary"
|
|
58
94
|
```
|
|
59
95
|
|
|
60
|
-
**`viewQuery`** — inline SQL (virtual view, no DB object created)
|
|
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:
|
|
61
98
|
```json
|
|
62
99
|
"viewQuery": "SELECT o.*, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id"
|
|
63
100
|
```
|
|
64
101
|
|
|
65
|
-
**`datatablesQuery`** —
|
|
66
|
-
|
|
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.
|
|
67
114
|
```json
|
|
68
|
-
"
|
|
115
|
+
"datatablesWhere": ["supplier_code", "supplier_name", "all"]
|
|
69
116
|
```
|
|
70
117
|
|
|
71
118
|
Write source is always `tableName` — `viewName`/`viewQuery` are read-only.
|
|
72
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
|
+
|
|
73
125
|
Ground every one of these keys with `codegen_get_query_declarative_catalog`
|
|
74
126
|
before writing them, and run the SQL through `codegen_validate_sql` first: a
|
|
75
127
|
JOIN or column typo here surfaces as a runtime 500 on `/datatables` or `/export`,
|
|
@@ -80,224 +132,292 @@ not as a payload validation error.
|
|
|
80
132
|
## Query File Reference
|
|
81
133
|
|
|
82
134
|
SQL queries can be stored in external `.sql` files using the `file:` prefix.
|
|
83
|
-
|
|
135
|
+
The path is relative to the `payload/` folder.
|
|
84
136
|
|
|
85
137
|
```json
|
|
86
|
-
"datatablesQuery": "file:
|
|
87
|
-
"exportQuery": "file:
|
|
138
|
+
"datatablesQuery": "file:query/orders-datatables.sql",
|
|
139
|
+
"exportQuery": "file:query/orders-export.sql"
|
|
88
140
|
```
|
|
89
141
|
|
|
90
142
|
Convention for folder structure:
|
|
91
143
|
```
|
|
92
144
|
payload/
|
|
93
145
|
├── order.json
|
|
94
|
-
└──
|
|
146
|
+
└── query/
|
|
95
147
|
├── orders-datatables.sql
|
|
96
148
|
└── orders-export.sql
|
|
97
149
|
```
|
|
98
150
|
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
101
154
|
|
|
102
155
|
The `file:` form hides the SQL from a quick payload review, which makes
|
|
103
156
|
`codegen_validate_sql` on the file content worth more here than for inline
|
|
104
|
-
queries.
|
|
105
|
-
`codegen_get_query_declarative_catalog`.
|
|
157
|
+
queries.
|
|
106
158
|
|
|
107
159
|
---
|
|
108
160
|
|
|
109
161
|
## Field Lookup
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
113
166
|
|
|
114
167
|
```json
|
|
115
|
-
{
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"fieldLookup": {
|
|
119
|
-
"apiPath": "/category",
|
|
120
|
-
"id": "category_id",
|
|
121
|
-
"text": "category_name"
|
|
122
|
-
}
|
|
168
|
+
"fieldNameLookup": {
|
|
169
|
+
"id": "category_id",
|
|
170
|
+
"text": "category_code||' - '||category_name as display_text"
|
|
123
171
|
}
|
|
124
172
|
```
|
|
125
173
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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 |
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"options": [
|
|
139
|
-
{ "id": "A", "text": "Option A" },
|
|
140
|
-
{ "id": "B", "text": "Option B" }
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
```
|
|
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`.
|
|
144
183
|
|
|
145
184
|
---
|
|
146
185
|
|
|
147
186
|
## Default Scope
|
|
148
187
|
|
|
149
|
-
Automatic WHERE
|
|
150
|
-
|
|
188
|
+
Automatic WHERE filter injected on the `lookup` and `read` actions only.
|
|
189
|
+
`/datatables` and `/first` are not affected.
|
|
151
190
|
|
|
152
191
|
```json
|
|
153
192
|
"defaultScope": {
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
{ "key": "is_active", "value": true },
|
|
157
|
-
{ "key": "company_id", "value": ":companyId" }
|
|
158
|
-
]
|
|
193
|
+
"lookup": { "is_active": true },
|
|
194
|
+
"read": { "is_active": true }
|
|
159
195
|
}
|
|
160
196
|
```
|
|
161
197
|
|
|
162
|
-
- `
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
|
|
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.
|
|
166
207
|
|
|
167
208
|
---
|
|
168
209
|
|
|
169
210
|
## Workflow (Change-Status)
|
|
170
211
|
|
|
171
|
-
Adds a
|
|
212
|
+
Adds a `POST /change-status` endpoint with state machine validation. Needs both
|
|
213
|
+
`action.workflow: true` and the `workflow` block.
|
|
172
214
|
|
|
173
215
|
```json
|
|
216
|
+
"action": { "workflow": true },
|
|
174
217
|
"workflow": {
|
|
175
218
|
"statusField": "status",
|
|
176
|
-
"transitions":
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
]
|
|
193
238
|
}
|
|
194
|
-
|
|
239
|
+
}
|
|
195
240
|
}
|
|
196
241
|
```
|
|
197
242
|
|
|
198
243
|
| Property | Notes |
|
|
199
244
|
|---|---|
|
|
200
|
-
| `statusField` |
|
|
201
|
-
| `transitions
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
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.
|
|
206
260
|
|
|
207
261
|
---
|
|
208
262
|
|
|
209
263
|
## Master-Detail (Composite)
|
|
210
264
|
|
|
211
|
-
Adds `/create-composite`, `/update-composite`, and `/read-composite
|
|
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.
|
|
212
270
|
|
|
213
271
|
```json
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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" }
|
|
224
292
|
}
|
|
225
|
-
|
|
293
|
+
}
|
|
226
294
|
```
|
|
227
295
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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`.
|
|
234
317
|
|
|
235
318
|
---
|
|
236
319
|
|
|
237
320
|
## Aggregate Config
|
|
238
321
|
|
|
239
|
-
|
|
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.
|
|
240
326
|
|
|
241
327
|
```json
|
|
328
|
+
"action": { "aggregate": true },
|
|
242
329
|
"aggregateConfig": {
|
|
243
|
-
"joins":
|
|
244
|
-
{
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
330
|
+
"joins": {
|
|
331
|
+
"warehouse": {
|
|
332
|
+
"tableName": "warehouse",
|
|
333
|
+
"joinType": "LEFT",
|
|
334
|
+
"sourceField": "warehouse_id",
|
|
335
|
+
"targetField": "warehouse_id",
|
|
336
|
+
"fields": ["warehouse_code", "warehouse_name"]
|
|
248
337
|
}
|
|
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
|
-
]
|
|
338
|
+
}
|
|
256
339
|
}
|
|
257
340
|
```
|
|
258
341
|
|
|
259
|
-
|
|
|
342
|
+
| `joins.<name>` key | Notes |
|
|
260
343
|
|---|---|
|
|
261
|
-
|
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
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` |
|
|
266
349
|
|
|
267
|
-
|
|
350
|
+
Request body:
|
|
268
351
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
272
380
|
|
|
273
381
|
---
|
|
274
382
|
|
|
275
383
|
## Adjust Config
|
|
276
384
|
|
|
277
|
-
Adds
|
|
278
|
-
|
|
385
|
+
Adds `POST /adjust` for atomic increments/decrements on numeric columns
|
|
386
|
+
(stock, balance, counters). Needs `action.adjust: true` and `adjustConfig`.
|
|
279
387
|
|
|
280
388
|
```json
|
|
389
|
+
"action": { "adjust": true },
|
|
281
390
|
"adjustConfig": {
|
|
282
|
-
"fields":
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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"
|
|
291
413
|
}
|
|
292
414
|
```
|
|
293
415
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
- The client sends `{ "field": "stock_qty", "amount": -5 }` in the request.
|
|
298
|
-
- Adjustment is executed as an atomic SQL UPDATE with WHERE guard.
|
|
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.
|
|
299
419
|
|
|
300
|
-
|
|
420
|
+
The guard is enforced at adjust time only. A bound that must hold for every
|
|
301
421
|
write path belongs in `fieldValidation` as well — check what the installed
|
|
302
422
|
platform offers with `codegen_get_field_validation_catalog`.
|
|
303
423
|
|
|
@@ -305,44 +425,47 @@ platform offers with `codegen_get_field_validation_catalog`.
|
|
|
305
425
|
|
|
306
426
|
## Import Config
|
|
307
427
|
|
|
308
|
-
|
|
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.
|
|
309
431
|
|
|
310
432
|
```json
|
|
433
|
+
"action": { "import": true },
|
|
311
434
|
"importConfig": {
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"
|
|
322
|
-
"
|
|
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
|
|
323
449
|
}
|
|
324
|
-
|
|
450
|
+
}
|
|
325
451
|
}
|
|
326
452
|
```
|
|
327
453
|
|
|
328
454
|
| Property | Notes |
|
|
329
455
|
|---|---|
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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.
|
|
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.
|
|
346
469
|
|
|
347
470
|
---
|
|
348
471
|
|
|
@@ -430,32 +553,34 @@ payload as a bare file name (no path form), unlike the dashboard generators.
|
|
|
430
553
|
|
|
431
554
|
## Kafka Event Publishing
|
|
432
555
|
|
|
433
|
-
Publishes
|
|
434
|
-
`KAFKA_ENABLED=true` in backend config.
|
|
556
|
+
Publishes an event to one Kafka topic after insert, update, or delete. Requires
|
|
557
|
+
`KAFKA_ENABLED=true` in the backend config as well.
|
|
435
558
|
|
|
436
559
|
```json
|
|
437
560
|
"kafka": {
|
|
438
|
-
"
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
]
|
|
561
|
+
"enabled": true,
|
|
562
|
+
"topic": "inventory.stock_inbound",
|
|
563
|
+
"keyField": "stock_inbound_id",
|
|
564
|
+
"publishOn": { "insert": true, "update": true, "delete": false }
|
|
443
565
|
}
|
|
444
566
|
```
|
|
445
567
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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`.
|
|
452
577
|
|
|
453
578
|
---
|
|
454
579
|
|
|
455
580
|
## Components (Lifecycle Hooks)
|
|
456
581
|
|
|
457
|
-
`components`
|
|
458
|
-
files. Added to a standard CRUD payload (one that has `tableName`).
|
|
582
|
+
`components` binds CRUD lifecycle events to functions in local JavaScript
|
|
583
|
+
handler files. Added to a standard CRUD payload (one that has `tableName`).
|
|
459
584
|
|
|
460
585
|
```json
|
|
461
586
|
{
|
|
@@ -474,7 +599,8 @@ files. Added to a standard CRUD payload (one that has `tableName`).
|
|
|
474
599
|
},
|
|
475
600
|
{
|
|
476
601
|
"name": "notifySlack",
|
|
477
|
-
"events": "onAfterInsert"
|
|
602
|
+
"events": "onAfterInsert",
|
|
603
|
+
"params": [{ "value": "{newData}" }]
|
|
478
604
|
}
|
|
479
605
|
]
|
|
480
606
|
}
|
|
@@ -485,11 +611,11 @@ files. Added to a standard CRUD payload (one that has `tableName`).
|
|
|
485
611
|
|
|
486
612
|
| Property | Required | Notes |
|
|
487
613
|
|---|---|---|
|
|
488
|
-
| `components[].properties.filename` | Yes |
|
|
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 |
|
|
489
615
|
| `components[].properties.methods` | Yes | List of method bindings |
|
|
490
616
|
| `methods[].name` | Yes | Function name exported from the handler file |
|
|
491
617
|
| `methods[].events` | Yes | Event hook (see table below) |
|
|
492
|
-
| `methods[].params` |
|
|
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 |
|
|
493
619
|
|
|
494
620
|
**Supported event hooks:**
|
|
495
621
|
|
|
@@ -500,6 +626,8 @@ files. Added to a standard CRUD payload (one that has `tableName`).
|
|
|
500
626
|
| `onBeforeDelete`, `onAfterDelete` | `/delete` endpoint |
|
|
501
627
|
| `onBeforeCompositeInsert`, `onAfterCompositeInsert` | `/create-composite` endpoint |
|
|
502
628
|
| `onBeforeCompositeUpdate`, `onAfterCompositeUpdate` | `/update-composite` endpoint |
|
|
629
|
+
| `onBeforeAdjust`, `onAfterAdjust` | `/adjust` endpoint |
|
|
630
|
+
| `onBeforeWorkflow`, `onAfterWorkflow` | `/change-status` endpoint |
|
|
503
631
|
|
|
504
632
|
**Template variables for `params[].value`:**
|
|
505
633
|
|
|
@@ -514,25 +642,54 @@ files. Added to a standard CRUD payload (one that has `tableName`).
|
|
|
514
642
|
| `{timestamp}` | Execution timestamp |
|
|
515
643
|
| `{record_id}` | Primary key of the affected record |
|
|
516
644
|
|
|
517
|
-
|
|
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`):
|
|
518
649
|
|
|
519
650
|
```javascript
|
|
520
|
-
async function
|
|
651
|
+
async function validateSupplierCode(requestData, userId, services) {
|
|
521
652
|
const { db, logger, redis, kafka, cache } = services;
|
|
522
|
-
|
|
523
|
-
|
|
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 };
|
|
524
657
|
}
|
|
525
|
-
module.exports = {
|
|
658
|
+
module.exports = { validateSupplierCode };
|
|
526
659
|
```
|
|
527
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
|
+
|
|
528
670
|
A lifecycle hook is not a substitute for declared validation: keep rule checks
|
|
529
671
|
that `fieldValidation` can express in the payload, grounded with
|
|
530
672
|
`codegen_get_field_validation_catalog`, and reserve `components` for logic the
|
|
531
673
|
catalog genuinely cannot express (external calls, cross-table effects,
|
|
532
|
-
notifications).
|
|
674
|
+
notifications, status locks).
|
|
533
675
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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 |
|