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.
- package/dist/bin/cli.js +206 -187
- package/dist/cone/cone-generator.js +3 -9
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +2 -2
- package/dist/database/puri.d.ts +17 -0
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +102 -3
- package/dist/migration/code-generation.js +6 -6
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +51 -6
- package/dist/ui-web/assets/{index-CDd6xT-F.js → index-D0MHYbxl.js} +2 -2
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +2 -2
- package/src/bin/cli.ts +283 -282
- package/src/cone/cone-generator.ts +2 -18
- package/src/database/__tests__/puri.test.ts +183 -0
- package/src/database/knex.ts +4 -1
- package/src/database/puri.ts +229 -2
- package/src/database/puri.types.test-d.ts +56 -0
- package/src/migration/code-generation.ts +5 -5
- package/src/migration/migrator.ts +81 -7
- package/src/skills/AGENTS.md +76 -50
- package/src/skills/sonamu/SKILL.md +46 -227
- package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
- package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
- package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
- package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
- package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
- package/src/skills/sonamu-config/SKILL.md +203 -0
- package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
- package/src/skills/sonamu-config/references/environments.md +178 -0
- package/src/skills/sonamu-config/references/server-options.md +400 -0
- package/src/skills/sonamu-entity/SKILL.md +180 -0
- package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
- package/src/skills/sonamu-entity/references/design-guides.md +243 -0
- package/src/skills/sonamu-entity/references/field-types.md +170 -0
- package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
- package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
- package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
- package/src/skills/sonamu-fixture/SKILL.md +180 -0
- package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
- package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
- package/src/skills/sonamu-frontend/SKILL.md +142 -0
- package/src/skills/sonamu-frontend/references/components.md +323 -0
- package/src/skills/sonamu-frontend/references/examples.md +64 -0
- package/src/skills/sonamu-frontend/references/hooks.md +273 -0
- package/src/skills/sonamu-frontend/references/runtime.md +165 -0
- package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
- package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
- package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
- package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
- package/src/skills/sonamu-query/SKILL.md +48 -0
- package/src/skills/sonamu-query/references/model-patterns.md +390 -0
- package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
- package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
- package/src/skills/sonamu-query/references/search.md +238 -0
- package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
- package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
- package/src/skills/sonamu-testing/SKILL.md +251 -0
- package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
- package/src/skills/sonamu-testing/references/helpers.md +185 -0
- package/src/skills/sonamu-testing/references/patterns.md +263 -0
- package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
- package/src/skills/sonamu-testing/references/quick-start.md +285 -0
- package/src/skills/sonamu-testing/references/type-safety.md +172 -0
- package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
- package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
- package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
- package/src/skills/commands/sonamu-skills.md +0 -20
- package/src/skills/project/README.md +0 -19
- package/src/skills/project/architecture.md +0 -373
- package/src/skills/sonamu/cdd.md +0 -129
- package/src/skills/sonamu/config.md +0 -772
- package/src/skills/sonamu/create-sonamu.md +0 -208
- package/src/skills/sonamu/entity-basic.md +0 -678
- package/src/skills/sonamu/framework-change.md +0 -96
- package/src/skills/sonamu/frontend.md +0 -915
- package/src/skills/sonamu/project-init.md +0 -477
- package/src/skills/sonamu/skill-contribution.md +0 -247
- 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
|
-
```
|