@voltro/cli 0.12.0 → 0.14.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 +378 -0
- package/dist/{apiBuild-GefjNI5d.js → apiBuild-370ITNRk.js} +3 -3
- package/dist/apiBuild-DatWP5hh.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-BmNi1RgZ.js → commands-D-teL_TM.js} +3756 -2539
- package/dist/dbCommand-B3CeMU3u.js +2 -0
- package/dist/{dbCommand-DNrkb58g.js → dbCommand-Dmi-vHfA.js} +72 -57
- package/dist/{dev-d0LjACxE.js → dev-CKyW_LAv.js} +1511 -1331
- package/dist/dev-CPKbckaN.js +3 -0
- package/dist/fileConventions-COg0ZytD.js +9 -0
- package/dist/frameworkTableAssembly-DCLhpHsF.js +601 -0
- package/dist/frameworkTableAssembly-DYzb2elg.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-uEfIS1vp.js → inspectMetrics-CyVMeofr.js} +962 -943
- package/dist/seedRunner-D6eu-u5U.js +229 -0
- package/dist/serveCommand-CU3Sw4ZM.js +1127 -0
- package/dist/serveEntry.js +3 -3
- package/dist/{start-DKcQd52a.js → start-D8ATvTzo.js} +246 -246
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +77 -3
- package/templates/AGENTS.md +78 -4
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +2 -2
- package/templates/agent-docs/authentication.md +39 -0
- package/templates/agent-docs/cli.md +114 -6
- package/templates/agent-docs/configuration.md +48 -0
- package/templates/agent-docs/data.md +193 -1
- package/templates/agent-docs/database/hosting.md +2 -2
- package/templates/agent-docs/database/migrations.md +88 -3
- package/templates/agent-docs/database/overview.md +11 -0
- package/templates/agent-docs/database/seedsdialects.md +53 -1
- package/templates/agent-docs/database/transactions.md +31 -0
- package/templates/agent-docs/internationalization.md +3 -3
- package/templates/agent-docs/introduction.md +133 -2
- package/templates/agent-docs/plugins.md +2 -1
- package/templates/agent-docs/reference.md +4 -4
- package/templates/agent-docs/routing.md +48 -43
- package/templates/agent-docs/schema-driven-ui.md +12 -2
- package/templates/agent-docs/templates/apibackends.md +1 -8
- package/templates/agent-docs/templates/appshells.md +24 -24
- package/templates/agent-docs/testing.md +40 -1
- package/templates/agent-docs/whats-new.md +51 -131
- package/templates/apps/api-ai/database/schema.ts +0 -2
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/database/schema.ts +0 -3
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/database/schema.ts +0 -1
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +1 -1
- package/templates/apps/api-backend-mail/database/schema.ts +0 -3
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/database/schema.ts +0 -3
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/database/schema.ts +0 -3
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/database/authors.entity.ts +0 -2
- package/templates/apps/api-data-advanced/database/books.entity.ts +0 -1
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-data-advanced/tests/queries.test.ts +6 -1
- package/templates/apps/api-durable/database/schema.ts +0 -3
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/database/schema.ts +0 -1
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/app.config.ts +0 -1
- package/templates/apps/api-governance/database/schema.ts +0 -1
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/database/schema.ts +0 -3
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-kv/tests/sync.test.ts +2 -2
- package/templates/apps/api-moderation/database/schema.ts +0 -2
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/database/schema.ts +0 -1
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/database/schema.ts +0 -1
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/README.md +53 -14
- package/templates/apps/api-rbac/app.config.ts +24 -26
- package/templates/apps/api-rbac/authz.ts +76 -0
- package/templates/apps/api-rbac/database/schema.ts +22 -7
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +7 -5
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +15 -8
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.server.ts +54 -0
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.ts +43 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.server.ts +19 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.ts +37 -0
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rbac/queries/notes.list.query.server.ts +12 -0
- package/templates/apps/api-rbac/queries/notes.list.query.ts +26 -0
- package/templates/apps/api-rbac/template.json +2 -2
- package/templates/apps/api-rbac/tests/authz.test.ts +233 -0
- package/templates/apps/api-rest/database/schema.ts +0 -3
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/database/schema.ts +0 -1
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/database/schema.ts +0 -1
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/database/schema.ts +0 -1
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/database/schema.ts +0 -1
- package/templates/apps/api-webhooks/package.json +9 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/changelog/src/pages/[locale]/{[slug].tsx → [slug].page.tsx} +2 -2
- package/templates/apps/changelog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/changelog/src/pages/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
- package/templates/apps/changelog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-admin/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
- package/templates/apps/frontend-admin/src/pages/admin/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-app/src/pages/{schema-ui.test.tsx → schema-ui.page.test.tsx} +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-contact/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-contact/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.test.tsx → settings.page.test.tsx} +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].tsx → [...slug].page.tsx} +3 -3
- package/templates/apps/frontend-docs/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-docs/src/pages/docs/{[...slug].test.tsx → [...slug].page.test.tsx} +1 -1
- package/templates/apps/frontend-docs/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-i18n/src/pages/[locale]/{about.tsx → about.page.tsx} +2 -2
- package/templates/apps/frontend-i18n/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-i18n/src/pages/{about.test.tsx → about.page.test.tsx} +1 -1
- package/templates/apps/frontend-i18n/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-landing/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
- package/templates/apps/frontend-landing/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/pages/{feed-swr.test.tsx → feed-swr.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/src/pages/{feed.test.tsx → feed.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].tsx → [slug].page.tsx} +2 -2
- package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
- package/templates/apps/frontend-static-blog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/dist/apiBuild-s7swuEU5.js +0 -2
- package/dist/dbCommand-Bt6BCG7g.js +0 -2
- package/dist/dev-YHbHmeXt.js +0 -3
- package/dist/seedRunner-DZmhoqNt.js +0 -821
- package/dist/serveCommand-B2-YV9TU.js +0 -1112
- package/templates/apps/api-rbac/tests/notes.create.test.ts +0 -80
- /package/templates/apps/changelog/src/pages/{[slug].tsx → [slug].page.tsx} +0 -0
- /package/templates/apps/changelog/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/admin/{[entity].tsx → [entity].page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/admin/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-app/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-app/src/pages/{schema-ui.tsx → schema-ui.page.tsx} +0 -0
- /package/templates/apps/frontend-blank/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-contact/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.tsx → settings.page.tsx} +0 -0
- /package/templates/apps/frontend-docs/src/pages/docs/{[...slug].tsx → [...slug].page.tsx} +0 -0
- /package/templates/apps/frontend-docs/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-i18n/src/pages/{about.tsx → about.page.tsx} +0 -0
- /package/templates/apps/frontend-i18n/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-landing/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-spa/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{feed-swr.tsx → feed-swr.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{feed.tsx → feed.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr-api/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].tsx → [slug].page.tsx} +0 -0
- /package/templates/apps/frontend-static-blog/src/pages/{index.tsx → index.page.tsx} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# What's new in 0.
|
|
1
|
+
# What's new in 0.13.0
|
|
2
2
|
|
|
3
3
|
Read this FIRST when a task touches an area you have not worked in recently.
|
|
4
4
|
It is the cheapest way to notice that the framework grew the thing you were
|
|
@@ -7,171 +7,91 @@ workaround for something that shipped two versions ago.
|
|
|
7
7
|
|
|
8
8
|
BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### ⚠ BREAKING
|
|
11
11
|
|
|
12
|
-
- **@voltro/protocol
|
|
12
|
+
- **@voltro/protocol, @voltro/plugin-scim, @voltro/plugin-prometheus** — SCIM was served UNAUTHENTICATED whenever its token was an empty string.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
resolveKey: async (hash) => {
|
|
16
|
-
const row = await findKey(hash)
|
|
17
|
-
return row && { ...row, metadata: { keyType: row.keyType, teamId: row.teamId } }
|
|
18
|
-
}
|
|
19
|
-
// a guard then reads subject.metadata.teamId — no second query
|
|
20
|
-
```
|
|
14
|
+
`checkBearer(headers, expected)` returned `true` when `expected` was unset or empty, documented as "no token configured = open; the caller decided not to gate this surface". Its one production caller had decided the opposite: `scimPlugin` declares `token: string`, and `scimPlugin({ token: process.env.SCIM_TOKEN ?? '' })` — the shape anyone writes — turned the gate off silently. The result was SCIM 2.0 Users and Groups readable with no credentials: a full directory dump plus the provisioning surface that can deactivate accounts. Likeliest exactly where it hurts, too: an env var set in production and missing in a preview environment.
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
- **@voltro/
|
|
24
|
-
- **@voltro/runtime** — `crud.list(table, { columns })` — SQL column projection, so a wide column a list view never shows is **never read**, not merely dropped at the wire boundary:
|
|
16
|
+
`checkBearer` is now fail-closed by default, with the permissive behaviour available as an explicit `{ openWhenUnset: true }` — a two-argument helper cannot know its caller's intent, so it must not assume the permissive one. `@voltro/plugin-prometheus` passes it (its token is documented as optional), and `scimPlugin` now throws at construction — i.e. at boot — rather than answering the first anonymous request.
|
|
17
|
+
- **@voltro/database, @voltro/cli** — `voltro db apply` and boot auto-migrate could report success while applying nothing, and then record a fingerprint that made every later boot short-circuit on "schema up to date".
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
export default crud.list('articles', { columns: ['id', 'title', 'createdAt'] })
|
|
28
|
-
// the large `body` / json blob is never SELECTed, transferred, or decoded
|
|
29
|
-
```
|
|
19
|
+
Reported from a live pod: `applied 31 op(s)` on every boot for two releases, with none of the 31 present in the database. Nothing was wrong with the transport, the lock or the transaction — the applier emitted statements that postgres accepted and that changed nothing. Two independent causes:
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
- A `ColumnSnapshot` carried no `vector` dimension / `array` element / `enum` name, so the applier's type renderers collapsed all three to `text`. A declared `vector(1536)` over a live `text` column planned an `alter-column-type` that emitted `ALTER COLUMN … TYPE text`. Valid, applied, no-op, re-planned forever. (Also meant an `add-column` for a vector, array, enum or PostGIS column created a plain `text` column.) - The default-clause renderers excluded ARRAYS, returning `null`, and the call site turned that into `SET DEFAULT NULL`. A declared `.default([])` on a `json()` column therefore never landed — thirty columns were stuck this way in the reporting schema.
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
Fixed: the snapshot carries the type parameters and the renderers delegate to `migrate.ts`'s canonical `sqlType`, so the applier and the CREATE-TABLE emitter cannot disagree; array defaults render (a real `text[]` literal on a native `array()` column, a jsonb literal otherwise); and a default the renderer cannot express now FAILS instead of degrading to `DEFAULT NULL`.
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
- **@voltro/
|
|
25
|
+
And the structural guard, which is the part that matters: **`applyPlan` re-plans against the live schema before it records a fingerprint, and refuses to record one if any operation remains.** DDL that changes nothing succeeds exactly as quietly as DDL that works, so the only evidence a plan applied is that the same planner has nothing left to do. `ApplyPlanCtx` gains a required `replan`; `AppliedMigration` gains `appliedOps` (what EXECUTED, not `plan.operations.length`), and the boot log quotes that.
|
|
26
|
+
- **@voltro/plugin-storage** — `storage.share`, `storage.revoke` and `storage.listGrants` performed no authorization at all.
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
export default crud.list('absenceRequests', {
|
|
40
|
-
filter: (input) => ({ employeeId: input.employeeId, status: input.status }), // → WHERE
|
|
41
|
-
paginate: true, // input.limit / input.offset (100 / 0)
|
|
42
|
-
sort: [{ column: 'createdAt', direction: 'desc' }], // multi-column
|
|
43
|
-
include: { employee: { with: { team: true } } }, // eager relations, nested filter/sort
|
|
44
|
-
redact: ['internalNote'],
|
|
45
|
-
})
|
|
46
|
-
```
|
|
28
|
+
Each took an object id straight off the wire and passed it to a service method that (correctly, for a trusted server-side API) checks nothing, with nothing in between. Any authenticated caller could grant themselves read or write on any object in the installation, revoke anyone else's grants, and enumerate who an object is shared with.
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
All optional and additive: `CrudListOptions extends CrudReadOptions`, so a bare `crud.list('t')` or `crud.list('t', { redact })` is unchanged. The descriptor's `input` schema declares the filter/pagination fields, and its `output` schema stays hand-written (deriving the projection from the output schema is a codegen concern — a table value can't enter a browser-loaded descriptor).
|
|
51
|
-
- **@voltro/runtime** — `crud.list`'s `paginate` now accepts **page-based** paging beside offset-based, and `crud.count` gives a page-based UI the total it needs:
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
// list: ?page=3&pageSize=20 (or ?limit=20&offset=40 — both work)
|
|
55
|
-
export default crud.list('absenceRequests', { paginate: true, filter, sort })
|
|
56
|
-
// total for "page 3 of 12"
|
|
57
|
-
export default crud.count('absenceRequests', { filter })
|
|
58
|
-
```
|
|
30
|
+
All three now require that the caller owns the object, or carries `admin:full`. A missing object and an unowned object report the same 403 — a 404 would let an unauthorized caller probe which ids exist. `GrantStore` gains `getById`, which `revoke` needs to resolve a grant id back to its object.
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Additive: paging style is detected from the request input, so an existing `paginate: true` list is unchanged.
|
|
63
|
-
- **@voltro/cli** — **`voltro inspect schedules --failing`** — roll up each schedule's recent runs and report only the broken ones, exiting **1** when anything is failing.
|
|
32
|
+
### Added
|
|
64
33
|
|
|
65
|
-
|
|
34
|
+
- **@voltro/runtime, @voltro/database** — API keys carry app-owned `metadata` — the second ownership axis.
|
|
66
35
|
|
|
67
|
-
|
|
36
|
+
`tenantId` and `onBehalfOf` are the two relationships the framework models. Plenty of apps have a third that actually authorizes the key: a team, a project, an environment. `ApiKeyRecord` in `@voltro/protocol` has carried a `metadata` slot all along — its doc comment even names `teamId` as the example — but the SERVICE had nowhere to store it and nowhere to return it. So an app with a team axis could authenticate through the built-in strategy and still not authorize, and `apiKeys: true` was unusable for it. Reported as the one thing that stopped an otherwise complete adoption; their alternatives were a second table joined on the hot auth path, or smuggling `team:<id>` into `scopes`, where `hasScope` would then see a scope that is not a scope.
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
- **@voltro/runtime** — `ctx.store.links(junction, anchor).setRows(rows)` — a diff-based reconcile for a many-to-many junction that carries PER-ROW PAYLOAD (a membership `role`, a `capacity` value), the case `set(targetIds)` couldn't model. Each row is `{ [targetColumn]: id, …payload }`; the diff is on the (source, target) pair — an added row is inserted with its payload, a removed row deleted, and a SURVIVING row whose payload actually changed is UPDATED. A row whose payload is unchanged is left untouched, so a reactive consumer sees a change only where the payload differs — the drop+reinsert replacement for a data-carrying junction. Returns `{ added, removed, updated }`. Payload is compared by strict per-column equality (scalars). Additive: a new `setRows` method on `JunctionLinks`.
|
|
71
|
-
- **@voltro/runtime, @voltro/cli** — **A query can now be projected to a public REST endpoint.** `publicApi` on a descriptor was only ever mounted for mutations and actions — a QUERY carrying it produced no route at all, silently. Both boot paths now mount queries too, so offering an API you don't consume from your own frontend is a one-line annotation:
|
|
38
|
+
`IssueInput`, `ApiKeyRow` and `ResolvedApiKey` now carry it, stored as JSON on `_voltro_api_keys`, and it survives `rotate` — a rotated key is the same credential with a new secret, so dropping it would silently de-authorize every rotated key.
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
export default defineQuery({
|
|
75
|
-
name: 'absenceRequests.list',
|
|
76
|
-
input: Schema.Struct({ status: Schema.optional(Schema.String), limit: Schema.optional(Schema.Number) }),
|
|
77
|
-
output: Schema.Array(AbsenceRequest),
|
|
78
|
-
guards: [requireScope('absences:read')],
|
|
79
|
-
publicApi: {}, // → GET /v1/absenceRequests/list?status=open&limit=20
|
|
80
|
-
})
|
|
81
|
-
```
|
|
40
|
+
It is app data, never identity. The strategy merges it UNDER the framework's own claims: `provider` and the acting `userId` are written afterwards from `onBehalfOf` and always win, including when the answer is "none". A bag that could set `userId` would let whoever minted the key choose who the request is. Pinned end-to-end, not just at the protocol layer.
|
|
82
41
|
|
|
83
|
-
|
|
42
|
+
`PublicApiKey` also gains `createdBy` and `onBehalfOf`, so `service.list` can answer the two questions an admin actually asks about a shared credential. Neither is a secret — they are the accountability record, and omitting them hid them from the person responsible for the key.
|
|
43
|
+
- **@voltro/protocol, @voltro/cli** — A boot warning when two auth strategies claim the same bearer-token prefix.
|
|
84
44
|
|
|
85
|
-
The
|
|
86
|
-
- **@voltro/protocol, @voltro/runtime, @voltro/cli** — **`publicApi: { stream: 'sse' }` on a query now streams.** The field was declared but unimplemented — a query annotated with it silently served the first snapshot as JSON. It now mounts a Server-Sent-Events endpoint: the initial `snapshot`, then a `delta` per change, until the client disconnects.
|
|
45
|
+
The chain is first-match-wins, so a duplicate claim is not a harmless redundancy: whichever strategy runs first decides the Subject. An app that already has its own `sk_` keys and then sets `apiKeys: true` gets the framework strategy appended on the same prefix — resolving without the app's own team binding — and *which strategy answered* decides whether authorization works. Reported by an app that had to pin a test asserting it never enables the flag.
|
|
87
46
|
|
|
88
|
-
|
|
89
|
-
export default defineQuery({
|
|
90
|
-
name: 'orders.live',
|
|
91
|
-
input: Schema.Struct({ status: Schema.optional(Schema.String) }),
|
|
92
|
-
output: Schema.Array(Order),
|
|
93
|
-
guards: [requireScope('orders:read')],
|
|
94
|
-
publicApi: { stream: 'sse' }, // → GET /v1/orders/live?status=open (text/event-stream)
|
|
95
|
-
})
|
|
96
|
-
```
|
|
47
|
+
`AuthStrategy` gains an optional `claimsBearerPrefix`, set by `apiKeyStrategy` from its `prefix` option. Making the claim declarative is what makes the collision detectable at all — the same "only what is declared can be checked" argument the scope rules run on. Checked in `buildResolveSubject`, which both `voltro dev` and `voltro serve` call, so the two boot paths cannot drift.
|
|
97
48
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
es.addEventListener('snapshot', (e) => setRows(JSON.parse(e.data).data))
|
|
101
|
-
es.addEventListener('delta', (e) => applyDelta(JSON.parse(e.data)))
|
|
102
|
-
```
|
|
49
|
+
A warning rather than a refusal: two strategies on one prefix can be deliberate (a migration window where old and new keys share a shape). What must not happen is that it goes unmentioned.
|
|
50
|
+
- **@voltro/protocol, @voltro/cli** — `auth.resolveScopes` — add scopes to an authenticated Subject from your own data, so ROLE-based authorization becomes declarable.
|
|
103
51
|
|
|
104
|
-
|
|
52
|
+
An app whose authorization is a database role (`requireCallerAdmin(ctx)` reading an `employees.role` column) is invisible to every static check the framework has: `voltro check`'s `rbac/unguarded-mutation` reports its writes as unguarded, and it is right to — nothing about the decision is declared. But the declarative alternative was unusable for exactly those apps: their subjects come from an external IdP's JWTs and carry no scopes, so `requireScope('employee:admin')` would lock out every real user. One app measured 1566 findings it had no way to act on.
|
|
105
53
|
|
|
106
|
-
|
|
54
|
+
Lifting the role into `subject.scopes` makes the SAME authorization declarable, visible in the manifest and checkable in CI. Deliberately narrow: the hook returns SCOPES, never a Subject — it cannot change `id` or `tenantId` (identity belongs to the auth strategy), and the result is unioned with the strategy's own scopes, so it can grant but never revoke. It runs per matched request, so cache the lookup yourself; the framework does not, because only the app knows how fast a role change must take effect. Wired identically in `voltro dev` and `voltro serve`.
|
|
55
|
+
- **@voltro/cli** — `voltro doctor` reports packages resolved at more than one version.
|
|
107
56
|
|
|
108
|
-
|
|
109
|
-
- **@voltro/runtime, @voltro/cli** — Boot audit for the `.serverOnly()` marker (#22): `voltro dev` now warns, naming the query + column, when a wire-reachable query's `output` schema DECLARES a `.serverOnly()` column of its `source` table. The `crud.*` read helpers strip these automatically, but a hand-written output can only be caught here — this is what would have flagged the reported dead `apiKeys.getByKeyId` query that shipped a `keyHash`.
|
|
57
|
+
A consumer reported type errors inside the GENERATED `rpcGroup.generated.ts` — `Property '[TypeId]' is missing`, `typeof Never is not assignable to All`, an `Rpc<…, Stream<…>, …>` refused where `Any` was expected — and reasonably concluded the framework emits bad types, because the errors land in a file they cannot edit and did not write. That is the signature of two copies of `effect` in one install: Effect's types are nominal, so a Schema built by one copy is not the type the other expects.
|
|
110
58
|
|
|
111
|
-
|
|
112
|
-
- **@voltro/
|
|
59
|
+
It deserves its own check because the RUNTIME usually stays green — two instances only diverge where identity matters — so an app boots, serves and passes its tests while `tsc` is red, which sends people looking at the compiler instead of the dependency tree. The report names the versions, the paths, and the errors it explains. Only identity-sensitive packages count (`effect`, `@effect/*`, `@voltro/*`, react/react-dom); a duplicated string utility is wasteful, not a bug class.
|
|
60
|
+
- **@voltro/cli** — `voltro doctor` flags an executor that never names its own descriptor.
|
|
113
61
|
|
|
114
|
-
|
|
115
|
-
keyHash: text().serverOnly(), // an auth middleware verifies it; a client never sees it
|
|
116
|
-
apiToken: text().encrypted().serverOnly(), // a column can carry both axes, or either
|
|
117
|
-
```
|
|
62
|
+
Descriptor/executor pairing is by FILENAME, which is right — and it means a `*.server.ts` can be a complete, correct executor with no reference at all to the contract it implements. Those are exactly the files where a hand-written input drifts from the wire.
|
|
118
63
|
|
|
119
|
-
|
|
64
|
+
Reported after a 426-executor migration to `ExecutorInput<typeof descriptor>`: three files were skipped by the app's own codemod for a reason no reviewer would guess — they never imported their descriptor, so there was no `typeof` to point at. In the same codebase, six executors had written `boardPurpose: string` where their descriptor declared `Schema.Literal(...)`, discarding the contract at the executor boundary. Only imports of a SIBLING module clear the finding: an executor importing nothing but `@voltro/*` and `node:*` has still not named its contract.
|
|
65
|
+
- **@voltro/database** — `updateManyRow(store, table, patch, { where })` — the last untyped write is now typed against its table.
|
|
120
66
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Additive: a `serverOnly()` builder method + a `serverOnly?` flag on `ColumnDefinition` + the `serverOnlyColumns` helper. Enforcement beyond the `crud.*` path — a runtime strip at the rpc wire boundary and a `serverOnly: true` whole-query primitive — is a planned follow-on (see `plans/framework-serverOnly-exposure.md`).
|
|
67
|
+
`insertRow` and `upsertRow` already were; `ctx.store.updateMany(table, row, { where })` still took a string table name and an untyped row literal. Worth closing because the typed versions were measured: migrating 29 `store.upsert` call sites to `upsertRow` produced 15 `tsc` errors across 8 distinct defects that no test had caught — including seven per-user mutations with no authentication check at all (they wrote `ctx.request.subject.id`, typed `string | null`, into a NOT NULL column, so an anonymous caller reached the database and got a raw statement failure instead of a typed refusal).
|
|
124
68
|
|
|
125
69
|
### Fixed
|
|
126
70
|
|
|
127
|
-
- **@voltro/
|
|
128
|
-
- **@voltro/cli** — `voltro version` now prints the real installed version (was a hardcoded `v0.0.0 (scaffold)` placeholder); usage / unknown-command / version output say "voltro" instead of "framework". `voltro <command> --help` (and `-h`) now prints the command summary and returns 0 for EVERY command instead of, in several cases, running the command — `voltro env --help` used to run the env check and could exit 1. Commands with their own richer help (inspect/logs/doctor/secret) still show it. Honest command summaries: `db` now lists its declarative-workflow subcommands (plan/apply/plans/drift/squash/restore-snapshot/files/…) and drops the never-implemented `generate`; `serverless` lists `dev`/`serve` and the `node` default target; `workflows` lists `start`. `voltro cloud deploy`/`rollback`, which are not yet implemented, now fail (non-zero, stderr) instead of printing a message and returning success, and are marked "coming soon" in help.
|
|
129
|
-
- **@voltro/cli** — Security: the inspect API's CORS now reflects the request `Origin` and allows credentials ONLY for loopback origins (localhost / 127.0.0.0/8 / ::1). Previously it echoed ANY origin with `access-control-allow-credentials: true`, so a website a developer visited could issue a credentialed cross-origin `fetch` and read the (default-open) inspect surface — DB rows, logs, schema, drift — and POST to fire schedules / start workflows. A non-loopback origin now gets no `access-control-allow-origin`, so the browser blocks the read; curl / same-origin / the local dashboard are unaffected. Also: the dashboard proxy (`/api/dashboard/proxy?target=`) now requires a loopback target (it forwards the caller's bearer, so an arbitrary target was an SSRF + bearer-harvest), and `~/.voltro/credentials.json` is written 0600 in a 0700 dir instead of world-readable.
|
|
130
|
-
- **@voltro/cli** — `voltro serverless serve`/`dev` now exits non-zero when the server fails to bind or crashes at boot (it returned 0 = success, so a CI/deploy wrapper read a dead server as up). The production `voltro serve` shutdown chain gained a terminal `.catch` + a 10s force-exit safety net, so a rejected/hung drain step (plugin deactivate, pool close) no longer strands the process until the container's SIGKILL grace timer. `voltro e2e` cleanup now escalates SIGTERM→SIGKILL for a child that ignores SIGTERM (was lingering on :4000/:5190 and EADDRINUSE-ing the next run). `voltro migrate` now warns when it falls back to the default `localhost:5432` DB with no DB_URL/DB_HOST set.
|
|
131
|
-
- **@voltro/cli** — CLI flag parsing is now shared (`src/cliArgs.ts`: `flagValue` / `takeFlag` / `hasFlag` / `positionals`) instead of each command hand-rolling its own `indexOf('--x'); args[i+1]`. Adopted across data, serve, cloud, db (14 sites), baseline, and secret — so every command accepts `--name value` AND `--name=value` uniformly. Group-command exit codes are standardized too: an unknown subcommand exits 2, a missing subcommand exits 1 (was a mix of 0/1/1-vs-1 including a dead `? 1 : 1` ternary in storage).
|
|
132
|
-
- **@voltro/runtime** — **`crud.list` now caps the page size (default 1000).** The page size is CALLER-controlled — `input.limit` / `input.pageSize` — and nothing anywhere clamped it, so `?limit=1000000000` was a one-request read of the whole table. That was already unwelcome over the WebSocket; it became a genuine exposure the moment a query could be projected to a public REST endpoint (`publicApi`), where the caller is anyone who can reach the URL.
|
|
133
|
-
|
|
134
|
-
- `limit` / `pageSize` above the cap are **clamped, not rejected** (a caller asking for too much gets the maximum page, not a 400). - `maxPageSize` raises it deliberately for an export-style endpoint. - A zero/negative `limit` clamps to 1 row, and a negative `offset` / `page < 1` clamps to 0 rather than producing a negative OFFSET (which dialects reject or treat oddly). Non-integers are floored.
|
|
135
|
-
|
|
136
|
-
Found by auditing the paging code introduced in this same series — the cap was missing from the start, so this is a fix, not a behaviour change anyone relied on.
|
|
137
|
-
- **@voltro/plugin-ai-flows** — **An AI-flow `MediaGenerator` can now require services.** Its type pinned the Effect requirement channel to `never`, so the common implementation — generate, then persist through `StorageService` — was untypeable while working perfectly at runtime. A type that forbids what the program does is a type lying about the program; the host was left carrying a documented cast.
|
|
138
|
-
|
|
139
|
-
`MediaGenerator` and its `EngineDeps` siblings (`resolveAgent`, `onEvent`, `memoryPrefix`) now declare `R = unknown`, the same shape `@voltro/ai` uses for tool bodies (`execute?: (input) => Effect<O, never, unknown>`). The engine runs inside the host's runtime, which HAS those services, and states that fact once in a `callDep` bridge rather than at every call site — mirroring `callBody` in `@voltro/ai`.
|
|
140
|
-
|
|
141
|
-
`apiSurface: compatible` — the change WIDENS the requirement channel on callbacks the app IMPLEMENTS. An existing implementation that requires nothing (`Effect<A, E, never>`) stays assignable to the widened type, so no downstream implementation breaks; the only consumer of the narrow form was the engine itself, which now bridges it. Verified by a full repo typecheck.
|
|
142
|
-
- **@voltro/cli** — **Four discovered conventions were missing from the serve bundle — a prod-only boot crash waiting for an app to use them.** `voltro build` bundles the modules matching `API_ENTRY_PATTERN`; `voltro serve` resolves every app module from that bundle, and a convention the pattern misses falls back to loading `.ts` SOURCE, which a plain-node boot cannot do. `*.outbox.ts`, `*.connection.ts`, `*.email.tsx` and `*.migration.ts` were all absent — and all four are loaded at serve time (`serveCommand` filters outbox handlers explicitly, noting that without them the transactional outbox enqueues rows in production that nothing delivers). Fixed, and the lockstep is now a TEST (`apiEntryPatternLockstep.test.ts`) that asserts a representative filename for every convention matches — it found these four the moment it was written.
|
|
143
|
-
|
|
144
|
-
**The file conventions are now single-sourced** (`fileConventions.ts`). They were declared in five modules — dev discovery, plugin codegen, framework-table assembly, migrate, the db command — and copies of a rule that IS the rule drift silently, because each copy is internally consistent. `WORKFLOW_PATTERN` had already drifted into two shapes: strict `\.workflow\.tsx$` in discovery, loose `\.workflow\.tsx?$` in table assembly. So a file named `orders.workflow.ts` got `_voltro_workflow_*` TABLES (the loose copies counted it) but was never registered as a workflow (the strict copy skipped it) — no error, no warning, just a workflow that did nothing.
|
|
71
|
+
- **@voltro/runtime** — A `cache:` declared on a query whose handler returns a COMPUTED value was silently ignored; it now says so.
|
|
145
72
|
|
|
146
|
-
The
|
|
147
|
-
- **@voltro/
|
|
73
|
+
The snapshot cache wraps the store read, and a computed query has none — its handler has already run by the time the binding is built. Caching one would mean wrapping the handler invocation, which is a different feature. Until that exists, the honest failure is a loud one: silently ignoring the config is how an author ends up believing a hot query is cached while every subscriber re-runs it. The data stays correct, so nothing else would ever tell them. Warned once per query name, not per subscribe.
|
|
74
|
+
- **@voltro/cli** — The minted `.env.local` is handed to the workspace's owner, and an unreadable env file explains itself.
|
|
148
75
|
|
|
149
|
-
|
|
76
|
+
A dev container running as root with the host workspace bind-mounted wrote `apps/api/.env.local` as `root:root 0600` INTO THE SHARED WORKSPACE. On the host, everything that loads env then died with EACCES — vitest, `voltro doctor`, the editor — and the developer could not even read the file, while the next container boot recreated it. Container-with-bind-mount is the ordinary dev shape, not an edge case.
|
|
150
77
|
|
|
151
|
-
|
|
152
|
-
- **@voltro/
|
|
78
|
+
`0600` stays (the file holds a real signing key), because loosening it to `0644` would make that key readable by every account on the machine for the far more common single-user case. Ownership was the wrong variable, so that is the one corrected: the mint chowns the file to whoever owns the directory, which root can do — exactly the case that needs it — and reports loudly when it cannot. A plain EACCES while loading an env file now names the owning uid, the mode and the current uid, because that pair IS the diagnosis and none of it appears in node's message.
|
|
79
|
+
- **@voltro/cli** — Framework-generated output is handed to the workspace's owner, not left owned by whoever the process happens to be.
|
|
153
80
|
|
|
154
|
-
|
|
81
|
+
The previous release fixed this for the minted `.env.local`. The report that followed showed the scope was wrong: it is EVERY directory the framework generates. A dev pod running as root with the host monorepo bind-mounted leaves `.framework/` and `app.graph.observed.*` as `root:root` inside the developer's own tree, and on the host:
|
|
155
82
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`RestRouteDescriptor` gains `streaming?: boolean`, `publicApiRoute` sets it for a `stream: 'sse'` query, and the generator emits `text/event-stream` (with no JSON response schema) for such a route.
|
|
160
|
-
|
|
161
|
-
Worth stating as a rule, since it is the second time this shape has cost something: a fact that tooling must act on belongs on the DESCRIPTOR, not in the closure that implements it. A wrong spec is worse than a missing one — clients are generated from it.
|
|
162
|
-
- **@voltro/runtime, @voltro/cli** — **A failing schedule handler is now an ERROR, not a warn.** A schedule fires unattended — there is no user watching a request fail — so the log line IS the discovery channel. It was `warn`, which `voltro logs --level error` does not show, and the only other surface is a dashboard nobody has open on staging.
|
|
163
|
-
|
|
164
|
-
The root cause was structural rather than a bad choice: `SchedulerLogger` had no error channel at all (`info` / `warn` / optional `debug`), so the level was not expressible. It now has one — optional, so an embedder passing a two-method logger still compiles, falling back to `warn` — and BOTH boot paths (`voltro dev`, `voltro serve`) wire it, since a channel nothing supplies would have changed nothing.
|
|
165
|
-
|
|
166
|
-
Reported from an app where three nightly jobs had been dead since a port — one of them an entire feature that never wrote a single row — each failing on EVERY firing, for months, at `warn`. The failure was already recorded in `_voltro_schedule_runs` (`status: 'failed'`) and published on the server-error bus; the log level was the one place that disagreed with both.
|
|
167
|
-
- **@voltro/runtime** — **`ctx.workflows.start` now validates the payload against the workflow's schema, and says so when it doesn't match.** The signature is `(workflowName: string, payload: unknown)` — the name was not checked against the registry and the payload was not checked against anything — so a caller that drifted from the workflow's schema failed DEEP inside the engine, where the message reads like the workflow itself misbehaved.
|
|
83
|
+
```
|
|
84
|
+
voltro build . → EACCES: permission denied, open '…/apps/display/.framework/index.html'
|
|
85
|
+
```
|
|
168
86
|
|
|
169
|
-
|
|
87
|
+
That is the harder failure. `.env.local` broke env loading; this breaks the production build of every web app outright, with no workaround short of chown-ing by hand after each pod boot. One team could only verify their frontends through test suites and live requests against the running pods.
|
|
170
88
|
|
|
171
|
-
|
|
89
|
+
`voltro dev` and `voltro build` now hand their generated output — `.framework`, `.env.local`, every `*.generated.*` — to the uid that owns the app root, and say so loudly when they cannot. A no-op on every ordinary run and in any container started with `--user <uid>:<gid>`: when the process already owns the root it returns without touching the tree. Only generated state is claimed; the framework never chowns a file a human wrote.
|
|
90
|
+
- **@voltro/cli** — The observed app-graph no longer restarts the dev server.
|
|
172
91
|
|
|
173
|
-
|
|
92
|
+
`app.graph.observed.json` was written into the watched app root every 10 seconds, and the supervisor's watcher fired on each write. A downstream pod measured two restarts before every boot over 2000 log lines — the rule, not an outlier — and paid a ~46 s boot three times per save.
|
|
174
93
|
|
|
175
|
-
|
|
94
|
+
The watcher excludes `<name>.generated.<ext>`, a substring rule chosen precisely because a per-extension whitelist had already let a generated file slip twice. This file slipped it a third time by not carrying the segment at all. It is now `app.graph.observed.generated.json`, which matches the convention instead of adding a fourth special case to a list that has drifted three times; a stale un-suffixed file from an older dev server is removed on boot so it cannot keep triggering restarts.
|
|
95
|
+
- **@voltro/cli** — Four tooling fixes, all from downstream reports:
|
|
176
96
|
|
|
177
|
-
-
|
|
97
|
+
- **`voltro check --offline` crashed on any app that declares a workflow.** It built workflow entries as `{ name }` behind an `as never` while `InspectWorkflowEntry` is keyed by `tag`, so the manifest's sort read `undefined` and threw — surfacing as "could not assemble the graph from source" rather than the type error underneath. The cast is what let the two shapes disagree. - **`voltro check --offline` reported plugin tables as `dangling-source`.** It collected only the app's own `*.entity.ts` tables, so a query reading `_voltro_storage_refs` was an `error` — which sets the exit code, failing the CI gate the offline mode exists for. It now uses the same `assembleFrameworkTables` the migrator does. - **`voltro test` now derives `resolve.alias` from the app's tsconfig `paths`.** An app mapping `@/* → ./src/*` could not test any module importing through it (`Cannot find package '@/locales/en'`), and the workaround was a local `vitest.config.ts` restating what tsconfig already said. - **The `raw-fetch` doctor rule follows the import graph.** Keyed on filename conventions it caught 9 of 39 outbound calls on the reporting app; the other 30 were in `lib/*.ts` helpers only server code imports. A file reachable from a server-convention file and from nothing else is server code; one a page also imports is not, and stays unflagged.
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/ai": "0.
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/ai": "0.14.0",
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/protocol": "0.14.0",
|
|
19
|
+
"@voltro/runtime": "0.14.0",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.
|
|
23
|
+
"@voltro/testing": "0.14.0",
|
|
24
24
|
"typescript": "^5.7.0",
|
|
25
25
|
"vitest": "^3.0.0"
|
|
26
26
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
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.
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/plugin-auth": "0.14.0",
|
|
19
|
+
"@voltro/protocol": "0.14.0",
|
|
20
|
+
"@voltro/runtime": "0.14.0",
|
|
21
|
+
"@voltro/sql-postgres": "0.14.0",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.14.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -48,9 +48,6 @@ export const notes = table('notes', {
|
|
|
48
48
|
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
49
|
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
50
|
.with(tenant())
|
|
51
|
-
// Opt into the matcher engine: subscriptions against `notes` get a
|
|
52
|
-
// fresh snapshot/delta whenever a mutation writes the table.
|
|
53
|
-
.reactive()
|
|
54
51
|
|
|
55
52
|
export type Note = InferRow<typeof notes>
|
|
56
53
|
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
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.
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.14.0",
|
|
19
|
+
"@voltro/protocol": "0.14.0",
|
|
20
|
+
"@voltro/runtime": "0.14.0",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.14.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
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.
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/plugin-deactivation": "0.14.0",
|
|
19
|
+
"@voltro/protocol": "0.14.0",
|
|
20
|
+
"@voltro/runtime": "0.14.0",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.14.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -15,7 +15,7 @@ import { database } from '../database/schema' // registers users / actors / tena
|
|
|
15
15
|
import createUser from '../mutations/users.create.mutation.server'
|
|
16
16
|
import deactivateUser from '../mutations/users.deactivate.mutation.server'
|
|
17
17
|
|
|
18
|
-
const subject = { type: 'user' as const, id: 'user_1', tenantId:
|
|
18
|
+
const subject = { type: 'user' as const, id: 'user_1', tenantId: 'acme' }
|
|
19
19
|
|
|
20
20
|
describe('users.create', () => {
|
|
21
21
|
it('inserts a user with an auto-injected id and no deactivation timestamp', async () => {
|
|
@@ -48,9 +48,6 @@ export const notes = table('notes', {
|
|
|
48
48
|
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
49
|
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
50
|
.with(tenant())
|
|
51
|
-
// Opt into the matcher engine: subscriptions against `notes` get a
|
|
52
|
-
// fresh snapshot/delta whenever a mutation writes the table.
|
|
53
|
-
.reactive()
|
|
54
51
|
|
|
55
52
|
export type Note = InferRow<typeof notes>
|
|
56
53
|
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@react-email/components": "^1.0.12",
|
|
14
14
|
"@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.
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/plugin-mail": "0.14.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.14.0",
|
|
20
|
+
"@voltro/protocol": "0.14.0",
|
|
21
|
+
"@voltro/runtime": "0.14.0",
|
|
22
22
|
"effect": "^3.21.2",
|
|
23
23
|
"react": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.14.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -48,9 +48,6 @@ export const notes = table('notes', {
|
|
|
48
48
|
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
49
|
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
50
|
.with(tenant())
|
|
51
|
-
// Opt into the matcher engine: subscriptions against `notes` get a
|
|
52
|
-
// fresh snapshot/delta whenever a mutation writes the table.
|
|
53
|
-
.reactive()
|
|
54
51
|
|
|
55
52
|
export type Note = InferRow<typeof notes>
|
|
56
53
|
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
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.
|
|
15
|
+
"@voltro/cli": "0.14.0",
|
|
16
|
+
"@voltro/database": "0.14.0",
|
|
17
|
+
"@voltro/env": "0.14.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.14.0",
|
|
19
|
+
"@voltro/plugin-storage": "0.14.0",
|
|
20
|
+
"@voltro/protocol": "0.14.0",
|
|
21
|
+
"@voltro/runtime": "0.14.0",
|
|
22
|
+
"@voltro/sql-mysql": "0.14.0",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.14.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -48,9 +48,6 @@ export const notes = table('notes', {
|
|
|
48
48
|
// tenant() pulls audit() transitively → adds tenantId + createdAt /
|
|
49
49
|
// updatedAt / createdBy / updatedBy (auto-stamped by the runtime).
|
|
50
50
|
.with(tenant())
|
|
51
|
-
// Opt into the matcher engine: subscriptions against `notes` get a
|
|
52
|
-
// fresh snapshot/delta whenever a mutation writes the table.
|
|
53
|
-
.reactive()
|
|
54
51
|
|
|
55
52
|
export type Note = InferRow<typeof notes>
|
|
56
53
|
|