create-restforge-skills 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -54
- package/cli/codex.js +42 -0
- package/cli/index.js +23 -8
- package/package.json +38 -30
- package/skills/restforge/SKILL.md +148 -54
- package/skills/restforge/agents/openai.yaml +4 -0
- package/skills/restforge/references/auth.md +2 -2
- package/skills/restforge/references/config-schema.md +238 -173
- package/skills/restforge/references/dbschema-catalog.md +245 -238
- package/skills/restforge/references/design-to-sdf.md +621 -618
- package/skills/restforge/references/field-validation.md +247 -173
- package/skills/restforge/references/rdf-advanced.md +368 -211
- package/skills/restforge/references/udf-catalog.md +623 -504
|
@@ -1,618 +1,621 @@
|
|
|
1
|
-
# Reference: Design-to-SDF Heuristics
|
|
2
|
-
|
|
3
|
-
> **Procedure, not a catalog dump.** This file is a classification procedure that
|
|
4
|
-
> produces a *draft* SDF. It does not replace `codegen_get_dbschema_catalog`:
|
|
5
|
-
> every draft is grounded against the catalog for valid options and MUST pass
|
|
6
|
-
> `codegen_dbschema_validate` before any DDL is generated. Inferences marked
|
|
7
|
-
> "confirm" are guesses — surface them to the user, never commit silently.
|
|
8
|
-
|
|
9
|
-
Use when the goal is to derive an SDF (Schema Definition File) table structure
|
|
10
|
-
from an external source. Exactly 5 source kinds are in scope — anything else
|
|
11
|
-
is out of scope and must be refused rather than guessed at:
|
|
12
|
-
|
|
13
|
-
| Source kind | Examples | Path |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| HTML | a UI mockup/template file | Inference path — Step 0-5 below |
|
|
16
|
-
| Image | screenshot, photo, pasted clipboard image (jpg/png) | Inference path — Step 0-5 below |
|
|
17
|
-
| JSON | sample API response, Figma API/plugin export | Direct-translation, low inference |
|
|
18
|
-
| Markdown | a written schema spec/data-dictionary document | Direct-translation, low inference |
|
|
19
|
-
| SQL DDL | `CREATE TABLE` statements/script, no live DB required | Direct-translation — see dedicated section below |
|
|
20
|
-
|
|
21
|
-
A "Figma export" is not its own category — it surfaces as either an Image
|
|
22
|
-
(screenshot/PNG export) or JSON (Figma API/plugin export), handled under
|
|
23
|
-
whichever of those two it actually is.
|
|
24
|
-
|
|
25
|
-
HTML and Image need **inference**: presentation must be classified into
|
|
26
|
-
stored/derived/snapshot/joined/relation/audit before any column is decided
|
|
27
|
-
(Step 0-5 below). JSON, Markdown, and SQL DDL are **already explicit** about
|
|
28
|
-
types and constraints — the work is mostly direct translation to catalog
|
|
29
|
-
syntax, with far less judgment-call risk. Do not run the Step 0-5
|
|
30
|
-
classification ceremony on these; map what is already stated.
|
|
31
|
-
|
|
32
|
-
Any source outside these 5 kinds (PDF requirements doc, verbal description,
|
|
33
|
-
ERD diagram tool export not covered above, etc.) is out of scope for this
|
|
34
|
-
reference — say so explicitly and ask how to proceed rather than improvising
|
|
35
|
-
a procedure for it.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Why a procedure is needed
|
|
40
|
-
|
|
41
|
-
A design shows *presentation*, not *storage*. The same screen mixes four kinds
|
|
42
|
-
of things that map very differently to a schema:
|
|
43
|
-
|
|
44
|
-
| Kind | Example in design | SDF treatment |
|
|
45
|
-
|---|---|---|
|
|
46
|
-
| Stored column | "Category Name" text input | a field in `fields` |
|
|
47
|
-
| Derived value | "No of Items" count in a table | NOT a column — computed via relation/query |
|
|
48
|
-
| Snapshot value | "Unit Price", "Grand Total" on an order | a field in `fields` — looks computed but must be frozen |
|
|
49
|
-
| Joined attribute | "Customer Email" on an order detail page | NOT a column on this table — belongs to the related entity |
|
|
50
|
-
| Relation | "Category" dropdown on an Items form | FK + `belongsTo` relation |
|
|
51
|
-
| Audit/system | "Created On", "Last Updated" | audit columns, not new fields |
|
|
52
|
-
|
|
53
|
-
Reading every visible label as a column is the most common error. Classify
|
|
54
|
-
first, then assign types.
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Step 0 — Confirm this is an SDF case at all
|
|
59
|
-
|
|
60
|
-
Not every design implies a new table. A **dashboard/analytics screen** maps to
|
|
61
|
-
the Dashboard RDF pipeline (`codegen_get_dashboard_catalog` →
|
|
62
|
-
`codegen_create_dashboard`, payload with `widgets` not `tableName`, page name
|
|
63
|
-
prefixed `dash-`), NOT to a new SDF table. Treating its numbers as columns
|
|
64
|
-
produces a fictitious table — e.g. a `dashboard` model with fields like
|
|
65
|
-
`sales`, `purchases`, `profit` is always wrong; those are query results, not
|
|
66
|
-
data ever written by an insert/update.
|
|
67
|
-
|
|
68
|
-
Signals that the source is a dashboard, not an entity to model:
|
|
69
|
-
- a global filter that parameterizes the whole view — a date range, a
|
|
70
|
-
cross-entity dropdown (e.g. "Filter by warehouse") — rather than a filter
|
|
71
|
-
scoped to one entity's list,
|
|
72
|
-
- stat cards / KPI tiles showing currency or count aggregates (Sales,
|
|
73
|
-
Purchases, Profit, Total Due) with no per-card edit affordance,
|
|
74
|
-
- a chart, especially a time-series one,
|
|
75
|
-
- the absence of all three Step 1 signals (no Add/Edit form, no per-entity
|
|
76
|
-
filter, no row-level list) in their normal shape.
|
|
77
|
-
|
|
78
|
-
If any of these are present, stop before running Step 1-5. Instead:
|
|
79
|
-
1. Identify which **already-existing** tables the numbers aggregate from (here:
|
|
80
|
-
`sale`, `purchase`, `sales_return`, `purchase_return`, `invoice`,
|
|
81
|
-
`warehouse`). These need their own SDF only if they don't already exist —
|
|
82
|
-
derived the normal way (via their own forms/lists), never from the
|
|
83
|
-
dashboard screen itself.
|
|
84
|
-
2. Ground via `codegen_get_dashboard_catalog`, then build the dashboard payload
|
|
85
|
-
with SQL widgets querying those tables, filtered by the date range /
|
|
86
|
-
warehouse parameters shown.
|
|
87
|
-
3. Do not call `codegen_dbschema_init`/`migrate` for the dashboard itself —
|
|
88
|
-
there is no table to create for it.
|
|
89
|
-
|
|
90
|
-
A design can mix both: a page with a dashboard section AND a genuine CRUD form
|
|
91
|
-
elsewhere. Apply this gate per-section, not to the whole file at once.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## Step 1 — Locate the authoritative element
|
|
96
|
-
|
|
97
|
-
Prefer signals in this order. Earlier signals are more reliable than later.
|
|
98
|
-
|
|
99
|
-
1. **Add/Edit form (modal or page)** — the create/update form is the single
|
|
100
|
-
best source of *stored* columns. Every editable input is a candidate column.
|
|
101
|
-
2. **Filter panel** — reveals enumerable values (status options) and filterable
|
|
102
|
-
relations (category dropdown).
|
|
103
|
-
3. **Table/list `<thead>`** — confirms which columns are *displayed*, but mixes
|
|
104
|
-
stored, derived, and relation-label columns. Use to cross-check, not as the
|
|
105
|
-
primary source.
|
|
106
|
-
|
|
107
|
-
A field that appears only in the table but never in the form is usually
|
|
108
|
-
**derived** or a **relation label**, not a stored column.
|
|
109
|
-
|
|
110
|
-
### When the given source has no form
|
|
111
|
-
|
|
112
|
-
A read-only detail/view page (e.g. an order "details" screen with no `<input>`)
|
|
113
|
-
is NOT an authoritative source by itself — it is a list/table-equivalent (signal
|
|
114
|
-
3), not signal 1. Before classifying from it directly:
|
|
115
|
-
|
|
116
|
-
- Look for a sibling Add/Edit page or modal. List and detail pages commonly link
|
|
117
|
-
to it (e.g. an "Edit" button pointing to `edit-order.html` / `add-order.html`).
|
|
118
|
-
If found, treat that form as the authoritative source for Step 2 and use the
|
|
119
|
-
detail page only to cross-check.
|
|
120
|
-
- If no Add/Edit form exists anywhere in the provided source(s), say so
|
|
121
|
-
explicitly and proceed with the detail page as the best available signal —
|
|
122
|
-
do not silently treat it as equivalent to a form.
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Step 2 — Classify each candidate
|
|
127
|
-
|
|
128
|
-
For every input found in the form, decide its kind before its type.
|
|
129
|
-
|
|
130
|
-
- **Stored column** — free input the user types/picks and that belongs to *this*
|
|
131
|
-
entity. → goes in `fields`.
|
|
132
|
-
- **Relation (FK)** — a dropdown/autocomplete that selects *another* entity
|
|
133
|
-
(e.g. an Items form with a "Category" select). The stored column is the
|
|
134
|
-
foreign key (`category_id`), and a `belongsTo` relation is added. The visible
|
|
135
|
-
label ("Category name") is NOT stored on this table.
|
|
136
|
-
- The FK reference uses the **actual PK column name** of the target table:
|
|
137
|
-
`fk:category.category_id`, NOT `fk:category.id`. RESTForge does not resolve
|
|
138
|
-
`.id` to the primary key — a non-existent target column is rejected.
|
|
139
|
-
- A `*` on the dropdown means the relation is mandatory → add `notnull` to the
|
|
140
|
-
FK column. Do NOT downgrade a required relation to a nullable free-text
|
|
141
|
-
label (e.g. modelling a required "Tax" select as `tax_label string:50` drops
|
|
142
|
-
both the relation and the constraint — wrong on two counts).
|
|
143
|
-
- If a target entity for the dropdown plausibly exists (its own admin/settings
|
|
144
|
-
page, e.g. Tax → tax-settings), model it as an FK. Only fall back to an enum
|
|
145
|
-
`checks` set when the options are a small fixed list with no backing entity.
|
|
146
|
-
- **Enum** — a select/radio whose options are a small, closed, *permanent* set
|
|
147
|
-
intrinsic to the data (e.g. Gender: Male / Female; Status: Active / Inactive;
|
|
148
|
-
Tax Type: Inclusive / Exclusive). → stored as `string` + a `checks` entry with
|
|
149
|
-
`in: [...]`. No separate table.
|
|
150
|
-
- **Lookup master** — a dropdown whose options are *managed data* that can grow,
|
|
151
|
-
shrink, or change over time, or that carry extra attributes (a rate, an image,
|
|
152
|
-
a description). → model as a separate **master table** and an FK, never as an
|
|
153
|
-
enum. See the decision rule below.
|
|
154
|
-
- **Joined attribute** — a value displayed on this screen but that is actually
|
|
155
|
-
a column of a *related* entity, shown via join (e.g. "Customer Email" /
|
|
156
|
-
"Customer Phone" on an order detail page — these belong to `customer`, not
|
|
157
|
-
`order`). → NOT a column on this table. Confirm: does this entity already have
|
|
158
|
-
its own table where the field belongs? If the related entity's FK is already
|
|
159
|
-
modelled (e.g. `customer_id`), the attribute is reachable through that
|
|
160
|
-
relation and must not be duplicated here.
|
|
161
|
-
- **Derived vs. Snapshot** — both look like "a number that's computed", but they
|
|
162
|
-
must be treated oppositely. Ask: *if the source data changes later, must this
|
|
163
|
-
value stay frozen on this record, or is it allowed to change?*
|
|
164
|
-
- **Derived** (allowed to change, recomputed live) — counts, running totals,
|
|
165
|
-
"No of Items", "Total Orders". → NOT a column. Note it for an aggregate
|
|
166
|
-
query (RDF `viewQuery`/`datatablesQuery`) or a `hasMany` relation count, and
|
|
167
|
-
exclude from `fields`.
|
|
168
|
-
- **Snapshot** (must stay frozen, computed or copied once at the moment of the
|
|
169
|
-
transaction) — IS a column, even though it looks computed or looks like it
|
|
170
|
-
duplicates a related entity's field. The principle is not limited to money:
|
|
171
|
-
anything copied from a related entity onto a transaction record, where that
|
|
172
|
-
related entity could change *after* the transaction while the transaction
|
|
173
|
-
must keep showing the original value at the time. Excluding it from
|
|
174
|
-
`fields` lets historical records change retroactively when the source
|
|
175
|
-
changes — a data-integrity bug, not a simplification. Recognize it by
|
|
176
|
-
asking "could the source of this value be edited or deleted later, while
|
|
177
|
-
this record must still show what was true at transaction time?" — examples:
|
|
178
|
-
- money: line `unit_price`, `subtotal`, `tax_amount`, `shipping_rate`,
|
|
179
|
-
`grand_total` on an order; a price/rate copied from a master table.
|
|
180
|
-
- identity/labels: a product's `name`/`sku` copied onto an order line (the
|
|
181
|
-
product can be renamed or discontinued later; the historical order line
|
|
182
|
-
must still show what was ordered).
|
|
183
|
-
- location: a `shipping_address`/`billing_address` copied onto an order
|
|
184
|
-
(the customer's address book entry can change later; the order must keep
|
|
185
|
-
shipping to where it said it would at the time of purchase).
|
|
186
|
-
- This overlaps with **Joined attribute** above — both involve a value that
|
|
187
|
-
*could* be reached via a relation. The deciding question is whether the
|
|
188
|
-
value is allowed to drift when the related entity changes (joined — do not
|
|
189
|
-
store) or must stay frozen (snapshot — store). When the field is tied to a
|
|
190
|
-
specific transaction/sale and could plausibly change at the source later
|
|
191
|
-
(price, product label, address), default to **snapshot** — under-storing
|
|
192
|
-
breaks historical accuracy; over-storing only costs a redundant column.
|
|
193
|
-
Pure identity/contact fields with no transactional freezing need (e.g. a
|
|
194
|
-
customer's current email shown on their own profile page) stay **joined**.
|
|
195
|
-
- **Audit/system** — "Created On/At", "Updated On/At", "Created By". → map to
|
|
196
|
-
the standard audit columns, do not invent new fields.
|
|
197
|
-
|
|
198
|
-
### Decision rule — lookup master table vs enum
|
|
199
|
-
|
|
200
|
-
When a field offers a fixed set of choices, decide between a **master table + FK**
|
|
201
|
-
and an inline **enum (`checks`)** by permanence and management:
|
|
202
|
-
|
|
203
|
-
- **Non-permanent / managed lookup → ALWAYS a master table + FK.** If the option
|
|
204
|
-
set is business data that an admin adds to, edits, deactivates, or that carries
|
|
205
|
-
extra attributes, it is a managed entity — model it as its own table and
|
|
206
|
-
reference it by FK. Examples: Category, Brand, Tax, Unit, Warehouse, City.
|
|
207
|
-
- **Simple, permanent, closed set → enum (`checks in:[...]`).** If the values are
|
|
208
|
-
intrinsic to the record, will not be managed through a UI, and the set is
|
|
209
|
-
stable, store a `string` with a `checks` constraint. Examples: Gender
|
|
210
|
-
(`MALE`/`FEMALE`), Active/Inactive status, Yes/No, Tax Type
|
|
211
|
-
(Inclusive/Exclusive).
|
|
212
|
-
|
|
213
|
-
These examples are illustrative, not fixed labels — the same field name can be
|
|
214
|
-
either depending on the actual design. Decide every case by the signals below,
|
|
215
|
-
not by matching the field's name against an example list.
|
|
216
|
-
|
|
217
|
-
Decisive signals for a master table (any one is sufficient):
|
|
218
|
-
- a dedicated admin/settings page exists for it (e.g. Tax → `tax-settings`,
|
|
219
|
-
Category → `categories`),
|
|
220
|
-
- options carry their own attributes (rate, image, description, code),
|
|
221
|
-
- the business is expected to add/remove options over time,
|
|
222
|
-
- a quick-add affordance sits directly on the dropdown in the same form (e.g. a
|
|
223
|
-
"+" button next to "Choose Category" / "Choose Brand") — this is direct UI
|
|
224
|
-
evidence of a managed entity and needs no separate admin page to confirm it.
|
|
225
|
-
|
|
226
|
-
The absence of a signal is not evidence for the opposite conclusion. In
|
|
227
|
-
particular, a dropdown with no "+" button is NOT thereby an enum — check the
|
|
228
|
-
other signals before concluding enum. A dropdown whose options are themselves
|
|
229
|
-
children of an already-managed entity (e.g. "Subcategories" depending on a
|
|
230
|
-
selected "Category") is still a master-table FK even with no quick-add button
|
|
231
|
-
of its own, because it inherits "managed" status from its parent entity.
|
|
232
|
-
|
|
233
|
-
Without any of these signals present in the provided source(s), do not assume
|
|
234
|
-
a master table — a label by itself (e.g. "Payment Method: Credit Card" on an
|
|
235
|
-
order detail page, with no payment-method admin page or quick-add affordance in
|
|
236
|
-
evidence) defaults to an enum. "Payment Method" in particular varies by system:
|
|
237
|
-
a simple storefront with a fixed set of methods is an enum; a system with a
|
|
238
|
-
payment-gateway settings page (fees, codes, enable/disable per method) is a
|
|
239
|
-
master table. Confirm with the user when the source gives no signal either way.
|
|
240
|
-
|
|
241
|
-
When uncertain, prefer the **master table** — promoting an enum to a table later
|
|
242
|
-
is a breaking migration, while a stable enum rarely needs to become a table.
|
|
243
|
-
Note: a master table can itself contain a permanent enum column (e.g. the `tax`
|
|
244
|
-
table stores `tax_type` as an enum).
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## Step 3 — Assign SDF types
|
|
249
|
-
|
|
250
|
-
Map the HTML control (or the visual control in an image) to a catalog field
|
|
251
|
-
type. All types below are from `codegen_get_dbschema_catalog`.
|
|
252
|
-
|
|
253
|
-
| Design control | SDF type | Notes |
|
|
254
|
-
|---|---|---|
|
|
255
|
-
| Short text input | `string:N` | N = confirm; default 255 if no hint |
|
|
256
|
-
| Long text / `<textarea>` | `text` | description, notes, address |
|
|
257
|
-
| Number (count, qty) | `integer` / `bigint` | bigint only if very large |
|
|
258
|
-
| Money / price | `decimal:15,2` | currency; scale 2 unless design says otherwise |
|
|
259
|
-
| Percentage / rate | `decimal:5,2` | confirm precision |
|
|
260
|
-
| Single checkbox / toggle | `boolean` | true/false only |
|
|
261
|
-
| Status / type select | `string:20` + `checks in:[...]` | enum, not boolean, when >2 states |
|
|
262
|
-
| Date picker | `date` | date only |
|
|
263
|
-
| Date + time | `timestamp` | |
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
- A
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
- every
|
|
302
|
-
- every
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
|
341
|
-
|
|
342
|
-
|
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
|
|
|
346
|
-
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
`
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
- Do **not**
|
|
369
|
-
`
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
- `
|
|
402
|
-
|
|
403
|
-
- `
|
|
404
|
-
- `
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
- "
|
|
464
|
-
child table `
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
`
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
- `
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1
|
+
# Reference: Design-to-SDF Heuristics
|
|
2
|
+
|
|
3
|
+
> **Procedure, not a catalog dump.** This file is a classification procedure that
|
|
4
|
+
> produces a *draft* SDF. It does not replace `codegen_get_dbschema_catalog`:
|
|
5
|
+
> every draft is grounded against the catalog for valid options and MUST pass
|
|
6
|
+
> `codegen_dbschema_validate` before any DDL is generated. Inferences marked
|
|
7
|
+
> "confirm" are guesses — surface them to the user, never commit silently.
|
|
8
|
+
|
|
9
|
+
Use when the goal is to derive an SDF (Schema Definition File) table structure
|
|
10
|
+
from an external source. Exactly 5 source kinds are in scope — anything else
|
|
11
|
+
is out of scope and must be refused rather than guessed at:
|
|
12
|
+
|
|
13
|
+
| Source kind | Examples | Path |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| HTML | a UI mockup/template file | Inference path — Step 0-5 below |
|
|
16
|
+
| Image | screenshot, photo, pasted clipboard image (jpg/png) | Inference path — Step 0-5 below |
|
|
17
|
+
| JSON | sample API response, Figma API/plugin export | Direct-translation, low inference |
|
|
18
|
+
| Markdown | a written schema spec/data-dictionary document | Direct-translation, low inference |
|
|
19
|
+
| SQL DDL | `CREATE TABLE` statements/script, no live DB required | Direct-translation — see dedicated section below |
|
|
20
|
+
|
|
21
|
+
A "Figma export" is not its own category — it surfaces as either an Image
|
|
22
|
+
(screenshot/PNG export) or JSON (Figma API/plugin export), handled under
|
|
23
|
+
whichever of those two it actually is.
|
|
24
|
+
|
|
25
|
+
HTML and Image need **inference**: presentation must be classified into
|
|
26
|
+
stored/derived/snapshot/joined/relation/audit before any column is decided
|
|
27
|
+
(Step 0-5 below). JSON, Markdown, and SQL DDL are **already explicit** about
|
|
28
|
+
types and constraints — the work is mostly direct translation to catalog
|
|
29
|
+
syntax, with far less judgment-call risk. Do not run the Step 0-5
|
|
30
|
+
classification ceremony on these; map what is already stated.
|
|
31
|
+
|
|
32
|
+
Any source outside these 5 kinds (PDF requirements doc, verbal description,
|
|
33
|
+
ERD diagram tool export not covered above, etc.) is out of scope for this
|
|
34
|
+
reference — say so explicitly and ask how to proceed rather than improvising
|
|
35
|
+
a procedure for it.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Why a procedure is needed
|
|
40
|
+
|
|
41
|
+
A design shows *presentation*, not *storage*. The same screen mixes four kinds
|
|
42
|
+
of things that map very differently to a schema:
|
|
43
|
+
|
|
44
|
+
| Kind | Example in design | SDF treatment |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Stored column | "Category Name" text input | a field in `fields` |
|
|
47
|
+
| Derived value | "No of Items" count in a table | NOT a column — computed via relation/query |
|
|
48
|
+
| Snapshot value | "Unit Price", "Grand Total" on an order | a field in `fields` — looks computed but must be frozen |
|
|
49
|
+
| Joined attribute | "Customer Email" on an order detail page | NOT a column on this table — belongs to the related entity |
|
|
50
|
+
| Relation | "Category" dropdown on an Items form | FK + `belongsTo` relation |
|
|
51
|
+
| Audit/system | "Created On", "Last Updated" | audit columns, not new fields |
|
|
52
|
+
|
|
53
|
+
Reading every visible label as a column is the most common error. Classify
|
|
54
|
+
first, then assign types.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 0 — Confirm this is an SDF case at all
|
|
59
|
+
|
|
60
|
+
Not every design implies a new table. A **dashboard/analytics screen** maps to
|
|
61
|
+
the Dashboard RDF pipeline (`codegen_get_dashboard_catalog` →
|
|
62
|
+
`codegen_create_dashboard`, payload with `widgets` not `tableName`, page name
|
|
63
|
+
prefixed `dash-`), NOT to a new SDF table. Treating its numbers as columns
|
|
64
|
+
produces a fictitious table — e.g. a `dashboard` model with fields like
|
|
65
|
+
`sales`, `purchases`, `profit` is always wrong; those are query results, not
|
|
66
|
+
data ever written by an insert/update.
|
|
67
|
+
|
|
68
|
+
Signals that the source is a dashboard, not an entity to model:
|
|
69
|
+
- a global filter that parameterizes the whole view — a date range, a
|
|
70
|
+
cross-entity dropdown (e.g. "Filter by warehouse") — rather than a filter
|
|
71
|
+
scoped to one entity's list,
|
|
72
|
+
- stat cards / KPI tiles showing currency or count aggregates (Sales,
|
|
73
|
+
Purchases, Profit, Total Due) with no per-card edit affordance,
|
|
74
|
+
- a chart, especially a time-series one,
|
|
75
|
+
- the absence of all three Step 1 signals (no Add/Edit form, no per-entity
|
|
76
|
+
filter, no row-level list) in their normal shape.
|
|
77
|
+
|
|
78
|
+
If any of these are present, stop before running Step 1-5. Instead:
|
|
79
|
+
1. Identify which **already-existing** tables the numbers aggregate from (here:
|
|
80
|
+
`sale`, `purchase`, `sales_return`, `purchase_return`, `invoice`,
|
|
81
|
+
`warehouse`). These need their own SDF only if they don't already exist —
|
|
82
|
+
derived the normal way (via their own forms/lists), never from the
|
|
83
|
+
dashboard screen itself.
|
|
84
|
+
2. Ground via `codegen_get_dashboard_catalog`, then build the dashboard payload
|
|
85
|
+
with SQL widgets querying those tables, filtered by the date range /
|
|
86
|
+
warehouse parameters shown.
|
|
87
|
+
3. Do not call `codegen_dbschema_init`/`migrate` for the dashboard itself —
|
|
88
|
+
there is no table to create for it.
|
|
89
|
+
|
|
90
|
+
A design can mix both: a page with a dashboard section AND a genuine CRUD form
|
|
91
|
+
elsewhere. Apply this gate per-section, not to the whole file at once.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Step 1 — Locate the authoritative element
|
|
96
|
+
|
|
97
|
+
Prefer signals in this order. Earlier signals are more reliable than later.
|
|
98
|
+
|
|
99
|
+
1. **Add/Edit form (modal or page)** — the create/update form is the single
|
|
100
|
+
best source of *stored* columns. Every editable input is a candidate column.
|
|
101
|
+
2. **Filter panel** — reveals enumerable values (status options) and filterable
|
|
102
|
+
relations (category dropdown).
|
|
103
|
+
3. **Table/list `<thead>`** — confirms which columns are *displayed*, but mixes
|
|
104
|
+
stored, derived, and relation-label columns. Use to cross-check, not as the
|
|
105
|
+
primary source.
|
|
106
|
+
|
|
107
|
+
A field that appears only in the table but never in the form is usually
|
|
108
|
+
**derived** or a **relation label**, not a stored column.
|
|
109
|
+
|
|
110
|
+
### When the given source has no form
|
|
111
|
+
|
|
112
|
+
A read-only detail/view page (e.g. an order "details" screen with no `<input>`)
|
|
113
|
+
is NOT an authoritative source by itself — it is a list/table-equivalent (signal
|
|
114
|
+
3), not signal 1. Before classifying from it directly:
|
|
115
|
+
|
|
116
|
+
- Look for a sibling Add/Edit page or modal. List and detail pages commonly link
|
|
117
|
+
to it (e.g. an "Edit" button pointing to `edit-order.html` / `add-order.html`).
|
|
118
|
+
If found, treat that form as the authoritative source for Step 2 and use the
|
|
119
|
+
detail page only to cross-check.
|
|
120
|
+
- If no Add/Edit form exists anywhere in the provided source(s), say so
|
|
121
|
+
explicitly and proceed with the detail page as the best available signal —
|
|
122
|
+
do not silently treat it as equivalent to a form.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Step 2 — Classify each candidate
|
|
127
|
+
|
|
128
|
+
For every input found in the form, decide its kind before its type.
|
|
129
|
+
|
|
130
|
+
- **Stored column** — free input the user types/picks and that belongs to *this*
|
|
131
|
+
entity. → goes in `fields`.
|
|
132
|
+
- **Relation (FK)** — a dropdown/autocomplete that selects *another* entity
|
|
133
|
+
(e.g. an Items form with a "Category" select). The stored column is the
|
|
134
|
+
foreign key (`category_id`), and a `belongsTo` relation is added. The visible
|
|
135
|
+
label ("Category name") is NOT stored on this table.
|
|
136
|
+
- The FK reference uses the **actual PK column name** of the target table:
|
|
137
|
+
`fk:category.category_id`, NOT `fk:category.id`. RESTForge does not resolve
|
|
138
|
+
`.id` to the primary key — a non-existent target column is rejected.
|
|
139
|
+
- A `*` on the dropdown means the relation is mandatory → add `notnull` to the
|
|
140
|
+
FK column. Do NOT downgrade a required relation to a nullable free-text
|
|
141
|
+
label (e.g. modelling a required "Tax" select as `tax_label string:50` drops
|
|
142
|
+
both the relation and the constraint — wrong on two counts).
|
|
143
|
+
- If a target entity for the dropdown plausibly exists (its own admin/settings
|
|
144
|
+
page, e.g. Tax → tax-settings), model it as an FK. Only fall back to an enum
|
|
145
|
+
`checks` set when the options are a small fixed list with no backing entity.
|
|
146
|
+
- **Enum** — a select/radio whose options are a small, closed, *permanent* set
|
|
147
|
+
intrinsic to the data (e.g. Gender: Male / Female; Status: Active / Inactive;
|
|
148
|
+
Tax Type: Inclusive / Exclusive). → stored as `string` + a `checks` entry with
|
|
149
|
+
`in: [...]`. No separate table.
|
|
150
|
+
- **Lookup master** — a dropdown whose options are *managed data* that can grow,
|
|
151
|
+
shrink, or change over time, or that carry extra attributes (a rate, an image,
|
|
152
|
+
a description). → model as a separate **master table** and an FK, never as an
|
|
153
|
+
enum. See the decision rule below.
|
|
154
|
+
- **Joined attribute** — a value displayed on this screen but that is actually
|
|
155
|
+
a column of a *related* entity, shown via join (e.g. "Customer Email" /
|
|
156
|
+
"Customer Phone" on an order detail page — these belong to `customer`, not
|
|
157
|
+
`order`). → NOT a column on this table. Confirm: does this entity already have
|
|
158
|
+
its own table where the field belongs? If the related entity's FK is already
|
|
159
|
+
modelled (e.g. `customer_id`), the attribute is reachable through that
|
|
160
|
+
relation and must not be duplicated here.
|
|
161
|
+
- **Derived vs. Snapshot** — both look like "a number that's computed", but they
|
|
162
|
+
must be treated oppositely. Ask: *if the source data changes later, must this
|
|
163
|
+
value stay frozen on this record, or is it allowed to change?*
|
|
164
|
+
- **Derived** (allowed to change, recomputed live) — counts, running totals,
|
|
165
|
+
"No of Items", "Total Orders". → NOT a column. Note it for an aggregate
|
|
166
|
+
query (RDF `viewQuery`/`datatablesQuery`) or a `hasMany` relation count, and
|
|
167
|
+
exclude from `fields`.
|
|
168
|
+
- **Snapshot** (must stay frozen, computed or copied once at the moment of the
|
|
169
|
+
transaction) — IS a column, even though it looks computed or looks like it
|
|
170
|
+
duplicates a related entity's field. The principle is not limited to money:
|
|
171
|
+
anything copied from a related entity onto a transaction record, where that
|
|
172
|
+
related entity could change *after* the transaction while the transaction
|
|
173
|
+
must keep showing the original value at the time. Excluding it from
|
|
174
|
+
`fields` lets historical records change retroactively when the source
|
|
175
|
+
changes — a data-integrity bug, not a simplification. Recognize it by
|
|
176
|
+
asking "could the source of this value be edited or deleted later, while
|
|
177
|
+
this record must still show what was true at transaction time?" — examples:
|
|
178
|
+
- money: line `unit_price`, `subtotal`, `tax_amount`, `shipping_rate`,
|
|
179
|
+
`grand_total` on an order; a price/rate copied from a master table.
|
|
180
|
+
- identity/labels: a product's `name`/`sku` copied onto an order line (the
|
|
181
|
+
product can be renamed or discontinued later; the historical order line
|
|
182
|
+
must still show what was ordered).
|
|
183
|
+
- location: a `shipping_address`/`billing_address` copied onto an order
|
|
184
|
+
(the customer's address book entry can change later; the order must keep
|
|
185
|
+
shipping to where it said it would at the time of purchase).
|
|
186
|
+
- This overlaps with **Joined attribute** above — both involve a value that
|
|
187
|
+
*could* be reached via a relation. The deciding question is whether the
|
|
188
|
+
value is allowed to drift when the related entity changes (joined — do not
|
|
189
|
+
store) or must stay frozen (snapshot — store). When the field is tied to a
|
|
190
|
+
specific transaction/sale and could plausibly change at the source later
|
|
191
|
+
(price, product label, address), default to **snapshot** — under-storing
|
|
192
|
+
breaks historical accuracy; over-storing only costs a redundant column.
|
|
193
|
+
Pure identity/contact fields with no transactional freezing need (e.g. a
|
|
194
|
+
customer's current email shown on their own profile page) stay **joined**.
|
|
195
|
+
- **Audit/system** — "Created On/At", "Updated On/At", "Created By". → map to
|
|
196
|
+
the standard audit columns, do not invent new fields.
|
|
197
|
+
|
|
198
|
+
### Decision rule — lookup master table vs enum
|
|
199
|
+
|
|
200
|
+
When a field offers a fixed set of choices, decide between a **master table + FK**
|
|
201
|
+
and an inline **enum (`checks`)** by permanence and management:
|
|
202
|
+
|
|
203
|
+
- **Non-permanent / managed lookup → ALWAYS a master table + FK.** If the option
|
|
204
|
+
set is business data that an admin adds to, edits, deactivates, or that carries
|
|
205
|
+
extra attributes, it is a managed entity — model it as its own table and
|
|
206
|
+
reference it by FK. Examples: Category, Brand, Tax, Unit, Warehouse, City.
|
|
207
|
+
- **Simple, permanent, closed set → enum (`checks in:[...]`).** If the values are
|
|
208
|
+
intrinsic to the record, will not be managed through a UI, and the set is
|
|
209
|
+
stable, store a `string` with a `checks` constraint. Examples: Gender
|
|
210
|
+
(`MALE`/`FEMALE`), Active/Inactive status, Yes/No, Tax Type
|
|
211
|
+
(Inclusive/Exclusive).
|
|
212
|
+
|
|
213
|
+
These examples are illustrative, not fixed labels — the same field name can be
|
|
214
|
+
either depending on the actual design. Decide every case by the signals below,
|
|
215
|
+
not by matching the field's name against an example list.
|
|
216
|
+
|
|
217
|
+
Decisive signals for a master table (any one is sufficient):
|
|
218
|
+
- a dedicated admin/settings page exists for it (e.g. Tax → `tax-settings`,
|
|
219
|
+
Category → `categories`),
|
|
220
|
+
- options carry their own attributes (rate, image, description, code),
|
|
221
|
+
- the business is expected to add/remove options over time,
|
|
222
|
+
- a quick-add affordance sits directly on the dropdown in the same form (e.g. a
|
|
223
|
+
"+" button next to "Choose Category" / "Choose Brand") — this is direct UI
|
|
224
|
+
evidence of a managed entity and needs no separate admin page to confirm it.
|
|
225
|
+
|
|
226
|
+
The absence of a signal is not evidence for the opposite conclusion. In
|
|
227
|
+
particular, a dropdown with no "+" button is NOT thereby an enum — check the
|
|
228
|
+
other signals before concluding enum. A dropdown whose options are themselves
|
|
229
|
+
children of an already-managed entity (e.g. "Subcategories" depending on a
|
|
230
|
+
selected "Category") is still a master-table FK even with no quick-add button
|
|
231
|
+
of its own, because it inherits "managed" status from its parent entity.
|
|
232
|
+
|
|
233
|
+
Without any of these signals present in the provided source(s), do not assume
|
|
234
|
+
a master table — a label by itself (e.g. "Payment Method: Credit Card" on an
|
|
235
|
+
order detail page, with no payment-method admin page or quick-add affordance in
|
|
236
|
+
evidence) defaults to an enum. "Payment Method" in particular varies by system:
|
|
237
|
+
a simple storefront with a fixed set of methods is an enum; a system with a
|
|
238
|
+
payment-gateway settings page (fees, codes, enable/disable per method) is a
|
|
239
|
+
master table. Confirm with the user when the source gives no signal either way.
|
|
240
|
+
|
|
241
|
+
When uncertain, prefer the **master table** — promoting an enum to a table later
|
|
242
|
+
is a breaking migration, while a stable enum rarely needs to become a table.
|
|
243
|
+
Note: a master table can itself contain a permanent enum column (e.g. the `tax`
|
|
244
|
+
table stores `tax_type` as an enum).
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Step 3 — Assign SDF types
|
|
249
|
+
|
|
250
|
+
Map the HTML control (or the visual control in an image) to a catalog field
|
|
251
|
+
type. All types below are from `codegen_get_dbschema_catalog`.
|
|
252
|
+
|
|
253
|
+
| Design control | SDF type | Notes |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| Short text input | `string:N` | N = confirm; default 255 if no hint |
|
|
256
|
+
| Long text / `<textarea>` | `text` | description, notes, address |
|
|
257
|
+
| Number (count, qty) | `integer` / `bigint` | bigint only if very large |
|
|
258
|
+
| Money / price | `decimal:15,2` | currency; scale 2 unless design says otherwise |
|
|
259
|
+
| Percentage / rate | `decimal:5,2` | confirm precision |
|
|
260
|
+
| Single checkbox / toggle | `boolean` | true/false only |
|
|
261
|
+
| Status / type select | `string:20` + `checks in:[...]` | enum, not boolean, when >2 states |
|
|
262
|
+
| Date picker | `date` | date only |
|
|
263
|
+
| Date + time | `timestamp` | wall-clock time in the app `TIMEZONE`; use `timestamptz` only for an absolute moment shared across zones (PostgreSQL only) — confirm |
|
|
264
|
+
| Time picker (time of day only) | `time` | e.g. shift start; not supported on Oracle (use `timestamp` there) |
|
|
265
|
+
| File / image upload | `string:500` | store the path/URL, never the binary |
|
|
266
|
+
| Email | `string:255` | |
|
|
267
|
+
| Phone | `string:30` | |
|
|
268
|
+
| Color picker / code | `string:7` or `string:36` | hex vs token |
|
|
269
|
+
| Entity dropdown (FK) | `string:36` or `integer` + `fk:<table>.<col>` | match the target PK type |
|
|
270
|
+
| JSON / key-value block | `json` | |
|
|
271
|
+
|
|
272
|
+
Rules carried from the catalog:
|
|
273
|
+
- `string` always needs a length; `decimal` always needs precision,scale.
|
|
274
|
+
- A `*` / "required" marker → add `notnull`.
|
|
275
|
+
- A select with fixed options → `checks: [{ field, in: [...] }]`, options
|
|
276
|
+
lowercased/normalized to storage values.
|
|
277
|
+
- FK uses dot notation `fk:<table>.<column>`; also add the `belongsTo` relation
|
|
278
|
+
with `onDelete` (default `restrict`; confirm).
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Step 4 — Add structural scaffolding
|
|
283
|
+
|
|
284
|
+
A design rarely shows these, but a RESTForge table needs them:
|
|
285
|
+
|
|
286
|
+
- **Primary key** — add `<entity>_id` as `string:36 pk` (UUID style) or
|
|
287
|
+
`integer pk` (serial style). Confirm the convention with the user/project.
|
|
288
|
+
- **Audit columns** — add the four standard audit columns
|
|
289
|
+
(`created_at`, `created_by`, `updated_at`, `updated_by`) unless this is a pure
|
|
290
|
+
lookup table. "Created On" in the design maps here.
|
|
291
|
+
- **Soft-delete** — only if the design shows a recycle bin / "restore" / archive
|
|
292
|
+
affordance. Then apply the soft-delete contract (PostgreSQL only).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Step 5 — Validate and confirm
|
|
297
|
+
|
|
298
|
+
1. Write the draft SDF (`defineModel`) using only catalog options.
|
|
299
|
+
2. Run `codegen_dbschema_validate`.
|
|
300
|
+
3. Present the **confirm list** to the user before migrating:
|
|
301
|
+
- every guessed `string` length and `decimal` precision,
|
|
302
|
+
- every enum option set,
|
|
303
|
+
- every FK target and its `onDelete`,
|
|
304
|
+
- PK convention (uuid vs serial),
|
|
305
|
+
- any "derived" column excluded from the schema (state it explicitly so the
|
|
306
|
+
omission is intentional, not silent).
|
|
307
|
+
|
|
308
|
+
Never call `codegen_dbschema_migrate`/`apply` on a design-derived SDF without
|
|
309
|
+
this confirmation.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## SQL DDL source — direct translation path
|
|
314
|
+
|
|
315
|
+
Applies when the source is `CREATE TABLE` statements (a `.sql` file or a
|
|
316
|
+
pasted script), with or without a live database to run it against. This is
|
|
317
|
+
the lowest-inference path: the DDL already states every type and constraint
|
|
318
|
+
explicitly. Do not run Step 0-5 — translate what is written, syntax to
|
|
319
|
+
syntax, dialect to dialect-agnostic SDF.
|
|
320
|
+
|
|
321
|
+
### Type mapping
|
|
322
|
+
|
|
323
|
+
| SQL DDL type | SDF type |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `VARCHAR(n)`, `CHARACTER VARYING(n)`, bounded `CHAR(n)` | `string:n` |
|
|
326
|
+
| `TEXT`, `CLOB`, unbounded `NVARCHAR` | `text` |
|
|
327
|
+
| `INT`, `INTEGER`, `INT4` | `integer` |
|
|
328
|
+
| `BIGINT`, `INT8` | `bigint` |
|
|
329
|
+
| `DECIMAL(p,s)`, `NUMERIC(p,s)` | `decimal:p,s` |
|
|
330
|
+
| `BOOLEAN`, `BOOL`, legacy `TINYINT(1)` flag | `boolean` |
|
|
331
|
+
| `DATE` | `date` |
|
|
332
|
+
| `TIMESTAMP`, `TIMESTAMP WITHOUT TIME ZONE`, `DATETIME` | `timestamp` |
|
|
333
|
+
| `TIMESTAMPTZ`, `TIMESTAMP WITH TIME ZONE` | `timestamptz` (PostgreSQL only; on another target dialect stop and ask whether `timestamp` is acceptable) |
|
|
334
|
+
| `TIME` | `time` (not supported on Oracle) |
|
|
335
|
+
| `UUID`, `UNIQUEIDENTIFIER` | `uuid` |
|
|
336
|
+
| `JSON`, `JSONB` | `json` |
|
|
337
|
+
|
|
338
|
+
### Constraint mapping
|
|
339
|
+
|
|
340
|
+
| SQL DDL | SDF |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `PRIMARY KEY` | `pk` |
|
|
343
|
+
| `NOT NULL` | `notnull` |
|
|
344
|
+
| `UNIQUE` | `unique` |
|
|
345
|
+
| non-unique `INDEX`/`KEY` | `index` |
|
|
346
|
+
| `DEFAULT value` | `default:value` — same quoting rule as the catalog: string quoted, numeric/boolean raw, SQL constant bare, function e.g. `now()` |
|
|
347
|
+
| `CHECK (col IN (...))` | `checks: [{ field, in: [...] }]` |
|
|
348
|
+
| `CHECK (col =/!=/>/>=/</<= value)` | `checks: [{ field, eq/neq/gt/gte/lt/lte: value }]` |
|
|
349
|
+
| composite `PRIMARY KEY`/`UNIQUE` (multiple columns) | see `composite-primary-key.md` / `composite-unique.md` in the handbook catalog — do not improvise the syntax |
|
|
350
|
+
|
|
351
|
+
### Foreign keys — shorthand vs `relations`
|
|
352
|
+
|
|
353
|
+
Apply the same mutual-exclusivity rule as the catalog (`foreign-keys.md`):
|
|
354
|
+
- `FOREIGN KEY ... REFERENCES table(col)` with no `ON DELETE`/`ON UPDATE`
|
|
355
|
+
clause (or only the dialect default) → `fk:table.col` shorthand.
|
|
356
|
+
- `FOREIGN KEY ... REFERENCES table(col) ON DELETE x ON UPDATE y` with an
|
|
357
|
+
explicit, non-default action → a `relations` entry with that `onDelete`/
|
|
358
|
+
`onUpdate`. Do NOT also write the `fk:` shorthand on the same field — the
|
|
359
|
+
catalog rejects having both.
|
|
360
|
+
- The reference column is always the **actual column name** named in
|
|
361
|
+
`REFERENCES table(column)` — this is given explicitly in DDL, so there is no
|
|
362
|
+
`.id`-guessing risk here (unlike inferring FK targets from a UI dropdown).
|
|
363
|
+
|
|
364
|
+
### What NOT to add
|
|
365
|
+
|
|
366
|
+
Unlike the HTML/Image path, DDL is already a deliberate, complete spec from a
|
|
367
|
+
real system — do not apply Step 4's scaffolding defaults on top of it:
|
|
368
|
+
- Do **not** auto-add audit columns (`created_at`/`created_by`/`updated_at`/
|
|
369
|
+
`updated_by`) if the DDL doesn't have them. Their absence is the source
|
|
370
|
+
system's actual design, not a gap to fill.
|
|
371
|
+
- Do **not** convert the primary key strategy. If the DDL uses
|
|
372
|
+
`SERIAL`/`AUTO_INCREMENT`/`GENERATED ALWAYS AS IDENTITY` (integer PK), keep
|
|
373
|
+
`integer pk` — do not silently switch it to a `string:36` UUID PK because
|
|
374
|
+
that happens to be the convention used elsewhere.
|
|
375
|
+
|
|
376
|
+
### Confirm-list additions specific to DDL
|
|
377
|
+
|
|
378
|
+
- If the DDL already has `is_deleted`/`deleted_at`/`deleted_by` columns, this
|
|
379
|
+
is a soft-delete table — set `softDelete.enabled: true` and verify the
|
|
380
|
+
target dialect is PostgreSQL (Phase 1 only); flag a conflict explicitly if
|
|
381
|
+
the DDL's dialect is MySQL/Oracle/SQLite.
|
|
382
|
+
- If the DDL's original dialect differs from the project's target dialect
|
|
383
|
+
(e.g. migrating MySQL DDL into a PostgreSQL project), call out type/storage
|
|
384
|
+
differences (e.g. MySQL boolean-as-`TINYINT(1)`) — the SDF type stays the
|
|
385
|
+
dialect-agnostic logical type (`boolean`), the platform handles physical
|
|
386
|
+
storage per target dialect.
|
|
387
|
+
- Named constraints in the source DDL (`pk_xxx`, `fk_xxx`) are not preserved —
|
|
388
|
+
RESTForge generates its own deterministic constraint names on migrate. Note
|
|
389
|
+
this only matters if something outside RESTForge depends on the original
|
|
390
|
+
constraint names.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Worked example — categories.html
|
|
395
|
+
|
|
396
|
+
Design signals: table columns (Category, No of Items, Created On, Status,
|
|
397
|
+
Actions); Add/Edit form (Category Image *, Category Name *); filter (Status:
|
|
398
|
+
Active / Inactive).
|
|
399
|
+
|
|
400
|
+
Classification:
|
|
401
|
+
- `Category Name` → stored, required → `string:255 notnull`.
|
|
402
|
+
- `Category Image` → file upload → `string:500` (path/URL).
|
|
403
|
+
- `Status` → enum (Active/Inactive) → `string:20 default:'active'` + check.
|
|
404
|
+
- `No of Items` → **derived** (count of `items` with FK to category) → excluded
|
|
405
|
+
from `fields`; expose later via aggregate query or `hasMany` count.
|
|
406
|
+
- `Created On` → audit → `created_at`.
|
|
407
|
+
- `Actions` → UI only → ignored.
|
|
408
|
+
|
|
409
|
+
Resulting draft SDF:
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
defineModel("category", {
|
|
413
|
+
fields: {
|
|
414
|
+
category_id: "string:36 pk",
|
|
415
|
+
category_name: "string:255 notnull",
|
|
416
|
+
image_url: "string:500",
|
|
417
|
+
status: "string:20 notnull default:'active'",
|
|
418
|
+
created_at: "timestamp default:now()",
|
|
419
|
+
created_by: "string:100",
|
|
420
|
+
updated_at: "timestamp",
|
|
421
|
+
updated_by: "string:100"
|
|
422
|
+
},
|
|
423
|
+
checks: [
|
|
424
|
+
{ field: "status", in: ["active", "inactive"] }
|
|
425
|
+
]
|
|
426
|
+
})
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Confirm before migrate: `category_name` length (255?), `status` option values,
|
|
430
|
+
PK convention (uuid vs serial), and that `No of Items` is intentionally derived,
|
|
431
|
+
not stored.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Worked example — items.html (master-detail + lookup decision)
|
|
436
|
+
|
|
437
|
+
Design signals: Add Item form (Item Image *, Item Name *, Description *,
|
|
438
|
+
Price *, Net Price *, Category * dropdown, Tax * dropdown); two repeatable
|
|
439
|
+
accordion sections inside the same form — "Variations" (Size *, Price * per
|
|
440
|
+
row) and "Add Ons" (Name *, Price($) * per row); item cards in the list show
|
|
441
|
+
a Veg/Non Veg badge not present anywhere in the Add form.
|
|
442
|
+
|
|
443
|
+
This source exercises three rules the categories.html example doesn't:
|
|
444
|
+
FK reference correctness, the lookup-master-vs-enum decision on two fields
|
|
445
|
+
from the *same* form, and master-detail decomposition.
|
|
446
|
+
|
|
447
|
+
Classification:
|
|
448
|
+
- `Item Name`, `Description`, `Price`, `Net Price` → stored, required.
|
|
449
|
+
- `Item Image` → file upload → `string:255`.
|
|
450
|
+
- `Category *` → **FK to a master table**. Decisive signal: a dedicated
|
|
451
|
+
`categories.html` admin page exists for it elsewhere in the same design set.
|
|
452
|
+
→ `category_id string:36 fk:category.category_id notnull` — the target
|
|
453
|
+
column is `category.category_id` (the table's actual PK), not `category.id`.
|
|
454
|
+
- `Tax *` → **FK to a master table**, same reasoning — a dedicated
|
|
455
|
+
`tax-settings.html` page exists. → `tax_id string:36 fk:tax.tax_id notnull`.
|
|
456
|
+
The `tax` table itself is derived separately from its own admin page (title,
|
|
457
|
+
rate, tax type), not invented here.
|
|
458
|
+
- `Veg / Non Veg` badge → **enum**, even though it appears only in the list,
|
|
459
|
+
not the Add form. It's a small, permanent, intrinsic set with no admin page
|
|
460
|
+
of its own (contrast with `Tax`, which looks similar — a fixed-looking
|
|
461
|
+
label — but does have one). → `food_type string:20 default:'non_veg'` +
|
|
462
|
+
`checks: [{ field: 'food_type', in: ['veg', 'non_veg'] }]`.
|
|
463
|
+
- "Variations" accordion (repeatable Size + Price rows) → **master-detail
|
|
464
|
+
child table**, not columns on `item`. One row in the design = one row in a
|
|
465
|
+
new `item_variation` table with `item_id` FK back to `item`.
|
|
466
|
+
- "Add Ons" accordion (repeatable Name + Price rows) → same pattern, a second
|
|
467
|
+
child table `item_addon`.
|
|
468
|
+
|
|
469
|
+
Resulting draft SDF (four tables — `tax` is shown in outline; the master
|
|
470
|
+
entity and both detail tables in full):
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
defineModel("tax", {
|
|
474
|
+
fields: {
|
|
475
|
+
tax_id: "string:36 pk",
|
|
476
|
+
title: "string:100 notnull",
|
|
477
|
+
rate: "decimal:5,2 notnull",
|
|
478
|
+
tax_type: "string:20 notnull default:'exclusive'",
|
|
479
|
+
created_at: "timestamp default:now()",
|
|
480
|
+
created_by: "string:100",
|
|
481
|
+
updated_at: "timestamp",
|
|
482
|
+
updated_by: "string:100"
|
|
483
|
+
},
|
|
484
|
+
checks: [
|
|
485
|
+
{ field: "tax_type", in: ["inclusive", "exclusive"] }
|
|
486
|
+
]
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
defineModel("item", {
|
|
490
|
+
fields: {
|
|
491
|
+
item_id: "string:36 pk",
|
|
492
|
+
category_id: "string:36 fk:category.category_id notnull",
|
|
493
|
+
item_name: "string:150 notnull",
|
|
494
|
+
description: "text notnull",
|
|
495
|
+
item_image: "string:255 notnull",
|
|
496
|
+
food_type: "string:20 notnull default:'non_veg'",
|
|
497
|
+
price: "decimal:10,2 notnull",
|
|
498
|
+
net_price: "decimal:10,2 notnull",
|
|
499
|
+
tax_id: "string:36 fk:tax.tax_id notnull",
|
|
500
|
+
created_at: "timestamp default:now()",
|
|
501
|
+
created_by: "string:100",
|
|
502
|
+
updated_at: "timestamp",
|
|
503
|
+
updated_by: "string:100"
|
|
504
|
+
},
|
|
505
|
+
checks: [
|
|
506
|
+
{ field: "food_type", in: ["veg", "non_veg"] }
|
|
507
|
+
]
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
defineModel("item_variation", {
|
|
511
|
+
fields: {
|
|
512
|
+
item_variation_id: "string:36 pk",
|
|
513
|
+
item_id: "string:36 fk:item.item_id notnull",
|
|
514
|
+
size_name: "string:50 notnull",
|
|
515
|
+
price: "decimal:10,2 notnull",
|
|
516
|
+
created_at: "timestamp default:now()",
|
|
517
|
+
created_by: "string:100",
|
|
518
|
+
updated_at: "timestamp",
|
|
519
|
+
updated_by: "string:100"
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
defineModel("item_addon", {
|
|
524
|
+
fields: {
|
|
525
|
+
item_addon_id: "string:36 pk",
|
|
526
|
+
item_id: "string:36 fk:item.item_id notnull",
|
|
527
|
+
addon_name: "string:100 notnull",
|
|
528
|
+
price: "decimal:10,2 notnull",
|
|
529
|
+
created_at: "timestamp default:now()",
|
|
530
|
+
created_by: "string:100",
|
|
531
|
+
updated_at: "timestamp",
|
|
532
|
+
updated_by: "string:100"
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Confirm before migrate: `item_name`/`item_image` lengths, `food_type` default
|
|
538
|
+
value, whether `tax` needs more fields than the admin page showed, and that
|
|
539
|
+
both accordions are intentionally separate tables rather than columns on
|
|
540
|
+
`item`.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Worked example — Order Details (no-form fallback + joined attribute + snapshot)
|
|
545
|
+
|
|
546
|
+
Design signals: a read-only "Order Details" page — **no Add/Edit form is
|
|
547
|
+
present in this source**. Panels: Order Info (Date Added, Payment Method,
|
|
548
|
+
Status), Customer Details (Name, Email, Phone, Country, State, Address),
|
|
549
|
+
Order Items (Product, Qty, Unit Price → line Total), summary (Subtotal, Tax,
|
|
550
|
+
Shipping Rate, Grand Total).
|
|
551
|
+
|
|
552
|
+
This source exercises three rules the previous two examples don't: the
|
|
553
|
+
no-form fallback, the joined-attribute exclusion, and the
|
|
554
|
+
derived-vs-snapshot distinction — including two fields (Address vs
|
|
555
|
+
Name/Email/Phone) that look identical in presentation but classify oppositely.
|
|
556
|
+
|
|
557
|
+
Classification:
|
|
558
|
+
- **No form in this source** → state that explicitly per the Step 1 fallback,
|
|
559
|
+
proceed with this detail page as the best available signal rather than
|
|
560
|
+
silently treating it as equivalent to a form.
|
|
561
|
+
- `Payment Method` → **enum**, not a master table. No admin/settings page or
|
|
562
|
+
quick-add affordance is present anywhere in this source.
|
|
563
|
+
- `Status` → enum, closed permanent set (`pending`/`processing`/`completed`/
|
|
564
|
+
`cancelled`).
|
|
565
|
+
- `Customer Name`, `Email`, `Phone` → **joined attribute**. These belong to
|
|
566
|
+
`customer`, reachable through `customer_id`; not stored on `order`.
|
|
567
|
+
- `Country`, `State`, `Address` → **snapshot**, not joined — even though they
|
|
568
|
+
look like the same kind of "customer info" as Name/Email/Phone just above
|
|
569
|
+
them. The deciding question: could the customer's saved address change
|
|
570
|
+
later while this order must still show where it actually shipped? Yes →
|
|
571
|
+
must stay frozen on the order record.
|
|
572
|
+
- `Product`, `Qty` → relation (FK to `product`) + stored quantity, on a
|
|
573
|
+
master-detail child table `order_item` (one row per line, same pattern as
|
|
574
|
+
`item_variation` in the previous example).
|
|
575
|
+
- `Unit Price` (per line) and `Subtotal`/`Tax`/`Shipping Rate`/`Grand Total`
|
|
576
|
+
(header summary) → **snapshot**. All are computed, but must not silently
|
|
577
|
+
change if the product's current price changes after the order is placed.
|
|
578
|
+
|
|
579
|
+
Resulting draft SDF:
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
defineModel("order", {
|
|
583
|
+
fields: {
|
|
584
|
+
order_id: "string:36 pk",
|
|
585
|
+
order_number: "string:20 notnull unique",
|
|
586
|
+
customer_id: "string:36 fk:customer.customer_id notnull",
|
|
587
|
+
payment_method: "string:20 notnull",
|
|
588
|
+
status: "string:20 notnull default:'pending'",
|
|
589
|
+
shipping_address: "text notnull",
|
|
590
|
+
shipping_country: "string:100 notnull",
|
|
591
|
+
shipping_state: "string:100 notnull",
|
|
592
|
+
subtotal: "decimal:15,2 notnull",
|
|
593
|
+
tax_amount: "decimal:15,2 notnull default:0",
|
|
594
|
+
shipping_rate: "decimal:15,2 notnull default:0",
|
|
595
|
+
grand_total: "decimal:15,2 notnull",
|
|
596
|
+
created_at: "timestamp default:now()",
|
|
597
|
+
created_by: "string:100"
|
|
598
|
+
},
|
|
599
|
+
checks: [
|
|
600
|
+
{ field: "payment_method", in: ["credit_card", "bank_transfer", "cash", "e_wallet"] },
|
|
601
|
+
{ field: "status", in: ["pending", "processing", "completed", "cancelled"] }
|
|
602
|
+
]
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
defineModel("order_item", {
|
|
606
|
+
fields: {
|
|
607
|
+
order_item_id: "string:36 pk",
|
|
608
|
+
order_id: "string:36 fk:order.order_id notnull",
|
|
609
|
+
product_id: "string:36 fk:product.product_id notnull",
|
|
610
|
+
qty: "integer notnull",
|
|
611
|
+
unit_price: "decimal:15,2 notnull"
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Confirm before migrate: `payment_method` option set (this source gave no
|
|
617
|
+
admin-page signal — verify with the user whether the real system manages it
|
|
618
|
+
as a master table instead), `order_number` format/length, PK convention, and
|
|
619
|
+
that Name/Email/Phone were intentionally excluded as joined while
|
|
620
|
+
Address/totals were intentionally kept as snapshot — these two decisions look
|
|
621
|
+
similar on the screen but are opposite in the schema.
|