@voltro/cli 0.17.0 → 0.18.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 +268 -0
- package/bin/voltro.mjs +6 -1
- package/dist/{apiBuild-ChdlLqGv.js → apiBuild-CPDTJHkH.js} +2 -2
- package/dist/apiBuild-D1UBJ4TM.js +2 -0
- package/dist/bin.js +2 -2
- package/dist/{commands-DYQbv-DG.js → commands-laJDMj2m.js} +1899 -1700
- package/dist/{dev-DB7pbLob.js → dev-BOFxC21E.js} +993 -930
- package/dist/{dev-BCmoJfBm.js → dev-DOK0w6ZW.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-DWh56Qas.js → inspectMetrics-D1DmLeJs.js} +507 -494
- package/dist/{serveCommand-DO6qr5Ok.js → serveCommand-CxcxHc9Y.js} +320 -320
- package/dist/serveEntry.js +2 -2
- package/dist/{start-ksY0wMZG.js → start-Clvz4IJb.js} +1 -1
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +38 -0
- package/templates/AGENTS.md +39 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/authentication.md +48 -0
- package/templates/agent-docs/cli.md +9 -5
- package/templates/agent-docs/database/querying.md +6 -2
- package/templates/agent-docs/introduction.md +15 -1
- package/templates/agent-docs/routing.md +2 -0
- package/templates/agent-docs/schema-driven-ui.md +6 -3
- package/templates/agent-docs/whats-new.md +199 -30
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-D5WB119b.js +0 -2
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Q as e } from "./inspectMetrics-
|
|
1
|
+
import { Q as e } from "./inspectMetrics-D1DmLeJs.js";
|
|
2
2
|
import { c as t } from "./seedRunner-D6eu-u5U.js";
|
|
3
3
|
import { r as n } from "./appModuleLoader-C9r9mxZt.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-CxcxHc9Y.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as e, B as t, C as n, E as r, G as i, I as a, K as o, N as s, O as c, U as l, W as ee, X as u, Y as d, _ as f, a as p, b as m, c as h, et as g, f as _, g as te, h as v, i as ne, it as y, j as b, k as x, m as S, nt as C, o as re, p as ie, q as w, r as ae, rt as T, s as E, st as D, t as O, tt as k, ut as A, v as j, w as oe, y as M } from "./inspectMetrics-
|
|
1
|
+
import { A as e, B as t, C as n, E as r, G as i, I as a, K as o, N as s, O as c, U as l, W as ee, X as u, Y as d, _ as f, a as p, b as m, c as h, et as g, f as _, g as te, h as v, i as ne, it as y, j as b, k as x, m as S, nt as C, o as re, p as ie, q as w, r as ae, rt as T, s as E, st as D, t as O, tt as k, ut as A, v as j, w as oe, y as M } from "./inspectMetrics-D1DmLeJs.js";
|
|
2
2
|
import { D as se, E as ce, T as le, a as N, p as ue, w as de } from "./inspect-Dwx0_tUj.js";
|
|
3
3
|
import { t as fe } from "./bootTiming-BdyP9nYw.js";
|
|
4
4
|
import { dirname as pe, extname as P, join as F, resolve as I } from "node:path";
|
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Q as e } from "./inspectMetrics-
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { Q as e } from "./inspectMetrics-D1DmLeJs.js";
|
|
2
|
+
import { t } from "./start-Clvz4IJb.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.107.0",
|
|
63
63
|
"@effect/sql": "^0.51.1",
|
|
64
64
|
"@effect/workflow": "^0.18.2",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.18.0",
|
|
66
|
+
"@voltro/cache": "0.18.0",
|
|
67
|
+
"@voltro/data-transfer": "0.18.0",
|
|
68
|
+
"@voltro/database": "0.18.0",
|
|
69
|
+
"@voltro/env": "0.18.0",
|
|
70
|
+
"@voltro/kv": "0.18.0",
|
|
71
|
+
"@voltro/logger": "0.18.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.18.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.18.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.18.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.18.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.18.0",
|
|
77
|
+
"@voltro/protocol": "0.18.0",
|
|
78
|
+
"@voltro/runtime": "0.18.0",
|
|
79
|
+
"@voltro/serverless": "0.18.0",
|
|
80
|
+
"@voltro/workflow": "0.18.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -449,6 +449,44 @@ aggregations, FTS, vectors/RAG, migrations, dialect parity → the **`database/*
|
|
|
449
449
|
docs (see index). Async-vs-Effect handlers, `EffectStore`, typed store errors →
|
|
450
450
|
**`data.md`**.
|
|
451
451
|
|
|
452
|
+
## What may cross the wire (three ORTHOGONAL markers — do not substitute one for another)
|
|
453
|
+
|
|
454
|
+
A column is not "protected" or "unprotected". Three separate questions get three
|
|
455
|
+
separate markers, and using one to answer another's question is the mistake:
|
|
456
|
+
|
|
457
|
+
| Marker | Answers | Enforced by |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **the boot audit — it FAILS the boot**, it does not warn |
|
|
460
|
+
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
|
+
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
export const users = table('users', {
|
|
465
|
+
id: id(),
|
|
466
|
+
email: text().sensitive('pii'), // exportable only through a profile
|
|
467
|
+
pinHash: text().serverOnly(), // never reaches a client, ever
|
|
468
|
+
ssn: text().encrypted().serverOnly(), // both — they are not the same claim
|
|
469
|
+
})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
+
query that declares such a column in its OUTPUT does not start: the boot audit
|
|
474
|
+
refuses. That is a feature — the failure happens at boot, not in a bundle.
|
|
475
|
+
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
|
+
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
|
+
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
|
478
|
+
Reading `.encrypted()` as "safe to expose" is a category error, and a plausible
|
|
479
|
+
one — say both when you mean both.
|
|
480
|
+
- **Masking is fail-closed**: an unclassified column blocks the export rather
|
|
481
|
+
than passing through, so `.sensitive()` / `.safe()` is a decision you make once
|
|
482
|
+
per column, not a filter you remember to apply.
|
|
483
|
+
- `crud.list` / `getById` / `create` / `update` are **redacted by construction** —
|
|
484
|
+
they strip `serverOnly` columns for you. Hand-rolling the same CRUD is where
|
|
485
|
+
that stripping gets forgotten.
|
|
486
|
+
|
|
487
|
+
Depth (classes, profiles, the export flow) → **`database/sensitivity`**; the
|
|
488
|
+
redacted CRUD surface → **`data/crud`**.
|
|
489
|
+
|
|
452
490
|
## Naming / RPC tags
|
|
453
491
|
|
|
454
492
|
- **camelCase** for vars, files, schemas, and rpc tags. The tag IS the wire
|
package/templates/AGENTS.md
CHANGED
|
@@ -449,6 +449,44 @@ aggregations, FTS, vectors/RAG, migrations, dialect parity → the **`database/*
|
|
|
449
449
|
docs (see index). Async-vs-Effect handlers, `EffectStore`, typed store errors →
|
|
450
450
|
**`data.md`**.
|
|
451
451
|
|
|
452
|
+
## What may cross the wire (three ORTHOGONAL markers — do not substitute one for another)
|
|
453
|
+
|
|
454
|
+
A column is not "protected" or "unprotected". Three separate questions get three
|
|
455
|
+
separate markers, and using one to answer another's question is the mistake:
|
|
456
|
+
|
|
457
|
+
| Marker | Answers | Enforced by |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **the boot audit — it FAILS the boot**, it does not warn |
|
|
460
|
+
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
|
+
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
export const users = table('users', {
|
|
465
|
+
id: id(),
|
|
466
|
+
email: text().sensitive('pii'), // exportable only through a profile
|
|
467
|
+
pinHash: text().serverOnly(), // never reaches a client, ever
|
|
468
|
+
ssn: text().encrypted().serverOnly(), // both — they are not the same claim
|
|
469
|
+
})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
+
query that declares such a column in its OUTPUT does not start: the boot audit
|
|
474
|
+
refuses. That is a feature — the failure happens at boot, not in a bundle.
|
|
475
|
+
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
|
+
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
|
+
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
|
478
|
+
Reading `.encrypted()` as "safe to expose" is a category error, and a plausible
|
|
479
|
+
one — say both when you mean both.
|
|
480
|
+
- **Masking is fail-closed**: an unclassified column blocks the export rather
|
|
481
|
+
than passing through, so `.sensitive()` / `.safe()` is a decision you make once
|
|
482
|
+
per column, not a filter you remember to apply.
|
|
483
|
+
- `crud.list` / `getById` / `create` / `update` are **redacted by construction** —
|
|
484
|
+
they strip `serverOnly` columns for you. Hand-rolling the same CRUD is where
|
|
485
|
+
that stripping gets forgotten.
|
|
486
|
+
|
|
487
|
+
Depth (classes, profiles, the export flow) → **`database/sensitivity`**; the
|
|
488
|
+
redacted CRUD surface → **`data/crud`**.
|
|
489
|
+
|
|
452
490
|
## Naming / RPC tags
|
|
453
491
|
|
|
454
492
|
- **camelCase** for vars, files, schemas, and rpc tags. The tag IS the wire
|
|
@@ -541,7 +579,7 @@ each plugin's own README.
|
|
|
541
579
|
|
|
542
580
|
| Topic | Open | Summary |
|
|
543
581
|
|---|---|---|
|
|
544
|
-
| **What's new in 0.
|
|
582
|
+
| **What's new in 0.18.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
545
583
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
546
584
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
547
585
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.18.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -569,6 +569,54 @@ Read users, sessions, keys. A strategy that runs domain writes while deciding
|
|
|
569
569
|
who the caller is has the two jobs the wrong way round; nothing in the type
|
|
570
570
|
stops you, and it is still wrong.
|
|
571
571
|
|
|
572
|
+
#### What the boot store carries, and what it does not
|
|
573
|
+
|
|
574
|
+
The line is **everything that does not need a Subject** — not "less than
|
|
575
|
+
`ctx.store`":
|
|
576
|
+
|
|
577
|
+
| | Boot store (`input.store`, `req.store`) | Request store (`ctx.store`) |
|
|
578
|
+
|---|---|---|
|
|
579
|
+
| `.encrypted()` columns decrypt / encrypt | ✓ | ✓ |
|
|
580
|
+
| Array columns round-trip on non-native dialects | ✓ | ✓ |
|
|
581
|
+
| Tenant scope | — | ✓ |
|
|
582
|
+
| Soft-delete filter | — | ✓ |
|
|
583
|
+
| Audit-column stamping | — | ✓ |
|
|
584
|
+
| Row-level security | — | ✓ |
|
|
585
|
+
|
|
586
|
+
The right-hand four need a resolved Subject, and a strategy runs *before* one
|
|
587
|
+
exists — so a read of tenant-owned rows here must derive and apply that scope
|
|
588
|
+
itself. The first two do not, and getting them wrong is silent: a `.encrypted()`
|
|
589
|
+
column read raw hands back the string `enc:v1:…`, which compares, concatenates,
|
|
590
|
+
renders and logs perfectly well, and simply never matches the token you compare
|
|
591
|
+
it to.
|
|
592
|
+
|
|
593
|
+
This is also what changes when you move a read **off** hand-written SQL and onto
|
|
594
|
+
the store. Raw SQL sees ciphertext and you decrypt it yourself — `decryptField`
|
|
595
|
+
from `@voltro/runtime` is the escape hatch for exactly that. Through either
|
|
596
|
+
store you get plaintext, so a hand-rolled `decryptField` on the way out will now
|
|
597
|
+
be handed a plaintext value; `decryptField` passes a non-ciphertext value
|
|
598
|
+
through unchanged, so the double call is harmless, but the manual step is no
|
|
599
|
+
longer doing anything.
|
|
600
|
+
|
|
601
|
+
#### Reading a plugin's own tables
|
|
602
|
+
|
|
603
|
+
A plugin's tables are declared through `extendSchema` like any others, so they
|
|
604
|
+
are in the same registry and the same store reads them. A public route that
|
|
605
|
+
needs a row a plugin wrote — a storage reference for an avatar proxy, say —
|
|
606
|
+
reads it directly:
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
const [ref] = await req.store.query(
|
|
610
|
+
queryFor(storageObjects).where(eq('id', objectId)).descriptor,
|
|
611
|
+
)
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Two things to keep in mind. The table is the plugin's contract with itself, not
|
|
615
|
+
with you, so it can change shape in any release — pin the version if you depend
|
|
616
|
+
on it. And this store applies no tenant scope, so a route reading a
|
|
617
|
+
tenant-owned plugin table must filter by tenant itself, from something the
|
|
618
|
+
request proves rather than something it claims.
|
|
619
|
+
|
|
572
620
|
## Composing the chain
|
|
573
621
|
|
|
574
622
|
`composeAuthStrategies` turns an ordered list of strategies into a single resolver. First `matched` wins; first `failed` short-circuits to anonymous.
|
|
@@ -600,19 +600,23 @@ export default {
|
|
|
600
600
|
}
|
|
601
601
|
```
|
|
602
602
|
|
|
603
|
-
#### Inspect token
|
|
603
|
+
#### Inspect token
|
|
604
604
|
|
|
605
|
-
The overlay's webhooks / traces / indexes panels poll each api's `/_voltro/inspect/*` endpoints
|
|
605
|
+
The overlay's webhooks / traces / indexes panels poll each api's `/_voltro/inspect/*` endpoints, and that surface is **fail-closed everywhere**: with no `VOLTRO_INSPECT_TOKEN` configured, nobody is authorised — `voltro dev` included.
|
|
606
606
|
|
|
607
|
-
The
|
|
607
|
+
**Under `voltro dev` you configure nothing.** The dev server mints a token per project and its proxy attaches the `Authorization: Bearer` header server-side, on the `/_voltro/api/<name>` route the panels fetch through. The token stays in the dev server's process; the browser never holds it.
|
|
608
|
+
|
|
609
|
+
That is deliberate rather than convenient. A token compiled into the client bundle is a live credential published to everyone who loads the page, so there is no env-var channel for it — `voltro dev` and `voltro build` set vite's `envPrefix` to a sentinel precisely so nothing leaks through `import.meta.env`.
|
|
610
|
+
|
|
611
|
+
For an api the dev proxy does not front — a `voltro start` deploy with `VOLTRO_INSPECT_TOKEN` set, say — pass the token explicitly, and note that whatever you pass ships in the bundle:
|
|
608
612
|
|
|
609
613
|
```tsx
|
|
610
614
|
import { VoltroDevtools } from '@voltro/devtools'
|
|
611
615
|
|
|
612
|
-
<VoltroDevtools inspectToken={
|
|
616
|
+
<VoltroDevtools inspectToken={myToken} />
|
|
613
617
|
```
|
|
614
618
|
|
|
615
|
-
|
|
619
|
+
Without the prop the overlay sends no `Authorization` header of its own, which is correct: under `voltro dev` the proxy has already added one. (The indexes panel's live SSE stream can't carry a header at all; against an api reached without the proxy it falls back to token-carrying HTTP polling.)
|
|
616
620
|
|
|
617
621
|
#### Overriding the overlay's labels
|
|
618
622
|
|
|
@@ -105,11 +105,15 @@ ctx.store.select('notes')
|
|
|
105
105
|
.where('col', '>', value)
|
|
106
106
|
.where('col', '>=', value)
|
|
107
107
|
.where('col', 'in', [a, b, c])
|
|
108
|
-
.where('col', '
|
|
109
|
-
.where('col', '
|
|
108
|
+
.where('col', 'contains', 'needle') // case-INsensitive substring (ILIKE '%…%')
|
|
109
|
+
.where('col', 'startsWith', 'awb_') // case-SENSITIVE prefix (LIKE 'awb\_%')
|
|
110
110
|
.where('col', 'fts', 'query string') // full-text fallback (LIKE-based here)
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
`contains` folds case because it is a search primitive — a human typing into a box means `hello` to find `Hello`. `startsWith` does not, because a prefix is a namespace: `awb_` and `AWB_` are two different key spaces, and quietly merging them is a bug. `startsWith` is also the only one of the two a database can answer from an index — `LIKE 'literal%'` is a btree range scan, `%…%` is not. `%` and `_` inside either value are escaped, so they match literally.
|
|
114
|
+
|
|
115
|
+
There is no `'like'`. It used to be here, and it was a lie: it mapped to `contains`, so `.where('path', 'like', '/api/%')` matched only rows literally containing the characters `/api/%` and the wildcard you wrote did nothing. An operator named after SQL's must honour your wildcards or not exist.
|
|
116
|
+
|
|
113
117
|
These are the only operators the ergonomic `.where(col, op, value)` form accepts. For `IS NULL` / `NOT IN` / `IS NOT NULL`, pass a predicate built with the `@voltro/database` helpers:
|
|
114
118
|
|
|
115
119
|
```ts
|
|
@@ -473,7 +473,7 @@ If yes, the promise belongs in the name — you cannot see a contract before you
|
|
|
473
473
|
|
|
474
474
|
| Suffix | Promise | Enforced by |
|
|
475
475
|
|---|---|---|
|
|
476
|
-
| `*.component.tsx` | exactly one component
|
|
476
|
+
| `*.component.tsx` | exactly one component | `component/one-per-file`, `component/no-hook-export` |
|
|
477
477
|
| `*.component.ui.tsx` | one component, **reads only** | `ui/no-write`, `ui/orphaned`, `ui/unlinked` |
|
|
478
478
|
| `*.hook.ts` | exactly one `use*` hook (+ types) | `hook/one-per-file`, `hook/no-component-export` |
|
|
479
479
|
| `*.types.ts` | zero runtime exports | `types/runtime-export` |
|
|
@@ -483,6 +483,10 @@ If yes, the promise belongs in the name — you cannot see a contract before you
|
|
|
483
483
|
| `*.client.ts` | it and its imports are browser-safe | boot-time import walk, `client/not-browser-safe` |
|
|
484
484
|
| `*.store.ts` | exactly one `defineStore`, no server state | `store/one-per-file`, `store/mirrors-server-state` |
|
|
485
485
|
|
|
486
|
+
A `*.component.tsx` promises exactly ONE component. It does not promise to export nothing else: types, and plain module-local values a `const COLUMNS = […]` beside the table that renders them, are fine and always were. What the rule counts is components — a declaration that renders — so an object, an array, a string or a `new` beside your component is not a second one, and neither is `export default Card` next to `export const Card`.
|
|
487
|
+
|
|
488
|
+
The BOUNDARY rules (`internal/foreign-import`, `fixture/production-import`, `ui/unlinked`) are assertions about your import graph, so it is worth knowing which edges they follow: relative specifiers, your tsconfig `paths` aliases (read from the nearest `tsconfig.json`, so a per-app `@/*` works when you run `voltro doctor` at the repo root), `export … from` re-exports, and dynamic `import()`. A package import is a leaf — the walk stops at the edge of your app.
|
|
489
|
+
|
|
486
490
|
## `*.component.ui.tsx` — reads, never writes
|
|
487
491
|
|
|
488
492
|
```tsx
|
|
@@ -536,6 +540,16 @@ A component then wires it up with `useTracking(checkoutTracking, props, sink)`
|
|
|
536
540
|
|
|
537
541
|
The payoff is not tidiness. "What do we send to third parties" becomes a file listing instead of an archaeology project — which is the only form in which that question can be answered on demand when someone asks about personal data.
|
|
538
542
|
|
|
543
|
+
## `convention/missing-test` — why a shallow test is still worth writing
|
|
544
|
+
|
|
545
|
+
Every suffix that declares a runtime contract also expects a test beside it, named mechanically: `Card.component.tsx` → `Card.component.test.tsx`. It is an advisory, not an error.
|
|
546
|
+
|
|
547
|
+
The usual objection is that a per-component test at any real size is low value, and for *assertions* that is often true. That is not what the rule buys. What it buys is that something **mounts** the component — and a render loop, a crashing effect, a missing provider or a broken context is invisible until something does.
|
|
548
|
+
|
|
549
|
+
That is not hypothetical. One app adopting the taxonomy wrote 251 of these, deliberately shallow (it mounts, it performs no domain write, it renders no raw catalogue key). The first run found a page whose breadcrumb effect rebuilt a fresh array literal on every render — effect → context state → re-render → new literal, without end. That one test took 423 seconds and exhausted the heap. Ten sibling pages memoised; exactly one did not, and in a browser the screen had looked usable. After the fix the whole web suite went from 645 s to 57 s.
|
|
550
|
+
|
|
551
|
+
So write them shallow if you like. The mount is the point.
|
|
552
|
+
|
|
539
553
|
## What deliberately has NO suffix
|
|
540
554
|
|
|
541
555
|
A generic "one component per file" rule would be worth enforcing everywhere, so tying it to a rename would make it opt-in — less coverage for more cost. The shape rules above fire only on files that *declared* the contract, because declaring it is what makes the promise mean something.
|
|
@@ -262,6 +262,8 @@ No naming trick is needed to keep something out of the router: the absence of th
|
|
|
262
262
|
|
|
263
263
|
The canonical form is **no trailing slash** — always link with `<Link to="/about">`, not `<Link to="/about/">`.
|
|
264
264
|
|
|
265
|
+
`Link` forwards every prop it does not consume itself to the underlying `<a>`, `ref` included — so it drops straight into a polymorphic slot (`<Button component={Link} to={url}>`) without a wrapper.
|
|
266
|
+
|
|
265
267
|
The framework does NOT emit a trailing-slash redirect on its own. If you need `/about/` → `/about` normalisation (for SEO), configure a 301 redirect at your reverse proxy.
|
|
266
268
|
|
|
267
269
|
## What pages CAN'T do
|
|
@@ -903,9 +903,12 @@ that affordance read-only rather than binding to a tag that does not resolve. Th
|
|
|
903
903
|
[`useCan`](/docs/ui/client-utilities/use-can); map them to your app's real RBAC
|
|
904
904
|
scopes.
|
|
905
905
|
|
|
906
|
-
The
|
|
907
|
-
|
|
908
|
-
|
|
906
|
+
The manifest GET is bearer-gated wherever it runs — `/_voltro/inspect/*` is
|
|
907
|
+
fail-closed, so no configured `VOLTRO_INSPECT_TOKEN` means `401`, not "everyone".
|
|
908
|
+
Under `voltro dev` that is handled for you (the dev server mints a token and its
|
|
909
|
+
proxy attaches it server-side). An admin UI pointed at a deployed api has to
|
|
910
|
+
supply the token itself — a deployment concern, not something this hook
|
|
911
|
+
handles.
|
|
909
912
|
|
|
910
913
|
|
|
911
914
|
|