sonamu 0.10.5 → 0.10.7

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 (99) hide show
  1. package/dist/api/base-frame.d.ts +2 -0
  2. package/dist/api/base-frame.d.ts.map +1 -1
  3. package/dist/api/base-frame.js +8 -1
  4. package/dist/api/decorators.js +1 -1
  5. package/dist/api/sonamu.js +1 -1
  6. package/dist/bin/cli.js +195 -187
  7. package/dist/cone/cone-generator.js +3 -9
  8. package/dist/database/base-model.js +2 -2
  9. package/dist/database/knex.d.ts.map +1 -1
  10. package/dist/database/knex.js +2 -2
  11. package/dist/database/puri.d.ts +17 -0
  12. package/dist/database/puri.d.ts.map +1 -1
  13. package/dist/database/puri.js +102 -3
  14. package/dist/entity/entity-manager.js +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/migration/code-generation.js +6 -6
  17. package/dist/migration/migrator.d.ts.map +1 -1
  18. package/dist/migration/migrator.js +52 -7
  19. package/dist/migration/slack-confirm.js +1 -1
  20. package/dist/naite/naite-reporter.js +1 -1
  21. package/dist/syncer/checksum.js +1 -1
  22. package/dist/syncer/code-generator.js +1 -1
  23. package/dist/syncer/entity-operations.js +1 -1
  24. package/dist/syncer/syncer-actions.js +1 -1
  25. package/dist/syncer/syncer.js +1 -1
  26. package/dist/ui-web/assets/{index-BLX4zN8c.js → index-D0MHYbxl.js} +1 -1
  27. package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
  28. package/dist/ui-web/index.html +2 -2
  29. package/dist/utils/formatter.js +1 -1
  30. package/package.json +3 -3
  31. package/src/api/base-frame.ts +14 -1
  32. package/src/bin/cli.ts +268 -280
  33. package/src/cone/cone-generator.ts +2 -18
  34. package/src/database/__tests__/puri.test.ts +183 -0
  35. package/src/database/knex.ts +4 -1
  36. package/src/database/puri.ts +229 -2
  37. package/src/database/puri.types.test-d.ts +56 -0
  38. package/src/migration/code-generation.ts +5 -5
  39. package/src/migration/migrator.ts +81 -7
  40. package/src/skills/AGENTS.md +76 -50
  41. package/src/skills/sonamu/SKILL.md +46 -227
  42. package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
  43. package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
  44. package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
  45. package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
  46. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
  47. package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
  48. package/src/skills/sonamu-config/SKILL.md +203 -0
  49. package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
  50. package/src/skills/sonamu-config/references/environments.md +178 -0
  51. package/src/skills/sonamu-config/references/server-options.md +400 -0
  52. package/src/skills/sonamu-entity/SKILL.md +180 -0
  53. package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
  54. package/src/skills/sonamu-entity/references/design-guides.md +243 -0
  55. package/src/skills/sonamu-entity/references/field-types.md +170 -0
  56. package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
  57. package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
  58. package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
  59. package/src/skills/sonamu-fixture/SKILL.md +180 -0
  60. package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
  61. package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
  62. package/src/skills/sonamu-frontend/SKILL.md +142 -0
  63. package/src/skills/sonamu-frontend/references/components.md +323 -0
  64. package/src/skills/sonamu-frontend/references/examples.md +64 -0
  65. package/src/skills/sonamu-frontend/references/hooks.md +273 -0
  66. package/src/skills/sonamu-frontend/references/runtime.md +165 -0
  67. package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
  68. package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
  69. package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
  70. package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
  71. package/src/skills/sonamu-query/SKILL.md +48 -0
  72. package/src/skills/sonamu-query/references/model-patterns.md +390 -0
  73. package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
  74. package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
  75. package/src/skills/sonamu-query/references/search.md +238 -0
  76. package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
  77. package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
  78. package/src/skills/sonamu-testing/SKILL.md +251 -0
  79. package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
  80. package/src/skills/sonamu-testing/references/helpers.md +185 -0
  81. package/src/skills/sonamu-testing/references/patterns.md +263 -0
  82. package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
  83. package/src/skills/sonamu-testing/references/quick-start.md +285 -0
  84. package/src/skills/sonamu-testing/references/type-safety.md +172 -0
  85. package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
  86. package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
  87. package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
  88. package/src/skills/commands/sonamu-skills.md +0 -20
  89. package/src/skills/project/README.md +0 -19
  90. package/src/skills/project/architecture.md +0 -373
  91. package/src/skills/sonamu/cdd.md +0 -129
  92. package/src/skills/sonamu/config.md +0 -772
  93. package/src/skills/sonamu/create-sonamu.md +0 -208
  94. package/src/skills/sonamu/entity-basic.md +0 -678
  95. package/src/skills/sonamu/framework-change.md +0 -96
  96. package/src/skills/sonamu/frontend.md +0 -915
  97. package/src/skills/sonamu/project-init.md +0 -477
  98. package/src/skills/sonamu/skill-contribution.md +0 -247
  99. package/src/skills/sonamu/testing.md +0 -2163
