sonamu 0.10.4 → 0.10.6

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.
Files changed (82) hide show
  1. package/dist/bin/cli.js +206 -187
  2. package/dist/cone/cone-generator.js +3 -9
  3. package/dist/database/knex.d.ts.map +1 -1
  4. package/dist/database/knex.js +2 -2
  5. package/dist/database/puri.d.ts +17 -0
  6. package/dist/database/puri.d.ts.map +1 -1
  7. package/dist/database/puri.js +102 -3
  8. package/dist/migration/code-generation.js +6 -6
  9. package/dist/migration/migrator.d.ts.map +1 -1
  10. package/dist/migration/migrator.js +51 -6
  11. package/dist/ui-web/assets/{index-CDd6xT-F.js → index-D0MHYbxl.js} +2 -2
  12. package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
  13. package/dist/ui-web/index.html +2 -2
  14. package/package.json +2 -2
  15. package/src/bin/cli.ts +283 -282
  16. package/src/cone/cone-generator.ts +2 -18
  17. package/src/database/__tests__/puri.test.ts +183 -0
  18. package/src/database/knex.ts +4 -1
  19. package/src/database/puri.ts +229 -2
  20. package/src/database/puri.types.test-d.ts +56 -0
  21. package/src/migration/code-generation.ts +5 -5
  22. package/src/migration/migrator.ts +81 -7
  23. package/src/skills/AGENTS.md +76 -50
  24. package/src/skills/sonamu/SKILL.md +46 -227
  25. package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
  26. package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
  27. package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
  28. package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
  29. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
  30. package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
  31. package/src/skills/sonamu-config/SKILL.md +203 -0
  32. package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
  33. package/src/skills/sonamu-config/references/environments.md +178 -0
  34. package/src/skills/sonamu-config/references/server-options.md +400 -0
  35. package/src/skills/sonamu-entity/SKILL.md +180 -0
  36. package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
  37. package/src/skills/sonamu-entity/references/design-guides.md +243 -0
  38. package/src/skills/sonamu-entity/references/field-types.md +170 -0
  39. package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
  40. package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
  41. package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
  42. package/src/skills/sonamu-fixture/SKILL.md +180 -0
  43. package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
  44. package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
  45. package/src/skills/sonamu-frontend/SKILL.md +142 -0
  46. package/src/skills/sonamu-frontend/references/components.md +323 -0
  47. package/src/skills/sonamu-frontend/references/examples.md +64 -0
  48. package/src/skills/sonamu-frontend/references/hooks.md +273 -0
  49. package/src/skills/sonamu-frontend/references/runtime.md +165 -0
  50. package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
  51. package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
  52. package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
  53. package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
  54. package/src/skills/sonamu-query/SKILL.md +48 -0
  55. package/src/skills/sonamu-query/references/model-patterns.md +390 -0
  56. package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
  57. package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
  58. package/src/skills/sonamu-query/references/search.md +238 -0
  59. package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
  60. package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
  61. package/src/skills/sonamu-testing/SKILL.md +251 -0
  62. package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
  63. package/src/skills/sonamu-testing/references/helpers.md +185 -0
  64. package/src/skills/sonamu-testing/references/patterns.md +263 -0
  65. package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
  66. package/src/skills/sonamu-testing/references/quick-start.md +285 -0
  67. package/src/skills/sonamu-testing/references/type-safety.md +172 -0
  68. package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
  69. package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
  70. package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
  71. package/src/skills/commands/sonamu-skills.md +0 -20
  72. package/src/skills/project/README.md +0 -19
  73. package/src/skills/project/architecture.md +0 -373
  74. package/src/skills/sonamu/cdd.md +0 -129
  75. package/src/skills/sonamu/config.md +0 -772
  76. package/src/skills/sonamu/create-sonamu.md +0 -208
  77. package/src/skills/sonamu/entity-basic.md +0 -678
  78. package/src/skills/sonamu/framework-change.md +0 -96
  79. package/src/skills/sonamu/frontend.md +0 -915
  80. package/src/skills/sonamu/project-init.md +0 -477
  81. package/src/skills/sonamu/skill-contribution.md +0 -247
  82. package/src/skills/sonamu/testing.md +0 -2163
