sonamu 0.10.7 → 0.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +5 -12
- package/dist/bin/cli.js +10 -253
- package/dist/database/base-model.d.ts +1 -0
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +14 -8
- package/dist/database/db.d.ts +2 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +13 -3
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +4 -11
- package/dist/database/transaction-context.d.ts +8 -3
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/transaction-context.js +12 -7
- package/dist/migration/code-generation.js +2 -2
- package/dist/naite/naite-reporter.d.ts +8 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +9 -4
- package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
- package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +3 -3
- package/src/api/decorators.ts +11 -22
- package/src/bin/cli.ts +12 -375
- package/src/database/__tests__/transaction-scope.test.ts +227 -0
- package/src/database/base-model.ts +31 -6
- package/src/database/db.ts +17 -2
- package/src/database/puri-wrapper.ts +5 -22
- package/src/database/transaction-context.ts +14 -6
- package/src/migration/code-generation.ts +1 -1
- package/src/naite/naite-reporter.ts +8 -3
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
- package/src/skills/AGENTS.md +0 -108
- package/src/skills/sonamu/SKILL.md +0 -75
- package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
- package/src/skills/sonamu-api/SKILL.md +0 -480
- package/src/skills/sonamu-auth/SKILL.md +0 -327
- package/src/skills/sonamu-auth/references/plugins.md +0 -310
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
- package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
- package/src/skills/sonamu-config/SKILL.md +0 -203
- package/src/skills/sonamu-config/references/database.md +0 -452
- package/src/skills/sonamu-config/references/environments.md +0 -178
- package/src/skills/sonamu-config/references/server-options.md +0 -400
- package/src/skills/sonamu-entity/SKILL.md +0 -180
- package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
- package/src/skills/sonamu-entity/references/design-guides.md +0 -243
- package/src/skills/sonamu-entity/references/field-types.md +0 -170
- package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
- package/src/skills/sonamu-entity/references/relations.md +0 -463
- package/src/skills/sonamu-entity/references/subset.md +0 -156
- package/src/skills/sonamu-fixture/SKILL.md +0 -180
- package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
- package/src/skills/sonamu-fixture/references/cone.md +0 -298
- package/src/skills/sonamu-frontend/SKILL.md +0 -142
- package/src/skills/sonamu-frontend/references/components.md +0 -323
- package/src/skills/sonamu-frontend/references/examples.md +0 -64
- package/src/skills/sonamu-frontend/references/hooks.md +0 -273
- package/src/skills/sonamu-frontend/references/runtime.md +0 -165
- package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
- package/src/skills/sonamu-i18n/SKILL.md +0 -287
- package/src/skills/sonamu-migration/SKILL.md +0 -316
- package/src/skills/sonamu-naite/SKILL.md +0 -266
- package/src/skills/sonamu-query/SKILL.md +0 -48
- package/src/skills/sonamu-query/references/model-patterns.md +0 -390
- package/src/skills/sonamu-query/references/model.md +0 -366
- package/src/skills/sonamu-query/references/puri.md +0 -413
- package/src/skills/sonamu-query/references/search.md +0 -238
- package/src/skills/sonamu-query/references/upsert.md +0 -324
- package/src/skills/sonamu-tasks/SKILL.md +0 -236
- package/src/skills/sonamu-testing/SKILL.md +0 -251
- package/src/skills/sonamu-testing/references/devrunner.md +0 -405
- package/src/skills/sonamu-testing/references/helpers.md +0 -185
- package/src/skills/sonamu-testing/references/patterns.md +0 -263
- package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
- package/src/skills/sonamu-testing/references/quick-start.md +0 -285
- package/src/skills/sonamu-testing/references/type-safety.md +0 -172
- package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
- package/src/skills/sonamu-vector/SKILL.md +0 -222
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
# UpsertBuilder — Batch Saving Relation Data
|
|
2
|
-
|
|
3
|
-
## UBRef Type
|
|
4
|
-
|
|
5
|
-
The reference object returned by `ubRegister()`:
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
type UBRef = {
|
|
9
|
-
uuid: string; // unique identifier
|
|
10
|
-
of: string; // table name
|
|
11
|
-
use?: string; // field to reference (default: "id")
|
|
12
|
-
};
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Basic Pattern
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
const wdb = this.getPuri("w");
|
|
19
|
-
|
|
20
|
-
// Register data (returns UBRef)
|
|
21
|
-
const userRef = wdb.ubRegister("users", { email: "john@test.com", username: "john" });
|
|
22
|
-
|
|
23
|
-
// Use UBRef in related data
|
|
24
|
-
wdb.ubRegister("employees", { user_id: userRef, department_id: deptId });
|
|
25
|
-
|
|
26
|
-
// Save in order inside a transaction
|
|
27
|
-
return wdb.transaction(async (trx) => {
|
|
28
|
-
await trx.ubUpsert("users"); // Save first (referenced by FK)
|
|
29
|
-
return trx.ubUpsert("employees"); // Save after (uses FK)
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## CRITICAL: All Required Fields Must Be Included
|
|
34
|
-
|
|
35
|
-
**ubUpsert uses PostgreSQL's `ON CONFLICT ... DO UPDATE` query.**
|
|
36
|
-
|
|
37
|
-
Even when updating, **all required fields (fields with NOT NULL constraints)** must be included.
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
// BAD - missing required field
|
|
41
|
-
wdb.ubRegister("posts", {
|
|
42
|
-
id: 1,
|
|
43
|
-
title: "Updated Title",
|
|
44
|
-
// content required field missing! → ON CONFLICT UPDATE tries to set NULL → DB error
|
|
45
|
-
});
|
|
46
|
-
// Error: null value in column "content" violates not-null constraint
|
|
47
|
-
|
|
48
|
-
// GOOD - all required fields included
|
|
49
|
-
wdb.ubRegister("posts", {
|
|
50
|
-
id: 1,
|
|
51
|
-
title: "Updated Title",
|
|
52
|
-
content: "Updated Content", // required field included!
|
|
53
|
-
author_id: 1, // FK also included if required!
|
|
54
|
-
});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**How to identify required fields**:
|
|
58
|
-
|
|
59
|
-
1. Check props in entity.json
|
|
60
|
-
2. Fields without `nullable: true` = required fields
|
|
61
|
-
3. `id`, `created_at`, fields with `dbDefault` can be omitted
|
|
62
|
-
|
|
63
|
-
```json
|
|
64
|
-
// entity.json example
|
|
65
|
-
{
|
|
66
|
-
"props": [
|
|
67
|
-
{ "name": "id", "type": "integer" }, // can be omitted
|
|
68
|
-
{ "name": "title", "type": "string" }, // required! (no nullable)
|
|
69
|
-
{ "name": "content", "type": "string" }, // required! (no nullable)
|
|
70
|
-
{ "name": "category", "type": "string", "nullable": true }, // optional
|
|
71
|
-
{ "name": "created_at", "type": "date", "dbDefault": "CURRENT_TIMESTAMP" } // can be omitted
|
|
72
|
-
]
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Save Order (Important!)
|
|
77
|
-
|
|
78
|
-
Save the table referenced by FK first:
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
await trx.ubUpsert("companies"); // 1. No dependencies
|
|
82
|
-
await trx.ubUpsert("departments"); // 2. Needs company_id
|
|
83
|
-
await trx.ubUpsert("users"); // 3. No dependencies
|
|
84
|
-
await trx.ubUpsert("employees"); // 4. Needs user_id, department_id
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Model save Pattern
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
@api({ httpMethod: "POST" })
|
|
91
|
-
async save(spa: UserSaveParams[]): Promise<number[]> {
|
|
92
|
-
const wdb = this.getPuri("w");
|
|
93
|
-
spa.forEach((sp) => wdb.ubRegister("users", sp));
|
|
94
|
-
|
|
95
|
-
return wdb.transaction(async (trx) => {
|
|
96
|
-
return trx.ubUpsert("users");
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Saving Related Data
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
await this.getPuri("w").transaction(async (trx) => {
|
|
105
|
-
// Register User
|
|
106
|
-
const userRef = trx.ubRegister("users", {
|
|
107
|
-
email: data.email,
|
|
108
|
-
username: data.username,
|
|
109
|
-
password: bcrypt.hashSync(data.password, 10),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Register Employee (using userRef)
|
|
113
|
-
trx.ubRegister("employees", {
|
|
114
|
-
user_id: userRef,
|
|
115
|
-
department_id: data.departmentId,
|
|
116
|
-
salary: data.salary,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Save in order
|
|
120
|
-
await trx.ubUpsert("users");
|
|
121
|
-
const [employeeId] = await trx.ubUpsert("employees");
|
|
122
|
-
return employeeId;
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Upsert (Insert or Update)
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
// INSERT when no id
|
|
130
|
-
wdb.ubRegister("users", { email: "new@test.com", username: "new" });
|
|
131
|
-
|
|
132
|
-
// UPDATE when id is present
|
|
133
|
-
wdb.ubRegister("users", { id: 1, email: "updated@test.com" });
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Conflict handling**: If the Entity has a unique index, automatically pre-fetches to populate the existing record's id, then performs UPDATE
|
|
137
|
-
|
|
138
|
-
## ManyToMany Relationships
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
await wdb.transaction(async (trx) => {
|
|
142
|
-
const projectRef = trx.ubRegister("projects", { name: "Project A" });
|
|
143
|
-
|
|
144
|
-
for (const empId of employeeIds) {
|
|
145
|
-
trx.ubRegister("projects__employees", {
|
|
146
|
-
project_id: projectRef,
|
|
147
|
-
employee_id: empId,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
await trx.ubUpsert("projects");
|
|
152
|
-
await trx.ubUpsert("projects__employees");
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Self-Reference
|
|
157
|
-
|
|
158
|
-
In hierarchical structures (e.g. categories, org charts), self-referential relationships are automatically processed level by level:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
await wdb.transaction(async (trx) => {
|
|
162
|
-
// Root category
|
|
163
|
-
const rootRef = trx.ubRegister("categories", { name: "Root", parent_id: null });
|
|
164
|
-
|
|
165
|
-
// Child category (references rootRef)
|
|
166
|
-
const childRef = trx.ubRegister("categories", { name: "Child", parent_id: rootRef });
|
|
167
|
-
|
|
168
|
-
// Grandchild category (references childRef)
|
|
169
|
-
trx.ubRegister("categories", { name: "Grandchild", parent_id: childRef });
|
|
170
|
-
|
|
171
|
-
// Internally processed level by level (Root → Child → Grandchild)
|
|
172
|
-
await trx.ubUpsert("categories");
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Bulk Insert / Bulk Upsert (Large Datasets)
|
|
177
|
-
|
|
178
|
-
**Principle (same as regular saves):** call `ubRegister` for every row **outside** the transaction, then call only `ubUpsert` / `ubInsertOnly` **inside** the transaction. Split large datasets with `chunkSize`. Calling `ubRegister` inside the transaction is reserved for the special case where a row depends on the real id produced by a preceding `ubUpsert`.
|
|
179
|
-
|
|
180
|
-
### (a) Bulk insert inside a Model
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
async createMany(params: PostCreateParams[]): Promise<number[]> {
|
|
184
|
-
const wdb = this.getPuri("w");
|
|
185
|
-
params.forEach((p) => wdb.ubRegister("posts", p));
|
|
186
|
-
|
|
187
|
-
return wdb.transaction(async (trx) => {
|
|
188
|
-
return trx.ubInsertOnly("posts", { chunkSize: 5000 });
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### (b) Standalone Puri (seed / batch scripts outside a Model)
|
|
194
|
-
|
|
195
|
-
Outside a Model there is no `this.getPuri("w")`, so build a `PuriWrapper` directly. Use `DB.getDB("w")` only in this script context — inside Models always use `getPuri("w")`.
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
import { DB, PuriWrapper, UpsertBuilder } from "sonamu";
|
|
199
|
-
|
|
200
|
-
const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
|
|
201
|
-
for (const row of rows) puri.ubRegister("audit_logs", row);
|
|
202
|
-
|
|
203
|
-
await puri.transaction(async (trx) => {
|
|
204
|
-
await trx.ubInsertOnly("audit_logs", { chunkSize: 5000 }); // or ubUpsert
|
|
205
|
-
});
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### (c) Multiple tables in one transaction (FK order)
|
|
209
|
-
|
|
210
|
-
Register every table outside the transaction, then upsert parents before children.
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
const puri = new PuriWrapper(DB.getDB("w"), new UpsertBuilder());
|
|
214
|
-
for (const row of postRows) puri.ubRegister("posts", row);
|
|
215
|
-
for (const row of tagRows) puri.ubRegister("post_tags", row);
|
|
216
|
-
|
|
217
|
-
await puri.transaction(async (trx) => {
|
|
218
|
-
await trx.ubInsertOnly("posts", { chunkSize: 5000 }); // parent first
|
|
219
|
-
await trx.ubInsertOnly("post_tags", { chunkSize: 5000 }); // child next
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## ubInsertOnly (INSERT Only)
|
|
224
|
-
|
|
225
|
-
Perform INSERT without UPDATE:
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
await trx.ubInsertOnly("logs", { chunkSize: 1000 });
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## ubUpdateBatch (Batch Update)
|
|
232
|
-
|
|
233
|
-
Bulk UPDATE operations:
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
// Register multiple records
|
|
237
|
-
wdb.ubRegister("users", { id: 1, status: "active" });
|
|
238
|
-
wdb.ubRegister("users", { id: 2, status: "active" });
|
|
239
|
-
wdb.ubRegister("users", { id: 3, status: "inactive" });
|
|
240
|
-
|
|
241
|
-
await wdb.transaction(async (trx) => {
|
|
242
|
-
await trx.ubUpdateBatch("users", {
|
|
243
|
-
chunkSize: 500, // batch size (default: 500)
|
|
244
|
-
where: "id", // WHERE condition column (default: "id")
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Composite key for WHERE condition
|
|
249
|
-
await trx.ubUpdateBatch("user_settings", {
|
|
250
|
-
where: ["user_id", "setting_key"],
|
|
251
|
-
});
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## UpsertOptions
|
|
255
|
-
|
|
256
|
-
Options for `ubUpsert()`:
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
type UpsertOptions = {
|
|
260
|
-
chunkSize?: number; // batch size
|
|
261
|
-
cleanOrphans?: string | string[]; // FK column(s) to use as basis for deleting orphan records
|
|
262
|
-
inherit?: string[]; // columns to preserve existing values on UPDATE
|
|
263
|
-
};
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### chunkSize
|
|
267
|
-
|
|
268
|
-
Specify batch size for large data processing:
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
await trx.ubUpsert("logs", { chunkSize: 1000 });
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### cleanOrphans
|
|
275
|
-
|
|
276
|
-
Automatically delete orphan records based on FK:
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// Single FK
|
|
280
|
-
await trx.ubUpsert("order_items", {
|
|
281
|
-
cleanOrphans: "order_id", // delete records with the same order_id that were not upserted this time
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Composite FK
|
|
285
|
-
await trx.ubUpsert("project_members", {
|
|
286
|
-
cleanOrphans: ["project_id", "team_id"],
|
|
287
|
-
});
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### inherit
|
|
291
|
-
|
|
292
|
-
Preserve existing values for specific columns on UPDATE:
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
await trx.ubUpsert("users", {
|
|
296
|
-
inherit: ["created_at", "password"], // these columns are excluded from UPDATE
|
|
297
|
-
});
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
## ubUpsertOrInsert (Conditional Mode)
|
|
301
|
-
|
|
302
|
-
Select upsert or insert mode at runtime.
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
await trx.ubUpsertOrInsert("logs", "insert"); // INSERT only
|
|
306
|
-
await trx.ubUpsertOrInsert("users", "upsert"); // UPSERT (default)
|
|
307
|
-
await trx.ubUpsertOrInsert("users", "upsert", { cleanOrphans: "team_id" });
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
| Parameter | Type | Description |
|
|
311
|
-
| ----------- | ------------------------ | --------------------------------------------------- |
|
|
312
|
-
| `tableName` | string | table name |
|
|
313
|
-
| `mode` | `"upsert"` \| `"insert"` | operation mode |
|
|
314
|
-
| `options` | `UpsertOptions` | chunkSize, cleanOrphans, inherit (same as ubUpsert) |
|
|
315
|
-
|
|
316
|
-
When `mode: "insert"`, unlike `insertOnly`, `UpsertOptions` (cleanOrphans, inherit) can be used.
|
|
317
|
-
|
|
318
|
-
## Rules
|
|
319
|
-
|
|
320
|
-
- MUST use inside `transaction()`
|
|
321
|
-
- MUST call `ubUpsert()` for FK-referenced tables first (correct order)
|
|
322
|
-
- UBRef can ONLY be used inside `ubRegister` (not for direct DB queries)
|
|
323
|
-
- Self-reference is auto-handled by level-based insertion
|
|
324
|
-
- Unique index conflicts are auto-resolved by pre-fetching existing IDs
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-tasks
|
|
3
|
-
description: Runs background work with Sonamu Tasks. Use when implementing a background job, scheduling recurring work, or building a multi-step process that must survive a restart. Covers durable steps, cron scheduling, retry policies, and the task worker.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Tasks (Workflow System)
|
|
7
|
-
|
|
8
|
-
PostgreSQL-based durable workflow engine. Uses the `@sonamu-kit/tasks` package.
|
|
9
|
-
|
|
10
|
-
**Source code:**
|
|
11
|
-
|
|
12
|
-
- Decorator: `modules/sonamu/src/tasks/decorator.ts`
|
|
13
|
-
- StepWrapper: `modules/sonamu/src/tasks/step-wrapper.ts`
|
|
14
|
-
- WorkflowManager: `modules/sonamu/src/tasks/workflow-manager.ts`
|
|
15
|
-
- @sonamu-kit/tasks: `modules/tasks/`
|
|
16
|
-
|
|
17
|
-
## Workflow Definition
|
|
18
|
-
|
|
19
|
-
Define with the `workflow()` function. When exported, the syncer automatically collects and registers it with WorkflowManager.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { workflow } from "sonamu";
|
|
23
|
-
|
|
24
|
-
// Method 1: decorator + function separated
|
|
25
|
-
export const myTask = workflow({
|
|
26
|
-
version: "1",
|
|
27
|
-
})(async ({ input, step, logger, version }) => {
|
|
28
|
-
// ...
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Method 2: decorator + function inlined
|
|
32
|
-
export const myTask = workflow(
|
|
33
|
-
{
|
|
34
|
-
version: "1",
|
|
35
|
-
},
|
|
36
|
-
async ({ input, step, logger, version }) => {
|
|
37
|
-
// ...
|
|
38
|
-
},
|
|
39
|
-
);
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### DefineWorkflowOptions
|
|
43
|
-
|
|
44
|
-
| Option | Type | Required | Description |
|
|
45
|
-
| ------------- | ------------------ | -------- | ------------------------------------------------------------------- |
|
|
46
|
-
| `version` | `string` | Y | Workflow version (distinguishes from existing runs when changed) |
|
|
47
|
-
| `name` | `string` | N | Workflow name (default: function name converted to underscore case) |
|
|
48
|
-
| `schema` | `StandardSchemaV1` | N | Input validation schema (Zod, etc.) |
|
|
49
|
-
| `schedules` | `Schedule[]` | N | Array of cron schedules |
|
|
50
|
-
| `retryPolicy` | `RetryPolicy` | N | Retry policy |
|
|
51
|
-
|
|
52
|
-
### Workflow Function Parameters
|
|
53
|
-
|
|
54
|
-
| Parameter | Type | Description |
|
|
55
|
-
| --------- | ---------------- | ----------------------------------------- |
|
|
56
|
-
| `input` | `Input` | Input value passed when the workflow runs |
|
|
57
|
-
| `step` | `StepWrapper` | Tool for defining/executing steps |
|
|
58
|
-
| `logger` | `Logger` | @logtape/logtape logger |
|
|
59
|
-
| `version` | `string \| null` | Current workflow version |
|
|
60
|
-
|
|
61
|
-
## Step
|
|
62
|
-
|
|
63
|
-
An atomic unit of execution within a workflow. On failure, retry begins from that step.
|
|
64
|
-
|
|
65
|
-
### step.define — Inline Function
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
const result = await step
|
|
69
|
-
.define({ name: "fetch-data" }, async () => {
|
|
70
|
-
const data = await fetchSomething();
|
|
71
|
-
return data;
|
|
72
|
-
})
|
|
73
|
-
.run();
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### step.get — Wrapping an Existing Method
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// Wrap a Model method as a Step
|
|
80
|
-
const result = await step.get(MyModel, "processData").run(inputData);
|
|
81
|
-
|
|
82
|
-
// Specify a custom name
|
|
83
|
-
const result = await step.get({ name: "custom_step" }, MyService, "execute").run(params);
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Overloads for `step.get`:
|
|
87
|
-
|
|
88
|
-
- `step.get(object, methodName)` — Step name is the methodName converted to underscore case
|
|
89
|
-
- `step.get({ name }, object, methodName)` — Step name specified directly
|
|
90
|
-
|
|
91
|
-
### step.sleep — Durable Wait
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
await step.sleep("wait-before-retry", "30m");
|
|
95
|
-
await step.sleep("daily-delay", "1d");
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The wait time is preserved even if the server restarts.
|
|
99
|
-
|
|
100
|
-
**DurationString format:** `{number}{unit}` — e.g. `"5s"`, `"30m"`, `"2h"`, `"7d"`, `"1w"`, `"1y"`
|
|
101
|
-
|
|
102
|
-
## Scheduling (cron)
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
export const dailyReport = workflow(
|
|
106
|
-
{
|
|
107
|
-
version: "1",
|
|
108
|
-
schedules: [
|
|
109
|
-
{
|
|
110
|
-
expression: "0 9 * * *", // every day at 9am
|
|
111
|
-
name: "daily-report", // optional (default: workflowName[expression])
|
|
112
|
-
input: () => ({ date: new Date().toISOString() }), // optional
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
async ({ input, step }) => {
|
|
117
|
-
// ...
|
|
118
|
-
},
|
|
119
|
-
);
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
| Field | Type | Required | Description |
|
|
123
|
-
| ------------ | ------------------- | -------- | ---------------------------------------------------- |
|
|
124
|
-
| `expression` | `string` | Y | cron expression |
|
|
125
|
-
| `name` | `string` | N | Schedule name (default: `workflowName[expression]`) |
|
|
126
|
-
| `input` | `Executable<Input>` | N | Input value to pass on execution (function or value) |
|
|
127
|
-
|
|
128
|
-
The timezone follows the `api.timezone` setting in `sonamu.config.ts`.
|
|
129
|
-
|
|
130
|
-
## Retry Policy
|
|
131
|
-
|
|
132
|
-
### Static Policy (Default)
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
export const reliableTask = workflow(
|
|
136
|
-
{
|
|
137
|
-
version: "1",
|
|
138
|
-
retryPolicy: {
|
|
139
|
-
maxAttempts: 5, // maximum retry attempts (default: 5)
|
|
140
|
-
initialIntervalMs: 1000, // first retry wait time (default: 1000ms)
|
|
141
|
-
backoffCoefficient: 2, // wait time multiplier (default: 2)
|
|
142
|
-
maximumIntervalMs: 60000, // maximum wait time
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
async ({ step }) => {
|
|
146
|
-
// ...
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Dynamic Policy
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
retryPolicy: {
|
|
155
|
-
maxAttempts: 10,
|
|
156
|
-
shouldRetry: (error, attempt) => ({
|
|
157
|
-
shouldRetry: error.message !== "FATAL",
|
|
158
|
-
delayMs: attempt * 2000,
|
|
159
|
-
}),
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## sonamu.config.ts Configuration
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
export default defineConfig({
|
|
167
|
-
tasks: {
|
|
168
|
-
enableWorker: true,
|
|
169
|
-
workerOptions: {
|
|
170
|
-
concurrency: 4, // concurrent execution count (default: CPU cores - 1)
|
|
171
|
-
usePubSub: true, // use DB pub/sub (default: true)
|
|
172
|
-
listenDelay: 500, // execution delay after pub/sub receive in ms (default: 500)
|
|
173
|
-
},
|
|
174
|
-
contextProvider: (defaultContext) => {
|
|
175
|
-
// build Context to use within workflows
|
|
176
|
-
return { ...defaultContext };
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
| Option | Type | Description |
|
|
183
|
-
| --------------------------- | ------------------ | -------------------------------------------------- |
|
|
184
|
-
| `enableWorker` | `boolean` | Whether to enable Worker (use only in daemon mode) |
|
|
185
|
-
| `workerOptions.concurrency` | `number` | Number of concurrently executing tasks |
|
|
186
|
-
| `workerOptions.usePubSub` | `boolean` | Use PostgreSQL pub/sub |
|
|
187
|
-
| `workerOptions.listenDelay` | `number` | Execution delay after pub/sub receive (ms) |
|
|
188
|
-
| `contextProvider` | `(ctx) => Context` | Context creation function when workflow runs |
|
|
189
|
-
|
|
190
|
-
## Manual Execution
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { Sonamu } from "sonamu";
|
|
194
|
-
|
|
195
|
-
// Run via WorkflowManager
|
|
196
|
-
const handle = await Sonamu.workflowManager.run(
|
|
197
|
-
{ name: "my-task", version: "1" },
|
|
198
|
-
{ target: "manual" },
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// Wait for result
|
|
202
|
-
const result = await handle.result();
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## File Placement
|
|
206
|
-
|
|
207
|
-
```
|
|
208
|
-
packages/api/src/application/
|
|
209
|
-
├── {domain}/
|
|
210
|
-
│ ├── {domain}.model.ts
|
|
211
|
-
│ ├── {domain}.types.ts
|
|
212
|
-
│ └── {domain}.workflow.ts ← workflow file
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Define with `workflow()` and export in the workflow file; the syncer will collect it automatically.
|
|
216
|
-
|
|
217
|
-
## Architecture
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
Dev Server startup
|
|
221
|
-
→ WorkflowManager initialization (BackendPostgres)
|
|
222
|
-
→ Worker startup (when enableWorker: true)
|
|
223
|
-
→ syncer collects .workflow.ts files
|
|
224
|
-
→ registers via WorkflowManager.synchronize()
|
|
225
|
-
→ cron schedules start automatically
|
|
226
|
-
|
|
227
|
-
On HMR
|
|
228
|
-
→ re-register workflows from changed files (synchronize)
|
|
229
|
-
|
|
230
|
-
Execution flow
|
|
231
|
-
→ run() called → workflow execution record created in DB
|
|
232
|
-
→ Worker receives via pub/sub → steps executed sequentially
|
|
233
|
-
→ checkpoint saved to DB on each step completion
|
|
234
|
-
→ on failure: retry from that step according to retryPolicy
|
|
235
|
-
→ on server restart: incomplete workflows recovered from DB and continue
|
|
236
|
-
```
|