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,588 +0,0 @@
|
|
|
1
|
-
# Pitfalls and Complex Scenarios
|
|
2
|
-
|
|
3
|
-
## Practical Notes (Common Pitfalls)
|
|
4
|
-
|
|
5
|
-
### 1. Fixture Data Preparation Required
|
|
6
|
-
|
|
7
|
-
**Problem:** Tests fail without base data due to foreign key constraints
|
|
8
|
-
|
|
9
|
-
**Solution:**
|
|
10
|
-
|
|
11
|
-
```sql
|
|
12
|
-
-- database/scripts/seed-initial-data.sql
|
|
13
|
-
INSERT INTO institutions (id, name, code) VALUES (1, 'HQ', 'HQ');
|
|
14
|
-
INSERT INTO departments (id, name, institution_id) VALUES (1, 'Research', 1);
|
|
15
|
-
INSERT INTO roles (id, code, name) VALUES (1, 'ADMIN', 'Administrator');
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# 1. apply seed data to test DB
|
|
20
|
-
PGPASSWORD=1234 psql -h 0.0.0.0 -U postgres -d project_test -f database/scripts/seed-initial-data.sql
|
|
21
|
-
|
|
22
|
-
# 2. create dump
|
|
23
|
-
pnpm dump
|
|
24
|
-
|
|
25
|
-
# 3. apply to fixture DB
|
|
26
|
-
pnpm seed
|
|
27
|
-
|
|
28
|
-
# 4. sonamu fixture sync (optional)
|
|
29
|
-
pnpm sonamu fixture sync
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 2. SaveParams Type Design (Partial)
|
|
33
|
-
|
|
34
|
-
**Problem 1:** Type error occurs when changing only some fields on update
|
|
35
|
-
|
|
36
|
-
**Problem 2:** Type error occurs when receiving overrides as Partial in test helpers
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
// WRONG - nullable fields not set to partial
|
|
40
|
-
export const QuestionSaveParams = QuestionBaseSchema.partial({
|
|
41
|
-
id: true,
|
|
42
|
-
created_at: true,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// test-helpers.ts
|
|
46
|
-
export async function createTestQuestion(
|
|
47
|
-
collectionId: number,
|
|
48
|
-
override?: Partial<QuestionSaveParams>,
|
|
49
|
-
) {
|
|
50
|
-
const [id] = await QuestionModel.save([
|
|
51
|
-
{
|
|
52
|
-
content: "test question",
|
|
53
|
-
parent_id: null,
|
|
54
|
-
answer_group_id: null,
|
|
55
|
-
...override, // type error: undefined cannot be assigned to null
|
|
56
|
-
},
|
|
57
|
-
]);
|
|
58
|
-
return id;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Solution:** Set nullable/dbDefault fields to partial
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// api/src/application/user/user.types.ts
|
|
66
|
-
export const UserSaveParams = UserBaseSchema.partial({
|
|
67
|
-
id: true, // needed for update
|
|
68
|
-
created_at: true, // dbDefault
|
|
69
|
-
password: true, // nullable
|
|
70
|
-
email: true, // nullable
|
|
71
|
-
phone: true, // nullable
|
|
72
|
-
user_type: true, // dbDefault
|
|
73
|
-
position_code: true, // nullable
|
|
74
|
-
position_name: true, // nullable
|
|
75
|
-
hire_date: true, // nullable
|
|
76
|
-
status: true, // dbDefault
|
|
77
|
-
department_id: true, // nullable relation
|
|
78
|
-
});
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Application criteria:**
|
|
82
|
-
|
|
83
|
-
- id, created_at, updated_at: always partial (auto-generated)
|
|
84
|
-
- Fields with dbDefault: set to partial
|
|
85
|
-
- FK fields with nullable: true: set to partial
|
|
86
|
-
- Regular fields with nullable: true (e.g. description): set to partial
|
|
87
|
-
|
|
88
|
-
**Key:** Required fields (employee_no, login_id, name, institution_id) are excluded from partial to maintain type safety
|
|
89
|
-
|
|
90
|
-
### 3. Excluding Relation Fields on Update
|
|
91
|
-
|
|
92
|
-
**Problem:** Subset includes relation objects, but SaveParams only has FK, causing errors
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
// WRONG
|
|
96
|
-
const user = await UserModel.findById("A", userId);
|
|
97
|
-
await UserModel.save([{ ...user, status: "inactive" }]);
|
|
98
|
-
// → "column 'department' does not exist" error
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**Solution:** Exclude relation fields + explicitly add FK
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
// CORRECT
|
|
105
|
-
const user = await UserModel.findById("A", userId);
|
|
106
|
-
const { institution, department, ...userData } = user;
|
|
107
|
-
await UserModel.save([
|
|
108
|
-
{
|
|
109
|
-
...userData,
|
|
110
|
-
institution_id: user.institution.id, // explicitly add FK
|
|
111
|
-
department_id: user.department?.id ?? null,
|
|
112
|
-
status: "inactive",
|
|
113
|
-
},
|
|
114
|
-
]);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Reason:** `UserSubsetA` includes `institution`, `department` objects, but does not include `institution_id`, `department_id` FKs
|
|
118
|
-
|
|
119
|
-
### 4. ubUpsert is an Upsert Operation
|
|
120
|
-
|
|
121
|
-
**Problem:** Unique constraint violation tests fail
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// failing test
|
|
125
|
-
test("employee number must be unique", async () => {
|
|
126
|
-
await UserModel.save([{ employee_no: "001", ... }]);
|
|
127
|
-
|
|
128
|
-
// attempt to create with duplicate employee number
|
|
129
|
-
await expect(
|
|
130
|
-
UserModel.save([{ employee_no: "001", ... }])
|
|
131
|
-
).rejects.toThrow(); // does not throw error, performs UPDATE instead
|
|
132
|
-
});
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Cause:** Sonamu's `save()` uses `ubUpsert` → on conflict, performs UPDATE instead of throwing error
|
|
136
|
-
|
|
137
|
-
**Solution:** Skip such tests
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
test.skip("employee number must be unique (skipped because ubUpsert performs upsert)", async () => {
|
|
141
|
-
// ...
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### 5. testAs Usage
|
|
146
|
-
|
|
147
|
-
**Problem:** Calling testAs inside test causes an error
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// WRONG
|
|
151
|
-
test("permission test", async () => {
|
|
152
|
-
await testAs(adminUser, "description", async () => { ... });
|
|
153
|
-
// → "Calling the test function inside another test function is not allowed" error
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// CORRECT - use as a replacement for test
|
|
157
|
-
testAs(adminUser, "permission test", async () => {
|
|
158
|
-
const result = await UserModel.del([userId]);
|
|
159
|
-
expect(result).toBe(1);
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### 6. Validating Model Queries with Naite
|
|
164
|
-
|
|
165
|
-
**Add Naite recording to Model:**
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// user.model.ts
|
|
169
|
-
import { Naite } from "sonamu";
|
|
170
|
-
|
|
171
|
-
async findMany(...) {
|
|
172
|
-
// ... build qb ...
|
|
173
|
-
|
|
174
|
-
// record query for testing
|
|
175
|
-
Naite.t("esq-query", qb.toQuery());
|
|
176
|
-
|
|
177
|
-
return this.executeSubsetQuery({ ... });
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Validate in test:**
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
test("should not have limit when num: 0", async () => {
|
|
185
|
-
await UserModel.findMany("A", { num: 0, page: 1 });
|
|
186
|
-
|
|
187
|
-
expect(Naite.get("esq-query").first()).not.contain("limit");
|
|
188
|
-
expect(Naite.get("esq-query").first()).not.contain("offset");
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### 7. Consider Multilingual Error Messages
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
// WRONG: only validates English message
|
|
196
|
-
await expect(UserModel.findById("A", 99999)).rejects.toThrow("not found");
|
|
197
|
-
|
|
198
|
-
// CORRECT: partial match on actual error message
|
|
199
|
-
await expect(UserModel.findById("A", 99999)).rejects.toThrow("does not exist");
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### 8. pnpm Workspace and Vitest Instance Conflicts
|
|
203
|
-
|
|
204
|
-
**Problem:** "Vitest failed to access its internal state" error
|
|
205
|
-
|
|
206
|
-
**Cause:** When sonamu is connected via `link:`, sonamu and the project's vitest are installed at separate paths with different peer dependency combinations
|
|
207
|
-
|
|
208
|
-
**Temporary fix (for testing):**
|
|
209
|
-
|
|
210
|
-
```json
|
|
211
|
-
// packages/api/package.json
|
|
212
|
-
{
|
|
213
|
-
"dependencies": {
|
|
214
|
-
"sonamu": "0.8.0" // specify version instead of link
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**Fundamental fix:** Contact sonamu developers (framework internal issue)
|
|
220
|
-
|
|
221
|
-
### 9. assert() for Truthy Checks
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
import assert from "assert";
|
|
225
|
-
|
|
226
|
-
test("create user", async () => {
|
|
227
|
-
const [userId] = await UserModel.save([{ ... }]);
|
|
228
|
-
|
|
229
|
-
// truthy check
|
|
230
|
-
assert(userId);
|
|
231
|
-
|
|
232
|
-
// userId is now safely inferred as number
|
|
233
|
-
const user = await UserModel.findById("A", userId);
|
|
234
|
-
});
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### 10. Create Test Data Directly
|
|
238
|
-
|
|
239
|
-
**miomock convention:** Minimize fixtures, create data directly within tests
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// recommended pattern
|
|
243
|
-
test("create user", async () => {
|
|
244
|
-
const [userId] = await UserModel.save([
|
|
245
|
-
{
|
|
246
|
-
employee_no: "2026001",
|
|
247
|
-
login_id: "testuser",
|
|
248
|
-
name: "Test User",
|
|
249
|
-
institution_id: 1,
|
|
250
|
-
// ... required fields
|
|
251
|
-
},
|
|
252
|
-
]);
|
|
253
|
-
|
|
254
|
-
const user = await UserModel.findById("A", userId);
|
|
255
|
-
expect(user.name).toBe("Test User");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Fixtures only for shared data
|
|
259
|
-
const f = await loadFixtures(["institution01"]); // only for shared data like institutions
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## Complex Entity Test Strategy
|
|
263
|
-
|
|
264
|
-
When dependencies between entities are complex (Institution → Department → User → Task → TaskParticipant), use test helper functions.
|
|
265
|
-
|
|
266
|
-
### Defining Test Helper Functions
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// api/src/testing/test-helpers.ts
|
|
270
|
-
import assert from "assert";
|
|
271
|
-
import { InstitutionModel } from "../application/institution/institution.model";
|
|
272
|
-
import { DepartmentModel } from "../application/department/department.model";
|
|
273
|
-
import { UserModel } from "../application/user/user.model";
|
|
274
|
-
import { TaskModel } from "../application/task/task.model";
|
|
275
|
-
|
|
276
|
-
// each helper requires only the minimum required fields and provides defaults for the rest
|
|
277
|
-
let counter = 0;
|
|
278
|
-
function uniqueId(prefix: string) {
|
|
279
|
-
return `${prefix}_${Date.now()}_${++counter}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export async function createTestInstitution(override?: Partial<InstitutionSaveParams>) {
|
|
283
|
-
const [id] = await InstitutionModel.save([
|
|
284
|
-
{
|
|
285
|
-
name: "Test Institution",
|
|
286
|
-
code: uniqueId("INST"),
|
|
287
|
-
...override,
|
|
288
|
-
},
|
|
289
|
-
]);
|
|
290
|
-
assert(id);
|
|
291
|
-
return id;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function createTestDepartment(
|
|
295
|
-
institutionId: number,
|
|
296
|
-
override?: Partial<DepartmentSaveParams>,
|
|
297
|
-
) {
|
|
298
|
-
const [id] = await DepartmentModel.save([
|
|
299
|
-
{
|
|
300
|
-
name: "Test Department",
|
|
301
|
-
code: uniqueId("DEPT"),
|
|
302
|
-
dept_type: "division",
|
|
303
|
-
institution_id: institutionId,
|
|
304
|
-
is_active: true,
|
|
305
|
-
sort_order: 0,
|
|
306
|
-
...override,
|
|
307
|
-
},
|
|
308
|
-
]);
|
|
309
|
-
assert(id);
|
|
310
|
-
return id;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export async function createTestUser(institutionId: number, override?: Partial<UserSaveParams>) {
|
|
314
|
-
const [id] = await UserModel.save([
|
|
315
|
-
{
|
|
316
|
-
employee_no: uniqueId("EMP"),
|
|
317
|
-
login_id: uniqueId("login"),
|
|
318
|
-
name: "Test User",
|
|
319
|
-
institution_id: institutionId,
|
|
320
|
-
...override,
|
|
321
|
-
},
|
|
322
|
-
]);
|
|
323
|
-
assert(id);
|
|
324
|
-
return id;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export async function createTestTask(
|
|
328
|
-
principalInvestigatorId: number,
|
|
329
|
-
override?: Partial<TaskSaveParams>,
|
|
330
|
-
) {
|
|
331
|
-
const [id] = await TaskModel.save([
|
|
332
|
-
{
|
|
333
|
-
task_no: uniqueId("TASK"),
|
|
334
|
-
title: "Test Task",
|
|
335
|
-
year: new Date().getFullYear(),
|
|
336
|
-
begin_date: new Date(),
|
|
337
|
-
end_date: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
|
|
338
|
-
principal_investigator_id: principalInvestigatorId,
|
|
339
|
-
...override,
|
|
340
|
-
},
|
|
341
|
-
]);
|
|
342
|
-
assert(id);
|
|
343
|
-
return id;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// create the entire dependency chain at once
|
|
347
|
-
export async function createTestTaskWithDeps(taskOverride?: Partial<TaskSaveParams>) {
|
|
348
|
-
const institutionId = await createTestInstitution();
|
|
349
|
-
const userId = await createTestUser(institutionId);
|
|
350
|
-
const taskId = await createTestTask(userId, taskOverride);
|
|
351
|
-
return { institutionId, userId, taskId };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export async function createTestUserWithDeps(userOverride?: Partial<UserSaveParams>) {
|
|
355
|
-
const institutionId = await createTestInstitution();
|
|
356
|
-
const userId = await createTestUser(institutionId, userOverride);
|
|
357
|
-
return { institutionId, userId };
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Using in Tests
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
import { createTestTaskWithDeps, createTestUser } from "../../testing/test-helpers";
|
|
365
|
-
|
|
366
|
-
describe("TaskModel", () => {
|
|
367
|
-
// GOOD: concise with helper functions
|
|
368
|
-
test("Create - create with minimum required fields", async () => {
|
|
369
|
-
const { taskId } = await createTestTaskWithDeps();
|
|
370
|
-
|
|
371
|
-
const task = await TaskModel.findById("D", taskId);
|
|
372
|
-
expect(task.id).toBe(taskId);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// GOOD: customize specific fields
|
|
376
|
-
test("Create - create with specific status", async () => {
|
|
377
|
-
const { taskId } = await createTestTaskWithDeps({
|
|
378
|
-
status: "approved",
|
|
379
|
-
title: "Approved Task",
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const task = await TaskModel.findById("D", taskId);
|
|
383
|
-
expect(task.status).toBe("approved");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// BAD: creating dependencies directly in every test (repetitive)
|
|
387
|
-
test("Create - direct creation (not recommended)", async () => {
|
|
388
|
-
const [institutionId] = await InstitutionModel.save([{ name: "...", code: "..." }]);
|
|
389
|
-
assert(institutionId);
|
|
390
|
-
const [userId] = await UserModel.save([{ ... }]);
|
|
391
|
-
assert(userId);
|
|
392
|
-
const [taskId] = await TaskModel.save([{ ... }]);
|
|
393
|
-
assert(taskId);
|
|
394
|
-
// ...
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### Subset → SaveParams Conversion Helper
|
|
400
|
-
|
|
401
|
-
When modifying findById results and saving again, relations must be converted to FKs:
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
// api/src/testing/test-helpers.ts
|
|
405
|
-
|
|
406
|
-
// Task Subset A → SaveParams conversion
|
|
407
|
-
export function taskToSaveParams(task: TaskSubsetA): TaskSaveParams {
|
|
408
|
-
const { program, project, principal_investigator, department, prev_task, ...rest } = task;
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
...rest,
|
|
412
|
-
program_id: program?.id ?? null,
|
|
413
|
-
project_id: project?.id ?? null,
|
|
414
|
-
principal_investigator_id: principal_investigator.id,
|
|
415
|
-
department_id: department?.id ?? null,
|
|
416
|
-
prev_task_id: prev_task?.id ?? null,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// generic helper (note: write directly if relation field names differ)
|
|
421
|
-
export function relationToFk<T extends Record<string, any>>(
|
|
422
|
-
data: T,
|
|
423
|
-
relationFields: string[],
|
|
424
|
-
): Record<string, any> {
|
|
425
|
-
const result: Record<string, any> = {};
|
|
426
|
-
|
|
427
|
-
for (const [key, value] of Object.entries(data)) {
|
|
428
|
-
if (relationFields.includes(key)) {
|
|
429
|
-
// relation → FK
|
|
430
|
-
result[`${key}_id`] = value?.id ?? null;
|
|
431
|
-
} else {
|
|
432
|
-
result[key] = value;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return result;
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Simplifying Update Tests
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
import { createTestTaskWithDeps, taskToSaveParams } from "../../testing/test-helpers";
|
|
444
|
-
|
|
445
|
-
test("Update - update task info", async () => {
|
|
446
|
-
const { taskId } = await createTestTaskWithDeps();
|
|
447
|
-
|
|
448
|
-
const task = await TaskModel.findById("A", taskId);
|
|
449
|
-
await TaskModel.save([
|
|
450
|
-
{
|
|
451
|
-
...taskToSaveParams(task),
|
|
452
|
-
title: "Updated Title",
|
|
453
|
-
},
|
|
454
|
-
]);
|
|
455
|
-
|
|
456
|
-
const updated = await TaskModel.findById("A", taskId);
|
|
457
|
-
expect(updated.title).toBe("Updated Title");
|
|
458
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Notes
|
|
462
|
-
|
|
463
|
-
**Do not use beforeAll/beforeEach:**
|
|
464
|
-
|
|
465
|
-
In sonamu's test environment, creating data with beforeAll/beforeEach may end up referencing sonamu internal code. Instead, call helper functions within each test.
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
// WRONG: using beforeAll
|
|
469
|
-
describe("TaskModel", () => {
|
|
470
|
-
let taskId: number;
|
|
471
|
-
beforeAll(async () => {
|
|
472
|
-
const result = await createTestTaskWithDeps();
|
|
473
|
-
taskId = result.taskId;
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
test("...", async () => {
|
|
477
|
-
// using taskId - may cause problems
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// CORRECT: create in each test
|
|
482
|
-
describe("TaskModel", () => {
|
|
483
|
-
test("...", async () => {
|
|
484
|
-
const { taskId } = await createTestTaskWithDeps();
|
|
485
|
-
// use taskId
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
---
|
|
491
|
-
|
|
492
|
-
## Common Mistakes and Solutions
|
|
493
|
-
|
|
494
|
-
### ubUpsert Does Not Throw Unique Constraint Errors
|
|
495
|
-
|
|
496
|
-
**→ See "Practical Notes #4. ubUpsert is an Upsert Operation" above**
|
|
497
|
-
|
|
498
|
-
### Transaction Isolation and Test Isolation
|
|
499
|
-
|
|
500
|
-
Each test runs in an independent transaction so data is isolated. Even within the same test, data you created may not be immediately visible in queries.
|
|
501
|
-
|
|
502
|
-
```typescript
|
|
503
|
-
// BAD: expecting exact count may fail
|
|
504
|
-
test("search by role name", async () => {
|
|
505
|
-
await createTestRole({ name: "AdminA" });
|
|
506
|
-
await createTestRole({ name: "AdminB" });
|
|
507
|
-
|
|
508
|
-
const { rows } = await RoleModel.findMany("A", {
|
|
509
|
-
keyword: "Admin",
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// may not see 2 due to transaction isolation
|
|
513
|
-
expect(rows.length).toBe(2);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// GOOD: use unique identifier and flexible assertion
|
|
517
|
-
test("search by role name", async () => {
|
|
518
|
-
// unique identifier to prevent conflicts
|
|
519
|
-
const testName = `SearchTest_${Date.now()}`;
|
|
520
|
-
await createTestRole({ name: `${testName}A` });
|
|
521
|
-
await createTestRole({ name: `${testName}B` });
|
|
522
|
-
|
|
523
|
-
const { rows } = await RoleModel.findMany("A", {
|
|
524
|
-
keyword: testName,
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// verify at least 1
|
|
528
|
-
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
529
|
-
// content validation
|
|
530
|
-
expect(rows.some((r) => r.name.includes(testName))).toBe(true);
|
|
531
|
-
});
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
**Patterns:**
|
|
535
|
-
|
|
536
|
-
- Use unique identifiers: `Date.now()`, `uuid()`, etc. to prevent conflicts
|
|
537
|
-
- Flexible assertions: use `toBeGreaterThanOrEqual(1)` instead of `toBe(2)`
|
|
538
|
-
- Content validation: verify actual data matches rather than count
|
|
539
|
-
|
|
540
|
-
### Conditional Validation for Sorting Tests
|
|
541
|
-
|
|
542
|
-
Since not all data may be returned in sorting tests, use conditional validation:
|
|
543
|
-
|
|
544
|
-
```typescript
|
|
545
|
-
// BAD: assumes two items are always returned
|
|
546
|
-
test("sort - newest ID first", async () => {
|
|
547
|
-
const id1 = await createTestRole({ name: "Role1" });
|
|
548
|
-
const id2 = await createTestRole({ name: "Role2" });
|
|
549
|
-
|
|
550
|
-
const { rows } = await RoleModel.findMany("A", {
|
|
551
|
-
orderBy: "id-desc",
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const id2Index = rows.findIndex((r) => r.id === id2);
|
|
555
|
-
const id1Index = rows.findIndex((r) => r.id === id1);
|
|
556
|
-
|
|
557
|
-
// fails if either is missing
|
|
558
|
-
expect(id2Index).toBeLessThan(id1Index);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// GOOD: conditional validation
|
|
562
|
-
test("sort - newest ID first", async () => {
|
|
563
|
-
const id1 = await createTestRole({ name: "Role1" });
|
|
564
|
-
const id2 = await createTestRole({ name: "Role2" });
|
|
565
|
-
|
|
566
|
-
const { rows } = await RoleModel.findMany("A", {
|
|
567
|
-
orderBy: "id-desc",
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
const testRoles = rows.filter((r) => [id1, id2].includes(r.id));
|
|
571
|
-
expect(testRoles.length).toBeGreaterThanOrEqual(1);
|
|
572
|
-
|
|
573
|
-
// only validate order when both roles are returned
|
|
574
|
-
if (testRoles.length === 2) {
|
|
575
|
-
const id2Index = rows.findIndex((r) => r.id === id2);
|
|
576
|
-
const id1Index = rows.findIndex((r) => r.id === id1);
|
|
577
|
-
expect(id2Index).toBeLessThan(id1Index);
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Key:** Accept the uncertainty caused by transaction isolation, and only assert when validation is possible.
|
|
583
|
-
|
|
584
|
-
---
|
|
585
|
-
|
|
586
|
-
## Fixture Data Creation Tips
|
|
587
|
-
|
|
588
|
-
**→ Detailed guide (unique constraint handling, gen vs fetch selection, DB sequence reset, FixtureGenerator customization): `sonamu-fixture` "Practical Tips" section**
|