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