@voltro/cli 0.23.0 → 0.25.0
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/CHANGELOG.md +537 -0
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/bin/voltro.mjs +39 -4
- package/dist/{apiBuild-JQtIhZPy.js → apiBuild-BqhCSytw.js} +46 -24
- package/dist/apiBuild-N1R4V792.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-Bsu9Buln.js → commands-7EmYJ9Xg.js} +2796 -2478
- package/dist/dbCommand-CIrdFLp9.js +2 -0
- package/dist/{dbCommand-GI7-BVZt.js → dbCommand-FUU12FvD.js} +285 -246
- package/dist/{dev-DRxF_qOB.js → dev-BvHT7WZa.js} +1 -1
- package/dist/{dev-jM07kq_D.js → dev-MacSQ1Ll.js} +2042 -1631
- package/dist/fileConventions-3bffWssN.js +30 -0
- package/dist/frameworkTableAssembly-BsnCKzQ6.js +2 -0
- package/dist/{frameworkTableAssembly-sg41s14F.js → frameworkTableAssembly-Cw5zJz6n.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/inspect-C9gjHwBk.js +2 -0
- package/dist/{inspect-_ldwsAwH.js → inspect-DuLUrZp9.js} +14 -1
- package/dist/{inspectMetrics-dcC7gnxu.js → inspectMetrics-EQwH7BI4.js} +15 -15
- package/dist/manifestBuild-BVwS1Z_6.js +2 -0
- package/dist/{manifestBuild-Bs1Uw22_.js → manifestBuild-Dneq4_Jx.js} +1 -1
- package/dist/serveCommand-5ZFiNO1R.js +1241 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-C_4VUDT8.js → start-C-ZWSDpg.js} +2 -2
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +12 -5
- package/templates/AGENTS.md +14 -7
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +3 -3
- package/templates/agent-docs/authentication.md +21 -0
- package/templates/agent-docs/cli.md +32 -0
- package/templates/agent-docs/data.md +491 -167
- package/templates/agent-docs/database/migrations.md +132 -63
- package/templates/agent-docs/database/schema.md +80 -0
- package/templates/agent-docs/database/seedsdialects.md +12 -0
- package/templates/agent-docs/deployment.md +75 -1
- package/templates/agent-docs/plugins.md +1 -1
- package/templates/agent-docs/reference.md +2 -2
- package/templates/agent-docs/security.md +18 -0
- package/templates/agent-docs/templates/apibackends.md +5 -5
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/whats-new.md +263 -169
- package/templates/agent-docs/workflows.md +74 -20
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/package.json +8 -7
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-durable/README.md +2 -1
- package/templates/apps/api-durable/events/orders.event.ts +18 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +17 -15
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +6 -4
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/README.md +2 -2
- package/templates/apps/api-webhooks/events/orders.event.ts +35 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/template.json +1 -1
- package/templates/apps/changelog/package.json +7 -6
- package/templates/apps/edge-functions/package.json +3 -2
- package/templates/apps/frontend-admin/package.json +9 -8
- package/templates/apps/frontend-app/package.json +9 -8
- package/templates/apps/frontend-blank/package.json +8 -7
- package/templates/apps/frontend-contact/package.json +8 -7
- package/templates/apps/frontend-contact/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/package.json +8 -7
- package/templates/apps/frontend-docs/package.json +8 -7
- package/templates/apps/frontend-docs/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/package.json +7 -6
- package/templates/apps/frontend-landing/package.json +8 -7
- package/templates/apps/frontend-landing/src/globals.d.ts +6 -0
- package/templates/apps/frontend-spa/package.json +8 -7
- package/templates/apps/frontend-spa/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr/package.json +8 -7
- package/templates/apps/frontend-ssr/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/package.json +9 -8
- package/templates/apps/frontend-static-blog/package.json +7 -6
- package/templates/apps/frontend-static-blog/src/globals.d.ts +6 -0
- package/dist/apiBuild-CD-4JLLA.js +0 -2
- package/dist/dbCommand-CSguuUQn.js +0 -2
- package/dist/fileConventions-CBNPZVOV.js +0 -29
- package/dist/frameworkTableAssembly-DRJrAVr1.js +0 -2
- package/dist/inspect-BA67TF6v.js +0 -2
- package/dist/manifestBuild-i-fRHg_H.js +0 -2
- package/dist/serveCommand-Cl8sicD5.js +0 -1162
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +0 -22
|
@@ -42,7 +42,7 @@ const { runId } = await run.run({})
|
|
|
42
42
|
| External side effects that need retry/resume semantics | Workflow |
|
|
43
43
|
| Human approval or webhook callback in the middle of work | Workflow + `awaitSignal` |
|
|
44
44
|
| External command that must validate and return a result | Workflow + `awaitUpdate` |
|
|
45
|
-
| Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.
|
|
45
|
+
| Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.publish(...)` |
|
|
46
46
|
| One-off durable delay inside an operation | Workflow + `sleep` |
|
|
47
47
|
| Recurring job such as "daily at 3am" | [Schedule](/docs/scheduling/overview) (`*.cron.tsx`) |
|
|
48
48
|
| Atomic database write | Mutation |
|
|
@@ -225,7 +225,7 @@ export default triggerWorkflow<{ userId: string; plan: string }, { userId: strin
|
|
|
225
225
|
Then emit the event from a mutation, action, incoming webhook, schedule, or workflow:
|
|
226
226
|
|
|
227
227
|
```ts
|
|
228
|
-
|
|
228
|
+
yield* ctx.events.publish(userSignedUp, { userId }, { plan })
|
|
229
229
|
```
|
|
230
230
|
|
|
231
231
|
Voltro records the event in `_voltro_workflow_events`, records each trigger delivery in `_voltro_workflow_event_deliveries`, and starts matching workflows with `source: event:<name>`. Emits inside mutations are post-commit safe: if the mutation rolls back, the workflow fan-out is not launched.
|
|
@@ -235,7 +235,7 @@ Use `event: '*'` for audit-style wildcard triggers that should see every emitted
|
|
|
235
235
|
## Runtime boundaries
|
|
236
236
|
|
|
237
237
|
- The workflow executor receives an `AppContext` built for the run. Starts from RPC handlers, mutations, actions, schedules, verified incoming handlers, and inspect tooling record a `source` such as `workflow-rpc`, `app-context`, `schedule:<name>`, `incoming:<id>`, or `inspect`. Request starts also carry the starter trace; authenticated request starts carry the resolved subject into the run record and executor context.
|
|
238
|
-
- `ctx.events.
|
|
238
|
+
- `ctx.events.publish(...)` is available when the app declares workflow event triggers. Triggered workflow starts carry the starter subject/trace and a `source` of `event:<name>`.
|
|
239
239
|
- Put tenant/user ids that the workflow must enforce into the payload. That keeps business authorization deterministic across retries, resumes, and future cross-replica handoff.
|
|
240
240
|
- Child workflows should be started with `ctx.workflows.child(...)`. Parent lineage and parent-close policy are persisted before the child start, so they survive cross-runner execution.
|
|
241
241
|
- External incoming handlers created with `defineIncomingWebhook(...)` receive `context.workflows`, so a verified webhook can start, signal, or update a workflow after signature and idempotency checks.
|
|
@@ -474,6 +474,36 @@ Workflow starts persist the starter subject, trace id, source, parent execution
|
|
|
474
474
|
|
|
475
475
|
Still include tenant/user ids that the business process must enforce in `payload`, validate them in the first step, and scope store reads/writes deliberately. Payload data is replay-safe and makes authorization decisions auditable across retries and deploys.
|
|
476
476
|
|
|
477
|
+
## Calling an HTTP API from a step
|
|
478
|
+
|
|
479
|
+
The framework's `HttpClient` is available inside a workflow executor — the same one handlers `yield*`, with the same SSRF allowlist and the same automatic `traceparent` propagation. `yield*` it in a step:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { HttpClient, HttpClientRequest } from '@effect/platform'
|
|
483
|
+
|
|
484
|
+
export const executor = defineWorkflowExecutor(syncInvoice, (payload) =>
|
|
485
|
+
Effect.gen(function* () {
|
|
486
|
+
const remote = yield* step({ name: 'fetch-invoice' }, () =>
|
|
487
|
+
Effect.gen(function* () {
|
|
488
|
+
const client = yield* HttpClient.HttpClient
|
|
489
|
+
const res = yield* client.execute(
|
|
490
|
+
HttpClientRequest.get(`https://billing.example.com/invoices/${payload.invoiceId}`),
|
|
491
|
+
)
|
|
492
|
+
return yield* res.json
|
|
493
|
+
}),
|
|
494
|
+
)
|
|
495
|
+
yield* step({ name: 'persist' }, () => database.invoices.update(payload.invoiceId, remote))
|
|
496
|
+
}),
|
|
497
|
+
)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Two things follow from where it sits:
|
|
501
|
+
|
|
502
|
+
- **Wrap the call in a `step`.** The result is then checkpointed, so a retry or a resume after a deploy replays the recorded response instead of calling the remote again. A bare `yield*` outside a step re-issues the request on every replay — which for a payment capture or an email send is the difference between once and several times.
|
|
503
|
+
- **It is the SSRF-guarded client.** Requests to internal targets are refused by the same policy handlers get; a workflow is not a way around it. Configure the allowlist once in `app.config.ts` under `http` — dev and serve read the same key, so the policy cannot differ between them.
|
|
504
|
+
|
|
505
|
+
Reaching for `fetch` instead loses both: no allowlist, no trace propagation, and nothing tying the call to the step that made it.
|
|
506
|
+
|
|
477
507
|
## Anti-patterns
|
|
478
508
|
|
|
479
509
|
- **Using `input` in `workflow({...})`.** The current API is `payload`.
|
|
@@ -1152,7 +1182,7 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
|
|
|
1152
1182
|
| `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy. |
|
|
1153
1183
|
| `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
|
|
1154
1184
|
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1155
|
-
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.
|
|
1185
|
+
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
|
|
1156
1186
|
| `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
|
|
1157
1187
|
|
|
1158
1188
|
Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
|
|
@@ -1353,16 +1383,23 @@ Validation runs on the **decoded** value, so a `Schema.Date` payload accepts a
|
|
|
1353
1383
|
<!-- source: en/workflows/event-triggers.md -->
|
|
1354
1384
|
## Event triggers
|
|
1355
1385
|
|
|
1356
|
-
_defineEventTrigger — bind a domain event name to a workflow. ctx.events.
|
|
1386
|
+
_defineEventTrigger — bind a domain event name to a workflow. ctx.events.publish(descriptor, key, payload) fans the event out to every matching trigger with filter, payload-mapping, and idempotency._
|
|
1357
1387
|
|
|
1358
|
-
A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a
|
|
1388
|
+
A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a **declared event** to a **workflow name**, and any handler that publishes that event fans it out to every trigger listening for it. The emitter never names the workflow — "something happened" is separated from "run this workflow", so you add reactions without touching the code that emits.
|
|
1359
1389
|
|
|
1360
1390
|
```tsx
|
|
1391
|
+
// events/orders.event.ts — the declaration both sides share
|
|
1392
|
+
export const orderPaid = defineEvent({
|
|
1393
|
+
name: 'order.paid',
|
|
1394
|
+
key: Schema.Struct({ orderId: Schema.String }),
|
|
1395
|
+
payload: Schema.Struct({ total: Schema.Number, tenantId: Schema.String }),
|
|
1396
|
+
})
|
|
1397
|
+
|
|
1361
1398
|
// triggers/orderPaid.trigger.tsx
|
|
1362
1399
|
import { defineEventTrigger } from '@voltro/runtime'
|
|
1363
1400
|
|
|
1364
1401
|
export default defineEventTrigger({
|
|
1365
|
-
|
|
1402
|
+
on: orderPaid, // the declared event to listen for
|
|
1366
1403
|
workflow: 'fulfilment.run', // the workflow to start
|
|
1367
1404
|
// Optional: skip when the predicate returns false.
|
|
1368
1405
|
filter: (e) => e.data.total > 0,
|
|
@@ -1377,23 +1414,21 @@ Discovery walks every `*.trigger.tsx`; the `default` export must be a `defineEve
|
|
|
1377
1414
|
|
|
1378
1415
|
## Emitting an event
|
|
1379
1416
|
|
|
1380
|
-
From any handler
|
|
1417
|
+
From any handler with a `ctx`, publish the declared event:
|
|
1381
1418
|
|
|
1382
1419
|
```ts
|
|
1383
|
-
const execute =
|
|
1420
|
+
const execute = (input: { orderId: string }, ctx: AppContext) => Effect.gen(function* () {
|
|
1384
1421
|
// ... mark the order paid ...
|
|
1385
|
-
|
|
1386
|
-
orderId: input.orderId,
|
|
1387
|
-
tenantId: ctx.request.subject.tenantId,
|
|
1422
|
+
yield* ctx.events.publish(orderPaid, { orderId: input.orderId }, {
|
|
1388
1423
|
total: order.total,
|
|
1424
|
+
tenantId: ctx.request.subject.tenantId,
|
|
1389
1425
|
})
|
|
1390
|
-
|
|
1391
|
-
// result.triggered — one entry per matched trigger:
|
|
1392
|
-
// { triggerId, workflowName, status: 'started' | 'skipped' | 'failed', run?, reason? }
|
|
1393
|
-
}
|
|
1426
|
+
})
|
|
1394
1427
|
```
|
|
1395
1428
|
|
|
1396
|
-
`
|
|
1429
|
+
`publish` reaches BOTH audiences from one call: the durable one (this page — the event log plus every matching trigger) and the ephemeral one ([connected clients](/docs/data/events)). Inside a mutation both fire on COMMIT and neither on rollback, so a client and a triggered workflow cannot disagree about whether the thing happened.
|
|
1430
|
+
|
|
1431
|
+
`on:` reads the event NAME off the descriptor, so renaming the event moves the trigger with it. The older string form (`event: 'order.paid'`) was removed in 0.25.0 — with a string, a rename left the trigger matching nothing and the workflow simply never ran again, without an error.
|
|
1397
1432
|
|
|
1398
1433
|
The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
|
|
1399
1434
|
|
|
@@ -1409,11 +1444,30 @@ The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
|
|
|
1409
1444
|
- **Idempotency.** Before starting, the runtime checks for an existing delivery with the same `idempotencyKey` (default `<eventId>:<triggerId>`) — a duplicate is recorded as `status: 'skipped', reason: 'duplicate'` and the workflow is not started twice.
|
|
1410
1445
|
- **Every emit is audited.** The event itself lands in `_voltro_workflow_events`; one row per trigger lands in `_voltro_workflow_event_deliveries` with `status` (`starting → started`, or `skipped` / `failed`), the started `executionId`, and any error message. Both tables are reactive — the dashboard's Workflows view surfaces deliveries live.
|
|
1411
1446
|
|
|
1447
|
+
## Retention — and why the delivery TTL is not just housekeeping
|
|
1448
|
+
|
|
1449
|
+
Both audit tables are append-only, so both are swept on a **30-day** default by the
|
|
1450
|
+
boot retention GC (postgres): `VOLTRO_WORKFLOW_EVENTS_TTL_HOURS` and
|
|
1451
|
+
`VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS`. The delivery log grows faster — one
|
|
1452
|
+
row per *trigger*, so three triggers on one event write four rows per emit.
|
|
1453
|
+
|
|
1454
|
+
**The delivery TTL is the deduplication window.** The idempotency check above looks
|
|
1455
|
+
for an existing delivery row, so once a row is swept its key is no longer
|
|
1456
|
+
deduplicated. With the default key (`<eventId>:<triggerId>`, and `eventId` is fresh
|
|
1457
|
+
per emit) a duplicate cannot occur and this costs nothing. It matters only when you
|
|
1458
|
+
supply your own `idempotencyKey`: if your app can re-emit the *same* stable key
|
|
1459
|
+
(`fulfil:order-123`) more than 30 days apart and must still be deduplicated, raise
|
|
1460
|
+
`VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS` past that horizon.
|
|
1461
|
+
|
|
1462
|
+
Unlike `_voltro_outbox`, the delivery log is **not** status-filtered — a stale
|
|
1463
|
+
`starting` row has no requeue path and no reader, so keeping it would leave the
|
|
1464
|
+
table unbounded for exactly the rows a crash produces.
|
|
1465
|
+
|
|
1412
1466
|
## When to use it
|
|
1413
1467
|
|
|
1414
1468
|
Reach for an event trigger when ONE thing happening should fan out to several independent reactions, or when you want the emitter to stay ignorant of the consumers:
|
|
1415
1469
|
|
|
1416
|
-
- `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `
|
|
1417
|
-
- A mutation
|
|
1470
|
+
- `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `publish`.
|
|
1471
|
+
- A mutation publishes `user.signedUp`; an onboarding workflow trigger starts the drip sequence. The signup mutation never imports the onboarding workflow.
|
|
1418
1472
|
|
|
1419
|
-
If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling
|
|
1473
|
+
If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling publish from react, or fanning one event out to many.
|
|
@@ -5,22 +5,23 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"test": "voltro test",
|
|
9
10
|
"typecheck": "tsc --noEmit"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@effect/platform": "^0.97.0",
|
|
13
14
|
"@effect/rpc": "^0.76.0",
|
|
14
|
-
"@voltro/ai": "0.
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.25.0",
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/protocol": "0.25.0",
|
|
20
|
+
"@voltro/runtime": "0.25.0",
|
|
20
21
|
"effect": "^3.22.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.25.0",
|
|
24
25
|
"typescript": "^6.0.3",
|
|
25
26
|
"vitest": "^4.1.10"
|
|
26
27
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"test": "voltro test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
@@ -12,17 +13,17 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@effect/platform": "^0.97.0",
|
|
14
15
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-auth": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
21
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.25.0",
|
|
20
|
+
"@voltro/protocol": "0.25.0",
|
|
21
|
+
"@voltro/runtime": "0.25.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.25.0",
|
|
22
23
|
"effect": "^3.22.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.25.0",
|
|
26
27
|
"typescript": "^6.0.3",
|
|
27
28
|
"vitest": "^4.1.10"
|
|
28
29
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"test": "voltro test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
@@ -12,16 +13,16 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@effect/platform": "^0.97.0",
|
|
14
15
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
20
|
+
"@voltro/protocol": "0.25.0",
|
|
21
|
+
"@voltro/runtime": "0.25.0",
|
|
21
22
|
"effect": "^3.22.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.25.0",
|
|
25
26
|
"typescript": "^6.0.3",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"test": "voltro test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
@@ -12,16 +13,16 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@effect/platform": "^0.97.0",
|
|
14
15
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-deactivation": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.25.0",
|
|
20
|
+
"@voltro/protocol": "0.25.0",
|
|
21
|
+
"@voltro/runtime": "0.25.0",
|
|
21
22
|
"effect": "^3.22.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.25.0",
|
|
25
26
|
"typescript": "^6.0.3",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"typecheck": "tsc --noEmit",
|
|
10
11
|
"test": "voltro test"
|
|
@@ -12,18 +13,18 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@react-email/components": "^1.0.12",
|
|
14
15
|
"@react-email/render": "^1.4.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-mail": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-mail": "0.25.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
21
|
+
"@voltro/protocol": "0.25.0",
|
|
22
|
+
"@voltro/runtime": "0.25.0",
|
|
22
23
|
"effect": "^3.22.0",
|
|
23
24
|
"react": "^19.0.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.25.0",
|
|
27
28
|
"typescript": "^6.0.3",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"test": "voltro test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
@@ -12,18 +13,18 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@effect/platform": "^0.97.0",
|
|
14
15
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/plugin-storage": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-mysql": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
20
|
+
"@voltro/plugin-storage": "0.25.0",
|
|
21
|
+
"@voltro/protocol": "0.25.0",
|
|
22
|
+
"@voltro/runtime": "0.25.0",
|
|
23
|
+
"@voltro/sql-mysql": "0.25.0",
|
|
23
24
|
"effect": "^3.22.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.25.0",
|
|
27
28
|
"typescript": "^6.0.3",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|
|
@@ -5,22 +5,23 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"typecheck": "tsc --noEmit",
|
|
10
11
|
"test": "voltro test"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@voltro/cli": "0.
|
|
14
|
-
"@voltro/database": "0.
|
|
15
|
-
"@voltro/env": "0.
|
|
16
|
-
"@voltro/plugin-multitenancy": "0.
|
|
17
|
-
"@voltro/plugin-storage": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.25.0",
|
|
15
|
+
"@voltro/database": "0.25.0",
|
|
16
|
+
"@voltro/env": "0.25.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
18
|
+
"@voltro/plugin-storage": "0.25.0",
|
|
19
|
+
"@voltro/protocol": "0.25.0",
|
|
20
|
+
"@voltro/runtime": "0.25.0",
|
|
20
21
|
"effect": "^3.22.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.25.0",
|
|
24
25
|
"typescript": "^6.0.3",
|
|
25
26
|
"vitest": "^4.1.10"
|
|
26
27
|
}
|
|
@@ -5,23 +5,24 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"test": "voltro test",
|
|
9
10
|
"typecheck": "tsc --noEmit"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@effect/platform": "^0.97.0",
|
|
13
14
|
"@effect/rpc": "^0.76.0",
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-governance": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.25.0",
|
|
16
|
+
"@voltro/database": "0.25.0",
|
|
17
|
+
"@voltro/env": "0.25.0",
|
|
18
|
+
"@voltro/plugin-governance": "0.25.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
20
|
+
"@voltro/protocol": "0.25.0",
|
|
21
|
+
"@voltro/runtime": "0.25.0",
|
|
21
22
|
"effect": "^3.22.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.25.0",
|
|
25
26
|
"typescript": "^6.0.3",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
@@ -28,7 +28,8 @@ state + multi-instance cluster workflow runners.
|
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| `app.config.ts` | app config | `store: 'memory'`, zero-infra boot. |
|
|
30
30
|
| `database/schema.ts` | schema | core `actors`/`tenants` + an `orders` table (`tenant()` + `.reactive()`). |
|
|
31
|
-
| `mutations/orders.place.mutation.*` | mutation | insert an order
|
|
31
|
+
| `mutations/orders.place.mutation.*` | mutation | insert an order and **publish `order.placed`** on commit via `ctx.events.publish`. Tenant write-guard. |
|
|
32
|
+
| `events/orders.event.ts` | event | the declared `order.placed` — one value shared by the publisher, the trigger and any client. |
|
|
32
33
|
| `triggers/order.placed.trigger.tsx` | event trigger | maps event `order.placed` → **start** the `orders.fulfill` workflow. |
|
|
33
34
|
| `workflows/order.fulfill.workflow.*` | durable workflow | `step('reserve-stock')` (with **`withCompensation`** saga rollback) → `sleep` → **`awaitSignal('approval')`** human gate → `step('ship')` / mark rejected. |
|
|
34
35
|
| `mutations/orders.approve.mutation.*` | mutation | inject the `approval` signal into the parked run via `ctx.workflows.signal(...)`. |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The declared domain event. Browser-safe: the SAME value is imported by the
|
|
2
|
+
// mutation that publishes it, the trigger that reacts to it, and any client that
|
|
3
|
+
// wants to watch it live — which is what makes the payload type identical
|
|
4
|
+
// everywhere and a rename impossible to half-apply.
|
|
5
|
+
|
|
6
|
+
import { defineEvent } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const orderPlaced = defineEvent({
|
|
10
|
+
name: 'order.placed',
|
|
11
|
+
// The ROUTING key: which subscribers receive this. Only what decides
|
|
12
|
+
// delivery belongs here — the tenant comes from the subject, never the key.
|
|
13
|
+
key: Schema.Struct({ orderId: Schema.String }),
|
|
14
|
+
payload: Schema.Struct({
|
|
15
|
+
orderId: Schema.String,
|
|
16
|
+
tenantId: Schema.String,
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
// `orders.place` — EXECUTOR (server-only, default export).
|
|
2
2
|
//
|
|
3
|
-
// Inserts the order
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Inserts the order and publishes a declared domain event in the SAME
|
|
4
|
+
// transaction. `ctx.events.publish(...)` fires on COMMIT and not at all on
|
|
5
|
+
// rollback, so a rolled-back order can neither start a fulfillment workflow nor
|
|
6
|
+
// tell a connected client that it exists — you do not have to sequence the two
|
|
7
|
+
// by hand.
|
|
8
8
|
|
|
9
9
|
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
10
|
+
import { Effect } from 'effect'
|
|
10
11
|
import type { AppContext } from '@voltro/runtime'
|
|
12
|
+
import { orderPlaced } from '../events/orders.event'
|
|
11
13
|
|
|
12
|
-
const execute =
|
|
14
|
+
const execute = (
|
|
13
15
|
input: { tenantId: string; customerName: string; amountCents: number },
|
|
14
16
|
ctx: AppContext,
|
|
15
|
-
) => {
|
|
17
|
+
) => Effect.gen(function* () {
|
|
16
18
|
// Cross-tenant write guard — reject a caller spoofing input.tenantId.
|
|
17
19
|
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
18
20
|
|
|
19
21
|
// Framework auto-injects an `order_…` id + the tenant/audit columns.
|
|
20
|
-
const row =
|
|
22
|
+
const row = yield* Effect.promise(() => ctx.store.insert('orders', {
|
|
21
23
|
status: 'placed',
|
|
22
24
|
customerName: input.customerName,
|
|
23
25
|
amountCents: input.amountCents,
|
|
24
26
|
tenantId: input.tenantId,
|
|
25
|
-
})
|
|
27
|
+
}))
|
|
26
28
|
|
|
27
|
-
//
|
|
28
|
-
// `orders.fulfill` workflow
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
29
|
+
// One publish, both audiences: the `order.placed` trigger starts the
|
|
30
|
+
// `orders.fulfill` workflow, and any client watching this order sees it live.
|
|
31
|
+
// Both on the commit boundary — they cannot disagree about whether it
|
|
32
|
+
// happened.
|
|
33
|
+
yield* ctx.events!.publish(orderPlaced, { orderId: row['id'] as string }, {
|
|
32
34
|
orderId: row['id'] as string,
|
|
33
35
|
tenantId: input.tenantId,
|
|
34
36
|
})
|
|
@@ -40,6 +42,6 @@ const execute = async (
|
|
|
40
42
|
amountCents: row['amountCents'] as number,
|
|
41
43
|
tenantId: row['tenantId'] as string,
|
|
42
44
|
}
|
|
43
|
-
}
|
|
45
|
+
})
|
|
44
46
|
|
|
45
47
|
export default execute
|
|
@@ -5,23 +5,24 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"test": "voltro test",
|
|
9
10
|
"typecheck": "tsc --noEmit"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@effect/platform": "^0.97.0",
|
|
13
14
|
"@effect/rpc": "^0.76.0",
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
20
|
-
"@voltro/workflow": "0.
|
|
15
|
+
"@voltro/cli": "0.25.0",
|
|
16
|
+
"@voltro/database": "0.25.0",
|
|
17
|
+
"@voltro/env": "0.25.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
19
|
+
"@voltro/protocol": "0.25.0",
|
|
20
|
+
"@voltro/runtime": "0.25.0",
|
|
21
|
+
"@voltro/workflow": "0.25.0",
|
|
21
22
|
"effect": "^3.22.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.25.0",
|
|
25
26
|
"typescript": "^6.0.3",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Event → workflow trigger. Auto-discovered (`*.trigger.tsx`); no
|
|
2
2
|
// app.config wiring needed.
|
|
3
3
|
//
|
|
4
|
-
// When any handler
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// When any handler publishes `order.placed` (the `orders.place` mutation does,
|
|
5
|
+
// on commit), this trigger starts the `orders.fulfill` workflow. It binds the
|
|
6
|
+
// DECLARED event rather than its name, so renaming the event moves this file
|
|
7
|
+
// with it instead of silently matching nothing. The event payload IS the workflow payload here (both are
|
|
7
8
|
// `{ orderId, tenantId }`), so no `payload` mapper is needed — pass one
|
|
8
9
|
// when the shapes differ.
|
|
9
10
|
//
|
|
@@ -11,11 +12,12 @@
|
|
|
11
12
|
// (crash + replay), the matching key short-circuits the second start.
|
|
12
13
|
|
|
13
14
|
import { defineEventTrigger } from '@voltro/runtime'
|
|
15
|
+
import { orderPlaced } from '../events/orders.event'
|
|
14
16
|
|
|
15
17
|
export default defineEventTrigger<
|
|
16
18
|
{ orderId: string; tenantId: string }
|
|
17
19
|
>({
|
|
18
|
-
|
|
20
|
+
on: orderPlaced,
|
|
19
21
|
workflow: 'orders.fulfill',
|
|
20
22
|
idempotencyKey: (event) => `order.placed:${event.data.orderId}`,
|
|
21
23
|
})
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
8
9
|
"migrate": "voltro migrate",
|
|
9
10
|
"test": "voltro test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
@@ -12,18 +13,18 @@
|
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@effect/platform": "^0.97.0",
|
|
14
15
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-flags": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.25.0",
|
|
17
|
+
"@voltro/database": "0.25.0",
|
|
18
|
+
"@voltro/env": "0.25.0",
|
|
19
|
+
"@voltro/plugin-flags": "0.25.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.25.0",
|
|
21
|
+
"@voltro/protocol": "0.25.0",
|
|
22
|
+
"@voltro/runtime": "0.25.0",
|
|
23
|
+
"@voltro/sql-postgres": "0.25.0",
|
|
23
24
|
"effect": "^3.22.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.25.0",
|
|
27
28
|
"typescript": "^6.0.3",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|