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.
Files changed (79) hide show
  1. package/dist/api/decorators.d.ts.map +1 -1
  2. package/dist/api/decorators.js +5 -12
  3. package/dist/bin/cli.js +10 -253
  4. package/dist/database/base-model.d.ts +1 -0
  5. package/dist/database/base-model.d.ts.map +1 -1
  6. package/dist/database/base-model.js +14 -8
  7. package/dist/database/db.d.ts +2 -1
  8. package/dist/database/db.d.ts.map +1 -1
  9. package/dist/database/db.js +13 -3
  10. package/dist/database/puri-wrapper.d.ts.map +1 -1
  11. package/dist/database/puri-wrapper.js +4 -11
  12. package/dist/database/transaction-context.d.ts +8 -3
  13. package/dist/database/transaction-context.d.ts.map +1 -1
  14. package/dist/database/transaction-context.js +12 -7
  15. package/dist/migration/code-generation.js +2 -2
  16. package/dist/naite/naite-reporter.d.ts +8 -3
  17. package/dist/naite/naite-reporter.d.ts.map +1 -1
  18. package/dist/naite/naite-reporter.js +9 -4
  19. package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
  20. package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
  21. package/dist/ui-web/index.html +2 -2
  22. package/package.json +3 -3
  23. package/src/api/decorators.ts +11 -22
  24. package/src/bin/cli.ts +12 -375
  25. package/src/database/__tests__/transaction-scope.test.ts +227 -0
  26. package/src/database/base-model.ts +31 -6
  27. package/src/database/db.ts +17 -2
  28. package/src/database/puri-wrapper.ts +5 -22
  29. package/src/database/transaction-context.ts +14 -6
  30. package/src/migration/code-generation.ts +1 -1
  31. package/src/naite/naite-reporter.ts +8 -3
  32. package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
  33. package/src/skills/AGENTS.md +0 -108
  34. package/src/skills/sonamu/SKILL.md +0 -75
  35. package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
  36. package/src/skills/sonamu-api/SKILL.md +0 -480
  37. package/src/skills/sonamu-auth/SKILL.md +0 -327
  38. package/src/skills/sonamu-auth/references/plugins.md +0 -310
  39. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
  40. package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
  41. package/src/skills/sonamu-config/SKILL.md +0 -203
  42. package/src/skills/sonamu-config/references/database.md +0 -452
  43. package/src/skills/sonamu-config/references/environments.md +0 -178
  44. package/src/skills/sonamu-config/references/server-options.md +0 -400
  45. package/src/skills/sonamu-entity/SKILL.md +0 -180
  46. package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
  47. package/src/skills/sonamu-entity/references/design-guides.md +0 -243
  48. package/src/skills/sonamu-entity/references/field-types.md +0 -170
  49. package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
  50. package/src/skills/sonamu-entity/references/relations.md +0 -463
  51. package/src/skills/sonamu-entity/references/subset.md +0 -156
  52. package/src/skills/sonamu-fixture/SKILL.md +0 -180
  53. package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
  54. package/src/skills/sonamu-fixture/references/cone.md +0 -298
  55. package/src/skills/sonamu-frontend/SKILL.md +0 -142
  56. package/src/skills/sonamu-frontend/references/components.md +0 -323
  57. package/src/skills/sonamu-frontend/references/examples.md +0 -64
  58. package/src/skills/sonamu-frontend/references/hooks.md +0 -273
  59. package/src/skills/sonamu-frontend/references/runtime.md +0 -165
  60. package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
  61. package/src/skills/sonamu-i18n/SKILL.md +0 -287
  62. package/src/skills/sonamu-migration/SKILL.md +0 -316
  63. package/src/skills/sonamu-naite/SKILL.md +0 -266
  64. package/src/skills/sonamu-query/SKILL.md +0 -48
  65. package/src/skills/sonamu-query/references/model-patterns.md +0 -390
  66. package/src/skills/sonamu-query/references/model.md +0 -366
  67. package/src/skills/sonamu-query/references/puri.md +0 -413
  68. package/src/skills/sonamu-query/references/search.md +0 -238
  69. package/src/skills/sonamu-query/references/upsert.md +0 -324
  70. package/src/skills/sonamu-tasks/SKILL.md +0 -236
  71. package/src/skills/sonamu-testing/SKILL.md +0 -251
  72. package/src/skills/sonamu-testing/references/devrunner.md +0 -405
  73. package/src/skills/sonamu-testing/references/helpers.md +0 -185
  74. package/src/skills/sonamu-testing/references/patterns.md +0 -263
  75. package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
  76. package/src/skills/sonamu-testing/references/quick-start.md +0 -285
  77. package/src/skills/sonamu-testing/references/type-safety.md +0 -172
  78. package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
  79. 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