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.
@@ -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.