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,266 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-naite
|
|
3
|
-
description: Traces values through Sonamu internals to explain unexpected behaviour. Use when a query or save produces the wrong result and the cause is unclear, or when a test needs to assert on internal state. Covers Naite.t(), Naite.get(), chaining filters (fromFile, fromFunction, where), wildcard keys, and DevRunner trace output.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Naite (Tracing System)
|
|
7
|
-
|
|
8
|
-
Naite is a tracing system for recording values in source code and verifying them in tests.
|
|
9
|
-
|
|
10
|
-
**Source code:** `modules/sonamu/src/naite/naite.ts`
|
|
11
|
-
|
|
12
|
-
**How it works:**
|
|
13
|
-
|
|
14
|
-
1. **Source code**: Record values with `Naite.t("key", value)`
|
|
15
|
-
2. **Test code**: Retrieve recorded values with `Naite.get("key")`
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Recording in Source Code (Naite.t)
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
// In Model or library code
|
|
23
|
-
import { Naite } from "sonamu";
|
|
24
|
-
|
|
25
|
-
// Record a query
|
|
26
|
-
Naite.t("esq-query", qb.toQuery());
|
|
27
|
-
|
|
28
|
-
// Inside UpsertBuilder
|
|
29
|
-
Naite.t("puri:ub-register", { tableName, uuid, isUuidReused, row });
|
|
30
|
-
Naite.t("puri:ub-upserted", { tableName, mode, rowCount, returnedIds });
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Verifying in Tests (Naite.get)
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// In test code
|
|
39
|
-
import { Naite } from "sonamu";
|
|
40
|
-
|
|
41
|
-
// Verify a recorded query
|
|
42
|
-
expect(Naite.get("esq-query").first()).not.contain("limit");
|
|
43
|
-
|
|
44
|
-
// Verify UpsertBuilder behavior
|
|
45
|
-
const trace = Naite.get("puri:ub-upserted").first();
|
|
46
|
-
expect(trace).toMatchObject({ tableName: "users", rowCount: 3 });
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## Built-in Naite Keys (Sonamu)
|
|
52
|
-
|
|
53
|
-
| Key | Description | Data |
|
|
54
|
-
| ------------------------------------ | ------------------------------ | --------------------------------------------------- |
|
|
55
|
-
| `esq-query` | Executed SQL query | Query string |
|
|
56
|
-
| `puri:executed-query` | Query executed by Puri | Query string |
|
|
57
|
-
| `puri:ub-register` | UpsertBuilder register call | `{ tableName, uuid, isUuidReused, row }` |
|
|
58
|
-
| `puri:ub-upserted` | UpsertBuilder upsert complete | `{ tableName, mode, rowCount, returnedIds }` |
|
|
59
|
-
| `puri:ub-ref-resolved` | UBRef → actual ID substitution | `{ tableName, field, from, to }` |
|
|
60
|
-
| `puri:ub-batch-updated` | updateBatch complete | `{ tableName, rowCount, whereColumns }` |
|
|
61
|
-
| `puri:ub-clean-orphans` | cleanOrphans executed | `{ tableName, cleanOrphans, deletedCount }` |
|
|
62
|
-
| `puri:ub-inherit` | inherit option applied | `{ tableName, inheritColumns, excludedFromUpdate }` |
|
|
63
|
-
| `mock:fs/promises:virtualFileSystem` | Virtual file system path | File path string |
|
|
64
|
-
| `fs/promises:writeFile` | writeFile call | `{ path, data }` |
|
|
65
|
-
| `fs/promises:rm` | rm call | `{ path, options }` |
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Recording with Custom Keys
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
// Record with a custom key in source code
|
|
73
|
-
Naite.t("user:created", { userId: 1, email: "test@test.com" });
|
|
74
|
-
|
|
75
|
-
// Virtual file system for mocking
|
|
76
|
-
Naite.t("mock:fs/promises:virtualFileSystem", "/path/to/virtual/file.ts");
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## Naite.get() Retrieval Methods
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
// Basic retrieval
|
|
85
|
-
Naite.get("key").first(); // first entry
|
|
86
|
-
Naite.get("key").last(); // last entry
|
|
87
|
-
Naite.get("key").at(2); // nth entry
|
|
88
|
-
Naite.get("key").result(); // all entries as an array
|
|
89
|
-
Naite.get("key").getTraces(); // raw trace array (includes call stack)
|
|
90
|
-
|
|
91
|
-
// Wildcard patterns
|
|
92
|
-
Naite.get("puri:*").result(); // all with puri: prefix
|
|
93
|
-
Naite.get("syncer:*:user").result(); // syncer:XXX:user pattern
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Chaining Filters
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
// Filter by file name
|
|
102
|
-
Naite.get("esq-query")
|
|
103
|
-
.fromFile("user.model.ts") // only entries recorded from this file
|
|
104
|
-
.result();
|
|
105
|
-
|
|
106
|
-
// Filter by function name
|
|
107
|
-
Naite.get("puri:executed-query")
|
|
108
|
-
.fromFunction("findById") // only entries called from this function
|
|
109
|
-
.result();
|
|
110
|
-
|
|
111
|
-
// fromFunction options
|
|
112
|
-
Naite.get("key")
|
|
113
|
-
.fromFunction("save", { from: "direct" }) // direct calls only (stack[0])
|
|
114
|
-
.fromFunction("save", { from: "indirect" }) // indirect calls only (stack[1+])
|
|
115
|
-
.fromFunction("save", { from: "both" }); // both (default)
|
|
116
|
-
|
|
117
|
-
// Filter by data path (radash get path)
|
|
118
|
-
Naite.get("puri:ub-register")
|
|
119
|
-
.where("data.tableName", "=", "users") // only where tableName is "users"
|
|
120
|
-
.where("data.rowCount", ">", 5) // rowCount > 5
|
|
121
|
-
.result();
|
|
122
|
-
|
|
123
|
-
// where operators: ">", "<", ">=", "<=", "=", "!=", "includes"
|
|
124
|
-
Naite.get("key")
|
|
125
|
-
.where("data.query", "includes", "WHERE") // check if string includes substring
|
|
126
|
-
.result();
|
|
127
|
-
|
|
128
|
-
// Combining filters
|
|
129
|
-
Naite.get("puri:executed-query")
|
|
130
|
-
.fromFunction("findMany")
|
|
131
|
-
.where("data", "includes", "users")
|
|
132
|
-
.first();
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## Naite.del() - Delete Values
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
Naite.t("mock:fs/promises:virtualFileSystem", "/virtual/path");
|
|
141
|
-
// ... test ...
|
|
142
|
-
Naite.del("mock:fs/promises:virtualFileSystem");
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Test Examples
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
test("query should not have a limit", async () => {
|
|
151
|
-
await UserModel.findMany("A", { num: 0, page: 1 });
|
|
152
|
-
|
|
153
|
-
expect(Naite.get("esq-query").first()).not.contain("limit");
|
|
154
|
-
expect(Naite.get("esq-query").first()).not.contain("offset");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("trace UpsertBuilder register", async () => {
|
|
158
|
-
const ub = new UpsertBuilder();
|
|
159
|
-
const ref = ub.register("users", { email: "test@test.com", username: "test" });
|
|
160
|
-
|
|
161
|
-
const trace = Naite.get("puri:ub-register").first();
|
|
162
|
-
expect(trace).toMatchObject({
|
|
163
|
-
tableName: "users",
|
|
164
|
-
uuid: ref.uuid,
|
|
165
|
-
isUuidReused: false,
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("trace upsert completion", async () => {
|
|
170
|
-
// ... run upsert ...
|
|
171
|
-
|
|
172
|
-
const trace = Naite.get("puri:ub-upserted").first();
|
|
173
|
-
expect(trace).toMatchObject({
|
|
174
|
-
tableName: "users",
|
|
175
|
-
mode: "upsert",
|
|
176
|
-
rowCount: 3,
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## Viewing Traces in DevRunner
|
|
184
|
-
|
|
185
|
-
Use the `sonamu test --traces` flag to view Naite traces directly in the CLI:
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
sonamu test user.model --traces
|
|
189
|
-
sonamu test user.model -t
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Example output:
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
Tests: 5 passed, 0 failed, 5 total
|
|
196
|
-
Duration: 791ms
|
|
197
|
-
|
|
198
|
-
Traces:
|
|
199
|
-
|
|
200
|
-
UserModel > BaseModel basic functionality > Model.findMany() with num = 0
|
|
201
|
-
user.model.test.ts
|
|
202
|
-
|
|
203
|
-
[esq-query] user.model.ts:113
|
|
204
|
-
select "users"."id" as "id", ...
|
|
205
|
-
|
|
206
|
-
[puri:executed-query] puri.ts:1349
|
|
207
|
-
select COUNT(*)::integer as "total" from "users" limit 1
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Trace data is fetched from `testCase.meta().traces` (collected in `afterEach` of `bootstrap.ts`) and serialized as the `SerializedTrace` type (exported from `naite.ts`).
|
|
211
|
-
|
|
212
|
-
See `sonamu-testing` for DevRunner details.
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
## Internal Structure
|
|
217
|
-
|
|
218
|
-
### NaiteStore
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
type NaiteStore = Map<string, NaiteTrace[]>;
|
|
222
|
-
|
|
223
|
-
interface NaiteTrace {
|
|
224
|
-
key: string;
|
|
225
|
-
data: any;
|
|
226
|
-
stack: StackFrame[]; // call stack information
|
|
227
|
-
at: Date;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
interface StackFrame {
|
|
231
|
-
functionName: string | null;
|
|
232
|
-
filePath: string; // path relative to TS file
|
|
233
|
-
lineNumber: number; // line number in TS file
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### SerializedTrace (for API responses / DevRunner)
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
type SerializedTrace = {
|
|
241
|
-
key: string;
|
|
242
|
-
value: any;
|
|
243
|
-
filePath: string;
|
|
244
|
-
lineNumber: number;
|
|
245
|
-
at: string;
|
|
246
|
-
};
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Debugging Uses
|
|
252
|
-
|
|
253
|
-
Naite can also be used for source code behavior analysis beyond tests:
|
|
254
|
-
|
|
255
|
-
- **Query tracing**: Use `esq-query` and `puri:executed-query` to see the actual SQL being executed
|
|
256
|
-
- **UpsertBuilder analysis**: Trace the full flow (register → upsert → ref-resolved → batch-updated) with `puri:ub-*` keys
|
|
257
|
-
- **File I/O tracing**: Use `fs/promises:*` keys to see file operations performed by the syncer and others
|
|
258
|
-
- **Isolate a specific function**: Use `.fromFunction("findById")` chaining to filter traces to only those originating from a specific method
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## References
|
|
263
|
-
|
|
264
|
-
- **Testing guide**: `sonamu-testing`
|
|
265
|
-
- **DevRunner details**: `sonamu-testing`
|
|
266
|
-
- **expectQuery helper** (Naite-based): see "Test Helper: expectQuery" in `sonamu-testing`
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-query
|
|
3
|
-
description: Reads and writes data through Sonamu Models and the Puri query builder. Use when implementing a Model CRUD method, writing a SELECT/WHERE/JOIN query, batch-saving relation data, or when a query returns unexpected rows or an excessively deep type error. Covers BaseModelClass, findMany, executeSubsetQuery, getPuri, transactions, UpsertBuilder, tsvector and PGroonga full-text search, pgvector, and pg_trgm.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Sonamu Data Access
|
|
7
|
-
|
|
8
|
-
**Working code references:**
|
|
9
|
-
|
|
10
|
-
- `sonamu/examples/miomock/api/src/application/employee/employee.model.ts` — basic CRUD
|
|
11
|
-
- `sonamu/examples/miomock/api/src/application/project/project.model.ts` — ManyToMany save
|
|
12
|
-
- `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` — tests
|
|
13
|
-
|
|
14
|
-
## Reference Map
|
|
15
|
-
|
|
16
|
-
| Need | Read |
|
|
17
|
-
| --- | --- |
|
|
18
|
-
| Model class structure, CRUD methods, getSubsetQueries, executeSubsetQuery options, enhancers, types file | `references/model.md` |
|
|
19
|
-
| Transactions, validation patterns, orderBy after scaffolding, code conventions | `references/model-patterns.md` |
|
|
20
|
-
| SELECT, WHERE, JOIN, GROUP BY, INSERT/UPDATE/DELETE, result methods | `references/puri.md` |
|
|
21
|
-
| tsvector / PGroonga full-text search, pgvector, pg_trgm fuzzy search | `references/search.md` |
|
|
22
|
-
| Batch-saving relations, save order, ManyToMany, bulk insert, UpsertOptions | `references/upsert.md` |
|
|
23
|
-
|
|
24
|
-
Embedding generation and chunking live in the `sonamu-vector` skill.
|
|
25
|
-
Subset definition lives in `sonamu-entity`.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Non-negotiable rules
|
|
30
|
-
|
|
31
|
-
**Puri**
|
|
32
|
-
|
|
33
|
-
- Puri is the standard for BOTH reads and writes in ALL contexts (Model, Frame, scripts) — do NOT run queries on a raw `DB.getDB()` handle (exceptions: migration files, `db.ts`, tests, and the `.knex` escape hatch)
|
|
34
|
-
- MUST use `getPuri("r")` for read queries, `getPuri("w")` for write queries
|
|
35
|
-
- MUST include a WHERE condition for UPDATE/DELETE
|
|
36
|
-
- MUST use `transaction()` for multiple write operations
|
|
37
|
-
- Inside a Frame, use the associated Model's `getPuri` (Frame exposes only `getDB` / `getUpsertBuilder`)
|
|
38
|
-
- Outside a Model, wrap knex with `new PuriWrapper(DB.getDB(which), new UpsertBuilder())`
|
|
39
|
-
- Use the `puri.knex` escape hatch only for non-entity / framework-internal tables (e.g. `workflow_runs`)
|
|
40
|
-
- JSON/JSONB columns are automatically JSON.stringify'd on insert/update
|
|
41
|
-
|
|
42
|
-
**UpsertBuilder**
|
|
43
|
-
|
|
44
|
-
- MUST use inside `transaction()`
|
|
45
|
-
- MUST call `ubUpsert()` for FK-referenced tables first (correct order)
|
|
46
|
-
- `UBRef` can ONLY be used inside `ubRegister`, never for direct DB queries
|
|
47
|
-
- Self-reference is auto-handled by level-based insertion
|
|
48
|
-
- Unique index conflicts are auto-resolved by pre-fetching existing IDs
|
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
# Model — Transactions, Validation, Conventions
|
|
2
|
-
|
|
3
|
-
## Transactions
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
await this.getPuri("w").transaction(async (trx) => {
|
|
7
|
-
await trx.table("users").where("id", fromId).decrement("points", amount);
|
|
8
|
-
await trx.table("users").where("id", toId).increment("points", amount);
|
|
9
|
-
});
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Validation Patterns
|
|
13
|
-
|
|
14
|
-
### Step-by-step Validation
|
|
15
|
-
|
|
16
|
-
A pattern for validating business rules step by step:
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
async enroll(courseId: number, userId: number): Promise<Enrollment> {
|
|
20
|
-
// Step 1: Duplicate check
|
|
21
|
-
const existing = await this.findOne("A", {
|
|
22
|
-
course_id: courseId,
|
|
23
|
-
user_id: userId,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (existing) {
|
|
27
|
-
throw new Error("Already enrolled in this course");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Step 2: Capacity check
|
|
31
|
-
const course = await CourseModel.findById("A", courseId);
|
|
32
|
-
const { total } = await this.findMany({ course_id: courseId });
|
|
33
|
-
|
|
34
|
-
if (total >= course.max_students) {
|
|
35
|
-
throw new Error("The course is full");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Step 3: Execute
|
|
39
|
-
const [id] = await this.save([{ course_id: courseId, user_id: userId }]);
|
|
40
|
-
return this.findById("A", id);
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Conditional Validation
|
|
45
|
-
|
|
46
|
-
Perform different validations depending on conditions:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
async save(spa: TaskSaveParams[]): Promise<number[]> {
|
|
50
|
-
for (const sp of spa) {
|
|
51
|
-
// completion date is required only when status is completed
|
|
52
|
-
if (sp.status === "completed" && !sp.completed_at) {
|
|
53
|
-
throw new Error("A completion date is required for completed status");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check amount range only when budget is present
|
|
57
|
-
if (sp.budget !== null && sp.budget < 0) {
|
|
58
|
-
throw new Error("Budget must be 0 or greater");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Save after validation passes
|
|
63
|
-
const wdb = this.getPuri("w");
|
|
64
|
-
spa.forEach((sp) => wdb.ubRegister("tasks", sp));
|
|
65
|
-
|
|
66
|
-
return wdb.transaction(async (trx) => {
|
|
67
|
-
return trx.ubUpsert("tasks");
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Validating Against Related Data
|
|
73
|
-
|
|
74
|
-
Validate relationships with other tables:
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
async save(spa: ResponseSaveParams[]): Promise<number[]> {
|
|
78
|
-
for (const sp of spa) {
|
|
79
|
-
// Check if the survey is still open
|
|
80
|
-
const collection = await CollectionModel.findById("A", sp.collection_id);
|
|
81
|
-
|
|
82
|
-
if (collection.status === "closed") {
|
|
83
|
-
throw new Error("This survey has already ended");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check response period
|
|
87
|
-
const now = new Date();
|
|
88
|
-
if (now < collection.begin_date || now > collection.end_date) {
|
|
89
|
-
throw new Error("This is not within the response period");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Save after validation passes
|
|
94
|
-
const wdb = this.getPuri("w");
|
|
95
|
-
spa.forEach((sp) => wdb.ubRegister("responses", sp));
|
|
96
|
-
|
|
97
|
-
return wdb.transaction(async (trx) => {
|
|
98
|
-
return trx.ubUpsert("responses");
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**Key points:**
|
|
104
|
-
|
|
105
|
-
- Clear error messages when validation fails
|
|
106
|
-
- Only save after all validations pass
|
|
107
|
-
- Enforce business rules through code
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## IMPORTANT: Verify orderBy After Scaffolding
|
|
112
|
-
|
|
113
|
-
### Problem
|
|
114
|
-
|
|
115
|
-
When scaffolding is run from Sonamu UI, the model file is **regenerated**, leaving only the default value (`id-desc`) and losing any custom orderBy cases.
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
Error: Argument of type 'xxx-asc' is not assignable to parameter of type 'never'
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Fix
|
|
122
|
-
|
|
123
|
-
After scaffolding, you must exhaustively handle **all orderBy enum cases** from entity.json in the model file.
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// entity.json orderBy enum
|
|
127
|
-
{ "TaskOrderBy": { "id-desc": "ID Latest", "created_at-desc": "By Date", "title-asc": "By Title" } }
|
|
128
|
-
|
|
129
|
-
// model - must verify/add after scaffolding
|
|
130
|
-
if (params.orderBy) {
|
|
131
|
-
if (params.orderBy === "id-desc") {
|
|
132
|
-
qb.orderBy("tasks.id", "desc");
|
|
133
|
-
} else if (params.orderBy === "created_at-desc") {
|
|
134
|
-
qb.orderBy("tasks.created_at", "desc");
|
|
135
|
-
} else if (params.orderBy === "title-asc") {
|
|
136
|
-
qb.orderBy("tasks.title", "asc");
|
|
137
|
-
} else {
|
|
138
|
-
exhaustive(params.orderBy); // compile error if any case is missing
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Checklist
|
|
144
|
-
|
|
145
|
-
- Verify orderBy cases in model after scaffolding
|
|
146
|
-
- Confirm they match the orderBy enum in entity.json
|
|
147
|
-
- Also check other custom logic such as search cases and enhancers
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## Code Quality and Consistency
|
|
152
|
-
|
|
153
|
-
### DRY principle: use this.modelName
|
|
154
|
-
|
|
155
|
-
Use `this.modelName` instead of hardcoding the model name in error messages.
|
|
156
|
-
|
|
157
|
-
**BAD: hardcoded model name**
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// department.model.ts
|
|
161
|
-
if (!rows[0]) {
|
|
162
|
-
throw new NotFoundException(SD("notFound")("Department", id));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// user.model.ts
|
|
166
|
-
if (!rows[0]) {
|
|
167
|
-
throw new NotFoundException(SD("notFound")("User", id));
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**GOOD: use this.modelName**
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
// Common to all Models
|
|
175
|
-
if (!rows[0]) {
|
|
176
|
-
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**Benefits:**
|
|
181
|
-
|
|
182
|
-
- Prevents copy-paste mistakes: no need to update the model name when copying from another model
|
|
183
|
-
- Consistency: all models use the same pattern
|
|
184
|
-
- Maintainability: changing modelName in the constructor automatically reflects in all error messages
|
|
185
|
-
|
|
186
|
-
### Consistent i18n Key Usage
|
|
187
|
-
|
|
188
|
-
Use the same i18n keys consistently for the same purpose across the entire project.
|
|
189
|
-
|
|
190
|
-
**BAD: duplicate i18n keys**
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
// Different keys used across models
|
|
194
|
-
throw new NotFoundException(SD("error.entityNotFound")(this.modelName, id));
|
|
195
|
-
throw new NotFoundException(SD("error.notFound")(this.modelName, id));
|
|
196
|
-
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
197
|
-
|
|
198
|
-
// Search field error
|
|
199
|
-
throw new BadRequestException(SD("error.unknownSearchField")(params.search));
|
|
200
|
-
throw new BadRequestException(SD("error.invalidSearchField")(params.search));
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**GOOD: use standard i18n keys**
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// Entity lookup failure - short and clear
|
|
207
|
-
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
208
|
-
|
|
209
|
-
// Search field error - search namespace
|
|
210
|
-
throw new BadRequestException(SD("search.invalidField")(params.search));
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
**Recommended i18n key patterns:**
|
|
214
|
-
| Situation | i18n key | Used in |
|
|
215
|
-
|------|---------|--------|
|
|
216
|
-
| Entity lookup failure | `notFound` | findById |
|
|
217
|
-
| Invalid search field | `search.invalidField` | findMany search |
|
|
218
|
-
| Missing required field | `validation.required` | save validation |
|
|
219
|
-
| Unauthorized | `error.forbidden` | guards failure |
|
|
220
|
-
| Login required | `error.loginRequired` | Context.user null |
|
|
221
|
-
|
|
222
|
-
### Bulk Refactoring Strategy
|
|
223
|
-
|
|
224
|
-
When consistently modifying multiple model files, use sed for automation:
|
|
225
|
-
|
|
226
|
-
**Step 1: Confirm pattern**
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
# Find files to modify
|
|
230
|
-
grep -r 'SD("error.entityNotFound")' packages/api/src/application/*/
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Step 2: Validate changes (dry-run)**
|
|
234
|
-
|
|
235
|
-
```bash
|
|
236
|
-
# Preview changes before applying
|
|
237
|
-
sed -n 's/SD("error.entityNotFound")(\(.*\), id)/SD("notFound")(this.modelName, id)/p' file.ts
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**Step 3: Apply in bulk**
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
# Modify all model files
|
|
244
|
-
find packages/api/src/application -name "*.model.ts" -exec sed -i '' \
|
|
245
|
-
's/SD("error.entityNotFound")(\(.*\), id)/SD("notFound")(this.modelName, id)/g' {} \;
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Step 4: Validate with build**
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
# TypeScript type check
|
|
252
|
-
pnpm typecheck
|
|
253
|
-
|
|
254
|
-
# Full build
|
|
255
|
-
pnpm build
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
**Cautions:**
|
|
259
|
-
|
|
260
|
-
- Always run after a git commit (to allow rollback)
|
|
261
|
-
- Confirm changes with dry-run first
|
|
262
|
-
- Check for type errors with build
|
|
263
|
-
- Verify behavior with tests
|
|
264
|
-
|
|
265
|
-
### Type Check Patterns
|
|
266
|
-
|
|
267
|
-
**satisfies vs as const:**
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// BAD: bypasses type checking with type assertion
|
|
271
|
-
const params = {
|
|
272
|
-
num: 24,
|
|
273
|
-
page: 1,
|
|
274
|
-
search: "id" as const,
|
|
275
|
-
orderBy: "wrong-value" as const, // error not detected
|
|
276
|
-
...rawParams,
|
|
277
|
-
} as RoleListParams;
|
|
278
|
-
|
|
279
|
-
// GOOD: compile-time validation with satisfies
|
|
280
|
-
const params = {
|
|
281
|
-
num: 24,
|
|
282
|
-
page: 1,
|
|
283
|
-
search: "id" as const,
|
|
284
|
-
orderBy: "wrong-value" as const, // compile error!
|
|
285
|
-
...rawParams,
|
|
286
|
-
} satisfies RoleListParams;
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
**Recommended usage locations:**
|
|
290
|
-
|
|
291
|
-
- Default values for params in findMany
|
|
292
|
-
- Complex object literals (where type checking is important)
|
|
293
|
-
|
|
294
|
-
### IMPORTANT: ListParams / findMany / SearchField Synchronization
|
|
295
|
-
|
|
296
|
-
The following three must always remain consistent. If any one is out of sync, the feature either exists as a declaration only with no behavior, or a runtime error will occur.
|
|
297
|
-
|
|
298
|
-
1. `SearchField` enum values in `entity.json`
|
|
299
|
-
2. `ListParams` field definitions in `types.ts`
|
|
300
|
-
3. Filter/search handling code in `findMany` in `model.ts`
|
|
301
|
-
|
|
302
|
-
**Checklist:**
|
|
303
|
-
|
|
304
|
-
- [ ] Are all values declared in SearchField implemented in findMany?
|
|
305
|
-
- [ ] If any filter branch is commented out, either remove it or implement it
|
|
306
|
-
- [ ] Are "filter by ~", "search by ~" features from requirements reflected in ListParams?
|
|
307
|
-
|
|
308
|
-
**In particular, entities with an approval workflow must always add a status filter.**
|
|
309
|
-
(Clicking count by stage → filter to show only that list is a commonly required pattern)
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
// types.ts - approval workflow entity example
|
|
313
|
-
export const AchievementListParams = AchievementBaseListParams.extend({
|
|
314
|
-
status: z.nativeEnum(AchievementStatus).optional(),
|
|
315
|
-
achievement_type: z.nativeEnum(AchievementType).optional(),
|
|
316
|
-
submitter_id: z.string().optional(),
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// model.ts - corresponding filter implementation
|
|
320
|
-
if (params.status) qb.where("achievements.status", params.status);
|
|
321
|
-
if (params.achievement_type) qb.where("achievements.achievement_type", params.achievement_type);
|
|
322
|
-
if (params.submitter_id) qb.where("achievements.submitter_id", params.submitter_id);
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**DO NOT - declaration/implementation mismatch:**
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
// SearchField "title" declared in entity.json
|
|
329
|
-
// model.ts only handles "id" case, "title" is commented out
|
|
330
|
-
if (params.search === "id") {
|
|
331
|
-
// ...
|
|
332
|
-
} /* else if (params.search === "title") {
|
|
333
|
-
// TODO: not implemented
|
|
334
|
-
} */
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Code Review Checklist
|
|
338
|
-
|
|
339
|
-
When writing a new Model:
|
|
340
|
-
|
|
341
|
-
- [ ] Use `this.modelName` (no hardcoding)
|
|
342
|
-
- [ ] Use standard i18n keys (`notFound`, `search.invalidField`)
|
|
343
|
-
- [ ] Use the `satisfies` keyword (type safety)
|
|
344
|
-
- [ ] Do not unnecessarily specify the debug option
|
|
345
|
-
- [ ] Exhaustively handle all orderBy cases
|
|
346
|
-
- [ ] If a ManyToMany relation exists, add \_ids array to SaveParams
|
|
347
|
-
- [ ] Does the `@upload` method have `@api` on it? (`@upload` is used standalone; using both together causes a build error)
|
|
348
|
-
- [ ] Do the SearchField enum and findMany implementation match?
|
|
349
|
-
- [ ] For entities with approval workflows, are status/type filters present in both ListParams and findMany?
|
|
350
|
-
|
|
351
|
-
When bulk-modifying 20+ Models:
|
|
352
|
-
|
|
353
|
-
- [ ] Compare patterns with reference code like miomock
|
|
354
|
-
- [ ] Prioritize inconsistent patterns
|
|
355
|
-
- [ ] Write an automation script using sed or similar
|
|
356
|
-
- [ ] Commit to git before making changes
|
|
357
|
-
- [ ] Validate changes with dry-run
|
|
358
|
-
- [ ] Check for type errors with pnpm typecheck
|
|
359
|
-
- [ ] Verify behavior with pnpm test
|
|
360
|
-
- [ ] Check for `any` type usage (prohibited)
|
|
361
|
-
|
|
362
|
-
### Prohibition on any type
|
|
363
|
-
|
|
364
|
-
The `any` type neutralizes TypeScript's type safety and must **never be used**.
|
|
365
|
-
|
|
366
|
-
**BAD: using any**
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
const { category_ids, ...data } = sp as any;
|
|
370
|
-
function process(input: any) { ... }
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**GOOD: use precise types or unknown**
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
// Destructure with a precise type
|
|
377
|
-
const { category_ids, ...data } = sp as QuestionCollectionSaveParams;
|
|
378
|
-
|
|
379
|
-
// Use unknown when the type is not known (instead of any)
|
|
380
|
-
function process(input: unknown) {
|
|
381
|
-
if (typeof input === "string") { ... }
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
**Rules:**
|
|
386
|
-
|
|
387
|
-
- `any` is prohibited
|
|
388
|
-
- When the type is unknown, use `unknown` and narrow with a type guard
|
|
389
|
-
- When a type assertion is needed during destructuring, specify the exact type name (`as ConcreteType`)
|
|
390
|
-
- Suppression comments like `eslint-disable @typescript-eslint/no-explicit-any` are also prohibited
|