sonamu 0.8.24 → 0.8.26
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/__tests__/config.test.js +189 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +7 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +14 -10
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/knex-adapter.d.ts +23 -0
- package/dist/auth/knex-adapter.d.ts.map +1 -0
- package/dist/auth/knex-adapter.js +163 -0
- package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
- package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
- package/dist/bin/cli.js +47 -9
- package/dist/bin/ts-loader-register.js +3 -29
- package/dist/bin/ts-loader-registration.d.ts +2 -0
- package/dist/bin/ts-loader-registration.d.ts.map +1 -0
- package/dist/bin/ts-loader-registration.js +42 -0
- package/dist/cone/cone-generator.js +3 -3
- package/dist/database/puri-subset.test-d.js +9 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/testing/fixture-generator.js +5 -5
- package/dist/ui/ai-client.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -14
- package/dist/ui/cdd-service.d.ts +15 -18
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +246 -222
- package/dist/ui/cdd-types.d.ts +41 -68
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +2 -2
- package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
- package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
- package/dist/ui-web/index.html +2 -2
- package/package.json +6 -2
- package/src/api/__tests__/config.test.ts +225 -0
- package/src/api/config.ts +10 -4
- package/src/api/sonamu.ts +16 -13
- package/src/auth/index.ts +1 -0
- package/src/auth/knex-adapter.ts +208 -0
- package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
- package/src/bin/cli.ts +52 -9
- package/src/bin/ts-loader-register.ts +2 -32
- package/src/bin/ts-loader-registration.ts +55 -0
- package/src/cone/cone-generator.ts +2 -2
- package/src/database/puri-subset.test-d.ts +102 -0
- package/src/database/puri-subset.types.ts +1 -1
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +179 -137
- package/src/skills/sonamu/ai-agents.md +69 -69
- package/src/skills/sonamu/api.md +147 -147
- package/src/skills/sonamu/auth-migration.md +220 -220
- package/src/skills/sonamu/auth-plugins.md +83 -83
- package/src/skills/sonamu/auth.md +106 -106
- package/src/skills/sonamu/cdd.md +65 -200
- package/src/skills/sonamu/cone.md +138 -138
- package/src/skills/sonamu/config.md +191 -191
- package/src/skills/sonamu/create-sonamu.md +66 -66
- package/src/skills/sonamu/database.md +158 -158
- package/src/skills/sonamu/entity-basic.md +292 -293
- package/src/skills/sonamu/entity-relations.md +246 -246
- package/src/skills/sonamu/entity-validation-checklist.md +124 -124
- package/src/skills/sonamu/fixture-cli.md +231 -231
- package/src/skills/sonamu/framework-change.md +37 -37
- package/src/skills/sonamu/frontend.md +223 -223
- package/src/skills/sonamu/i18n.md +82 -82
- package/src/skills/sonamu/migration.md +77 -77
- package/src/skills/sonamu/model.md +222 -222
- package/src/skills/sonamu/naite.md +86 -86
- package/src/skills/sonamu/project-init.md +228 -228
- package/src/skills/sonamu/puri.md +122 -122
- package/src/skills/sonamu/scaffolding.md +154 -154
- package/src/skills/sonamu/skill-contribution.md +124 -124
- package/src/skills/sonamu/subset.md +46 -46
- package/src/skills/sonamu/tasks.md +82 -82
- package/src/skills/sonamu/testing-devrunner.md +147 -147
- package/src/skills/sonamu/testing.md +673 -673
- package/src/skills/sonamu/upsert.md +79 -79
- package/src/skills/sonamu/vector.md +67 -67
- package/src/testing/fixture-generator.ts +4 -4
- package/src/ui/ai-client.ts +1 -1
- package/src/ui/api.ts +18 -17
- package/src/ui/cdd-service.ts +264 -254
- package/src/ui/cdd-types.ts +40 -75
- package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
- package/src/skills/sonamu/workflow.md +0 -317
package/src/skills/sonamu/api.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sonamu-api
|
|
3
|
-
description:
|
|
3
|
+
description: Expose Model methods as HTTP endpoints with the Sonamu @api decorator. Configure httpMethod, guards, and clients options. Use when exposing Model methods as API endpoints.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# @api
|
|
6
|
+
# @api Decorator
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Basic Usage
|
|
9
9
|
|
|
10
10
|
```typescript
|
|
11
11
|
@api({ httpMethod: "GET" })
|
|
@@ -13,35 +13,35 @@ async findById(id: number): Promise<User> { }
|
|
|
13
13
|
// → GET /user/findById?id=1
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Options
|
|
17
17
|
|
|
18
|
-
|
|
|
19
|
-
|
|
18
|
+
| Option | Description | Default |
|
|
19
|
+
|--------|-------------|---------|
|
|
20
20
|
| `httpMethod` | GET, POST, PUT, DELETE, PATCH | GET |
|
|
21
|
-
| `clients` |
|
|
22
|
-
| `resourceName` | TanStack Query
|
|
23
|
-
| `guards` |
|
|
24
|
-
| `path` |
|
|
25
|
-
| `description` | API
|
|
26
|
-
| `timeout` |
|
|
27
|
-
| `contentType` |
|
|
28
|
-
| `cacheControl` | Cache-Control
|
|
29
|
-
| `compress` |
|
|
30
|
-
|
|
31
|
-
## clients
|
|
32
|
-
|
|
33
|
-
| Client |
|
|
34
|
-
|
|
35
|
-
| `axios` |
|
|
36
|
-
| `axios-multipart` |
|
|
37
|
-
| `tanstack-query` |
|
|
38
|
-
| `tanstack-mutation` |
|
|
39
|
-
| `tanstack-mutation-multipart` |
|
|
40
|
-
| `window-fetch` |
|
|
41
|
-
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
###
|
|
21
|
+
| `clients` | Client types to generate | `["axios"]` |
|
|
22
|
+
| `resourceName` | queryKey for TanStack Query | - |
|
|
23
|
+
| `guards` | Authentication/authorization guards | - |
|
|
24
|
+
| `path` | Custom path | `/{model}/{method}` |
|
|
25
|
+
| `description` | API description (for documentation) | - |
|
|
26
|
+
| `timeout` | Request timeout (ms) | - |
|
|
27
|
+
| `contentType` | Response Content-Type | `application/json` |
|
|
28
|
+
| `cacheControl` | Cache-Control header setting | - |
|
|
29
|
+
| `compress` | Response compression setting (can disable with `false`) | - |
|
|
30
|
+
|
|
31
|
+
## clients Options
|
|
32
|
+
|
|
33
|
+
| Client | Purpose |
|
|
34
|
+
|--------|---------|
|
|
35
|
+
| `axios` | General API calls |
|
|
36
|
+
| `axios-multipart` | File upload (axios) |
|
|
37
|
+
| `tanstack-query` | Query hook for reads |
|
|
38
|
+
| `tanstack-mutation` | Mutation hook for writes |
|
|
39
|
+
| `tanstack-mutation-multipart` | File upload Mutation |
|
|
40
|
+
| `window-fetch` | Browser fetch API |
|
|
41
|
+
|
|
42
|
+
## Pattern Examples
|
|
43
|
+
|
|
44
|
+
### Read API
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
47
|
@api({
|
|
@@ -52,7 +52,7 @@ async findById(id: number): Promise<User> { }
|
|
|
52
52
|
async findMany(params: UserListParams): Promise<ListResult<User>> { }
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### Write API
|
|
56
56
|
|
|
57
57
|
```typescript
|
|
58
58
|
@api({
|
|
@@ -62,14 +62,14 @@ async findMany(params: UserListParams): Promise<ListResult<User>> { }
|
|
|
62
62
|
async save(params: UserSaveParams[]): Promise<number[]> { }
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
###
|
|
65
|
+
### API Requiring Authorization
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
68
|
@api({ httpMethod: "POST", guards: ["admin"] })
|
|
69
69
|
async del(ids: number[]): Promise<number> { }
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
## Context
|
|
72
|
+
## Context Access
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
75
|
import { Sonamu } from "sonamu";
|
|
@@ -81,60 +81,60 @@ async me(): Promise<User | null> {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
| Context
|
|
85
|
-
|
|
86
|
-
| `user` |
|
|
87
|
-
| `session` |
|
|
84
|
+
| Context Property | Description |
|
|
85
|
+
|-----------------|-------------|
|
|
86
|
+
| `user` | Authenticated user (better-auth User, null if unauthenticated) |
|
|
87
|
+
| `session` | Current session info (better-auth Session, null if unauthenticated) |
|
|
88
88
|
| `request` | FastifyRequest |
|
|
89
89
|
| `reply` | FastifyReply |
|
|
90
|
-
| `headers` | HTTP
|
|
91
|
-
| `bufferedFiles` |
|
|
92
|
-
| `uploadedFiles` |
|
|
93
|
-
| `locale` |
|
|
90
|
+
| `headers` | HTTP request headers |
|
|
91
|
+
| `bufferedFiles` | Buffer mode uploaded files |
|
|
92
|
+
| `uploadedFiles` | Stream mode uploaded files |
|
|
93
|
+
| `locale` | Request locale |
|
|
94
94
|
|
|
95
|
-
##
|
|
95
|
+
## File Upload (@upload)
|
|
96
96
|
|
|
97
|
-
> **CRITICAL: `@upload
|
|
98
|
-
> `@upload
|
|
99
|
-
> `@api
|
|
97
|
+
> **CRITICAL: `@upload` is used standalone without `@api`.**
|
|
98
|
+
> Adding `@upload` **automatically generates** a POST endpoint and `axios-multipart`/`tanstack-mutation-multipart` clients.
|
|
99
|
+
> Adding `@api` alongside it causes a **build error** due to `checkSingleDecorator` conflict.
|
|
100
100
|
|
|
101
101
|
```typescript
|
|
102
102
|
// CORRECT
|
|
103
103
|
@upload({ limits: { files: 10 }, guards: ["user"] })
|
|
104
104
|
async upload(...): Promise<number[]> { }
|
|
105
105
|
|
|
106
|
-
// WRONG —
|
|
106
|
+
// WRONG — causes build error
|
|
107
107
|
@api({ httpMethod: "POST", clients: ["axios-multipart"] })
|
|
108
108
|
@upload({ limits: { files: 10 } })
|
|
109
109
|
async upload(...): Promise<number[]> { }
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
**`@upload`
|
|
112
|
+
**`@upload` supported options** (`httpMethod`, `clients` are not supported — set automatically)
|
|
113
113
|
|
|
114
|
-
|
|
|
115
|
-
|
|
116
|
-
| `guards` |
|
|
117
|
-
| `limits` |
|
|
118
|
-
| `consume` | `"buffer"` (
|
|
119
|
-
| `description` | API
|
|
120
|
-
| `destination` |
|
|
121
|
-
| `keyGenerator` |
|
|
114
|
+
| Option | Description |
|
|
115
|
+
|--------|-------------|
|
|
116
|
+
| `guards` | Authentication/authorization guards |
|
|
117
|
+
| `limits` | File count/size limits (`{ files: N }`) |
|
|
118
|
+
| `consume` | `"buffer"` (default) or `"stream"` |
|
|
119
|
+
| `description` | API documentation description |
|
|
120
|
+
| `destination` | Stream mode only: storage driver key |
|
|
121
|
+
| `keyGenerator` | Stream mode only: function to generate storage path |
|
|
122
122
|
|
|
123
|
-
###
|
|
123
|
+
### Parameter Rule: Must Wrap in a Single Object
|
|
124
124
|
|
|
125
|
-
> **CRITICAL: `@upload`
|
|
125
|
+
> **CRITICAL: If an `@upload` method has 2 or more parameters, they must be wrapped into a single object.**
|
|
126
126
|
>
|
|
127
|
-
> primitive
|
|
127
|
+
> Using multiple primitive parameters causes a codegen bug in `services.template.ts` that generates `useUploadMutation` incorrectly.
|
|
128
128
|
|
|
129
129
|
```typescript
|
|
130
|
-
// WRONG — codegen
|
|
130
|
+
// WRONG — codegen breaks (missing mutationFn argument)
|
|
131
131
|
async upload(entity_type: string, entity_id: number, file_type: string)
|
|
132
132
|
|
|
133
|
-
// CORRECT —
|
|
133
|
+
// CORRECT — wrap in a single object
|
|
134
134
|
async upload(params: { entity_type: string; entity_id: number; file_type: string })
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
Call site pattern:
|
|
138
138
|
```typescript
|
|
139
139
|
uploadMutation.mutate({
|
|
140
140
|
params: { entity_type, entity_id, file_type },
|
|
@@ -142,40 +142,40 @@ uploadMutation.mutate({
|
|
|
142
142
|
})
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
>
|
|
145
|
+
> For detailed root cause analysis, see the `@upload multiple parameters` section in `framework-change.md`.
|
|
146
146
|
|
|
147
|
-
###
|
|
147
|
+
### Buffer Mode (Default)
|
|
148
148
|
|
|
149
149
|
```typescript
|
|
150
150
|
@upload({ limits: { files: 10 } })
|
|
151
151
|
async uploadFiles(): Promise<{ files: SonamuFile[] }> {
|
|
152
152
|
const { bufferedFiles } = Sonamu.getContext();
|
|
153
|
-
// bufferedFiles[].buffer
|
|
153
|
+
// Access file data via bufferedFiles[].buffer
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
###
|
|
157
|
+
### Stream Mode (Large Files)
|
|
158
158
|
|
|
159
159
|
```typescript
|
|
160
160
|
@upload({
|
|
161
161
|
consume: "stream",
|
|
162
|
-
destination: "s3", //
|
|
162
|
+
destination: "s3", // or "fs"
|
|
163
163
|
keyGenerator: (file) => `uploads/${Date.now()}-${file.filename}`,
|
|
164
164
|
limits: { files: 5 },
|
|
165
165
|
})
|
|
166
166
|
async uploadLargeFiles(): Promise<{ urls: string[] }> {
|
|
167
167
|
const { uploadedFiles } = Sonamu.getContext();
|
|
168
|
-
// uploadedFiles[].key
|
|
168
|
+
// Access stored path via uploadedFiles[].key
|
|
169
169
|
}
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
---
|
|
173
173
|
|
|
174
|
-
##
|
|
174
|
+
## Real-world Business Logic Patterns
|
|
175
175
|
|
|
176
|
-
###
|
|
176
|
+
### Transaction with History Logging
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
Pattern for atomically handling main data and history together when changing state:
|
|
179
179
|
|
|
180
180
|
```typescript
|
|
181
181
|
// consultation.model.ts
|
|
@@ -189,7 +189,7 @@ async changeStatus(
|
|
|
189
189
|
const wdb = this.getPuri("w");
|
|
190
190
|
|
|
191
191
|
return wdb.transaction(async (trx) => {
|
|
192
|
-
// 1.
|
|
192
|
+
// 1. Update consultation
|
|
193
193
|
await trx.ubRegister("consultations", {
|
|
194
194
|
id,
|
|
195
195
|
status,
|
|
@@ -197,7 +197,7 @@ async changeStatus(
|
|
|
197
197
|
});
|
|
198
198
|
await trx.ubUpsert("consultations");
|
|
199
199
|
|
|
200
|
-
// 2.
|
|
200
|
+
// 2. Record status change history
|
|
201
201
|
await trx.ubRegister("consultation_histories", {
|
|
202
202
|
consultation_id: id,
|
|
203
203
|
status,
|
|
@@ -206,20 +206,20 @@ async changeStatus(
|
|
|
206
206
|
});
|
|
207
207
|
await trx.ubUpsert("consultation_histories");
|
|
208
208
|
|
|
209
|
-
// 3.
|
|
209
|
+
// 3. Return result
|
|
210
210
|
return this.findById("A", id);
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
- ubRegister + ubUpsert
|
|
218
|
-
-
|
|
215
|
+
**Key points:**
|
|
216
|
+
- Atomicity guaranteed by transaction
|
|
217
|
+
- ubRegister + ubUpsert pattern
|
|
218
|
+
- Return latest data after change
|
|
219
219
|
|
|
220
|
-
###
|
|
220
|
+
### Validation Logic and Business Rules
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
Pattern for complex validation such as duplicate checks and capacity checks before registration:
|
|
223
223
|
|
|
224
224
|
```typescript
|
|
225
225
|
@api({ httpMethod: "POST", guards: ["user"] })
|
|
@@ -227,49 +227,49 @@ async enroll(
|
|
|
227
227
|
courseId: number,
|
|
228
228
|
userId: number
|
|
229
229
|
): Promise<Enrollment> {
|
|
230
|
-
// 1.
|
|
230
|
+
// 1. Prevent duplicate registration
|
|
231
231
|
const existing = await this.findOne("A", {
|
|
232
232
|
course_id: courseId,
|
|
233
233
|
user_id: userId,
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
if (existing) {
|
|
237
|
-
throw new Error("
|
|
237
|
+
throw new Error("Already enrolled in this course");
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// 2.
|
|
240
|
+
// 2. Check capacity
|
|
241
241
|
const course = await CourseModel.findById("A", courseId);
|
|
242
242
|
const { total } = await this.findMany({ course_id: courseId });
|
|
243
243
|
|
|
244
244
|
if (total >= course.max_students) {
|
|
245
|
-
throw new Error("
|
|
245
|
+
throw new Error("Course is at capacity");
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// 3.
|
|
248
|
+
// 3. Enroll
|
|
249
249
|
const [id] = await this.save([{ course_id: courseId, user_id: userId }]);
|
|
250
250
|
return this.findById("A", id);
|
|
251
251
|
}
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
254
|
+
**Key points:**
|
|
255
|
+
- Step-by-step validation (duplicate → capacity)
|
|
256
|
+
- Clear error messages
|
|
257
|
+
- Save after validation passes
|
|
258
258
|
|
|
259
|
-
###
|
|
259
|
+
### Using Authorization Guards
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
Access control based on user role:
|
|
262
262
|
|
|
263
263
|
```typescript
|
|
264
|
-
//
|
|
264
|
+
// Regular user only
|
|
265
265
|
@api({ httpMethod: "POST", guards: ["user"] })
|
|
266
266
|
async save(spa: PostSaveParams[]): Promise<number[]> { }
|
|
267
267
|
|
|
268
|
-
//
|
|
268
|
+
// Admin only
|
|
269
269
|
@api({ httpMethod: "POST", guards: ["admin"] })
|
|
270
270
|
async del(ids: number[]): Promise<number> { }
|
|
271
271
|
|
|
272
|
-
//
|
|
272
|
+
// Using currently logged-in user info
|
|
273
273
|
@api({ httpMethod: "GET", guards: ["user"] })
|
|
274
274
|
async myConsultations(): Promise<ListResult<Consultation>> {
|
|
275
275
|
const { user } = Sonamu.getContext();
|
|
@@ -277,56 +277,56 @@ async myConsultations(): Promise<ListResult<Consultation>> {
|
|
|
277
277
|
}
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
### API
|
|
280
|
+
### Writing API Tests
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
Validating custom APIs in Business Logic tests:
|
|
283
283
|
|
|
284
284
|
```typescript
|
|
285
285
|
// consultation.test.ts
|
|
286
286
|
describe("E. Business Logic", () => {
|
|
287
|
-
test("
|
|
287
|
+
test("Status change API", async () => {
|
|
288
288
|
const { consultationId } = await createTestConsultationWithDeps();
|
|
289
289
|
|
|
290
|
-
//
|
|
290
|
+
// Call custom API
|
|
291
291
|
const updated = await ConsultationModel.changeStatus(
|
|
292
292
|
consultationId,
|
|
293
293
|
"completed",
|
|
294
|
-
"
|
|
294
|
+
"Consultation complete"
|
|
295
295
|
);
|
|
296
296
|
|
|
297
297
|
expect(updated.status).toBe("completed");
|
|
298
298
|
|
|
299
|
-
//
|
|
299
|
+
// Verify history was recorded
|
|
300
300
|
const histories = await ConsultationHistoryModel.findMany({
|
|
301
301
|
consultation_id: consultationId,
|
|
302
302
|
});
|
|
303
303
|
expect(histories.rows).toHaveLength(1);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
-
test("
|
|
306
|
+
test("Enrollment validation", async () => {
|
|
307
307
|
const courseId = 1;
|
|
308
308
|
const userId = 1;
|
|
309
309
|
|
|
310
|
-
//
|
|
310
|
+
// First enrollment succeeds
|
|
311
311
|
await EnrollmentModel.enroll(courseId, userId);
|
|
312
312
|
|
|
313
|
-
//
|
|
313
|
+
// Duplicate enrollment fails
|
|
314
314
|
await expect(
|
|
315
315
|
EnrollmentModel.enroll(courseId, userId)
|
|
316
|
-
).rejects.toThrow("
|
|
316
|
+
).rejects.toThrow("Already enrolled in this course");
|
|
317
317
|
});
|
|
318
318
|
});
|
|
319
319
|
```
|
|
320
320
|
|
|
321
321
|
---
|
|
322
322
|
|
|
323
|
-
##
|
|
323
|
+
## Conventions and Best Practices
|
|
324
324
|
|
|
325
|
-
###
|
|
325
|
+
### Error Message Pattern
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
Use `this.modelName` and the `SD()` function for consistent error messages.
|
|
328
328
|
|
|
329
|
-
**BAD:
|
|
329
|
+
**BAD: Hardcoded model name**
|
|
330
330
|
```typescript
|
|
331
331
|
// findById
|
|
332
332
|
if (!rows[0]) {
|
|
@@ -337,27 +337,27 @@ if (!rows[0]) {
|
|
|
337
337
|
throw new BadRequestException(SD("error.unknownSearchField")(params.search));
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
**GOOD: this.modelName
|
|
340
|
+
**GOOD: Using this.modelName**
|
|
341
341
|
```typescript
|
|
342
|
-
// findById -
|
|
342
|
+
// findById - auto-detects model name
|
|
343
343
|
if (!rows[0]) {
|
|
344
344
|
throw new NotFoundException(SD("notFound")(this.modelName, id));
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// findMany -
|
|
347
|
+
// findMany - short and clear key
|
|
348
348
|
throw new BadRequestException(SD("search.invalidField")(params.search));
|
|
349
349
|
```
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
- DRY
|
|
353
|
-
-
|
|
354
|
-
-
|
|
351
|
+
**Benefits:**
|
|
352
|
+
- DRY principle: model name managed in one place
|
|
353
|
+
- Refactoring safe: error messages auto-reflect model name changes
|
|
354
|
+
- Short i18n keys: `notFound`, `search.invalidField` are more concise
|
|
355
355
|
|
|
356
|
-
### satisfies
|
|
356
|
+
### satisfies Keyword
|
|
357
357
|
|
|
358
|
-
TypeScript
|
|
358
|
+
Use TypeScript's satisfies keyword to preserve type inference while checking types.
|
|
359
359
|
|
|
360
|
-
**BAD:
|
|
360
|
+
**BAD: Loss of type inference**
|
|
361
361
|
```typescript
|
|
362
362
|
const params: RoleListParams = {
|
|
363
363
|
num: 24,
|
|
@@ -368,7 +368,7 @@ const params: RoleListParams = {
|
|
|
368
368
|
};
|
|
369
369
|
```
|
|
370
370
|
|
|
371
|
-
**GOOD:
|
|
371
|
+
**GOOD: Type check + preserved inference with satisfies**
|
|
372
372
|
```typescript
|
|
373
373
|
const params = {
|
|
374
374
|
num: 24,
|
|
@@ -379,27 +379,27 @@ const params = {
|
|
|
379
379
|
} satisfies RoleListParams;
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
385
|
-
- IDE
|
|
382
|
+
**Benefits:**
|
|
383
|
+
- Compile-time verification: checks that params satisfies the RoleListParams type
|
|
384
|
+
- Preserved type inference: params keeps its narrowed type
|
|
385
|
+
- Better IDE support: more accurate autocomplete and type checking
|
|
386
386
|
|
|
387
|
-
### debug
|
|
387
|
+
### debug Option
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
The debug option in executeSubsetQuery defaults to false, so it does not need to be specified explicitly.
|
|
390
390
|
|
|
391
|
-
**BAD:
|
|
391
|
+
**BAD: Unnecessary debug: false**
|
|
392
392
|
```typescript
|
|
393
393
|
return this.executeSubsetQuery({
|
|
394
394
|
subset,
|
|
395
395
|
qb,
|
|
396
396
|
params,
|
|
397
397
|
enhancers,
|
|
398
|
-
debug: false, //
|
|
398
|
+
debug: false, // unnecessary — it's the default
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
-
**GOOD:
|
|
402
|
+
**GOOD: Use the default**
|
|
403
403
|
```typescript
|
|
404
404
|
return this.executeSubsetQuery({
|
|
405
405
|
subset,
|
|
@@ -409,20 +409,20 @@ return this.executeSubsetQuery({
|
|
|
409
409
|
});
|
|
410
410
|
```
|
|
411
411
|
|
|
412
|
-
**debug: true
|
|
412
|
+
**When to use debug: true:**
|
|
413
413
|
```typescript
|
|
414
|
-
//
|
|
414
|
+
// Only specify when debugging
|
|
415
415
|
return this.executeSubsetQuery({
|
|
416
416
|
subset,
|
|
417
417
|
qb,
|
|
418
418
|
params,
|
|
419
|
-
debug: true, // SQL
|
|
419
|
+
debug: true, // Print SQL query log
|
|
420
420
|
});
|
|
421
421
|
```
|
|
422
422
|
|
|
423
|
-
## @stream
|
|
423
|
+
## @stream Decorator (SSE)
|
|
424
424
|
|
|
425
|
-
Server-Sent Events
|
|
425
|
+
Creates a Server-Sent Events endpoint.
|
|
426
426
|
|
|
427
427
|
```typescript
|
|
428
428
|
import { stream } from "sonamu";
|
|
@@ -439,29 +439,29 @@ import { z } from "zod";
|
|
|
439
439
|
async processStream() { ... }
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
-
|
|
|
443
|
-
|
|
444
|
-
| `type` | `"sse"` (
|
|
445
|
-
| `events` |
|
|
446
|
-
| `path` |
|
|
447
|
-
| `resourceName` |
|
|
448
|
-
| `guards` |
|
|
442
|
+
| Option | Description | Required |
|
|
443
|
+
|--------|-------------|----------|
|
|
444
|
+
| `type` | `"sse"` (only SSE currently supported) | Yes |
|
|
445
|
+
| `events` | Define event keys and payloads with Zod schema | Yes |
|
|
446
|
+
| `path` | Custom path | - |
|
|
447
|
+
| `resourceName` | Resource name | - |
|
|
448
|
+
| `guards` | Authentication/authorization guards | - |
|
|
449
449
|
|
|
450
|
-
## @transactional
|
|
450
|
+
## @transactional Decorator
|
|
451
451
|
|
|
452
|
-
|
|
452
|
+
Wraps the entire method in an automatic transaction. Reuses an existing transaction context if one is already active.
|
|
453
453
|
|
|
454
454
|
```typescript
|
|
455
455
|
import { transactional } from "sonamu";
|
|
456
456
|
|
|
457
457
|
@transactional({ isolation: "serializable" })
|
|
458
458
|
async transferFunds(fromId: number, toId: number, amount: number) {
|
|
459
|
-
// this.getPuri("w")
|
|
459
|
+
// this.getPuri("w") automatically runs inside the transaction
|
|
460
460
|
}
|
|
461
461
|
```
|
|
462
462
|
|
|
463
|
-
|
|
|
464
|
-
|
|
465
|
-
| `isolation` |
|
|
466
|
-
| `readOnly` |
|
|
467
|
-
| `dbPreset` | DB
|
|
463
|
+
| Option | Description | Default |
|
|
464
|
+
|--------|-------------|---------|
|
|
465
|
+
| `isolation` | Transaction isolation level (read uncommitted/read committed/repeatable read/serializable) | - |
|
|
466
|
+
| `readOnly` | Read-only transaction | `false` |
|
|
467
|
+
| `dbPreset` | DB preset | `"w"` |
|