@@ -1,915 +0,0 @@
1
- ---
2
- name: sonamu-frontend
3
- description: Sonamu frontend integration. Auto-generated Service, TanStack Query hooks, useTypeForm/useListParams/useSelection, FileInput, MultiSelect, SonamuProvider (react-components v0.1.8+). Use when calling APIs, building forms, handling file uploads, or managing list/selection states.
4
- ---
5
-
6
- # Frontend Service
7
-
8
- ## Quick Reference
9
-
10
- ### Hooks
11
-
12
- | Hook | Purpose | Key Return Values |
13
- | --------------- | --------------------------------- | ------------------------------------------------ |
14
- | `useTypeForm` | Form state management (Zod-based) | form, setForm, register, submit, addError, reset |
15
- | `useListParams` | URL-synced list parameters | listParams, setListParams, register |
16
- | `useSelection` | Checkbox multi-selection | selectedKeys, toggle, selectAll, deselectAll |
17
- | `useModal` | Modal state management | open, modal |
18
- | `useToast` | Toast notifications | toast |
19
-
20
- ### Components
21
-
22
- | Component | Purpose | Key Props |
23
- | ------------- | ---------------- | --------------------------------------------------- |
24
- | `Input` | Text input | value, onValueChange |
25
- | `Textarea` | Multi-line input | value, onValueChange |
26
- | `Checkbox` | Checkbox | value (boolean), onValueChange, label |
27
- | `Select` | Single select | items, value, onValueChange, placeholder, clearable |
28
- | `MultiSelect` | Multi-select | options, value (array), onValueChange, maxCount |
29
- | `EnumSelect` | Enum select | enum, labels, value, onValueChange |
30
- | `FileInput` | File upload | uploadMode, viewMode, multiple, maxFiles |
31
-
32
- ### Service (Auto-generated)
33
-
34
- | Method | Purpose | Example |
35
- | ----------------- | -------------------- | ----------------------------------- |
36
- | `get{Entity}` | Fetch single record | `UserService.getUser("A", 123)` |
37
- | `get{Entities}` | Fetch list | `UserService.getUsers("P", params)` |
38
- | `save` | Save (create/update) | `UserService.save([data])` |
39
- | `del` | Delete | `UserService.del([1, 2, 3])` |
40
- | `use{Entity}` | Single fetch hook | `UserService.useUser("A", id)` |
41
- | `use{Entities}` | List fetch hook | `UserService.useUsers("P", params)` |
42
- | `useSaveMutation` | Save mutation | `UserService.useSaveMutation()` |
43
-
44
- ### Utilities
45
-
46
- | Function | Purpose | Example |
47
- | ------------------ | ------------------------ | ------------------------------------------------- |
48
- | `dateF` | Date formatting | `dateF(new Date())` → `"2024-01-15"` |
49
- | `datetimeF` | Datetime formatting | `datetimeF(new Date())` → `"2024-01-15 10:30:00"` |
50
- | `numF` | Number formatting | `numF(1234567)` → `"1,234,567"` |
51
- | `hidden` | Conditional hidden class | `hidden(true)` → `"hidden"` |
52
- | `arrayableToArray` | Convert to array | `arrayableToArray("a")` → `["a"]` |
53
-
54
- ### Configuration
55
-
56
- | Item | Description | Required |
57
- | ---------------- | -------------------------------------------------- | ------------------------------------------ |
58
- | `SonamuProvider` | Global configuration Provider (uploader, auth, SD) | Required (uploader required for FileInput) |
59
- | `uploader` | File upload function | Required when using FileInput |
60
- | `auth` | Authentication state and functions | Optional |
61
- | `SD` | Internationalization function | Optional |
62
-
63
- ---
64
-
65
- # Frontend Service
66
-
67
- ## Basic Usage
68
-
69
- ```typescript
70
- import { UserService } from "@/services/services.generated";
71
-
72
- // Single fetch (Subset required) - get{Entity} form
73
- const user = await UserService.getUser("A", 123);
74
-
75
- // List fetch - get{Entities} form
76
- const { rows, total } = await UserService.getUsers("P", { num: 20, page: 1 });
77
-
78
- // Save
79
- const [userId] = await UserService.save([{ email: "new@test.com", username: "newuser" }]);
80
-
81
- // Delete
82
- const count = await UserService.del([1, 2, 3]);
83
- ```
84
-
85
- ## TanStack Query Hook
86
-
87
- ### useQuery
88
-
89
- ```typescript
90
- function UserProfile({ userId }: { userId: number }) {
91
- // use{Entity} form (single), use{Entities} form (list)
92
- const { data: user, isLoading, error } = UserService.useUser("A", userId);
93
-
94
- if (isLoading) return <div>Loading...</div>;
95
- if (error) return <div>Error: {error.message}</div>;
96
-
97
- return <h1>{user?.username}</h1>;
98
- }
99
- ```
100
-
101
- ### useMutation
102
-
103
- ```typescript
104
- function EditProfile() {
105
- const saveMutation = UserService.useSaveMutation();
106
-
107
- async function handleSubmit(data: UserSaveParams) {
108
- saveMutation.mutate({ spa: [data] }, {
109
- onSuccess: ([userId]) => console.log("Saved:", userId),
110
- onError: (error) => console.error("Failed:", error),
111
- });
112
- }
113
-
114
- return <button disabled={saveMutation.isPending}>Save</button>;
115
- }
116
- ```
117
-
118
- ### Conditional Fetching
119
-
120
- ```typescript
121
- const { data } = UserService.useUser("A", userId!, {
122
- enabled: userId !== null,
123
- });
124
- ```
125
-
126
- ### Cache Invalidation
127
-
128
- ```typescript
129
- const queryClient = useQueryClient();
130
- queryClient.invalidateQueries({ queryKey: ["User", "findById", "A", userId] });
131
- ```
132
-
133
- ## useTypeForm
134
-
135
- Type-safe form management based on Zod schemas (react-components v0.1.8+)
136
-
137
- ### Return Values
138
-
139
- ```typescript
140
- const { form, setForm, register, submit, addError, removeError, clearError, reset } = useTypeForm(
141
- Schema,
142
- defaultValue,
143
- );
144
- ```
145
-
146
- | Return Value | Type | Description |
147
- | ------------- | --------------------------------------------------- | --------------------------------- |
148
- | `form` | `z.infer<Schema>` | Current form data |
149
- | `setForm` | `React.Dispatch<SetStateAction<...>>` | Form state update function |
150
- | `register` | `(field: string) => RegisterReturn` | Field registration function |
151
- | `submit` | `(callback) => () => Promise<R>` | Submit handler factory |
152
- | `addError` | `(path: string, error: string \| ErrorObj) => void` | Manually add an error |
153
- | `removeError` | `(path: string) => void` | Remove error for a specific field |
154
- | `clearError` | `() => void` | Clear all errors |
155
- | `reset` | `() => void` | Reset form to defaultValue |
156
-
157
- ### register Return Object
158
-
159
- ```typescript
160
- register(fieldName) // Returns:
161
- {
162
- value: any, // Current field value
163
- onValueChange: (value: any) => void, // Value change handler
164
- error?: { content: string } // Error object (if present)
165
- }
166
- ```
167
-
168
- ### Basic Usage
169
-
170
- ```tsx
171
- import { useTypeForm } from "@sonamu-kit/react-components/lib";
172
- import { Input } from "@sonamu-kit/react-components/components";
173
- import { UserSaveParams } from "@/services/user/user.types";
174
-
175
- function RegisterForm() {
176
- const { form, setForm, register, submit } = useTypeForm(UserSaveParams, {
177
- email: "",
178
- username: "",
179
- password: "",
180
- });
181
-
182
- const handleSubmit = submit(async (form) => {
183
- await UserService.save([form]);
184
- });
185
-
186
- // Method 1: spread operator (recommended)
187
- const emailProps = register("email");
188
-
189
- return (
190
- <form>
191
- <Input {...emailProps} />
192
- {emailProps.error && <span className="error">{emailProps.error.content}</span>}
193
-
194
- {/* Method 2: inline (for short cases) */}
195
- <Input {...register("username")} />
196
- {register("username").error && (
197
- <span className="error">{register("username").error.content}</span>
198
- )}
199
-
200
- <button onClick={handleSubmit}>Register</button>
201
- </form>
202
- );
203
- }
204
- ```
205
-
206
- ### IMPORTANT: react-components UI Component Usage
207
-
208
- All UI components in react-components follow the `value/onValueChange` pattern:
209
-
210
- ```tsx
211
- import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/components";
212
-
213
- // Input (string)
214
- <Input {...register("email")} />
215
-
216
- // Textarea (string)
217
- <Textarea {...register("content")} />
218
-
219
- // Checkbox (boolean)
220
- <Checkbox {...register("agreed")} />
221
-
222
- // Select (using items prop)
223
- <Select
224
- {...register("status")}
225
- items={[
226
- { value: "active", label: "Active" },
227
- { value: "inactive", label: "Inactive" }
228
- ]}
229
- placeholder="Select status"
230
- />
231
-
232
- // Select simple form (string[] | number[])
233
- <Select
234
- {...register("priority")}
235
- items={["high", "medium", "low"]}
236
- placeholder="Priority"
237
- />
238
- ```
239
-
240
- **Select component key props:**
241
-
242
- - `items`: Array of selectable items (`V[]` or `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
243
- - `placeholder`: Text shown before selection
244
- - `clearable`: Whether the X button can deselect
245
- - `renderItem`: Custom render function
246
-
247
- ### IMPORTANT: Form Required Field Initial Values
248
-
249
- Fields defined as required in SaveParams **must be included** in the form initial values:
250
-
251
- | Type | Initial Value |
252
- | ----------------- | ------------------------------ |
253
- | string (required) | `""` |
254
- | number (required) | `0` |
255
- | Date (required) | `new Date()` |
256
- | enum (required) | Default value (e.g. `"draft"`) |
257
- | FK (required) | `0` |
258
- | nullable | `null` |
259
-
260
- ```typescript
261
- const { form, setForm, register } = useTypeForm(TaskSaveParams, {
262
- title: "", // string required
263
- status: "draft", // enum required
264
- budget: 0, // number required
265
- begin_date: new Date(), // Date required
266
- description: null, // nullable
267
- institution_id: 0, // FK required
268
- });
269
- ```
270
-
271
- ### IMPORTANT: Accessing Relation Objects When Loading Data
272
-
273
- If a scaffolded form accesses relation objects like `row.collection?.id`, that relation must be included in subset A.
274
-
275
- **Error**: `Property 'collection' does not exist on type` → Add `"collection.id"` to subset A in entity.json
276
-
277
- ```json
278
- // question.entity.json > subsets > A
279
- ["id", "content", "collection.id", "collection.title", "parent.id", "answer_group.id"]
280
- ```
281
-
282
- **Alternative**: If the FK is already on `row`, using `...row` alone is sufficient without accessing the relation (no subset modification needed)
283
-
284
- ### IMPORTANT: SD() Translation Key for FK Fields
285
-
286
- Scaffolded forms use `SD("entity.Task.institution_id")`, but `sd.generated.ts` only generates keys without `_id`.
287
-
288
- **Fix**: Manually add the `_id` key to `ko.ts`
289
-
290
- ```typescript
291
- // packages/api/src/i18n/ko.ts
292
- "entity.Task.institution_id": "Institution",
293
- "entity.Question.collection_id": "Collection",
294
- ```
295
-
296
- Since `ko.ts` is copied from api → web, you only need to add it once.
297
-
298
- ## useListParams
299
-
300
- List parameter management synchronized with URL query parameters (pagination, filtering)
301
-
302
- ```typescript
303
- import { useListParams } from "@sonamu-kit/react-components/lib";
304
- import { z } from "zod";
305
-
306
- const ListParamsSchema = z.object({
307
- page: z.coerce.number().default(1),
308
- num: z.coerce.number().default(20),
309
- search: z.string().optional(),
310
- status: z.enum(["active", "inactive"]).optional(),
311
- });
312
-
313
- function UserListPage() {
314
- const { listParams, setListParams, register } = useListParams(
315
- ListParamsSchema,
316
- { page: 1, num: 20 }
317
- );
318
-
319
- const { data } = UserService.useUsers("P", listParams);
320
-
321
- return (
322
- <div>
323
- {/* Search (resets page=1 on change) */}
324
- <Input {...register("search")} placeholder="Search" />
325
-
326
- {/* Filter (resets page=1 on change) */}
327
- <Select {...register("status")} items={["active", "inactive"]} />
328
-
329
- {/* Pagination (changes page only) */}
330
- <button onClick={() => setListParams({ ...listParams, page: listParams.page - 1 })}>
331
- Previous
332
- </button>
333
- <span>Page {listParams.page}</span>
334
- <button onClick={() => setListParams({ ...listParams, page: listParams.page + 1 })}>
335
- Next
336
- </button>
337
- </div>
338
- );
339
- }
340
- ```
341
-
342
- **Key points:**
343
-
344
- - Automatically syncs with URL (`?page=2&status=active`)
345
- - `register` automatically resets page to 1 when any field other than page changes
346
- - Type safety guaranteed by Zod schema
347
-
348
- ## useSelection
349
-
350
- Checkbox multi-selection management (supports Shift-click range selection)
351
-
352
- ```typescript
353
- import { useSelection } from "@sonamu-kit/react-components/lib";
354
-
355
- function UserListPage() {
356
- const { data } = UserService.useUsers("P", { num: 20, page: 1 });
357
- const userIds = data?.rows.map(row => row.id) ?? [];
358
-
359
- const {
360
- getSelected,
361
- toggle,
362
- selectedKeys,
363
- selectAll,
364
- deselectAll,
365
- isAllSelected,
366
- handleCheckboxClick
367
- } = useSelection(userIds);
368
-
369
- const handleDelete = async () => {
370
- await UserService.del(selectedKeys);
371
- deselectAll();
372
- };
373
-
374
- return (
375
- <div>
376
- <Checkbox
377
- value={isAllSelected}
378
- onValueChange={isAllSelected ? deselectAll : selectAll}
379
- label="Select all"
380
- />
381
- <button onClick={handleDelete} disabled={selectedKeys.length === 0}>
382
- Delete selected ({selectedKeys.length})
383
- </button>
384
-
385
- {data?.rows.map((user, index) => (
386
- <div key={user.id} onClick={(e) => handleCheckboxClick(e, index)}>
387
- <Checkbox
388
- value={getSelected(user.id)}
389
- onValueChange={() => toggle(user.id)}
390
- />
391
- <span>{user.name}</span>
392
- </div>
393
- ))}
394
- </div>
395
- );
396
- }
397
- ```
398
-
399
- **Key points:**
400
-
401
- - Range selection with Shift + click
402
- - `selectedKeys`: Array of currently selected keys
403
- - `isAllSelected`: Whether all items are selected
404
-
405
- ## IdAsyncSelect
406
-
407
- Component for asynchronously searching and selecting Entity records. The generic type must be specified according to the Entity's Primary Key type.
408
-
409
- ### Basic Usage
410
-
411
- IdAsyncSelect is typically used as a per-Entity wrapper component:
412
-
413
- ```typescript
414
- import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
415
- import { UserAsyncIdConfig } from "@/services/services.generated";
416
- import type { UserSubsetKey, UserSubsetMapping } from "@/services/sonamu.generated";
417
- import type { UserListParams } from "@/services/user/user.types";
418
-
419
- export type UserIdAsyncSelectProps<T extends UserSubsetKey> = {
420
- subset: T;
421
- baseListParams?: UserListParams;
422
- displayField?: keyof UserSubsetMapping[T] & string;
423
- valueField?: keyof UserSubsetMapping[T] & string;
424
- placeholder?: string;
425
- clearable?: boolean;
426
- disabled?: boolean;
427
- className?: string;
428
- multiple?: boolean;
429
- value?: number | number[] | null; // Number PK
430
- onValueChange?: (value: number | number[] | undefined) => void;
431
- };
432
-
433
- export function UserIdAsyncSelect<T extends UserSubsetKey>({
434
- subset,
435
- value,
436
- onValueChange,
437
- baseListParams,
438
- displayField = "name",
439
- valueField = "id",
440
- placeholder = "User",
441
- clearable,
442
- disabled,
443
- className,
444
- multiple = false,
445
- }: UserIdAsyncSelectProps<T>) {
446
- return (
447
- <IdAsyncSelect<number> // Number PK
448
- config={UserAsyncIdConfig}
449
- subset={subset}
450
- baseListParams={baseListParams}
451
- displayField={displayField}
452
- valueField={valueField}
453
- placeholder={placeholder}
454
- clearable={clearable}
455
- disabled={disabled}
456
- className={className}
457
- multiple={multiple}
458
- value={value}
459
- onValueChange={onValueChange}
460
- />
461
- );
462
- }
463
- ```
464
-
465
- **Key Props:**
466
-
467
- - `config`: Auto-generated AsyncIdConfig (EntityAsyncIdConfig form)
468
- - `subset`: Subset key to query
469
- - `baseListParams`: List filter parameters
470
- - `displayField`: Field name to display (default varies by Entity)
471
- - `valueField`: Field name used as value (default: "id")
472
- - `multiple`: Whether multi-selection is enabled
473
- - `value`: Currently selected value (number or string depending on PK type)
474
- - `onValueChange`: Value change handler
475
-
476
- ### Cascade Dropdown Pattern (Hierarchical Selection)
477
-
478
- When lower-level lists should change based on higher-level selection (e.g. Department → Division → Lab), pass `baseListParams` dynamically.
479
-
480
- **Key behavior**: When the `baseListParams` prop changes, the React Query inside `IdAsyncSelect` automatically re-fetches with the new parameters. (Bug fixed in v0.2.5+ — previous versions only used the initial value and did not reflect changes)
481
-
482
- ```tsx
483
- // Example: 3-level cascade Department → Division → Lab
484
- function UserForm() {
485
- const { form, register, setForm } = useTypeForm(UserSaveParams, {
486
- dept_id: null,
487
- division_id: null,
488
- lab_id: null,
489
- });
490
-
491
- return (
492
- <form>
493
- {/* Level 1: Department selection (full list → preload or default IdAsyncSelect) */}
494
- <DepartmentIdAsyncSelect
495
- subset="A"
496
- {...register("dept_id")}
497
- onValueChange={(v) => {
498
- // Reset lower values when department changes
499
- setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
500
- }}
501
- />
502
-
503
- {/* Level 2: Division selection (only divisions in selected department) */}
504
- <DivisionIdAsyncSelect
505
- subset="A"
506
- baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
507
- disabled={!form.dept_id}
508
- {...register("division_id")}
509
- onValueChange={(v) => {
510
- // Reset lab when division changes
511
- setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
512
- }}
513
- />
514
-
515
- {/* Level 3: Lab selection (only labs in selected division) */}
516
- <LabIdAsyncSelect
517
- subset="A"
518
- baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
519
- disabled={!form.division_id}
520
- {...register("lab_id")}
521
- />
522
- </form>
523
- );
524
- }
525
- ```
526
-
527
- **Notes**:
528
-
529
- - You must explicitly reset lower values to `null` when a higher-level value changes. IdAsyncSelect does not reset automatically.
530
- - Using the `disabled` prop to disable lower levels when the parent is not selected improves UX.
531
- - If `baseListParams` is `undefined`, IdAsyncSelect stays in enabled=false state and does not fetch.
532
-
533
- **Items to specify in Spec** (recommended to add to acceptanceCriteria in spec.json when cascade is present):
534
-
535
- ```json
536
- "acceptanceCriteria": [
537
- "When a department is selected, only divisions belonging to that department appear in the dropdown",
538
- "When a division is selected, only labs belonging to that division appear in the dropdown",
539
- "When the department changes, the lower division/lab selections are reset"
540
- ]
541
- ```
542
-
543
- ### IMPORTANT: String Primary Key Support
544
-
545
- Most Entities use Number PK (`IdAsyncSelect<number>`), but better-auth related Entities use String PK.
546
-
547
- **String PK Entities**: User, Account, Session, Verification
548
-
549
- **Points to change** (manual modification required after scaffolding):
550
-
551
- ```typescript
552
- // Number PK (default)
553
- value?: number | number[] | null;
554
- onValueChange?: (value: number | number[] | undefined) => void;
555
- <IdAsyncSelect<number> config={PostAsyncIdConfig} ... />
556
-
557
- // String PK (User, Account, etc.) — change all 3 places to string
558
- value?: string | string[] | null;
559
- onValueChange?: (value: string | string[] | undefined) => void;
560
- <IdAsyncSelect<string> config={AccountAsyncIdConfig} ... />
561
- ```
562
-
563
- ### Usage in Forms
564
-
565
- ```tsx
566
- function PostForm() {
567
- const { form, setForm, register } = useTypeForm(PostSaveParams, {
568
- title: "",
569
- author_id: 0, // or "" for string PK
570
- });
571
-
572
- return (
573
- <form>
574
- <Input {...register("title")} />
575
-
576
- {/* Number PK */}
577
- <UserIdAsyncSelect subset="A" {...register("author_id")} />
578
-
579
- {/* String PK */}
580
- <AccountIdAsyncSelect subset="A" {...register("account_id")} />
581
- </form>
582
- );
583
- }
584
- ```
585
-
586
- ## FileInput
587
-
588
- File upload component (image/general files, eager/lazy modes)
589
-
590
- ```typescript
591
- import { FileInput } from "@sonamu-kit/react-components/components";
592
- import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
593
-
594
- function ProfileForm() {
595
- const { form, setForm, register, submit } = useTypeForm(ProfileSaveParams, {
596
- avatar: null, // SonamuFile | File | null
597
- documents: [], // (SonamuFile | File)[]
598
- });
599
-
600
- return (
601
- <form>
602
- {/* Single image - eager upload */}
603
- <FileInput
604
- {...register("avatar")}
605
- uploadMode="eager"
606
- viewMode="image"
607
- placeholder="Profile image"
608
- accept="image/*"
609
- previewSize="md"
610
- />
611
-
612
- {/* Multiple files - lazy upload */}
613
- <FileInput
614
- {...register("documents")}
615
- uploadMode="lazy"
616
- viewMode="file"
617
- multiple
618
- maxFiles={5}
619
- placeholder="Attach documents"
620
- />
621
-
622
- <button onClick={submit(async (form) => {
623
- // lazy mode: auto-uploads on submit
624
- await ProfileService.save([form]);
625
- })}>Save</button>
626
- </form>
627
- );
628
- }
629
- ```
630
-
631
- **Props:**
632
-
633
- - `uploadMode`: `"eager"` (upload immediately) | `"lazy"` (upload on submit)
634
- - `viewMode`: `"image"` (image preview) | `"file"` (filename)
635
- - `multiple`: Whether multiple files can be selected
636
- - `maxFiles`: Maximum number of files
637
- - `previewSize`: `"sm" | "md" | "lg" | "xl"`
638
- - `clearable`: Whether the X button can remove the file
639
-
640
- **IMPORTANT**: uploader function must be configured in SonamuProvider (see below)
641
-
642
- ## Select (Multi-select Mode)
643
-
644
- Setting `multiple: true` on the `Select` component enables multi-select mode.
645
-
646
- ```typescript
647
- import { Select } from "@sonamu-kit/react-components/components";
648
-
649
- function TagForm() {
650
- const { register } = useTypeForm(PostSaveParams, {
651
- tag_ids: [], // number[]
652
- });
653
-
654
- const items = [
655
- { value: 1, label: "JavaScript" },
656
- { value: 2, label: "TypeScript" },
657
- { value: 3, label: "React" },
658
- { value: 4, label: "Vue" },
659
- ];
660
-
661
- return (
662
- <Select
663
- {...register("tag_ids")}
664
- items={items}
665
- multiple
666
- placeholder="Select tags"
667
- />
668
- );
669
- }
670
- ```
671
-
672
- **Multi-select specific Props:**
673
-
674
- - `multiple`: `true` (enables multi-select)
675
- - `maxCount`: Maximum number of badges to display
676
- - `hideSelectAll`: Hide the select all button
677
- - `searchable`: Enable search input
678
-
679
- **Common Props:**
680
-
681
- - `items`: `SelectItemDef[]` (values only or `{ value, label, disabled }` form)
682
- - `placeholder`: Text shown before selection
683
- - `clearable`: X button to deselect all
684
- - `disabled`: Disable the component
685
- - `renderItem`: Custom render function
686
- - `async`: When set to `true`, supports async search via `onSearch` callback
687
-
688
- ## EnumSelect
689
-
690
- Select integrated with Zod enum (label mapping)
691
-
692
- ```typescript
693
- import { EnumSelect } from "@sonamu-kit/react-components/components";
694
- import { z } from "zod";
695
-
696
- const StatusEnum = z.enum(["draft", "published", "archived"]);
697
-
698
- const statusLabels = {
699
- draft: "Draft",
700
- published: "Published",
701
- archived: "Archived",
702
- } as const;
703
-
704
- function PostForm() {
705
- const { register } = useTypeForm(PostSaveParams, {
706
- status: "draft",
707
- });
708
-
709
- return (
710
- <EnumSelect
711
- {...register("status")}
712
- enum={StatusEnum}
713
- labels={statusLabels}
714
- placeholder="Select status"
715
- clearable
716
- />
717
- );
718
- }
719
- ```
720
-
721
- **Key points:**
722
-
723
- - Zod enum type safety
724
- - Display name mapping via labels object
725
- - Automatically converts enum.options to items
726
-
727
- ## SonamuProvider
728
-
729
- Global configuration used across react-components
730
-
731
- ```typescript
732
- // App.tsx or root component
733
- import { SonamuProvider } from "@sonamu-kit/react-components/contexts";
734
- import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
735
-
736
- function App() {
737
- // File uploader function (used by FileInput, useTypeForm)
738
- const uploader = async (files: File[]): Promise<SonamuFile[]> => {
739
- const formData = new FormData();
740
- files.forEach(file => formData.append("files", file));
741
-
742
- const response = await fetch("/api/upload", {
743
- method: "POST",
744
- body: formData,
745
- });
746
-
747
- return response.json();
748
- };
749
-
750
- // Authentication state (optional)
751
- const auth = {
752
- user: currentUser,
753
- loading: isLoading,
754
- login: async (params) => { /* ... */ },
755
- logout: async () => { /* ... */ },
756
- refetch: async () => { /* ... */ },
757
- };
758
-
759
- // Internationalization function (optional)
760
- const SD = (key: string) => dictionary[key] ?? key;
761
-
762
- return (
763
- <SonamuProvider uploader={uploader} auth={auth} SD={SD}>
764
- {children}
765
- </SonamuProvider>
766
- );
767
- }
768
- ```
769
-
770
- **Required Props:**
771
-
772
- - `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - Used by FileInput
773
- - `auth`: Authentication state and functions (optional)
774
- - `SD`: Internationalization function (optional)
775
-
776
- ## Utility Functions
777
-
778
- ```typescript
779
- import {
780
- dateF,
781
- datetimeF,
782
- numF,
783
- hidden,
784
- arrayableToArray,
785
- sqlDateToDateString,
786
- } from "@sonamu-kit/react-components/lib";
787
-
788
- // Date formatting
789
- dateF(new Date()); // "2024-01-15"
790
- dateF("2024-01-15T10:30:00"); // "2024-01-15"
791
- datetimeF(new Date()); // "2024-01-15 10:30:00"
792
-
793
- // Number formatting
794
- numF(1234567); // "1,234,567"
795
-
796
- // Conditional hidden class
797
- <div className={hidden(isHidden)}>...</div>
798
-
799
- // SQL date → date string
800
- sqlDateToDateString("2024-01-15T10:30:00.000Z"); // "2024-01-15"
801
-
802
- // Convert to array
803
- arrayableToArray("single"); // ["single"]
804
- arrayableToArray(["a", "b"]); // ["a", "b"]
805
- arrayableToArray(undefined); // []
806
- ```
807
-
808
- ## Error Handling
809
-
810
- ```typescript
811
- import { isSonamuError } from "@/lib/sonamu.shared";
812
-
813
- try {
814
- await UserService.save([
815
- {
816
- /* ... */
817
- },
818
- ]);
819
- } catch (error) {
820
- if (isSonamuError(error)) {
821
- console.log("Status:", error.code);
822
- console.log("Message:", error.message);
823
- error.issues.forEach((issue) => {
824
- console.log(`${issue.path.join(".")}: ${issue.message}`);
825
- });
826
- }
827
- }
828
- ```
829
-
830
- ## SSR
831
-
832
- ```typescript
833
- // api/src/ssr/routes.ts
834
- import { registerSSR } from "sonamu/ssr";
835
-
836
- registerSSR({
837
- path: "/companies/:companyId",
838
- preload: (params) => [UserService.me(), CompanyService.findById("A", Number(params.companyId))],
839
- });
840
- ```
841
-
842
- ## Initial Project Setup
843
-
844
- **→ See the "Project Name Change" section in `create-sonamu.md`** (change 4 files: index.html, \_\_root.tsx, index.tsx, Sidebar.tsx)
845
-
846
- ## Rules
847
-
848
- - NEVER manually modify `services.generated.ts`
849
- - MUST specify Subset parameter when calling APIs
850
- - Use `Promise.all([...])` for parallel requests
851
-
852
- ---
853
-
854
- ## Full Component Implementation Examples
855
-
856
- ### List Page
857
-
858
- ```typescript
859
- function ConsultationListPage() {
860
- const [params, setParams] = useState({ num: 20, page: 1 });
861
- const { data, isLoading } = ConsultationService.useConsultations("P", params);
862
-
863
- return (
864
- <div>
865
- {/* Manage selection with useSelection */}
866
- {data?.rows.map((row) => (
867
- <div key={row.id}>{row.title} - {row.status}</div>
868
- ))}
869
- {/* Pagination: manipulate params.page */}
870
- </div>
871
- );
872
- }
873
- ```
874
-
875
- ### Edit Page
876
-
877
- ```typescript
878
- function ConsultationFormPage() {
879
- const { id } = useParams();
880
- const { form, setForm, register, submit } = useTypeForm(ConsultationSaveParams, {
881
- title: "", content: "", status: "pending", user_id: 0,
882
- });
883
-
884
- // Edit mode: load data
885
- useEffect(() => {
886
- if (id) ConsultationService.getConsultation("A", Number(id)).then((row) => setForm((prev) => ({ ...prev, ...row })));
887
- }, [id]);
888
-
889
- const saveMutation = ConsultationService.useSaveMutation();
890
- const handleSubmit = submit(async (form) => {
891
- const [cId] = await saveMutation.mutateAsync({ spa: [form] });
892
- navigate(`/consultations/${cId}`);
893
- });
894
-
895
- return (
896
- <form>
897
- <Input {...register("title")} />
898
- <Textarea {...register("content")} />
899
- <Select {...register("status")} items={[{value:"pending",label:"Pending"},{value:"completed",label:"Completed"}]} />
900
- <button onClick={handleSubmit} disabled={saveMutation.isPending}>Save</button>
901
- </form>
902
- );
903
- }
904
- ```
905
-
906
- ### Cache Invalidation
907
-
908
- ```typescript
909
- const queryClient = useQueryClient();
910
- await ConsultationService.changeStatus(id, newStatus, "Status change");
911
- queryClient.invalidateQueries({
912
- queryKey: ["Consultation", "findById", "A", id],
913
- });
914
- queryClient.invalidateQueries({ queryKey: ["Consultation", "findMany"] });
915
- ```