@@ -1,678 +0,0 @@
1
- ---
2
- name: sonamu-entity-basic
3
- description: Reference for creating or modifying Sonamu entities. Field types, requirements analysis, parent-child relationships, OrderBy/Enum rules. Use when creating or modifying entities. See entity-validation-checklist.md for the validation checklist.
4
- ---
5
-
6
- # Entity Basics
7
-
8
- **Working code references:**
9
-
10
- - `sonamu/examples/miomock/api/src/application/user/user.entity.json` — basic entity example
11
- - `sonamu/examples/miomock/api/src/application/project/project.entity.json` — complex entity example
12
- - `sonamu/examples/miomock/api/src/application/employee/employee.entity.json` — BelongsToOne relationship example
13
-
14
- ## When a User Requests a New System
15
-
16
- When the user requests a system to be built, proceed in the following order:
17
-
18
- **1. Analyze requirements** (identify missing entities)
19
-
20
- - "Do you need a User entity?"
21
- - "Are there any other entities needed?"
22
-
23
- **2. Confirm relationships between entities** (one question at a time)
24
-
25
- - "Is A to B a 1:N or N:M relationship?"
26
- - "Should chapters be managed as children of courses?"
27
-
28
- **3. Decide whether to use parentId**
29
-
30
- - "Can it exist without a parent?"
31
- - "Should it be created and deleted together with the parent?"
32
-
33
- **4. Final confirmation with the user**
34
-
35
- - Finalize entity list
36
- - Provide a relationship diagram or clear description
37
-
38
- ### Entity Design Done Checklist
39
-
40
- - [ ] All required entities identified
41
- - [ ] Relationships between entities defined
42
- - [ ] parentId usage decided
43
- - [ ] User confirmation complete
44
-
45
- **When done:** proceed to "Entity Creation Workflow"
46
-
47
- **Full workflow reference:** `.claude/workflow/project_init.md`
48
-
49
- ---
50
-
51
- ## Entity Creation Workflow
52
-
53
- **Prerequisite: CRITICAL!**
54
-
55
- **Always run `pnpm dev` in `/packages/api` before starting!**
56
-
57
- ```bash
58
- cd packages/api
59
- pnpm dev # keep this running during all work
60
- ```
61
-
62
- > **Why**: In dev mode, the syncer detects changes to entity.json and auto-generates types.ts.
63
- >
64
- > Dev mode is required **for all entity creation**, not just auth entities.
65
-
66
- ### Step 1: Generate stub
67
-
68
- **CRITICAL: EntityId must always start with an uppercase letter!**
69
-
70
- ```bash
71
- pnpm sonamu stub entity {EntityId}
72
- ```
73
-
74
- **Correct examples:**
75
-
76
- - `pnpm sonamu stub entity Course` (correct)
77
- - `pnpm sonamu stub entity User` (correct)
78
- - `pnpm sonamu stub entity ConsultationHistory` (correct)
79
-
80
- **Incorrect examples:**
81
-
82
- - `pnpm sonamu stub entity course` (wrong — starts with lowercase)
83
- - `pnpm gen stub entity Course` (wrong — incorrect command)
84
-
85
- Generated file: `api/src/application/{entity}/{entity}.entity.json`
86
-
87
- ### Step 2: Edit the stub file
88
-
89
- Add props, relations, and subsets to the generated entity.json file.
90
-
91
- ### Step 3: Validate and generate required files
92
-
93
- **CRITICAL: Always validate before running sync!**
94
-
95
- **A. Validate entity.json** (see `entity-validation-checklist.md` PHASE 1)
96
-
97
- - [ ] Does every index have a `type` field?
98
- - [ ] Does the subset use `relation.id` format instead of directly referencing FK columns?
99
- - [ ] Is the Boolean `dbDefault` a string (`"true"` / `"false"`)?
100
- - [ ] Does the OrderBy enum contain only `id-desc`?
101
- - [ ] Is the Enum `dbDefault` using escaped double quotes? (e.g., `"\"pending\""`)
102
-
103
- **B. model.ts (must be created manually)**
104
-
105
- - Must be created manually
106
- - Reference another entity's model.ts as a template
107
- - Required methods: findById, findOne, findMany, save, del
108
- - See `entity-validation-checklist.md` PHASE 2 for template
109
-
110
- **C. types.ts (auto-generated — wait for it)**
111
-
112
- - **If `pnpm dev` is running**, the syncer will auto-generate it within 2–3 seconds
113
- - Check: `ls packages/api/src/application/{entity}/{entity}.types.ts`
114
- - If not generated:
115
- 1. Verify `pnpm dev` is running
116
- 2. If still not generated, create manually (see `entity-validation-checklist.md` PHASE 2 for template)
117
-
118
- **Done criteria:**
119
-
120
- - [ ] entity.json validation passed
121
- - [ ] model.ts exists
122
- - [ ] types.ts exists (auto-generated or manually created)
123
-
124
- ### Step 4: Sync
125
-
126
- ```bash
127
- pnpm sonamu sync
128
- ```
129
-
130
- **Note:** Do not write entity JSON by hand. Always generate using the stub command, then edit.
131
-
132
- ### Step 5: Migration and Scaffolding
133
-
134
- 1. Generate and apply migration
135
- 2. Run scaffolding
136
- 3. Test the build
137
-
138
- **Full workflow:** see step-by-step checklist in `entity-validation-checklist.md`
139
-
140
- ### Step 6: Handle nullable fields in types.ts (required)
141
-
142
- **CRITICAL: Do this immediately after scaffolding, before writing tests!**
143
-
144
- After scaffolding completes, nullable fields in the generated `*.types.ts` must be handled.
145
-
146
- ```typescript
147
- // Auto-generated types.ts
148
- export const FAQSaveParams = FAQBaseSchema.partial({
149
- id: true,
150
- created_at: true,
151
- });
152
-
153
- // CORRECT: update immediately — add nullable fields
154
- export const FAQSaveParams = FAQBaseSchema.partial({
155
- id: true,
156
- created_at: true,
157
- category: true, // nullable added
158
- order_num: true, // nullable added
159
- }).extend({
160
- category: z.string().nullish(),
161
- order_num: z.number().nullish(),
162
- updated_at: z.date().nullish(),
163
- });
164
- ```
165
-
166
- **Detailed guide:** see "Actions to take immediately after entity creation" in `testing.md`
167
-
168
- ## Checklist for New Entity Creation
169
-
170
- 1. **id**: PascalCase (e.g., `User`, `BlogPost`)
171
- 2. **table**: snake_case plural (e.g., `users`, `blog_posts`) — can be omitted
172
- 3. **title**: display name
173
- 4. **Recommended props**: `id`, `created_at` (not enforced by schema but best practice)
174
- 5. **Recommended enums**: `{EntityId}OrderBy`, `{EntityId}SearchField` (not enforced but best practice)
175
-
176
- ## IMPORTANT: Analyze Requirements Before Creating Entity
177
-
178
- **STOP! Ask questions one at a time before creating any entity.**
179
-
180
- ### Identify missing entities
181
-
182
- Do not only create entities explicitly mentioned by the user. **Ask one at a time:**
183
-
184
- - "Do you need a User entity?" → wait for response
185
- - "Does the User have multiple roles?" → wait for response
186
- - "Are there any other entities needed?" → wait for response
187
-
188
- **Note on User entity**: `id` is an auto-increment sequence (PK) and is not a login ID. When using better-auth, a separate `login_id` is not needed (managed by the auth table).
189
-
190
- **Commonly missed entities**: Content (Comment, Like, Tag, Category), Commerce (Review, Cart, Payment), Reservation (Reservation, Schedule), Education (Enrollment, Progress)
191
-
192
- ### When multiple entities are requested — confirm relationships
193
-
194
- When 2+ entities are requested, **ask about relationships one at a time before writing any code**:
195
-
196
- - Which relationship type: BelongsToOne, HasMany, ManyToMany, or parentId
197
- - Whether it is a parent-child dependency (delete together) or independent
198
-
199
- ### Always confirm before designing
200
-
201
- **1. Polymorphic Association** (`entity_type + entity_id` pattern):
202
-
203
- - If there is a string PK entity (e.g., better-auth User) → use `string` type for `entity_id` uniformly
204
- - Otherwise → `integer` is fine
205
-
206
- **2. Domain term ↔ entity English ID mapping**: finalize with the user before writing any code (e.g., "위탁연구과제" → `ResearchContract`). Changing this later requires a full rename.
207
-
208
- ## Parent-Child Relationships (parentId)
209
-
210
- ### What is parentId?
211
-
212
- A top-level option used when a child entity is managed as a dependent of a parent entity.
213
-
214
- - With parentId: the child has no independent CRUD — it is created, updated, and deleted through the parent
215
- - Without parentId: an independent entity with its own CRUD
216
-
217
- ### When to use parentId
218
-
219
- | Situation | parentId | Example |
220
- | ---------------------------------------- | -------- | ---------------------------------- |
221
- | Cannot exist without a parent | Yes | OrderItem → Order |
222
- | Created and deleted together with parent | Yes | Chapter → Course, Lesson → Chapter |
223
- | Can be independently CRUD'd | No | Comment → Post |
224
- | Can belong to multiple parents | No | Tag → Post (ManyToMany) |
225
-
226
- ### parentId usage example
227
-
228
- ```json
229
- {
230
- "id": "OrderItem",
231
- "table": "order_items",
232
- "title": "Order Item",
233
- "parentId": "Order",
234
- "props": [...]
235
- }
236
- ```
237
-
238
- ### Child entities with parentId do not generate types.ts
239
-
240
- Child entities with parentId set (e.g., Chapter, Lesson) do not get their own `types.ts` file. This is expected behavior — child entities are managed through the parent. If you need independent CRUD and types.ts, do not use parentId.
241
-
242
- ### Folder location for parentId child entities
243
-
244
- Child entities with parentId must be placed **in the same folder as the root parent entity**.
245
-
246
- | Structure | Description |
247
- | ---------------------------- | ------------------------------------------------- |
248
- | `course/course.entity.json` | Root entity |
249
- | `course/chapter.entity.json` | parentId: "Course" → same folder |
250
- | `course/lesson.entity.json` | parentId: "Chapter" → same folder (based on root) |
251
- | `course/course.types.ts` | types.ts generated only for root |
252
-
253
- **Note:** Do not create child entities in a separate folder (e.g., `chapter/chapter.entity.json`).
254
-
255
- ### IMPORTANT: When Uncertain - Ask User (Never Guess)
256
-
257
- **Do not guess — ask.** In situations like the following, ask the user directly:
258
-
259
- - "Should chapters be managed as children of courses, or created as an independent entity?"
260
- - "Should order items be saved together with the order, or managed separately?"
261
-
262
- **When in doubt, ask. One question is better than a wrong design.**
263
-
264
- **Helpful questions to ask the user:**
265
-
266
- - "Will this data ever need to be queried or updated independently without a parent?"
267
- - "Should this data be deleted when the parent is deleted?"
268
- - "Does the admin UI need a separate list page for this?"
269
-
270
- ## Minimal Template
271
-
272
- ```json
273
- {
274
- "id": "Product",
275
- "table": "products",
276
- "title": "Product",
277
- "props": [
278
- { "name": "id", "type": "integer", "desc": "ID" },
279
- {
280
- "name": "created_at",
281
- "type": "date",
282
- "dbDefault": "CURRENT_TIMESTAMP",
283
- "desc": "Created at"
284
- },
285
- { "name": "name", "type": "string", "length": 255, "desc": "Product name" }
286
- ],
287
- "indexes": [],
288
- "subsets": { "A": ["id", "name", "created_at"] },
289
- "enums": {
290
- "ProductOrderBy": { "id-desc": "Newest" },
291
- "ProductSearchField": { "id": "ID", "name": "Name" }
292
- }
293
- }
294
- ```
295
-
296
- ## Situation-Specific Guides
297
-
298
- ### Adding a string field
299
-
300
- ```json
301
- { "name": "title", "type": "string", "length": 255, "desc": "Title" }
302
- ```
303
-
304
- - Omitting `length` → stored as `text` type (for long text)
305
-
306
- ### Adding an enum field
307
-
308
- ```json
309
- // 1. Add to props
310
- { "name": "status", "type": "enum", "id": "ProductStatus", "desc": "Status" }
311
-
312
- // 2. Define in enums (MUST — missing this causes errors)
313
- "ProductStatus": { "draft": "Draft", "published": "Published", "archived": "Archived" }
314
- ```
315
-
316
- ### IMPORTANT: Always use enum for fixed-value fields
317
-
318
- Defining a field with a fixed set of choices as `string` breaks DB integrity.
319
-
320
- **Rule: "Can this value be freely entered from outside the code?"** No → enum, Yes → string
321
-
322
- **Enum candidates**: strings that look like `faker.helpers.arrayElement([...])`, fields described as "one of the following / type / category", select boxes / radio buttons
323
-
324
- ```json
325
- // WRONG: defined as string
326
- { "name": "budget_item", "type": "string", "desc": "Budget item" }
327
- // CORRECT: defined as enum
328
- { "name": "budget_item", "type": "enum", "id": "BudgetItem", "desc": "Budget item" }
329
- ```
330
-
331
- ### Adding a nullable field
332
-
333
- ```json
334
- { "name": "deleted_at", "type": "date", "nullable": true, "desc": "Deleted at" }
335
- ```
336
-
337
- **CRITICAL: Importance of the nullable attribute**
338
-
339
- A field without `nullable: true` is treated as **required**.
340
-
341
- Sonamu's `ubUpsert` uses PostgreSQL `ON CONFLICT ... DO UPDATE`, so **all required fields** must be included even on updates.
342
-
343
- ```json
344
- // example
345
- {
346
- "props": [
347
- { "name": "title", "type": "string" }, // required (no nullable)
348
- { "name": "content", "type": "string" }, // required (no nullable)
349
- { "name": "category", "type": "string", "nullable": true } // optional
350
- ]
351
- }
352
- ```
353
-
354
- **Rules**:
355
-
356
- - Do not add `nullable: true` to fields that are not optional
357
- - Always specify `nullable: true` for optional fields
358
- - Required fields must always have a value in tests and API calls
359
-
360
- **Details:** see "Quick Start" in `testing.md` and "CRITICAL: Required fields must be included" in `upsert.md`
361
-
362
- ### Adding a JSON field
363
-
364
- ```json
365
- {
366
- "name": "metadata",
367
- "type": "json",
368
- "id": "ProductMetadata",
369
- "desc": "Metadata"
370
- }
371
- ```
372
-
373
- - `id` is required (used as the type name)
374
- - A separate TypeScript type definition is needed
375
-
376
- ### Adding a searchText field (for pg_trgm Fuzzy Search)
377
-
378
- A dedicated prop type that consolidates multiple columns into a single generated column. Used with a GIN index.
379
-
380
- ```json
381
- {
382
- "props": [
383
- { "name": "title_ko", "type": "string" },
384
- { "name": "title_en", "type": "string" },
385
- { "name": "tags", "type": "string[]" },
386
- {
387
- "name": "search_text",
388
- "type": "searchText",
389
- "sourceColumns": [
390
- { "name": "title_ko" },
391
- { "name": "title_en", "caseInsensitive": true },
392
- { "name": "tags" }
393
- ]
394
- }
395
- ],
396
- "indexes": [
397
- {
398
- "name": "idx_items_search_text",
399
- "type": "index",
400
- "columns": [{ "name": "search_text", "opclass": "gin_trgm_ops" }],
401
- "using": "gin"
402
- }
403
- ]
404
- }
405
- ```
406
-
407
- SQL expressions per source column type:
408
-
409
- | source type | caseInsensitive: true | caseInsensitive: false (default) |
410
- | ---------------------------- | ----------------------------- | ------------------------------------ |
411
- | `string` | `lower(COALESCE(col, ''))` | `COALESCE(col, '')` |
412
- | `string[]` | `sonamu_text_array_agg(col)` | `sonamu_text_array_agg(col, false)` |
413
- | `json` (z.array(z.string())) | `sonamu_jsonb_array_agg(col)` | `sonamu_jsonb_array_agg(col, false)` |
414
-
415
- - If a `string[]` or `json(string[])` source is present, helper function DDL is automatically inserted in the migration
416
- - `searchText` columns are generated columns and are excluded from SaveParams — INSERT/UPDATE is not allowed
417
- - For query usage: see the "pg_trgm Fuzzy Search" section in `puri.md`
418
-
419
- ### Adding a unique constraint
420
-
421
- ```json
422
- {
423
- "name": "products_sku_unique",
424
- "type": "unique",
425
- "columns": [{ "name": "sku" }]
426
- }
427
- ```
428
-
429
- ### Composite unique constraint
430
-
431
- ```json
432
- {
433
- "name": "cart_items_unique",
434
- "type": "unique",
435
- "columns": [{ "name": "user_id" }, { "name": "product_id" }]
436
- }
437
- ```
438
-
439
- ### Partial index (`where`)
440
-
441
- `where` declares a PostgreSQL partial index predicate. Provide a raw SQL condition **without** the `WHERE` keyword; it is appended to the generated `CREATE INDEX`. Works for every index type (`index`, `unique`, `hnsw`, `ivfflat`, pgroonga).
442
-
443
- ```json
444
- {
445
- "name": "uniq_users_email_active",
446
- "type": "unique",
447
- "columns": [{ "name": "email" }],
448
- "where": "deleted_at IS NULL"
449
- }
450
- ```
451
-
452
- → `CREATE UNIQUE INDEX uniq_users_email_active ON users (email) WHERE deleted_at IS NULL;`
453
- (Enforces email uniqueness only among non-deleted rows.)
454
-
455
- ### `nullsNotDistinct` (unique only)
456
-
457
- By default PostgreSQL treats `NULL`s as distinct, so a unique index allows multiple `NULL` rows. Set `nullsNotDistinct: true` to emit `NULLS NOT DISTINCT`, treating `NULL`s as equal (at most one `NULL` allowed).
458
-
459
- ```json
460
- {
461
- "name": "uniq_accounts_external_id",
462
- "type": "unique",
463
- "columns": [{ "name": "external_id" }],
464
- "nullsNotDistinct": true
465
- }
466
- ```
467
-
468
- → `CREATE UNIQUE INDEX uniq_accounts_external_id ON accounts (external_id) NULLS NOT DISTINCT;`
469
-
470
- ### IMPORTANT: Use the actual DB column name in indexes
471
-
472
- **The way FK columns are referenced differs between indexes and subsets. Do not confuse them.**
473
-
474
- | Location | Format | Example |
475
- | --------- | -------------------------- | ------------------------------------- |
476
- | `indexes` | Actual DB column name | `role_id`, `user_id`, `department_id` |
477
- | `subsets` | FieldExpr (relation.field) | `role.id`, `user.id`, `department.id` |
478
-
479
- **DO NOT:**
480
-
481
- ```json
482
- // Using FieldExpr in indexes → error
483
- "indexes": [
484
- { "name": "ix_role", "type": "index", "columns": [{ "name": "role.id" }] }
485
- ]
486
- ```
487
-
488
- **DO:**
489
-
490
- ```json
491
- // indexes use actual DB column names
492
- "indexes": [
493
- { "name": "ix_role_id", "type": "index", "columns": [{ "name": "role_id" }] }
494
- ]
495
-
496
- // subsets use FieldExpr
497
- "subsets": {
498
- "A": ["id", "role.id", "role.name"]
499
- }
500
- ```
501
-
502
- ### IMPORTANT: Unique constraints based on business rules
503
-
504
- Not a technical decision — ask **"What if the same combination is inserted twice?"** → if it should error, use unique; if it should be allowed, use index only.
505
-
506
- **Patterns that need composite unique**: per-year settings (`type, dept_id, year`), user-role mappings, per-year budgets (`project_id, year, budget_item`), likes/bookmarks (`user_id, entity_id`)
507
-
508
- ## Common Mistakes
509
-
510
- | Mistake | Fix |
511
- | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
512
- | Missing `id` prop | Recommended to add (needed by most Model logic) |
513
- | Missing `created_at` prop | Recommended to add with `dbDefault: "CURRENT_TIMESTAMP"` |
514
- | Missing `OrderBy` enum | Add `{EntityId}OrderBy` (needed for findMany sorting) |
515
- | Missing `SearchField` enum | Add `{EntityId}SearchField` (needed for search) |
516
- | enum prop `id` not defined in enums | Add definition to the enums section |
517
- | Missing `id` on json prop | Add the `id` field |
518
- | Using `"type": "text"` directly | `text` is invalid. Use `"type": "string"` without a length |
519
- | Adding multiple values to `OrderBy` enum | **Default is `id-desc` only** (see below) |
520
- | Defining fixed-choice fields as `string` | Convert to enum (check for fields with arrayElement-style fixtureGenerator) |
521
- | Yearly/mapping tables without unique constraints | Add composite unique based on business rules |
522
- | Using `number` type for integer fields | Use `integer` (use `numeric` only when decimal precision is needed) |
523
- | Using `role.id` format in indexes | indexes use actual DB column name (`role_id`); only subsets use FieldExpr (`role.id`) |
524
-
525
- ## Resolving Entity Schema Validation Errors
526
-
527
- **→ See `entity-validation-checklist.md` PHASE 1** (missing index type, Subset FieldExpr, duplicate columns, Boolean dbDefault, etc.)
528
-
529
- **Quick checklist:**
530
-
531
- - [ ] Does every index have a `type` field? (`"index"` | `"unique"` | `"hnsw"` | `"ivfflat"`)
532
- - [ ] Does the subset reference FK using `relation.id` format? (`user_id` ✗ → `user.id` ✓)
533
- - [ ] No duplicate definition of BelongsToOne relation and FK column?
534
- - [ ] Is Boolean `dbDefault` a string (`"true"` / `"false"`)? (0, 1 ✗)
535
- - [ ] Are all fields included in Subset A?
536
- - [ ] Do index columns use actual DB column names (`role_id`)? (FieldExpr `role.id` ✗)
537
-
538
- ## IMPORTANT: OrderBy Enum Generation Rule
539
-
540
- **IMPORTANT: It is strongly recommended to use only `id-desc` during initial scaffolding.**
541
-
542
- ```json
543
- // RECOMMENDED — for initial scaffolding
544
- "ProductOrderBy": { "id-desc": "Newest" }
545
-
546
- // AVOID — do not add these before scaffolding
547
- "ProductOrderBy": { "id-desc": "Newest", "name-asc": "Name (A-Z)", "created_at-desc": "Newest first" }
548
- ```
549
-
550
- ### Why only id-desc?
551
-
552
- The model code generated by scaffolding handles only `id-desc` automatically. If the OrderBy enum has other values:
553
-
554
- 1. Scaffolding succeeds, but a type error occurs in the model's `exhaustive()` function
555
- 2. The developer must manually add cases to the model
556
- 3. If this is missed, a runtime error may occur
557
-
558
- **This is not a technical constraint — it is scaffolding best practice.** Add complex OrderBy values after scaffolding is complete.
559
-
560
- ### When additional sort options are needed
561
-
562
- When sort options are required later:
563
-
564
- 1. Add values to the OrderBy enum in entity.json
565
- 2. Add the corresponding cases to the orderBy branch in the model
566
-
567
- ```typescript
568
- // model.ts — adding orderBy cases
569
- if (params.orderBy === "id-desc") {
570
- qb.orderBy("products.id", "desc");
571
- } else if (params.orderBy === "name-asc") {
572
- // added
573
- qb.orderBy("products.name", "asc");
574
- } else {
575
- exhaustive(params.orderBy);
576
- }
577
- ```
578
-
579
- **Rule**: start with `id-desc` only → add others as needed after scaffolding
580
-
581
- ## IMPORTANT: integer vs number Type Selection
582
-
583
- **CRITICAL: Always follow the criteria below when creating numeric fields. Wrong type choice causes unnecessary ALTER migrations.**
584
-
585
- PostgreSQL mapping:
586
-
587
- - `integer` → DB `integer` (whole numbers)
588
- - `number` → DB `numeric(p,s)` (precise decimal numbers)
589
-
590
- | Use case | Entity type | Example |
591
- | --------------------------------------- | --------------------------------- | -------------------------------- |
592
- | PK, FK, count, order, quantity | `integer` | id, user_id, order_num, quantity |
593
- | Amount, ratio, values requiring decimal | `number` (+ `precision`, `scale`) | price, rate, weight, score |
594
-
595
- **DO NOT:**
596
-
597
- ```json
598
- { "name": "order_num", "type": "number", "desc": "Sort order" }
599
- { "name": "quantity", "type": "number", "desc": "Quantity" }
600
- ```
601
-
602
- **DO:**
603
-
604
- ```json
605
- { "name": "order_num", "type": "integer", "desc": "Sort order" }
606
- { "name": "quantity", "type": "integer", "desc": "Quantity" }
607
- { "name": "price", "type": "number", "precision": 12, "scale": 2, "desc": "Price" }
608
- { "name": "rate", "type": "number", "precision": 5, "scale": 2, "desc": "Rate" }
609
- ```
610
-
611
- **Decision rule: "Does this value ever need a decimal point?"**
612
-
613
- - No → `integer`
614
- - Yes → `number` (always specify `precision` and `scale`)
615
-
616
- ## Common Options (CommonProp)
617
-
618
- Options applicable to all prop types:
619
-
620
- | Option | Type | Description |
621
- | ---------- | ------- | --------------------------------------------------------- |
622
- | `name` | string | Field name (required) |
623
- | `desc` | string | Field description |
624
- | `nullable` | boolean | Whether NULL is allowed (default: false) |
625
- | `toFilter` | true | Register as a sonamuFilter filtering target. See model.md |
626
- | `cone` | Cone | LLM-based fixture generation metadata. See cone.md |
627
-
628
- ## Required Options by Type
629
-
630
- | Type | Required | Optional |
631
- | ------------ | ------------ | ------------------------------------------------------------------ |
632
- | `string` | — | `length` (text if omitted), `zodFormat` (email, uuid, etc.) |
633
- | `integer` | — | — |
634
- | `bigInteger` | — | — |
635
- | `number` | — | `precision`, `scale`, `numberType` (real/double precision/numeric) |
636
- | `numeric` | — | `precision`, `scale` |
637
- | `enum` | `id` | `nullable`, `dbDefault`, `length` |
638
- | `json` | `id` | `dbDefault: "{}"` |
639
- | `date` | — | `dbDefault`, `precision` |
640
- | `boolean` | — | `dbDefault: "false"` |
641
- | `virtual` | `id` | `virtualType` (query/code, default: code) |
642
- | `vector` | `dimensions` | — |
643
- | `tsvector` | — | — |
644
-
645
- ## IMPORTANT: ENUM Type dbDefault Setting
646
-
647
- When setting a default value on an ENUM field, the value must be wrapped in **escaped double quotes**.
648
-
649
- ### DO NOT - Incorrect Examples
650
-
651
- ```json
652
- // Incorrect: no quotes — interpreted as a column reference in SQL, causes error
653
- { "name": "status", "type": "enum", "id": "ApprovalStatus", "dbDefault": "pending" }
654
- // Error: cannot use column reference in DEFAULT expression
655
-
656
- // Incorrect: single quotes — causes oxfmt format error
657
- { "name": "status", "type": "enum", "id": "ApprovalStatus", "dbDefault": "'pending'" }
658
- ```
659
-
660
- ### DO - Correct Example
661
-
662
- ```json
663
- // Correct: escaped double quotes
664
- {
665
- "name": "status",
666
- "type": "enum",
667
- "id": "ApprovalStatus",
668
- "dbDefault": "\"pending\"",
669
- "desc": "Approval status"
670
- }
671
- ```
672
-
673
- ### Generated SQL
674
-
675
- ```sql
676
- -- Correct output
677
- "status" text not null default 'pending'
678
- ```