sonamu 0.10.7 → 0.10.9
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/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +5 -12
- package/dist/bin/cli.js +10 -253
- package/dist/database/base-model.d.ts +1 -0
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +14 -8
- package/dist/database/db.d.ts +2 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +13 -3
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +4 -11
- package/dist/database/transaction-context.d.ts +8 -3
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/transaction-context.js +12 -7
- package/dist/migration/code-generation.js +2 -2
- package/dist/naite/naite-reporter.d.ts +8 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +9 -4
- package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
- package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +3 -3
- package/src/api/decorators.ts +11 -22
- package/src/bin/cli.ts +12 -375
- package/src/database/__tests__/transaction-scope.test.ts +227 -0
- package/src/database/base-model.ts +31 -6
- package/src/database/db.ts +17 -2
- package/src/database/puri-wrapper.ts +5 -22
- package/src/database/transaction-context.ts +14 -6
- package/src/migration/code-generation.ts +1 -1
- package/src/naite/naite-reporter.ts +8 -3
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
- package/src/skills/AGENTS.md +0 -108
- package/src/skills/sonamu/SKILL.md +0 -75
- package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
- package/src/skills/sonamu-api/SKILL.md +0 -480
- package/src/skills/sonamu-auth/SKILL.md +0 -327
- package/src/skills/sonamu-auth/references/plugins.md +0 -310
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
- package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
- package/src/skills/sonamu-config/SKILL.md +0 -203
- package/src/skills/sonamu-config/references/database.md +0 -452
- package/src/skills/sonamu-config/references/environments.md +0 -178
- package/src/skills/sonamu-config/references/server-options.md +0 -400
- package/src/skills/sonamu-entity/SKILL.md +0 -180
- package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
- package/src/skills/sonamu-entity/references/design-guides.md +0 -243
- package/src/skills/sonamu-entity/references/field-types.md +0 -170
- package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
- package/src/skills/sonamu-entity/references/relations.md +0 -463
- package/src/skills/sonamu-entity/references/subset.md +0 -156
- package/src/skills/sonamu-fixture/SKILL.md +0 -180
- package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
- package/src/skills/sonamu-fixture/references/cone.md +0 -298
- package/src/skills/sonamu-frontend/SKILL.md +0 -142
- package/src/skills/sonamu-frontend/references/components.md +0 -323
- package/src/skills/sonamu-frontend/references/examples.md +0 -64
- package/src/skills/sonamu-frontend/references/hooks.md +0 -273
- package/src/skills/sonamu-frontend/references/runtime.md +0 -165
- package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
- package/src/skills/sonamu-i18n/SKILL.md +0 -287
- package/src/skills/sonamu-migration/SKILL.md +0 -316
- package/src/skills/sonamu-naite/SKILL.md +0 -266
- package/src/skills/sonamu-query/SKILL.md +0 -48
- package/src/skills/sonamu-query/references/model-patterns.md +0 -390
- package/src/skills/sonamu-query/references/model.md +0 -366
- package/src/skills/sonamu-query/references/puri.md +0 -413
- package/src/skills/sonamu-query/references/search.md +0 -238
- package/src/skills/sonamu-query/references/upsert.md +0 -324
- package/src/skills/sonamu-tasks/SKILL.md +0 -236
- package/src/skills/sonamu-testing/SKILL.md +0 -251
- package/src/skills/sonamu-testing/references/devrunner.md +0 -405
- package/src/skills/sonamu-testing/references/helpers.md +0 -185
- package/src/skills/sonamu-testing/references/patterns.md +0 -263
- package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
- package/src/skills/sonamu-testing/references/quick-start.md +0 -285
- package/src/skills/sonamu-testing/references/type-safety.md +0 -172
- package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
- package/src/skills/sonamu-vector/SKILL.md +0 -222
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
# Relationships
|
|
2
|
-
|
|
3
|
-
## Relationship Selection Guide
|
|
4
|
-
|
|
5
|
-
### 1:N vs N:M Decision Criteria
|
|
6
|
-
|
|
7
|
-
| Question | 1:N (BelongsToOne) | N:M (ManyToMany or intermediate entity) |
|
|
8
|
-
| -------------------------------------------------- | ------------------ | --------------------------------------- |
|
|
9
|
-
| Can one A belong to multiple Bs? | No | Yes |
|
|
10
|
-
| Does the relationship need additional information? | No | Yes → intermediate entity |
|
|
11
|
-
| Can it be expressed as "A belongs to B"? | Yes | No |
|
|
12
|
-
|
|
13
|
-
**Examples:**
|
|
14
|
-
|
|
15
|
-
- Post → Author: 1:N (a post has one author)
|
|
16
|
-
- Post ↔ Tag: N:M (multiple posts have multiple tags)
|
|
17
|
-
- Researcher ↔ Task: N:M + intermediate entity (participation rate, role, and other additional info)
|
|
18
|
-
|
|
19
|
-
### When an Intermediate Entity Is Needed
|
|
20
|
-
|
|
21
|
-
If an N:M relationship has **additional information**, use an **intermediate entity** instead of ManyToMany.
|
|
22
|
-
|
|
23
|
-
| Situation | ManyToMany | Intermediate entity |
|
|
24
|
-
| -------------------------------------- | ---------- | ------------------- |
|
|
25
|
-
| Only a simple connection is needed | ✓ | |
|
|
26
|
-
| Relationship needs a date/period | | ✓ |
|
|
27
|
-
| Relationship needs a role/status | | ✓ |
|
|
28
|
-
| Relationship needs a quantity/ratio | | ✓ |
|
|
29
|
-
| Relationship history management needed | | ✓ |
|
|
30
|
-
|
|
31
|
-
**Intermediate entity example:**
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
Researcher ↔ Task
|
|
35
|
-
└─ ProjectResearcher (intermediate entity)
|
|
36
|
-
- researcher: BelongsToOne → User
|
|
37
|
-
- task: BelongsToOne → Task
|
|
38
|
-
- role: enum (lead/participant)
|
|
39
|
-
- participation_rate: integer
|
|
40
|
-
- begin_at, end_at: date
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Joint Ownership / Joint Achievement Pattern
|
|
44
|
-
|
|
45
|
-
When multiple people are connected to a single result:
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
Achievement ↔ Researcher
|
|
49
|
-
└─ AchievementParticipant (intermediate entity)
|
|
50
|
-
- achievement: BelongsToOne → Achievement
|
|
51
|
-
- researcher: BelongsToOne → User
|
|
52
|
-
- is_primary: boolean (whether they are the original registrant)
|
|
53
|
-
- contribution_rate: integer (optional)
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Key point**: The achievement is registered once, and participants are linked to it (prevents duplication)
|
|
57
|
-
|
|
58
|
-
### Status History Pattern
|
|
59
|
-
|
|
60
|
-
When status change history needs to be managed:
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
Application (ApplyDeliberation)
|
|
64
|
-
└─ ApplicationHistory (ApplyDeliberationHistory)
|
|
65
|
-
- apply_deliberation: BelongsToOne → ApplyDeliberation
|
|
66
|
-
- status: enum (previous status or changed status)
|
|
67
|
-
- changed_at: date
|
|
68
|
-
- changed_by: BelongsToOne → User
|
|
69
|
-
- reason: string
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Change Request Pattern
|
|
73
|
-
|
|
74
|
-
When data changes require an approval process:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
Task
|
|
78
|
-
└─ TaskChangeRequest
|
|
79
|
-
- task: BelongsToOne → Task
|
|
80
|
-
- status: enum (requested/approved/rejected)
|
|
81
|
-
- reason: string
|
|
82
|
-
- requested_by: BelongsToOne → User
|
|
83
|
-
- requested_at: date
|
|
84
|
-
- approved_by: BelongsToOne → User (nullable)
|
|
85
|
-
- approved_at: date (nullable)
|
|
86
|
-
|
|
87
|
-
└─ TaskChangeHistory
|
|
88
|
-
- change_request: BelongsToOne → TaskChangeRequest
|
|
89
|
-
- change_type: enum (add/delete/update)
|
|
90
|
-
- target_user: BelongsToOne → User (subject of change)
|
|
91
|
-
- before_value: json (before change, optional)
|
|
92
|
-
- after_value: json (after change, optional)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Common Domain Patterns
|
|
98
|
-
|
|
99
|
-
### Organizational Structure (Institution-Department-User)
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
Institution
|
|
103
|
-
└─ departments: HasMany → Department
|
|
104
|
-
|
|
105
|
-
Department
|
|
106
|
-
└─ institution: BelongsToOne → Institution
|
|
107
|
-
└─ employees: HasMany → User
|
|
108
|
-
|
|
109
|
-
User
|
|
110
|
-
└─ institution: BelongsToOne → Institution
|
|
111
|
-
└─ department: BelongsToOne → Department
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Project Participation (Project-Participant)
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
Project
|
|
118
|
-
└─ participants: HasMany → ProjectParticipant
|
|
119
|
-
└─ owner: BelongsToOne → User
|
|
120
|
-
|
|
121
|
-
ProjectParticipant [intermediate entity]
|
|
122
|
-
└─ project: BelongsToOne → Project
|
|
123
|
-
└─ user: BelongsToOne → User
|
|
124
|
-
└─ role: enum
|
|
125
|
-
└─ participation_rate: integer
|
|
126
|
-
└─ begin_at, end_at: date
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Committee-Member
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
Committee
|
|
133
|
-
└─ members: HasMany → CommitteeMember
|
|
134
|
-
|
|
135
|
-
CommitteeMember [intermediate entity]
|
|
136
|
-
└─ committee: BelongsToOne → Committee
|
|
137
|
-
└─ user: BelongsToOne → User
|
|
138
|
-
└─ member_type: enum (internal/external)
|
|
139
|
-
└─ participate_year: string
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Review/Evaluation (Target-Reviewer-Result)
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
EvaluationTarget
|
|
146
|
-
└─ committee: BelongsToOne → Committee
|
|
147
|
-
└─ target_entity: BelongsToOne → Task (or polymorphic)
|
|
148
|
-
|
|
149
|
-
EvaluationResult
|
|
150
|
-
└─ target: BelongsToOne → EvaluationTarget
|
|
151
|
-
└─ evaluator: BelongsToOne → CommitteeMember
|
|
152
|
-
└─ score: integer or enum (approved/rejected)
|
|
153
|
-
└─ opinion: string
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Step-by-Step Data Flow (Application → Confirmation)
|
|
157
|
-
|
|
158
|
-
When data moves through stages:
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
ApplyDeliberation
|
|
162
|
-
└─ task: OneToOne → Task (references the task created on approval)
|
|
163
|
-
|
|
164
|
-
Task
|
|
165
|
-
└─ apply_deliberation: BelongsToOne → ApplyDeliberation (references the original application)
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**Key point**: Bidirectional references allow querying from either side
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## Which Relationship to Use?
|
|
173
|
-
|
|
174
|
-
| Situation | Relationship type | Example |
|
|
175
|
-
| -------------------------- | ----------------- | -------------------- |
|
|
176
|
-
| "A belongs to B" (N:1) | `BelongsToOne` | Post → User (author) |
|
|
177
|
-
| "A has many Bs" (1:N) | `HasMany` | User → Posts |
|
|
178
|
-
| "A and B are 1:1" | `OneToOne` | User ↔ Employee |
|
|
179
|
-
| "A and B are many-to-many" | `ManyToMany` | Post ↔ Tag |
|
|
180
|
-
|
|
181
|
-
## BelongsToOne (N:1) - Most Common
|
|
182
|
-
|
|
183
|
-
**Situation**: When a Post belongs to a User (author)
|
|
184
|
-
|
|
185
|
-
```json
|
|
186
|
-
{
|
|
187
|
-
"type": "relation",
|
|
188
|
-
"name": "author",
|
|
189
|
-
"with": "User",
|
|
190
|
-
"relationType": "BelongsToOne",
|
|
191
|
-
"nullable": true,
|
|
192
|
-
"onUpdate": "CASCADE",
|
|
193
|
-
"onDelete": "SET NULL",
|
|
194
|
-
"desc": "author"
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**Auto-generated**: `author_id` column (FK)
|
|
199
|
-
|
|
200
|
-
**Note**: Do not define `author_id` directly in props (it is auto-generated)
|
|
201
|
-
|
|
202
|
-
**Optional options:**
|
|
203
|
-
|
|
204
|
-
- `customJoinClause`: custom JOIN condition SQL (specify JOIN condition directly instead of FK)
|
|
205
|
-
- `useConstraint`: whether to create FK constraint (default: `true`). If `false`, the FK column is created but no DB constraint is generated
|
|
206
|
-
|
|
207
|
-
### Using FK in Code
|
|
208
|
-
|
|
209
|
-
Since a BelongsToOne relationship automatically creates a `{name}_id` column, use the correct field name when working with it directly in Model, FixtureGenerator, etc.
|
|
210
|
-
|
|
211
|
-
**Correct pattern:**
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// Entity definition
|
|
215
|
-
{
|
|
216
|
-
"type": "relation",
|
|
217
|
-
"name": "company",
|
|
218
|
-
"with": "Company",
|
|
219
|
-
"relationType": "BelongsToOne"
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// In Model save() or FixtureGenerator
|
|
223
|
-
const department = {
|
|
224
|
-
name: "Engineering",
|
|
225
|
-
company_id: 1 // ✓ CORRECT: {relation_name}_id form
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
await puri.ubRegister("departments", department);
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**Wrong pattern (common mistake):**
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
// ✗ WRONG: using relation name directly
|
|
235
|
-
const department = {
|
|
236
|
-
name: "Engineering",
|
|
237
|
-
company: 1, // FK is not set! company_id is saved as NULL
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// ✗ WRONG: passing as object
|
|
241
|
-
const department = {
|
|
242
|
-
name: "Engineering",
|
|
243
|
-
company: { id: 1 }, // FK is not set!
|
|
244
|
-
};
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**FixtureGenerator example:**
|
|
248
|
-
|
|
249
|
-
```typescript
|
|
250
|
-
// inside fixture-generator.ts
|
|
251
|
-
if (isBelongsToOneRelationProp(prop) || (isOneToOneRelationProp(prop) && prop.hasJoinColumn)) {
|
|
252
|
-
const relationValue = await this.generateRelationValue(entity, prop, context);
|
|
253
|
-
|
|
254
|
-
// ✓ CORRECT: set FK as {prop.name}_id
|
|
255
|
-
fixture[`${prop.name}_id`] = relationValue;
|
|
256
|
-
} else {
|
|
257
|
-
fixture[prop.name] = relationValue;
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Key points:**
|
|
262
|
-
|
|
263
|
-
- Entity JSON: `"name": "company"` (relation name)
|
|
264
|
-
- DB column: `company_id` (auto-generated)
|
|
265
|
-
- TypeScript code: use `company_id` (setting FK)
|
|
266
|
-
- Entity subset: `"company.id"` form (FieldExpr)
|
|
267
|
-
|
|
268
|
-
## HasMany (1:N) - For Reverse Lookup
|
|
269
|
-
|
|
270
|
-
**Situation**: When you want to query a User's Posts
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{
|
|
274
|
-
"type": "relation",
|
|
275
|
-
"name": "posts",
|
|
276
|
-
"with": "Post",
|
|
277
|
-
"relationType": "HasMany",
|
|
278
|
-
"joinColumn": "author_id",
|
|
279
|
-
"desc": "authored posts"
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
**Required**: `joinColumn` = FK column name in the related table
|
|
284
|
-
|
|
285
|
-
**Optional**: `fromColumn` = matching column in your own table (default: `id`). Use when JOIN needs to use a non-standard PK
|
|
286
|
-
|
|
287
|
-
**Important**: If the `joinColumn` field is not defined, a Zod schema validation error will occur.
|
|
288
|
-
|
|
289
|
-
**No DB column is created** (virtual)
|
|
290
|
-
|
|
291
|
-
**When is it needed?**
|
|
292
|
-
|
|
293
|
-
- When reverse lookup like `user.posts.title` is needed in a Subset
|
|
294
|
-
- Can be omitted if not needed
|
|
295
|
-
|
|
296
|
-
### HasMany Performance Optimization
|
|
297
|
-
|
|
298
|
-
HasMany relationships are automatically optimized using the **DataLoader pattern**:
|
|
299
|
-
|
|
300
|
-
- Parent record IDs are collected in batches
|
|
301
|
-
- All child records are queried in a single `whereIn` query
|
|
302
|
-
- **No N+1 query problem**
|
|
303
|
-
|
|
304
|
-
This optimization is applied automatically and requires no additional configuration.
|
|
305
|
-
|
|
306
|
-
**Implementation location**: `processLoaders` method in `modules/sonamu/src/database/base-model.ts`
|
|
307
|
-
|
|
308
|
-
## OneToOne (1:1)
|
|
309
|
-
|
|
310
|
-
**Situation**: When User and Employee are 1:1
|
|
311
|
-
|
|
312
|
-
**The side holding the FK** (Employee):
|
|
313
|
-
|
|
314
|
-
```json
|
|
315
|
-
{
|
|
316
|
-
"type": "relation",
|
|
317
|
-
"name": "user",
|
|
318
|
-
"with": "User",
|
|
319
|
-
"relationType": "OneToOne",
|
|
320
|
-
"hasJoinColumn": true,
|
|
321
|
-
"onUpdate": "CASCADE",
|
|
322
|
-
"onDelete": "CASCADE",
|
|
323
|
-
"desc": "user"
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
**The side without the FK** (User):
|
|
328
|
-
|
|
329
|
-
```json
|
|
330
|
-
{
|
|
331
|
-
"type": "relation",
|
|
332
|
-
"name": "employee",
|
|
333
|
-
"with": "Employee",
|
|
334
|
-
"relationType": "OneToOne",
|
|
335
|
-
"nullable": true,
|
|
336
|
-
"desc": "employee info"
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Key point**: FK is only created on the side with `hasJoinColumn: true` (omitting it means no FK; it is an optional option)
|
|
341
|
-
|
|
342
|
-
**Optional options (when `hasJoinColumn: true`):**
|
|
343
|
-
|
|
344
|
-
- `customJoinClause`: custom JOIN condition SQL
|
|
345
|
-
- `useConstraint`: whether to create FK constraint (default: `true`)
|
|
346
|
-
|
|
347
|
-
## ManyToMany (N:M)
|
|
348
|
-
|
|
349
|
-
**Situation**: When Post and Tag are many-to-many
|
|
350
|
-
|
|
351
|
-
```json
|
|
352
|
-
{
|
|
353
|
-
"type": "relation",
|
|
354
|
-
"name": "tags",
|
|
355
|
-
"with": "Tag",
|
|
356
|
-
"relationType": "ManyToMany",
|
|
357
|
-
"joinTable": "posts__tags",
|
|
358
|
-
"onUpdate": "CASCADE",
|
|
359
|
-
"onDelete": "CASCADE",
|
|
360
|
-
"desc": "tags"
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
**Required**: `joinTable`, `onUpdate`, `onDelete`
|
|
365
|
-
|
|
366
|
-
### ManyToMany Naming Conventions
|
|
367
|
-
|
|
368
|
-
**joinTable (join table name)**: use **double** underscore
|
|
369
|
-
|
|
370
|
-
```
|
|
371
|
-
User ↔ Role → user__roles
|
|
372
|
-
Post ↔ Tag → posts__tags (alphabetical order recommended)
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
**joinColumn (join table column name)**: use **single** underscore
|
|
376
|
-
|
|
377
|
-
```
|
|
378
|
-
user__roles table:
|
|
379
|
-
- user_id (single underscore)
|
|
380
|
-
- role_id (single underscore)
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
**Example**:
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
// Entity: User
|
|
387
|
-
{
|
|
388
|
-
"name": "roles",
|
|
389
|
-
"relationType": "ManyToMany",
|
|
390
|
-
"with": "Role",
|
|
391
|
-
"joinTable": "user__roles" // double underscore
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// In Model save():
|
|
395
|
-
puri.ubRegister("user__roles", {
|
|
396
|
-
user_id, // single underscore
|
|
397
|
-
role_id // single underscore
|
|
398
|
-
});
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
## Self-Reference
|
|
402
|
-
|
|
403
|
-
**Situation**: When an Employee's manager is also an Employee
|
|
404
|
-
|
|
405
|
-
```json
|
|
406
|
-
{
|
|
407
|
-
"type": "relation",
|
|
408
|
-
"name": "manager",
|
|
409
|
-
"with": "Employee",
|
|
410
|
-
"relationType": "BelongsToOne",
|
|
411
|
-
"nullable": true,
|
|
412
|
-
"onUpdate": "CASCADE",
|
|
413
|
-
"onDelete": "SET NULL",
|
|
414
|
-
"desc": "direct manager"
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
**Required**: `nullable: true` (top-level has no manager)
|
|
419
|
-
|
|
420
|
-
## IMPORTANT: parentId and Parent Subset HasMany Cannot Be Used Together
|
|
421
|
-
|
|
422
|
-
### Problem
|
|
423
|
-
|
|
424
|
-
When parentId is set, the **FK column is removed from the child entity's BaseSchema**.
|
|
425
|
-
In this state, if the parent's subset includes the child via HasMany, the SSO LoaderQuery
|
|
426
|
-
executes `whereIn("child.parent_fk", fromIds)`, but since the FK is missing, a TypeScript error occurs.
|
|
427
|
-
|
|
428
|
-
```
|
|
429
|
-
Error: '{child_table}.{parent_fk}' is not assignable to type 'AvailableColumns'
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
### Solution: Choose One of the Two
|
|
433
|
-
|
|
434
|
-
| Requirement | Choice | parentId | HasMany in parent subset |
|
|
435
|
-
| ----------------------------------------------- | ------------------ | ---------- | ------------------------ |
|
|
436
|
-
| Query child list together in parent detail view | Independent entity | ✗ not used | ✓ possible |
|
|
437
|
-
| Child is CRUD'd only through parent | Use parentId | ✓ used | ✗ not possible |
|
|
438
|
-
|
|
439
|
-
### Decision Criteria
|
|
440
|
-
|
|
441
|
-
| Question | Yes → Independent entity | No → parentId |
|
|
442
|
-
| ----------------------------------------------------------- | ------------------------ | ------------- |
|
|
443
|
-
| Will the child ever be queried/modified standalone? | ✓ | |
|
|
444
|
-
| Does the admin screen need a separate child list page? | ✓ | |
|
|
445
|
-
| Is the child list queried as a subset in the parent detail? | ✓ | |
|
|
446
|
-
|
|
447
|
-
### Example
|
|
448
|
-
|
|
449
|
-
```json
|
|
450
|
-
// DO NOT - Incorrect (causes error)
|
|
451
|
-
// entity: ApplyDeliberationResearcher
|
|
452
|
-
{ "parentId": "apply_deliberation_id" } // FK removed from BaseSchema
|
|
453
|
-
|
|
454
|
-
// entity: ApplyDeliberation subset
|
|
455
|
-
{ "A": ["*", { "researchers": ["*"] }] } // SSO LoaderQuery error
|
|
456
|
-
|
|
457
|
-
// DO - Correct (change to independent entity, remove parentId)
|
|
458
|
-
// entity: ApplyDeliberationResearcher - no parentId, FK is preserved in BaseSchema
|
|
459
|
-
// entity: ApplyDeliberation subset
|
|
460
|
-
{ "A": ["*", { "researchers": ["*"] }] } // works correctly
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
---
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
# Subset — Response Field Scope
|
|
2
|
-
|
|
3
|
-
## Basic Structure
|
|
4
|
-
|
|
5
|
-
```json
|
|
6
|
-
{
|
|
7
|
-
"subsets": {
|
|
8
|
-
"A": ["id", "created_at", "username", "email", "role"],
|
|
9
|
-
"P": ["id", "username", "employee.department.name"],
|
|
10
|
-
"SS": ["id", "username"]
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Naming Conventions
|
|
16
|
-
|
|
17
|
-
**WARNING: Only use A, P, and SS as Subset names. Do not use arbitrary names like S, D, or L.**
|
|
18
|
-
|
|
19
|
-
| Subset | Purpose |
|
|
20
|
-
| ---------- | ------------------------------------------------------------------------------ |
|
|
21
|
-
| `A` | All - all fields (detail view, admin). **Required** |
|
|
22
|
-
| `P` | Partial/Profile - partial fields including relations (for list views) |
|
|
23
|
-
| `SS` | Super Simple/Summary - minimal fields, just ID + name (for dropdowns, selects) |
|
|
24
|
-
| `P2`, `P3` | Additional profiles (only for special cases) |
|
|
25
|
-
|
|
26
|
-
### IMPORTANT: Subset A Must Include All Fields
|
|
27
|
-
|
|
28
|
-
**Subset A must include all regular fields and major relation fields of the Entity.**
|
|
29
|
-
|
|
30
|
-
**DO:**
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"props": [
|
|
35
|
-
{ "name": "id", "type": "integer" },
|
|
36
|
-
{ "name": "created_at", "type": "date" },
|
|
37
|
-
{ "name": "title", "type": "string" },
|
|
38
|
-
{ "name": "status", "type": "enum", "id": "Status" },
|
|
39
|
-
{ "type": "relation", "name": "author", "with": "User", "relationType": "BelongsToOne" }
|
|
40
|
-
],
|
|
41
|
-
"subsets": {
|
|
42
|
-
"A": ["id", "created_at", "title", "status", "author.id", "author.name"]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**DO NOT:**
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"subsets": {
|
|
52
|
-
"A": ["id", "title"] // created_at, status, author omitted - incorrect
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Rules:**
|
|
58
|
-
|
|
59
|
-
- Include all regular fields (id, created_at, business fields, etc.)
|
|
60
|
-
- For BelongsToOne relations, include at least `.id` and a display field (`.name`, `.title`, etc.)
|
|
61
|
-
- HasMany relations are optional (include only when needed)
|
|
62
|
-
|
|
63
|
-
### When Only a Single Subset Is Needed
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
// DO - Correct: create only A
|
|
67
|
-
{ "subsets": { "A": ["id", "name", "created_at"] } }
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### DO NOT - Incorrect Subset Names
|
|
71
|
-
|
|
72
|
-
```json
|
|
73
|
-
// Incorrect: do not use S, D, L, etc.
|
|
74
|
-
{
|
|
75
|
-
"subsets": {
|
|
76
|
-
"A": [...],
|
|
77
|
-
"S": [...], // NEVER - use SS instead
|
|
78
|
-
"D": [...], // NEVER - do not use
|
|
79
|
-
"L": [...] // NEVER - use P instead
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Relation Fields (Dot Notation)
|
|
85
|
-
|
|
86
|
-
```json
|
|
87
|
-
{
|
|
88
|
-
"subsets": {
|
|
89
|
-
"P": ["id", "username", "employee.salary", "employee.department.name"]
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
- BelongsToOne/OneToOne: automatic LEFT JOIN
|
|
95
|
-
- HasMany/ManyToMany: automatically optimized with DataLoader
|
|
96
|
-
|
|
97
|
-
## ID-Only Reference Optimization
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
{ "SS": ["id", "title", "user.id"] }
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- Referencing only `user.id` reads the `user_id` column directly without a JOIN
|
|
104
|
-
|
|
105
|
-
## Internal Fields
|
|
106
|
-
|
|
107
|
-
```json
|
|
108
|
-
{
|
|
109
|
-
"subsets": {
|
|
110
|
-
"A": ["id", "username", { "field": "password_hash", "internal": true }]
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
- Included in the query but excluded from the API response type
|
|
116
|
-
|
|
117
|
-
## Usage in Models
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// findById
|
|
121
|
-
const user = await UserModel.findById("P", 1);
|
|
122
|
-
|
|
123
|
-
// findMany
|
|
124
|
-
const { rows } = await UserModel.findMany("P", { num: 20, page: 1 });
|
|
125
|
-
|
|
126
|
-
// getSubsetQueries + executeSubsetQuery
|
|
127
|
-
const { qb } = UserModel.getSubsetQueries("P");
|
|
128
|
-
qb.where("users.role", "admin");
|
|
129
|
-
|
|
130
|
-
const result = await UserModel.executeSubsetQuery({
|
|
131
|
-
subset: "P",
|
|
132
|
-
qb,
|
|
133
|
-
params: { num: 20, page: 1 },
|
|
134
|
-
});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Notes
|
|
138
|
-
|
|
139
|
-
- The base Subset `A` is required
|
|
140
|
-
- Nesting depth of 3 or fewer levels is recommended
|
|
141
|
-
- Exclude unnecessary relations from list-purpose Subsets
|
|
142
|
-
- **Use relation notation for FK columns**: FK columns for BelongsToOne relations (e.g. `user_id`) must be accessed in Subsets using the `user.id` form. This is because Sonamu recognizes the relation notation and automatically optimizes by reading the FK column directly when only `.id` is referenced.
|
|
143
|
-
|
|
144
|
-
```json
|
|
145
|
-
// DO NOT - Incorrect: using FK column directly
|
|
146
|
-
{ "A": ["id", "user_id", "title"] }
|
|
147
|
-
|
|
148
|
-
// DO - Correct: use relation.field format (auto-optimized)
|
|
149
|
-
{ "A": ["id", "user.id", "title"] }
|
|
150
|
-
// → Sonamu reads the user_id column directly (no JOIN)
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Working code references:**
|
|
154
|
-
|
|
155
|
-
- `sonamu/examples/miomock/api/src/application/project/project.entity.json` - Subset definition examples
|
|
156
|
-
- `sonamu/examples/miomock/api/src/application/employee/employee.entity.json` - BelongsToOne relation examples
|