create-restforge-skills 0.1.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 +137 -0
- package/cli/index.js +235 -0
- package/cli/mcp.js +94 -0
- package/package.json +30 -0
- package/skills/restforge/SKILL.md +421 -0
- package/skills/restforge/references/config-schema.md +173 -0
- package/skills/restforge/references/dbschema-catalog.md +238 -0
- package/skills/restforge/references/design-to-sdf.md +618 -0
- package/skills/restforge/references/field-validation.md +173 -0
- package/skills/restforge/references/rdf-advanced.md +488 -0
- package/skills/restforge/references/udf-catalog.md +489 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Reference: Field Validation Catalog
|
|
2
|
+
|
|
3
|
+
> **Offline mirror.** This file mirrors `codegen_get_field_validation_catalog`
|
|
4
|
+
> from the installed RESTForge platform. The live tool is authoritative — when
|
|
5
|
+
> this file and the tool disagree, trust the tool, then update this file. Always
|
|
6
|
+
> re-ground with the tool before defining content; do not rely on this mirror
|
|
7
|
+
> alone.
|
|
8
|
+
|
|
9
|
+
Source: `codegen_get_field_validation_catalog` — installed platform version.
|
|
10
|
+
Use as grounding before defining `fieldValidation` in a payload.
|
|
11
|
+
Schema version: 1.0.
|
|
12
|
+
|
|
13
|
+
Summary: 12 types, 32 constraints, 4 format presets.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Table of Contents
|
|
18
|
+
|
|
19
|
+
1. [Types and Applicable Constraints](#types-and-applicable-constraints)
|
|
20
|
+
2. [Constraints (full)](#constraints-full)
|
|
21
|
+
3. [Format Presets](#format-presets)
|
|
22
|
+
4. [Audit Columns in Payload](#audit-columns-in-payload)
|
|
23
|
+
5. [Message Override Pattern](#message-override-pattern)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Types and Applicable Constraints
|
|
28
|
+
|
|
29
|
+
Use the `applicableConstraints` column to validate constraint scope per type.
|
|
30
|
+
Constraints not listed for a given type will be rejected.
|
|
31
|
+
|
|
32
|
+
| Type | Database Types | Applicable Constraints |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `string` | VARCHAR, TEXT, CHAR | required, unique, default, primaryKey, autoGenerate, nullable, minLength, maxLength, pattern, patternMessage, format, enum, trim, lowercase, uppercase |
|
|
35
|
+
| `integer` | INTEGER, INT, BIGINT | required, unique, default, primaryKey, nullable, min, max, precision, scale, positive, negative, integer |
|
|
36
|
+
| `decimal` | DECIMAL, NUMERIC | required, unique, default, primaryKey, nullable, min, max, precision, scale, positive, negative, integer |
|
|
37
|
+
| `number` | NUMERIC | required, unique, default, primaryKey, nullable, min, max, precision, scale, positive, negative, integer |
|
|
38
|
+
| `boolean` | BOOLEAN | required, unique, default, primaryKey, nullable, strict |
|
|
39
|
+
| `date` | DATE | required, unique, default, primaryKey, autoGenerate, nullable, format, min, max, before, after |
|
|
40
|
+
| `datetime` | TIMESTAMP | required, unique, default, primaryKey, autoGenerate, nullable, format, min, max, before, after |
|
|
41
|
+
| `timestamp` | TIMESTAMPTZ | required, unique, default, primaryKey, autoGenerate, nullable, format, min, max, before, after |
|
|
42
|
+
| `time` | TIME | required, unique, default, primaryKey, nullable |
|
|
43
|
+
| `uuid` | UUID | required, unique, default, primaryKey, autoGenerate, nullable |
|
|
44
|
+
| `json` | JSON, JSONB | required, unique, default, primaryKey, nullable, schema |
|
|
45
|
+
| `array` | ARRAY | required, unique, default, primaryKey, nullable, minItems, maxItems, uniqueItems |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Constraints (full)
|
|
50
|
+
|
|
51
|
+
### General (applies to all types)
|
|
52
|
+
|
|
53
|
+
| Constraint | Value type | Notes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `required` | boolean | Field must be present and non-empty |
|
|
56
|
+
| `unique` | boolean | Unique value across rows (enforced by DB, not app validation) |
|
|
57
|
+
| `default` | any | Default value when field is absent |
|
|
58
|
+
| `primaryKey` | boolean | Mark as primary key |
|
|
59
|
+
| `autoGenerate` | boolean | Auto-generate value at runtime (uuid, string, timestamp, datetime, date) |
|
|
60
|
+
| `nullable` | boolean | Allow null values |
|
|
61
|
+
|
|
62
|
+
### String scope
|
|
63
|
+
|
|
64
|
+
| Constraint | Value type | Message override key | Example |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `minLength` | integer | `minLengthMessage` | `"minLength": 3` |
|
|
67
|
+
| `maxLength` | integer | `maxLengthMessage` | `"maxLength": 100` |
|
|
68
|
+
| `pattern` | string | `patternMessage` | `"pattern": "^[A-Z]{3}\\d{4}$"` |
|
|
69
|
+
| `patternMessage` | string | — | `"patternMessage": "Invalid format"` |
|
|
70
|
+
| `format` | string | `formatMessage` | `"format": "email"` (see format presets) |
|
|
71
|
+
| `enum` | array | `enumMessage` | `"enum": ["active", "inactive"]` |
|
|
72
|
+
| `trim` | boolean | — | `"trim": true` |
|
|
73
|
+
| `lowercase` | boolean | — | `"lowercase": true` |
|
|
74
|
+
| `uppercase` | boolean | — | `"uppercase": true` |
|
|
75
|
+
|
|
76
|
+
> `trim`, `lowercase`, and `uppercase` are **normalization transforms** applied to
|
|
77
|
+
> the stored value, not validators. `uppercase: true` forces the value to upper
|
|
78
|
+
> case; it does not reject non-uppercase input. To *reject* input that is not
|
|
79
|
+
> upper case, use `pattern` (e.g. `"^[A-Z ]+$"`). To enforce case at the database
|
|
80
|
+
> level, use an SDF check constraint, not `fieldValidation`.
|
|
81
|
+
|
|
82
|
+
### Number scope (integer, decimal, number)
|
|
83
|
+
|
|
84
|
+
| Constraint | Value type | Message override key | Example |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `min` | number | `minMessage` | `"min": 0` |
|
|
87
|
+
| `max` | number | `maxMessage` | `"max": 9999999.99` |
|
|
88
|
+
| `precision` | integer | `precisionMessage` | `"precision": 10` |
|
|
89
|
+
| `scale` | integer | — | `"scale": 2` |
|
|
90
|
+
| `positive` | boolean | `positiveMessage` | `"positive": true` |
|
|
91
|
+
| `negative` | boolean | `negativeMessage` | `"negative": true` |
|
|
92
|
+
| `integer` | boolean | `integerMessage` | `"integer": true` |
|
|
93
|
+
|
|
94
|
+
### Date scope (date, datetime, timestamp)
|
|
95
|
+
|
|
96
|
+
| Constraint | Value type | Message override key | Example |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `format` | string | — | `"format": "DD/MM/YYYY"` |
|
|
99
|
+
| `min` | string | `minMessage` | `"min": "01/01/2020"` |
|
|
100
|
+
| `max` | string | `maxMessage` | `"max": "31/12/2030"` |
|
|
101
|
+
| `before` | string (field name) | `beforeMessage` | `"before": "end_date"` |
|
|
102
|
+
| `after` | string (field name) | `afterMessage` | `"after": "start_date"` |
|
|
103
|
+
|
|
104
|
+
### Boolean scope
|
|
105
|
+
|
|
106
|
+
| Constraint | Value type | Notes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `strict` | boolean | Reject coercion — only accept native boolean values, not the strings "true"/"false" |
|
|
109
|
+
|
|
110
|
+
### Array scope
|
|
111
|
+
|
|
112
|
+
| Constraint | Value type | Message override key | Example |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `minItems` | integer | `minItemsMessage` | `"minItems": 1` |
|
|
115
|
+
| `maxItems` | integer | `maxItemsMessage` | `"maxItems": 100` |
|
|
116
|
+
| `uniqueItems` | boolean | `uniqueItemsMessage` | `"uniqueItems": true` |
|
|
117
|
+
|
|
118
|
+
### JSON scope
|
|
119
|
+
|
|
120
|
+
| Constraint | Value type | Example |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `schema` | object | `"schema": { "type": "object", "properties": { ... } }` |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Format Presets
|
|
127
|
+
|
|
128
|
+
Applies to the `format` constraint on type `string`.
|
|
129
|
+
|
|
130
|
+
| Preset | Notes |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `email` | Validates email address format |
|
|
133
|
+
| `phone` | Validates phone number format |
|
|
134
|
+
| `url` | Validates URL format |
|
|
135
|
+
| `uuid` | Validates UUID format (v7 generated by app layer; v4 legacy remains valid) |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Audit Columns in Payload
|
|
140
|
+
|
|
141
|
+
The `auditColumns` key in a payload controls which audit columns are managed by the runtime.
|
|
142
|
+
|
|
143
|
+
| Value | Behavior |
|
|
144
|
+
|---|---|
|
|
145
|
+
| absent (no key) | Use 4 default audit columns: created_at, created_by, updated_at, updated_by |
|
|
146
|
+
| `false` | Disable audit columns |
|
|
147
|
+
| `null` | Disable audit columns |
|
|
148
|
+
| object | Override column names; required keys: `createdAt`, `createdBy`, `updatedAt`, `updatedBy` |
|
|
149
|
+
|
|
150
|
+
Rejected values: `true`, string, array, number. Error message:
|
|
151
|
+
`"Invalid auditColumns value for <tableName>: must be false, null, or object"`.
|
|
152
|
+
|
|
153
|
+
Auto-update of `updated_at` is implemented entirely in the RDF runtime (BaseModel
|
|
154
|
+
auditColumns helper) based on naming convention — not by an SDF marker.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Message Override Pattern
|
|
159
|
+
|
|
160
|
+
Any constraint that has a `messageOverrideKey` can be overridden by adding a
|
|
161
|
+
sibling key named `{constraintName}Message`.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"fieldName": "supplier_code",
|
|
167
|
+
"type": "string",
|
|
168
|
+
"fieldValidation": [
|
|
169
|
+
{ "required": true, "requiredMessage": "Supplier code is required" },
|
|
170
|
+
{ "minLength": 3, "minLengthMessage": "Minimum 3 characters" }
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
@@ -0,0 +1,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
|
+
---
|
|
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.
|