sonamu 0.10.7 → 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +10 -253
- package/dist/migration/code-generation.js +2 -2
- 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/bin/cli.ts +12 -375
- package/src/migration/code-generation.ts +1 -1
- 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,366 +0,0 @@
|
|
|
1
|
-
# Model — Structure, CRUD, Subset Queries
|
|
2
|
-
|
|
3
|
-
## Basic Structure
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
import { api, BaseModelClass, ListResult, NotFoundException } from "sonamu";
|
|
7
|
-
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
|
|
8
|
-
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
|
|
9
|
-
import type { UserListParams, UserSaveParams } from "./user.types";
|
|
10
|
-
|
|
11
|
-
class UserModelClass extends BaseModelClass<
|
|
12
|
-
UserSubsetKey,
|
|
13
|
-
UserSubsetMapping,
|
|
14
|
-
typeof userSubsetQueries,
|
|
15
|
-
typeof userLoaderQueries
|
|
16
|
-
> {
|
|
17
|
-
constructor() {
|
|
18
|
-
super("User", userSubsetQueries, userLoaderQueries);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const UserModel = new UserModelClass();
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## CRUD Pattern
|
|
26
|
-
|
|
27
|
-
Sonamu Model provides the following basic methods:
|
|
28
|
-
|
|
29
|
-
| Method | Purpose | Notes |
|
|
30
|
-
| ---------- | ---------------------- | ------------------ |
|
|
31
|
-
| `findById` | Retrieve single record | |
|
|
32
|
-
| `findMany` | Retrieve list | |
|
|
33
|
-
| `save` | Create/update | upsert behavior |
|
|
34
|
-
| `del` | Delete | Note: not `delete` |
|
|
35
|
-
|
|
36
|
-
**Avoiding JavaScript reserved words:** `delete` is a JS reserved word, so it is named `del`. While TypeScript allows `delete` as a method name without a compile error, it can cause runtime issues, so Sonamu uses `del`.
|
|
37
|
-
|
|
38
|
-
### findById
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "User" })
|
|
42
|
-
async findById<T extends UserSubsetKey>(subset: T, id: number): Promise<UserSubsetMapping[T]> {
|
|
43
|
-
const { rows } = await this.findMany(subset, { id, num: 1, page: 1 });
|
|
44
|
-
if (!rows[0]) throw new NotFoundException(`User ID ${id} not found`);
|
|
45
|
-
return rows[0];
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### findMany
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "Users" })
|
|
53
|
-
async findMany<T extends UserSubsetKey>(
|
|
54
|
-
subset: T,
|
|
55
|
-
params: UserListParams = { num: 10, page: 1 }
|
|
56
|
-
): Promise<ListResult<UserListParams, UserSubsetMapping[T]>> {
|
|
57
|
-
const { qb } = this.getSubsetQueries(subset);
|
|
58
|
-
|
|
59
|
-
if (params.id) qb.whereIn("users.id", asArray(params.id));
|
|
60
|
-
if (params.keyword) qb.where("users.email", "like", `%${params.keyword}%`);
|
|
61
|
-
if (params.orderBy === "id-desc") qb.orderBy("users.id", "desc");
|
|
62
|
-
|
|
63
|
-
return this.executeSubsetQuery({ subset, qb, params });
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### save
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
71
|
-
async save(spa: UserSaveParams[]): Promise<number[]> {
|
|
72
|
-
const wdb = this.getPuri("w");
|
|
73
|
-
spa.forEach((sp) => wdb.ubRegister("users", sp));
|
|
74
|
-
|
|
75
|
-
return wdb.transaction(async (trx) => {
|
|
76
|
-
return trx.ubUpsert("users");
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### del
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
@api({ httpMethod: "POST", guards: ["admin"] })
|
|
85
|
-
async del(ids: number[]): Promise<number> {
|
|
86
|
-
const wdb = this.getPuri("w");
|
|
87
|
-
await wdb.transaction(async (trx) => {
|
|
88
|
-
return trx.table("users").whereIn("id", ids).delete();
|
|
89
|
-
});
|
|
90
|
-
return ids.length;
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## BaseModel Methods
|
|
95
|
-
|
|
96
|
-
| Method | Description |
|
|
97
|
-
| ----------------------------- | ------------------------------------------------- |
|
|
98
|
-
| `getPuri("r")` | Read query builder |
|
|
99
|
-
| `getPuri("w")` | Write query builder |
|
|
100
|
-
| `getSubsetQueries(subset)` | Subset query builder (returns `{ qb, onSubset }`) |
|
|
101
|
-
| `executeSubsetQuery(options)` | Execute subset query |
|
|
102
|
-
| `createEnhancers(enhancers)` | Enhancer object creation helper (type inference) |
|
|
103
|
-
|
|
104
|
-
## getSubsetQueries
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
const { qb, onSubset } = this.getSubsetQueries(subset);
|
|
108
|
-
|
|
109
|
-
// qb: query builder for adding conditions
|
|
110
|
-
qb.where("users.status", "active");
|
|
111
|
-
|
|
112
|
-
// onSubset: when you need the type for a specific subset
|
|
113
|
-
const typedQb = onSubset("A"); // infers as subset A's type
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## executeSubsetQuery Options
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
return this.executeSubsetQuery({
|
|
120
|
-
subset, // subset key
|
|
121
|
-
qb, // query builder
|
|
122
|
-
params, // ListParams (num, page, queryMode, sonamuFilter, etc.)
|
|
123
|
-
debug: true, // print query log (default: false)
|
|
124
|
-
optimizeCountQuery: true, // COUNT query optimization - removes unnecessary LEFT JOINs (default: false)
|
|
125
|
-
enhancers, // Enhancer function object (optional)
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
> **CRITICAL: Do not directly mutate the object returned by `executeSubsetQuery()`.**
|
|
130
|
-
>
|
|
131
|
-
> Replacing rows via `result.rows = result.rows.map(...)` or `(result as any).rows = ...`
|
|
132
|
-
> will break the `total` count and cause pagination to malfunction.
|
|
133
|
-
>
|
|
134
|
-
> Use the `enhancers` pattern for virtual fields that require additional computation:
|
|
135
|
-
>
|
|
136
|
-
> ```typescript
|
|
137
|
-
> // WRONG — breaks pagination
|
|
138
|
-
> const result = await this.executeSubsetQuery({ subset, qb, params });
|
|
139
|
-
> (result as any).rows = result.rows.map((row) => ({ ...row, extra: "value" }));
|
|
140
|
-
> return result as any;
|
|
141
|
-
>
|
|
142
|
-
> // CORRECT — enhancers pattern
|
|
143
|
-
> const enhancers = this.createEnhancers({
|
|
144
|
-
> A: (row) => ({ ...row, extra: "value" }),
|
|
145
|
-
> });
|
|
146
|
-
> return this.executeSubsetQuery({ subset, qb, params, enhancers });
|
|
147
|
-
> ```
|
|
148
|
-
|
|
149
|
-
### queryMode
|
|
150
|
-
|
|
151
|
-
Pass queryMode in params to control the return value:
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// List only (skip COUNT query) - performance optimization
|
|
155
|
-
const { rows } = await this.findMany(subset, { ...params, queryMode: "list" });
|
|
156
|
-
|
|
157
|
-
// Count only (skip list)
|
|
158
|
-
const { total } = await this.findMany(subset, { ...params, queryMode: "count" });
|
|
159
|
-
|
|
160
|
-
// Both (default)
|
|
161
|
-
const { rows, total } = await this.findMany(subset, { ...params, queryMode: "both" });
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### sonamuFilter (FilterQuery)
|
|
165
|
-
|
|
166
|
-
Automatically apply filter conditions via params.sonamuFilter:
|
|
167
|
-
|
|
168
|
-
**Prerequisite:** The corresponding prop in entity.json must have `"toFilter": true` set. Fields without this setting are excluded from filtering.
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// Filter passed from the client
|
|
172
|
-
const params = {
|
|
173
|
-
num: 10,
|
|
174
|
-
page: 1,
|
|
175
|
-
sonamuFilter: {
|
|
176
|
-
status: "active", // eq (default)
|
|
177
|
-
age: { gte: 18 }, // >=
|
|
178
|
-
role: { in: ["admin", "user"] },
|
|
179
|
-
email: { contains: "@test" }, // LIKE %...%
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// Automatically applied in the Model
|
|
184
|
-
return this.executeSubsetQuery({ subset, qb, params });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
**Allowed operators by type:**
|
|
188
|
-
|
|
189
|
-
| Type | Operators |
|
|
190
|
-
| ----------------- | -------------------------------------------------------------------- |
|
|
191
|
-
| `string` | eq, ne, contains, startsWith, endsWith, in, notIn, isNull, isNotNull |
|
|
192
|
-
| `integer` | eq, ne, gt, gte, lt, lte, in, notIn, between, isNull, isNotNull |
|
|
193
|
-
| `numeric` | eq, ne, gt, gte, lt, lte, in, notIn, between, isNull, isNotNull |
|
|
194
|
-
| `boolean` | eq, ne, isNull, isNotNull |
|
|
195
|
-
| `date`/`datetime` | eq, ne, before, after, between, isNull, isNotNull |
|
|
196
|
-
| `enum` | eq, ne, in, notIn, isNull, isNotNull |
|
|
197
|
-
| `json` | isNull, isNotNull |
|
|
198
|
-
|
|
199
|
-
**Operator examples:**
|
|
200
|
-
|
|
201
|
-
| Operator | SQL | Example |
|
|
202
|
-
| --------------------- | --------------- | ----------------------------------------- |
|
|
203
|
-
| `eq` (default) | `=` | `{ status: "active" }` |
|
|
204
|
-
| `ne` | `!=` | `{ status: { ne: "deleted" } }` |
|
|
205
|
-
| `gt`, `gte` | `>`, `>=` | `{ age: { gte: 18 } }` |
|
|
206
|
-
| `lt`, `lte` | `<`, `<=` | `{ price: { lte: 1000 } }` |
|
|
207
|
-
| `in`, `notIn` | `IN`, `NOT IN` | `{ role: { in: ["a", "b"] } }` |
|
|
208
|
-
| `contains` | `LIKE %...%` | `{ name: { contains: "kim" } }` |
|
|
209
|
-
| `startsWith` | `LIKE ...%` | `{ code: { startsWith: "A" } }` |
|
|
210
|
-
| `endsWith` | `LIKE %...` | `{ ext: { endsWith: ".pdf" } }` |
|
|
211
|
-
| `isNull`, `isNotNull` | `IS NULL` | `{ deleted_at: { isNull: true } }` |
|
|
212
|
-
| `before`, `after` | `<`, `>` (date) | `{ created_at: { after: "2024-01-01" } }` |
|
|
213
|
-
| `between` | `BETWEEN` | `{ price: { between: [100, 500] } }` |
|
|
214
|
-
|
|
215
|
-
**Type definition (`ApplySonamuFilter`):**
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
import type { ApplySonamuFilter } from "sonamu";
|
|
219
|
-
|
|
220
|
-
// Define sonamuFilter type in ListParams
|
|
221
|
-
type ProjectListParams = {
|
|
222
|
-
num: number;
|
|
223
|
-
page: number;
|
|
224
|
-
sonamuFilter?: ApplySonamuFilter<
|
|
225
|
-
ProjectSubsetA, // entity type
|
|
226
|
-
"id" | "created_at", // fields to exclude (TOmitKeys)
|
|
227
|
-
"budget" // fields to treat as numeric (TNumericKeys)
|
|
228
|
-
>;
|
|
229
|
-
};
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Enhancers
|
|
233
|
-
|
|
234
|
-
Post-query processing for virtual field computation and similar needs:
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// Define enhancer
|
|
238
|
-
const enhancers = this.createEnhancers({
|
|
239
|
-
A: async (row) => ({
|
|
240
|
-
...row,
|
|
241
|
-
fullName: `${row.first_name} ${row.last_name}`,
|
|
242
|
-
}),
|
|
243
|
-
D: async (row) => ({
|
|
244
|
-
...row,
|
|
245
|
-
age: calculateAge(row.birth_date),
|
|
246
|
-
}),
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// Use in executeSubsetQuery
|
|
250
|
-
return this.executeSubsetQuery({ subset, qb, params, enhancers });
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## Types File
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// user.types.ts
|
|
257
|
-
import { z } from "zod";
|
|
258
|
-
import {
|
|
259
|
-
UserOrderBy,
|
|
260
|
-
UserSearchField,
|
|
261
|
-
UserBaseSchema,
|
|
262
|
-
UserBaseListParams,
|
|
263
|
-
} from "../sonamu.generated";
|
|
264
|
-
|
|
265
|
-
export const UserListParams = UserBaseListParams;
|
|
266
|
-
export type UserListParams = z.infer<typeof UserListParams>;
|
|
267
|
-
|
|
268
|
-
// Basic pattern: partial from BaseSchema
|
|
269
|
-
export const UserSaveParams = UserBaseSchema.partial({
|
|
270
|
-
id: true,
|
|
271
|
-
created_at: true,
|
|
272
|
-
});
|
|
273
|
-
export type UserSaveParams = z.infer<typeof UserSaveParams>;
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### SaveParams Patterns
|
|
277
|
-
|
|
278
|
-
**Basic pattern (no relations):**
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
import { UserBaseSchema, UserBaseListParams } from "../sonamu.generated";
|
|
282
|
-
|
|
283
|
-
export const UserListParams = UserBaseListParams;
|
|
284
|
-
export type UserListParams = z.infer<typeof UserListParams>;
|
|
285
|
-
|
|
286
|
-
export const UserSaveParams = UserBaseSchema.partial({
|
|
287
|
-
id: true,
|
|
288
|
-
created_at: true,
|
|
289
|
-
});
|
|
290
|
-
export type UserSaveParams = z.infer<typeof UserSaveParams>;
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
**If a ManyToMany relation exists:**
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
// ManyToMany relation: add {relation_name}_ids array
|
|
297
|
-
export const ProjectSaveParams = ProjectBaseSchema.partial({
|
|
298
|
-
id: true,
|
|
299
|
-
created_at: true,
|
|
300
|
-
})
|
|
301
|
-
.extend({
|
|
302
|
-
employee_ids: z.array(z.number().int().positive()),
|
|
303
|
-
tag_ids: z.array(z.number().int().positive()),
|
|
304
|
-
})
|
|
305
|
-
.omit({
|
|
306
|
-
// omit virtual fields, system-generated fields, etc.
|
|
307
|
-
virtual_test: true,
|
|
308
|
-
});
|
|
309
|
-
export type ProjectSaveParams = z.infer<typeof ProjectSaveParams>;
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**Handling nullable fields in BelongsToOne relations:**
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
// Nullable relations are automatically optional, so no extra partial is needed
|
|
316
|
-
export const ResponseSaveParams = ResponseBaseSchema.partial({
|
|
317
|
-
id: true,
|
|
318
|
-
created_at: true,
|
|
319
|
-
updated_at: true, // also make timestamp fields partial
|
|
320
|
-
});
|
|
321
|
-
export type ResponseSaveParams = z.infer<typeof ResponseSaveParams>;
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
**Reference working code:**
|
|
325
|
-
|
|
326
|
-
- `sonamu/examples/miomock/api/src/application/project/project.types.ts` - ManyToMany SaveParams example
|
|
327
|
-
- `sonamu/examples/miomock/api/src/application/employee/employee.types.ts` - BelongsToOne SaveParams example
|
|
328
|
-
|
|
329
|
-
### Handling Relations in the Model
|
|
330
|
-
|
|
331
|
-
**Removing relation objects on update:**
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
// Pattern used in tests for updates
|
|
335
|
-
const original = await UserModel.findById("A", userId);
|
|
336
|
-
|
|
337
|
-
// Remove relation object and extract FK only
|
|
338
|
-
const { institution, ...userData } = original;
|
|
339
|
-
|
|
340
|
-
await UserModel.save([
|
|
341
|
-
{
|
|
342
|
-
...userData,
|
|
343
|
-
institution_id: institution?.id ?? null, // explicitly add FK
|
|
344
|
-
name: "Updated Name",
|
|
345
|
-
},
|
|
346
|
-
]);
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
**ManyToMany save:**
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
// ManyToMany is passed as an _ids array
|
|
353
|
-
await ProjectModel.save([
|
|
354
|
-
{
|
|
355
|
-
id: projectId,
|
|
356
|
-
title: "Updated",
|
|
357
|
-
employee_ids: [1, 2, 3],
|
|
358
|
-
tag_ids: [4, 5],
|
|
359
|
-
},
|
|
360
|
-
]);
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**Reference working code:**
|
|
364
|
-
|
|
365
|
-
- `sonamu/examples/miomock/api/src/application/project/project.model.ts` - ManyToMany save implementation
|
|
366
|
-
- `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` - Save test example
|