@noy-db/hub 0.7.0-pre.11 → 0.7.0-pre.13
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 +142 -0
- package/dist/as/index.js +3 -2
- package/dist/as/index.js.map +1 -1
- package/dist/{backup-VASRVLDZ.js → backup-R6A4VZRX.js} +4 -4
- package/dist/cargo/index.js +2 -2
- package/dist/chunk-66TQOPFM.js +37 -0
- package/dist/chunk-66TQOPFM.js.map +1 -0
- package/dist/{chunk-OEA5NVSU.js → chunk-AJQY44CE.js} +86 -6
- package/dist/chunk-AJQY44CE.js.map +1 -0
- package/dist/{chunk-S5AMPTNE.js → chunk-BMELYFHU.js} +2 -2
- package/dist/{chunk-T6PSWLAR.js → chunk-BUE2D7PX.js} +25 -2
- package/dist/chunk-BUE2D7PX.js.map +1 -0
- package/dist/chunk-BWXGSSFX.js +1589 -0
- package/dist/chunk-BWXGSSFX.js.map +1 -0
- package/dist/{chunk-HN4FNA4X.js → chunk-COTPDJR5.js} +8 -8
- package/dist/{chunk-E3EBMC4G.js → chunk-E2MYJGSG.js} +2 -2
- package/dist/{chunk-JJR2JIN4.js → chunk-FFBYRWDJ.js} +2 -2
- package/dist/{chunk-WQF3QABE.js → chunk-IHFXR2VQ.js} +9 -79
- package/dist/chunk-IHFXR2VQ.js.map +1 -0
- package/dist/chunk-MGOXJIBV.js +176 -0
- package/dist/chunk-MGOXJIBV.js.map +1 -0
- package/dist/chunk-PDD75NUZ.js +87 -0
- package/dist/chunk-PDD75NUZ.js.map +1 -0
- package/dist/chunk-RTBJ5IOO.js +87 -0
- package/dist/chunk-RTBJ5IOO.js.map +1 -0
- package/dist/chunk-RXLYKVTF.js +17 -0
- package/dist/chunk-RXLYKVTF.js.map +1 -0
- package/dist/{chunk-ESVREVWN.js → chunk-SXCYYMVS.js} +2 -2
- package/dist/chunk-TFMM5DBB.js +20 -0
- package/dist/chunk-TFMM5DBB.js.map +1 -0
- package/dist/chunk-TPHAGLUH.js +30 -0
- package/dist/chunk-TPHAGLUH.js.map +1 -0
- package/dist/{chunk-THNJ3IKU.js → chunk-ULUMAZ5S.js} +2 -2
- package/dist/{chunk-NO272T7Q.js → chunk-V4PXLYWM.js} +2 -31
- package/dist/chunk-V4PXLYWM.js.map +1 -0
- package/dist/{chunk-A5WZEPXJ.js → chunk-VGICZPVE.js} +5 -3
- package/dist/{chunk-A5WZEPXJ.js.map → chunk-VGICZPVE.js.map} +1 -1
- package/dist/{chunk-KHKKD62N.js → chunk-VYYM3MEN.js} +2 -16
- package/dist/chunk-VYYM3MEN.js.map +1 -0
- package/dist/{chunk-SWVNZQVR.js → chunk-YPXNAP72.js} +2 -169
- package/dist/chunk-YPXNAP72.js.map +1 -0
- package/dist/{chunk-I6DGKPTY.js → chunk-ZVCC36XQ.js} +101 -1273
- package/dist/chunk-ZVCC36XQ.js.map +1 -0
- package/dist/{collection-facade-MGGHQ3HU.js → collection-facade-4ACKIDDY.js} +4 -3
- package/dist/derivations/index.js +21 -6
- package/dist/dispatch-MSTR5VBI.js +255 -0
- package/dist/dispatch-MSTR5VBI.js.map +1 -0
- package/dist/{dispatch-44J4TL72.js → dispatch-PDP5TA73.js} +5 -5
- package/dist/{executor-KSPGNVDO.js → executor-YDDC5XGY.js} +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +39 -35
- package/dist/index.js.map +1 -1
- package/dist/introspection/index.js +3 -2
- package/dist/kernel/collection.d.ts +11 -6
- package/dist/kernel/match-pairs.d.ts +30 -0
- package/dist/lazy/index.js +3 -2
- package/dist/materialized-views/index.js +4 -3
- package/dist/{noydb-QJFZAH34.js → noydb-2C6QQJ5S.js} +17 -12
- package/dist/pod/index.js +17 -12
- package/dist/registry-D2YXKRYC.js +33 -0
- package/dist/search/index.js +1 -1
- package/dist/{stale-KSPG6LFX.js → stale-2CHKZTMK.js} +2 -2
- package/dist/{walk-G3CZIL4L.js → walk-P3E6QSNC.js} +4 -3
- package/dist/with-formula/derivations/dispatch.d.ts +16 -1
- package/dist/with-formula/derivations/registry.d.ts +20 -6
- package/dist/with-formula/derivations/trigger-match.d.ts +52 -0
- package/dist/with-formula/derivations/types.d.ts +23 -0
- package/dist/with-shape/introspection/behaviors.d.ts +7 -0
- package/dist/with-shape/introspection/describe.d.ts +30 -0
- package/package.json +3 -3
- package/dist/chunk-I6DGKPTY.js.map +0 -1
- package/dist/chunk-KHKKD62N.js.map +0 -1
- package/dist/chunk-NO272T7Q.js.map +0 -1
- package/dist/chunk-OEA5NVSU.js.map +0 -1
- package/dist/chunk-SWVNZQVR.js.map +0 -1
- package/dist/chunk-T6PSWLAR.js.map +0 -1
- package/dist/chunk-UB5CRTFF.js +0 -344
- package/dist/chunk-UB5CRTFF.js.map +0 -1
- package/dist/chunk-WQF3QABE.js.map +0 -1
- package/dist/chunk-YPMW26SL.js +0 -81
- package/dist/chunk-YPMW26SL.js.map +0 -1
- package/dist/dispatch-IM52HDXT.js +0 -194
- package/dist/dispatch-IM52HDXT.js.map +0 -1
- package/dist/registry-FZXPQRAB.js +0 -18
- /package/dist/{backup-VASRVLDZ.js.map → backup-R6A4VZRX.js.map} +0 -0
- /package/dist/{chunk-S5AMPTNE.js.map → chunk-BMELYFHU.js.map} +0 -0
- /package/dist/{chunk-HN4FNA4X.js.map → chunk-COTPDJR5.js.map} +0 -0
- /package/dist/{chunk-E3EBMC4G.js.map → chunk-E2MYJGSG.js.map} +0 -0
- /package/dist/{chunk-JJR2JIN4.js.map → chunk-FFBYRWDJ.js.map} +0 -0
- /package/dist/{chunk-ESVREVWN.js.map → chunk-SXCYYMVS.js.map} +0 -0
- /package/dist/{chunk-THNJ3IKU.js.map → chunk-ULUMAZ5S.js.map} +0 -0
- /package/dist/{collection-facade-MGGHQ3HU.js.map → collection-facade-4ACKIDDY.js.map} +0 -0
- /package/dist/{dispatch-44J4TL72.js.map → dispatch-PDP5TA73.js.map} +0 -0
- /package/dist/{executor-KSPGNVDO.js.map → executor-YDDC5XGY.js.map} +0 -0
- /package/dist/{noydb-QJFZAH34.js.map → noydb-2C6QQJ5S.js.map} +0 -0
- /package/dist/{registry-FZXPQRAB.js.map → registry-D2YXKRYC.js.map} +0 -0
- /package/dist/{stale-KSPG6LFX.js.map → stale-2CHKZTMK.js.map} +0 -0
- /package/dist/{walk-G3CZIL4L.js.map → walk-P3E6QSNC.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,147 @@
|
|
|
1
1
|
# Changelog — hub
|
|
2
2
|
|
|
3
|
+
## 0.7.0-pre.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A FAILED non-strict lazy re-derive no longer clears the stale flag (#1258).
|
|
8
|
+
|
|
9
|
+
`resolveStaleOnRead` consumes the pending flag before reading the source (a
|
|
10
|
+
recursion guard for self-write outputs). A STRICT failure throws and the catch
|
|
11
|
+
restores it; a NON-STRICT failure warned and continued, leaving the flag
|
|
12
|
+
consumed — so the record was served as fresh and never retried, permanently,
|
|
13
|
+
because nothing would mark it stale again.
|
|
14
|
+
|
|
15
|
+
The decision this needed was what `strict: false` means. It means "a failed
|
|
16
|
+
derivation must not break the read", NOT "report this output as current". The
|
|
17
|
+
record is still served, the warning still fires, and the flag now survives so
|
|
18
|
+
the next read retries — strict-mode behaviour minus the throw.
|
|
19
|
+
|
|
20
|
+
Deliberate cost, stated so it is not later mistaken for a bug: a
|
|
21
|
+
permanently-failing non-strict derivation now re-runs on every read of that id
|
|
22
|
+
rather than once. That is louder and more expensive than silently serving a
|
|
23
|
+
stale value forever, and it is the trade made everywhere else here — a degraded
|
|
24
|
+
state must not render as a healthy one.
|
|
25
|
+
|
|
26
|
+
- A rollup child that RE-PARENTS now recomputes the old parent as well as the new
|
|
27
|
+
one (#1257).
|
|
28
|
+
|
|
29
|
+
The dispatcher read the rollup key from the incoming record only, so moving a
|
|
30
|
+
child from parent A to parent B recomputed B and left A's aggregate holding a
|
|
31
|
+
number that was correct before the move. That is the dangerous shape: a stale
|
|
32
|
+
total reads as data, not as an error, so nothing prompts anyone to re-check it.
|
|
33
|
+
|
|
34
|
+
Same old-value class as the `triggerBy` update fan-out fixed in #1249 — and it
|
|
35
|
+
is a small change rather than a new mechanism only because that fix added
|
|
36
|
+
prior-record capture to the write path, which this reuses.
|
|
37
|
+
|
|
38
|
+
Consistent with the rest of the derivation surface: a write that does not thread
|
|
39
|
+
a prior record (a sync-applied wave write, a tiers restore) degrades to
|
|
40
|
+
new-parent-only, the previous behaviour, rather than guessing at the old key.
|
|
41
|
+
|
|
42
|
+
- `schemaFieldKeys` now unwraps `z.preprocess()` on both Zod majors, so the two
|
|
43
|
+
field-typo guards (#1253's `fieldMeta`, #1249's `triggerBy` match) see through
|
|
44
|
+
it (#1262, reported by the pilot).
|
|
45
|
+
|
|
46
|
+
The `0.7.0-pre.12` fix followed refinement effects only and put `preprocess` on
|
|
47
|
+
the carve-out side with `transform`. That was wrong: `z.preprocess(fn, inner)`
|
|
48
|
+
rewrites the INPUT and then parses with `inner`, so the parsed record's keys ARE
|
|
49
|
+
`inner`'s keys — measured, `preprocess({a,b})` parses to `['a','b']` while
|
|
50
|
+
`transform` parses to `['c']`.
|
|
51
|
+
|
|
52
|
+
Scope, measured rather than assumed: this left the **sync** `describe()` path
|
|
53
|
+
unguarded for wrapped schemas. The async path was never affected —
|
|
54
|
+
`derivePersistedSchema` sees through `preprocess` on its own, so
|
|
55
|
+
`buildDescription` gets a populated field map and never reaches the
|
|
56
|
+
`schemaFieldKeys` fallback. The sync path is public, is what tooling reaches
|
|
57
|
+
for first, and is the only path some Zod 3 consumers can reach at all (the
|
|
58
|
+
async path needs the `zod-to-json-schema` peer), so the gap was real — but it
|
|
59
|
+
was one path, not both.
|
|
60
|
+
|
|
61
|
+
Replaced the effect-kind allowlist with one rule that covers every wrapper on
|
|
62
|
+
both majors: **follow the output side.** These keys describe the parsed record,
|
|
63
|
+
so the only question a wrapper raises is whether it changes the parsed shape.
|
|
64
|
+
Zod 3 `ZodEffects` follows `_def.schema` for `refinement` and `preprocess`;
|
|
65
|
+
Zod 4 wraps both `z.preprocess()` and `.transform()` as a `ZodPipe` and
|
|
66
|
+
following `_def.out` resolves them with no effect-kind test at all — preprocess
|
|
67
|
+
reaches the object, `.transform()` reaches a shapeless `ZodTransform` and stays
|
|
68
|
+
silent on its own. **This also fixes Zod 4 `z.preprocess()`, which the narrower
|
|
69
|
+
one-line widening would have missed.**
|
|
70
|
+
|
|
71
|
+
`.transform()` remains deliberately silent on both majors: it replaces the
|
|
72
|
+
output, so the inner keys would describe a record that is never stored. The two
|
|
73
|
+
are one string apart and mean opposite things, so tests pin them against each
|
|
74
|
+
other on both majors.
|
|
75
|
+
|
|
76
|
+
- `triggerBy` match targets are now checked against where the field actually
|
|
77
|
+
LIVES, not just whether it is declared (#1266, reported by the pilot).
|
|
78
|
+
|
|
79
|
+
A derivation matcher reads STORED records. A `mode: 'virtual'` computed field is
|
|
80
|
+
evaluated on the read path and never persisted — but it appears in `computed:`
|
|
81
|
+
(and in `via(computed(...))`) exactly like a materialized one, so registration
|
|
82
|
+
accepted it and the fan-out then matched nothing, forever. That is the precise
|
|
83
|
+
failure the #1249 match guard exists to prevent, reached THROUGH the guard
|
|
84
|
+
rather than around it, and it is the first thing a consumer reaches for when a
|
|
85
|
+
match target is not already stored.
|
|
86
|
+
|
|
87
|
+
Refused at registration rather than supported: matching a virtual field means
|
|
88
|
+
running user code for every candidate row, turning an indexed narrow into a full
|
|
89
|
+
scan. `mode: 'materialized'` is stored, already works, and is what the error
|
|
90
|
+
names. Both sides are refused — `to` reads the source record and `from` reads
|
|
91
|
+
the written record, and both are the stored shape. (The report covered `to`;
|
|
92
|
+
`from` had the identical defect.)
|
|
93
|
+
|
|
94
|
+
The virtual check runs even when the schema's field list is unreadable, unlike
|
|
95
|
+
the typo guard beside it. The typo guard needs a field list to compare against
|
|
96
|
+
and stays silent without one; this one does not — "declared, but never stored"
|
|
97
|
+
is provable from the declaration alone.
|
|
98
|
+
|
|
99
|
+
Second defect fixed in the same change: `viaFields` was missing from the guard's
|
|
100
|
+
key set entirely, so a `via()`-declared MATERIALIZED field — a perfectly valid
|
|
101
|
+
match target — was rejected as an undeclared typo. A guard that refuses valid
|
|
102
|
+
configurations is how people learn to stop trusting it, so both directions ship
|
|
103
|
+
together, each with a test that fails without its half of the fix.
|
|
104
|
+
|
|
105
|
+
## 0.7.0-pre.12
|
|
106
|
+
|
|
107
|
+
### Minor Changes
|
|
108
|
+
|
|
109
|
+
- `withDerivation`'s `triggerBy` accepts a multi-field `match` form (#1249):
|
|
110
|
+
`{ collection, match: [{ from, to }] }` fans a write out to every source
|
|
111
|
+
record where ALL pairs satisfy `String(source[to]) === String(written[from])`.
|
|
112
|
+
`from: 'id'` reads the written record's id, making the existing `on` form the
|
|
113
|
+
single-pair special case (it is unchanged and stays supported). This makes
|
|
114
|
+
shared-key ("reverse") relationships and composite keys like
|
|
115
|
+
`(clientId, cycle)` expressible without denormalising a synthetic key.
|
|
116
|
+
|
|
117
|
+
Also, for BOTH forms:
|
|
118
|
+
|
|
119
|
+
- a LOCAL UPDATE that changes any matched field fans out on old-match ∪
|
|
120
|
+
new-match, so records addressed by the previous value no longer go
|
|
121
|
+
silently stale; a sync-applied wave write or a tiers restore, which don't
|
|
122
|
+
thread the prior record, fan out on the new tuple only;
|
|
123
|
+
- a parent DELETE now fans out using the tombstoned record's values —
|
|
124
|
+
previously deletes fired no triggers at all, leaving matched sources
|
|
125
|
+
stale. The fan-out runs after the delete commits, so cap/strict errors
|
|
126
|
+
can surface from `delete()` (same as the existing rollup-on-delete
|
|
127
|
+
precedent);
|
|
128
|
+
- match fields are validated against the collections' enumerable field sets at
|
|
129
|
+
`vault.collection()` (the #1253 pattern): a provable typo throws instead of
|
|
130
|
+
silently matching nothing forever; TS-generic collections stay unguarded by
|
|
131
|
+
design.
|
|
132
|
+
|
|
133
|
+
`maxFanout` caps the unioned matched set per written event.
|
|
134
|
+
|
|
135
|
+
### Patch Changes
|
|
136
|
+
|
|
137
|
+
- `schemaFieldKeys` now unwraps Zod 3 `ZodEffects` created by object-level
|
|
138
|
+
`.refine()`/`.superRefine()` (reported by the pilot): the wrapper hides
|
|
139
|
+
`.shape`, so both field-typo guards — #1253's `fieldMeta` validation and
|
|
140
|
+
#1249's `triggerBy` match validation — were silent for exactly the schemas
|
|
141
|
+
most worth guarding. Unwrapping follows refinement effects only: a
|
|
142
|
+
`.transform()`/`.preprocess()` changes the output shape, so those stay
|
|
143
|
+
silent by design. Zod 4 keeps `.shape` through `.refine()` and is unaffected.
|
|
144
|
+
|
|
3
145
|
## 0.7.0-pre.11
|
|
4
146
|
|
|
5
147
|
### Patch Changes
|
package/dist/as/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "../chunk-UBSFPGVJ.js";
|
|
2
|
-
import "../chunk-
|
|
2
|
+
import "../chunk-VGICZPVE.js";
|
|
3
3
|
import {
|
|
4
4
|
applyListProjection
|
|
5
5
|
} from "../chunk-VG2ZSFYD.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-RXLYKVTF.js";
|
|
7
|
+
import "../chunk-VYYM3MEN.js";
|
|
7
8
|
import "../chunk-URHRIAV6.js";
|
|
8
9
|
import "../chunk-Y2NBBT6K.js";
|
|
9
10
|
import "../chunk-WWNYSV7S.js";
|
package/dist/as/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/port/as/active.ts"],"sourcesContent":["/**\n * `withFormats()` — the service that consumes the `as-*` port.\n *\n * Everything with a correctness or security consequence lives here rather\n * than in each format: the capability gate, the vault read, and redaction.\n * A {@link NoydbFormat} receives records and returns bytes.\n *\n * That is the whole point of the inversion (ADR 0004). The conformance kit it\n * replaces could only check implementors who ran it; a format that cannot\n * reach a vault is gated whether its author cooperated or not.\n *\n * @module\n */\nimport type { NoydbFormat, ImportPlan } from './types.js'\nimport type {\n FormatsStrategy,\n FormatsContext,\n FormatExportOptions,\n FormatImportOptions,\n} from '../with/formats-strategy.js'\nimport type { ExportChunk } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { ImportPolicy } from './types.js'\nimport { applyListProjection } from '../../with-shape/introspection/index.js'\nimport type { CollectionDescription, ListProjectionOptions } from '../../with-shape/introspection/index.js'\nimport { ValidationError } from '../../kernel/errors.js'\n\n/** Apply the redaction projection hub owns, so a format never asks for it. */\nfunction redactChunk(\n chunk: ExportChunk,\n ctx: FormatsContext,\n redact: FormatExportOptions['redact'],\n): ExportChunk {\n if (redact === undefined) return chunk\n const described = ctx.describe(chunk.collection) as CollectionDescription | undefined\n if (!described) {\n throw new Error(\n `vault.export: redaction was requested but '${chunk.collection}' has no description to redact against. ` +\n 'Open the collection with its classifiedFields / fieldMeta options first.',\n )\n }\n const opts: ListProjectionOptions | undefined =\n redact === true ? undefined : { sensitivity: redact.sensitivity as never }\n const records = chunk.records.map((r) =>\n applyListProjection(described, r as Record<string, unknown>, opts),\n )\n return { ...chunk, records: records as typeof chunk.records }\n}\n\n/**\n * Derive the narrow, format-facing capability from the vault.\n *\n * Lives here rather than on `Vault` because `kernel-surface` said so, and it\n * was right: this is service capability, not spine. The spine keeps two\n * delegating methods and nothing else.\n */\nfunction contextFor(vault: Vault): FormatsContext {\n return {\n // The empty-id guards are NOT defensive padding. The\n // plaintext tier is gated per-format, so a missing id means the calling\n // binding is broken — and without this it would reach the gate, miss the\n // allowlist, and surface as `ExportCapabilityError`: \"the owner did not\n // grant this\", which sends the reader to the keyring instead of to the\n // format. A loud TypeError names the real fault.\n assertCanExport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanExport('bundle')\n if (!format) throw new TypeError(\"assertCanExport('plaintext') requires a non-empty format id\")\n vault.assertCanExport('plaintext', format)\n },\n assertCanImport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanImport('bundle')\n if (!format) throw new TypeError(\"assertCanImport('plaintext') requires a non-empty format id\")\n vault.assertCanImport('plaintext', format)\n },\n // ⚠️ Filtered HERE, not passed to exportStream — `ExportStreamOptions`\n // has no `collections` field, so the option I first passed was silently\n // dropped and hub read every collection. Caught by an as-json test\n // expecting ['invoices'] and getting ['invoices','payments'].\n //\n // Post-read filtering is correct but not free: hub still DECRYPTS the\n // collections the caller excluded. Pushing the filter down into\n // exportStream is the real fix and is a hub change beyond this port.\n chunks: async function* (collections) {\n const wanted = collections ? new Set(collections) : null\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (wanted && !wanted.has(chunk.collection)) continue\n yield chunk\n }\n },\n // NOT swallowed. A caller who asked for redaction and silently got\n // unredacted output is the exact failure this port exists to remove — a\n // security duty that fails quiet. If the description cannot be read, the\n // export fails instead.\n describe: (collection) => vault.collection(collection).describe() as never,\n plan: async (records, policy: ImportPolicy, formatId: string, idKey?: string) => {\n const { diffVault } = await import('../../with-cargo/vault-diff.js')\n const plan = await diffVault(vault, records as never, idKey ? { idKey } : {})\n return {\n plan,\n policy,\n // The apply walk every as-* package had independently: added always,\n // modified unless insert-only, deleted only on replace — in one\n // transaction so a partial failure rolls back. Owned once, here.\n apply: async () => {\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n const reason = `import:${formatId}`\n for (const e of plan.added) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n if (policy !== 'insert-only') {\n for (const e of plan.modified) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n }\n if (policy === 'replace') {\n for (const e of plan.deleted) txVault.collection(e.collection).delete(e.id)\n }\n })\n },\n }\n },\n }\n}\n\n/**\n * Enable `vault.export()` / `vault.import()`.\n *\n * ```ts\n * const db = await createNoydb({ store, user, secret, formatsStrategy: withFormats() })\n * const csv = await vault.export(asCsv(), { collections: ['invoices'] })\n * ```\n */\nexport function withFormats(): FormatsStrategy {\n return {\n enabled: true,\n\n async exportWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n options: FormatExportOptions = {},\n ): Promise<Out> {\n const ctx = contextFor(vault)\n // FIRST, and before a single record is read. The format is not in scope\n // yet and could not reach the vault if it were.\n if (format.tier === 'bundle') ctx.assertCanExport('bundle')\n else ctx.assertCanExport('plaintext', format.id)\n\n const chunks: ExportChunk[] = []\n for await (const chunk of ctx.chunks(options.collections)) {\n chunks.push(redactChunk(chunk, ctx, options.redact))\n }\n return format.encode(chunks)\n },\n\n async importWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n input: Out,\n options: FormatImportOptions = {},\n ): Promise<ImportPlan> {\n const ctx = contextFor(vault)\n // Mirrors the export side: the import gate is two-tier as well, and a\n // bundle format importing under the plaintext tier would be a silent\n // downgrade in the other direction.\n if (format.tier === 'bundle') ctx.assertCanImport('bundle')\n else ctx.assertCanImport('plaintext', format.id)\n if (!format.decode) {\n throw new ValidationError(\n `vault.import: the '${format.id}' format is export-only — it declares no decode(). ` +\n 'Pick a format that round-trips, or write the records directly.',\n )\n }\n const decoded = await format.decode(input)\n const candidate: Record<string, readonly unknown[]> = {}\n for (const chunk of decoded) {\n const target = chunk.collection || options.collection\n if (!target) {\n throw new ValidationError(\n `vault.import: the '${format.id}' input names no collection and none was supplied. ` +\n 'Pass { collection } for formats whose payload is collection-less.',\n )\n }\n candidate[target] = [...(candidate[target] ?? []), ...chunk.records]\n }\n return ctx.plan(candidate, options.policy ?? 'merge', format.id, options.idKey)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/port/as/active.ts"],"sourcesContent":["/**\n * `withFormats()` — the service that consumes the `as-*` port.\n *\n * Everything with a correctness or security consequence lives here rather\n * than in each format: the capability gate, the vault read, and redaction.\n * A {@link NoydbFormat} receives records and returns bytes.\n *\n * That is the whole point of the inversion (ADR 0004). The conformance kit it\n * replaces could only check implementors who ran it; a format that cannot\n * reach a vault is gated whether its author cooperated or not.\n *\n * @module\n */\nimport type { NoydbFormat, ImportPlan } from './types.js'\nimport type {\n FormatsStrategy,\n FormatsContext,\n FormatExportOptions,\n FormatImportOptions,\n} from '../with/formats-strategy.js'\nimport type { ExportChunk } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { ImportPolicy } from './types.js'\nimport { applyListProjection } from '../../with-shape/introspection/index.js'\nimport type { CollectionDescription, ListProjectionOptions } from '../../with-shape/introspection/index.js'\nimport { ValidationError } from '../../kernel/errors.js'\n\n/** Apply the redaction projection hub owns, so a format never asks for it. */\nfunction redactChunk(\n chunk: ExportChunk,\n ctx: FormatsContext,\n redact: FormatExportOptions['redact'],\n): ExportChunk {\n if (redact === undefined) return chunk\n const described = ctx.describe(chunk.collection) as CollectionDescription | undefined\n if (!described) {\n throw new Error(\n `vault.export: redaction was requested but '${chunk.collection}' has no description to redact against. ` +\n 'Open the collection with its classifiedFields / fieldMeta options first.',\n )\n }\n const opts: ListProjectionOptions | undefined =\n redact === true ? undefined : { sensitivity: redact.sensitivity as never }\n const records = chunk.records.map((r) =>\n applyListProjection(described, r as Record<string, unknown>, opts),\n )\n return { ...chunk, records: records as typeof chunk.records }\n}\n\n/**\n * Derive the narrow, format-facing capability from the vault.\n *\n * Lives here rather than on `Vault` because `kernel-surface` said so, and it\n * was right: this is service capability, not spine. The spine keeps two\n * delegating methods and nothing else.\n */\nfunction contextFor(vault: Vault): FormatsContext {\n return {\n // The empty-id guards are NOT defensive padding. The\n // plaintext tier is gated per-format, so a missing id means the calling\n // binding is broken — and without this it would reach the gate, miss the\n // allowlist, and surface as `ExportCapabilityError`: \"the owner did not\n // grant this\", which sends the reader to the keyring instead of to the\n // format. A loud TypeError names the real fault.\n assertCanExport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanExport('bundle')\n if (!format) throw new TypeError(\"assertCanExport('plaintext') requires a non-empty format id\")\n vault.assertCanExport('plaintext', format)\n },\n assertCanImport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanImport('bundle')\n if (!format) throw new TypeError(\"assertCanImport('plaintext') requires a non-empty format id\")\n vault.assertCanImport('plaintext', format)\n },\n // ⚠️ Filtered HERE, not passed to exportStream — `ExportStreamOptions`\n // has no `collections` field, so the option I first passed was silently\n // dropped and hub read every collection. Caught by an as-json test\n // expecting ['invoices'] and getting ['invoices','payments'].\n //\n // Post-read filtering is correct but not free: hub still DECRYPTS the\n // collections the caller excluded. Pushing the filter down into\n // exportStream is the real fix and is a hub change beyond this port.\n chunks: async function* (collections) {\n const wanted = collections ? new Set(collections) : null\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (wanted && !wanted.has(chunk.collection)) continue\n yield chunk\n }\n },\n // NOT swallowed. A caller who asked for redaction and silently got\n // unredacted output is the exact failure this port exists to remove — a\n // security duty that fails quiet. If the description cannot be read, the\n // export fails instead.\n describe: (collection) => vault.collection(collection).describe() as never,\n plan: async (records, policy: ImportPolicy, formatId: string, idKey?: string) => {\n const { diffVault } = await import('../../with-cargo/vault-diff.js')\n const plan = await diffVault(vault, records as never, idKey ? { idKey } : {})\n return {\n plan,\n policy,\n // The apply walk every as-* package had independently: added always,\n // modified unless insert-only, deleted only on replace — in one\n // transaction so a partial failure rolls back. Owned once, here.\n apply: async () => {\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n const reason = `import:${formatId}`\n for (const e of plan.added) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n if (policy !== 'insert-only') {\n for (const e of plan.modified) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n }\n if (policy === 'replace') {\n for (const e of plan.deleted) txVault.collection(e.collection).delete(e.id)\n }\n })\n },\n }\n },\n }\n}\n\n/**\n * Enable `vault.export()` / `vault.import()`.\n *\n * ```ts\n * const db = await createNoydb({ store, user, secret, formatsStrategy: withFormats() })\n * const csv = await vault.export(asCsv(), { collections: ['invoices'] })\n * ```\n */\nexport function withFormats(): FormatsStrategy {\n return {\n enabled: true,\n\n async exportWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n options: FormatExportOptions = {},\n ): Promise<Out> {\n const ctx = contextFor(vault)\n // FIRST, and before a single record is read. The format is not in scope\n // yet and could not reach the vault if it were.\n if (format.tier === 'bundle') ctx.assertCanExport('bundle')\n else ctx.assertCanExport('plaintext', format.id)\n\n const chunks: ExportChunk[] = []\n for await (const chunk of ctx.chunks(options.collections)) {\n chunks.push(redactChunk(chunk, ctx, options.redact))\n }\n return format.encode(chunks)\n },\n\n async importWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n input: Out,\n options: FormatImportOptions = {},\n ): Promise<ImportPlan> {\n const ctx = contextFor(vault)\n // Mirrors the export side: the import gate is two-tier as well, and a\n // bundle format importing under the plaintext tier would be a silent\n // downgrade in the other direction.\n if (format.tier === 'bundle') ctx.assertCanImport('bundle')\n else ctx.assertCanImport('plaintext', format.id)\n if (!format.decode) {\n throw new ValidationError(\n `vault.import: the '${format.id}' format is export-only — it declares no decode(). ` +\n 'Pick a format that round-trips, or write the records directly.',\n )\n }\n const decoded = await format.decode(input)\n const candidate: Record<string, readonly unknown[]> = {}\n for (const chunk of decoded) {\n const target = chunk.collection || options.collection\n if (!target) {\n throw new ValidationError(\n `vault.import: the '${format.id}' input names no collection and none was supplied. ` +\n 'Pass { collection } for formats whose payload is collection-less.',\n )\n }\n candidate[target] = [...(candidate[target] ?? []), ...chunk.records]\n }\n return ctx.plan(candidate, options.policy ?? 'merge', format.id, options.idKey)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MANIFEST_COLLECTION
|
|
3
3
|
} from "./chunk-EPFF7LOX.js";
|
|
4
|
-
import {
|
|
5
|
-
SCHEMAS_COLLECTION
|
|
6
|
-
} from "./chunk-WWNYSV7S.js";
|
|
7
4
|
import {
|
|
8
5
|
SEQUENCE_COLLECTION
|
|
9
6
|
} from "./chunk-MMOZQTXV.js";
|
|
7
|
+
import {
|
|
8
|
+
SCHEMAS_COLLECTION
|
|
9
|
+
} from "./chunk-WWNYSV7S.js";
|
|
10
10
|
import {
|
|
11
11
|
PERIODS_COLLECTION,
|
|
12
12
|
PERIOD_ARCHIVES_COLLECTION,
|
|
@@ -262,4 +262,4 @@ export {
|
|
|
262
262
|
loadVault,
|
|
263
263
|
verifyBackupIntegrity
|
|
264
264
|
};
|
|
265
|
-
//# sourceMappingURL=backup-
|
|
265
|
+
//# sourceMappingURL=backup-R6A4VZRX.js.map
|
package/dist/cargo/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
describeExtraction,
|
|
6
6
|
unsealDeks,
|
|
7
7
|
withCargo
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-SXCYYMVS.js";
|
|
9
9
|
import {
|
|
10
10
|
extractPartition,
|
|
11
11
|
walkClosure
|
|
@@ -29,10 +29,10 @@ import {
|
|
|
29
29
|
isQuorum,
|
|
30
30
|
runDrainBarrier
|
|
31
31
|
} from "../chunk-VTFVLCUH.js";
|
|
32
|
-
import "../chunk-WWNYSV7S.js";
|
|
33
32
|
import {
|
|
34
33
|
CustodyApi
|
|
35
34
|
} from "../chunk-Y4MJQ2UG.js";
|
|
35
|
+
import "../chunk-WWNYSV7S.js";
|
|
36
36
|
import "../chunk-PKJ43CMD.js";
|
|
37
37
|
import "../chunk-MLO75SFJ.js";
|
|
38
38
|
import {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Lru,
|
|
3
|
+
parseBytes
|
|
4
|
+
} from "./chunk-V4PXLYWM.js";
|
|
5
|
+
|
|
6
|
+
// src/port/with/lazy-strategy.ts
|
|
7
|
+
function buildLazyCache(collection, cache) {
|
|
8
|
+
if (!cache || cache.maxRecords === void 0 && cache.maxBytes === void 0) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Collection "${collection}": lazy mode (prefetch: false) requires a cache option with maxRecords and/or maxBytes. An unbounded lazy cache defeats the purpose.`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const lruOptions = {};
|
|
14
|
+
if (cache.maxRecords !== void 0) lruOptions.maxRecords = cache.maxRecords;
|
|
15
|
+
if (cache.maxBytes !== void 0) lruOptions.maxBytes = parseBytes(cache.maxBytes);
|
|
16
|
+
return new Lru(lruOptions);
|
|
17
|
+
}
|
|
18
|
+
var implicitLazyWarned = false;
|
|
19
|
+
var IMPLICIT_LAZY = {
|
|
20
|
+
createCache(collection, cache) {
|
|
21
|
+
if (!implicitLazyWarned) {
|
|
22
|
+
implicitLazyWarned = true;
|
|
23
|
+
if (!(typeof process !== "undefined" && process.env["NODE_ENV"] === "test")) {
|
|
24
|
+
console.warn(
|
|
25
|
+
`[noy-db] Collection "${collection}": lazy mode (prefetch: false) without the lazy service is deprecated \u2014 pass \`lazyStrategy: withLazy()\` (from "@noy-db/hub/lazy") to createNoydb(). The implicit path will be removed at 1.0.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return buildLazyCache(collection, cache);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
buildLazyCache,
|
|
35
|
+
IMPLICIT_LAZY
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=chunk-66TQOPFM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/port/with/lazy-strategy.ts"],"sourcesContent":["/**\n * Lazy-mode capability contract (#267 Track A tail — lazy-mode promoted out\n * of `routing` into its own `lazy` service). Lives on the `/with` port (the\n * one seam the kernel spine may import statically) so `Collection` can hold\n * the back-compat default without a spine→service static import.\n *\n * Lazy mode (`prefetch: false`) skips bulk hydration: reads go per-id\n * against the store and land in a bounded LRU working set. The strategy owns\n * constructing that LRU:\n *\n * - `withLazy()` (`with-store/lazy/active.ts`, subpath `@noy-db/hub/lazy`)\n * is the explicit catalog opt-in — silent, forward-stable.\n * - {@link IMPLICIT_LAZY} is the floor default reached when a collection\n * declares `prefetch: false` WITHOUT `lazyStrategy: withLazy()`. It keeps\n * the pre-#267 behavior byte-for-byte (back-compat delegation, pre-1.0)\n * but emits a one-time deprecation warn outside test env. It will become\n * a throwing stub at 1.0, letting the LRU leave the floor bundle\n * entirely once the per-record-CEK cache stops pinning `Lru` there.\n *\n * The on-demand read/write branches themselves stay kernel-resident (they\n * are `if (this.lazy)` forks on the hot get/put/scan paths); the service\n * seam owns cache construction + budget validation, which is where the\n * opt-in and the future tree-shake win live.\n * @internal\n */\nimport { Lru, parseBytes } from '../../kernel/cache/index.js'\nimport type { CacheOptions } from '../../kernel/collection.js'\n\nexport interface LazyStrategy {\n /**\n * Build the bounded LRU working-set cache for a lazy collection.\n * MUST throw when `cache` declares neither `maxRecords` nor `maxBytes` —\n * an unbounded lazy cache defeats the purpose.\n */\n createCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V>\n}\n\n/**\n * Shared budget validation + LRU construction — the one implementation both\n * {@link IMPLICIT_LAZY} and `withLazy()` run, so the two paths cannot drift.\n * @internal\n */\nexport function buildLazyCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V> {\n if (!cache || (cache.maxRecords === undefined && cache.maxBytes === undefined)) {\n throw new Error(\n `Collection \"${collection}\": lazy mode (prefetch: false) requires a cache option ` +\n `with maxRecords and/or maxBytes. An unbounded lazy cache defeats the purpose.`,\n )\n }\n const lruOptions: { maxRecords?: number; maxBytes?: number } = {}\n if (cache.maxRecords !== undefined) lruOptions.maxRecords = cache.maxRecords\n if (cache.maxBytes !== undefined) lruOptions.maxBytes = parseBytes(cache.maxBytes)\n return new Lru<string, V>(lruOptions)\n}\n\nlet implicitLazyWarned = false\n\n/**\n * Back-compat default (deprecated): `prefetch: false` without\n * `lazyStrategy: withLazy()`. Identical behavior to the explicit opt-in,\n * plus a one-time deprecation warn (suppressed under NODE_ENV=test,\n * mirroring the listPage fallback warn). @internal\n */\nexport const IMPLICIT_LAZY: LazyStrategy = {\n createCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V> {\n if (!implicitLazyWarned) {\n implicitLazyWarned = true\n if (!(typeof process !== 'undefined' && process.env['NODE_ENV'] === 'test')) {\n console.warn(\n `[noy-db] Collection \"${collection}\": lazy mode (prefetch: false) without the lazy ` +\n `service is deprecated — pass \\`lazyStrategy: withLazy()\\` (from \"@noy-db/hub/lazy\") ` +\n `to createNoydb(). The implicit path will be removed at 1.0.`,\n )\n }\n }\n return buildLazyCache<V>(collection, cache)\n },\n}\n"],"mappings":";;;;;;AA0CO,SAAS,eAAkB,YAAoB,OAAiD;AACrG,MAAI,CAAC,SAAU,MAAM,eAAe,UAAa,MAAM,aAAa,QAAY;AAC9E,UAAM,IAAI;AAAA,MACR,eAAe,UAAU;AAAA,IAE3B;AAAA,EACF;AACA,QAAM,aAAyD,CAAC;AAChE,MAAI,MAAM,eAAe,OAAW,YAAW,aAAa,MAAM;AAClE,MAAI,MAAM,aAAa,OAAW,YAAW,WAAW,WAAW,MAAM,QAAQ;AACjF,SAAO,IAAI,IAAe,UAAU;AACtC;AAEA,IAAI,qBAAqB;AAQlB,IAAM,gBAA8B;AAAA,EACzC,YAAe,YAAoB,OAAiD;AAClF,QAAI,CAAC,oBAAoB;AACvB,2BAAqB;AACrB,UAAI,EAAE,OAAO,YAAY,eAAe,QAAQ,IAAI,UAAU,MAAM,SAAS;AAC3E,gBAAQ;AAAA,UACN,wBAAwB,UAAU;AAAA,QAGpC;AAAA,MACF;AAAA,IACF;AACA,WAAO,eAAkB,YAAY,KAAK;AAAA,EAC5C;AACF;","names":[]}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
normalizeTriggerBy
|
|
3
|
+
} from "./chunk-TPHAGLUH.js";
|
|
4
|
+
import {
|
|
5
|
+
ViaGraph,
|
|
6
|
+
computedEntryParts,
|
|
7
|
+
schemaFieldKeys
|
|
8
|
+
} from "./chunk-BWXGSSFX.js";
|
|
4
9
|
import {
|
|
5
10
|
sha256Hex
|
|
6
11
|
} from "./chunk-SL3JAAKN.js";
|
|
7
12
|
import {
|
|
8
13
|
DerivationCycleError,
|
|
9
|
-
DuplicateBehaviorNameError
|
|
14
|
+
DuplicateBehaviorNameError,
|
|
15
|
+
ValidationError
|
|
10
16
|
} from "./chunk-7O3Y4PPD.js";
|
|
11
17
|
|
|
12
18
|
// src/with-formula/derivations/strategy-hash.ts
|
|
@@ -30,6 +36,23 @@ function stripArtifactSuffix(displayId) {
|
|
|
30
36
|
const suffix = `.${WHOLE_RECORD}`;
|
|
31
37
|
return displayId.endsWith(suffix) ? displayId.slice(0, -suffix.length) : displayId;
|
|
32
38
|
}
|
|
39
|
+
function matchTargetFieldNames(opts) {
|
|
40
|
+
const declared = [];
|
|
41
|
+
const virtual = [];
|
|
42
|
+
for (const [field, entry] of Object.entries(opts.computed ?? {})) {
|
|
43
|
+
declared.push(field);
|
|
44
|
+
const parts = computedEntryParts(entry);
|
|
45
|
+
if (parts.mode === "virtual") virtual.push(field);
|
|
46
|
+
}
|
|
47
|
+
for (const [field, spec] of Object.entries(opts.viaFields ?? {})) {
|
|
48
|
+
declared.push(field);
|
|
49
|
+
const descriptors = spec.descriptors ?? [];
|
|
50
|
+
for (const d of descriptors) {
|
|
51
|
+
if (d._viaBrand === "computed" && d.mode === "virtual") virtual.push(field);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { declared, virtual };
|
|
55
|
+
}
|
|
33
56
|
var DerivationRegistry = class {
|
|
34
57
|
_bySource = /* @__PURE__ */ new Map();
|
|
35
58
|
_byOutput = /* @__PURE__ */ new Map();
|
|
@@ -46,7 +69,8 @@ var DerivationRegistry = class {
|
|
|
46
69
|
}
|
|
47
70
|
const outputKeys = Object.keys(spec.outputs);
|
|
48
71
|
const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive, spec.sources);
|
|
49
|
-
const
|
|
72
|
+
const triggers = normalizeTriggerBy(spec.triggerBy);
|
|
73
|
+
const reg = { spec, strategyHash, triggers };
|
|
50
74
|
if (spec.name !== void 0) this._byName.set(spec.name, reg);
|
|
51
75
|
const fromSource = this._bySource.get(spec.source);
|
|
52
76
|
if (fromSource) fromSource.push(reg);
|
|
@@ -56,7 +80,7 @@ var DerivationRegistry = class {
|
|
|
56
80
|
if (fromExtra) fromExtra.push(reg);
|
|
57
81
|
else this._bySource.set(extra, [reg]);
|
|
58
82
|
}
|
|
59
|
-
for (const t of
|
|
83
|
+
for (const t of triggers) {
|
|
60
84
|
const fromTrigger = this._bySource.get(t.collection);
|
|
61
85
|
if (fromTrigger) fromTrigger.push(reg);
|
|
62
86
|
else this._bySource.set(t.collection, [reg]);
|
|
@@ -81,6 +105,62 @@ var DerivationRegistry = class {
|
|
|
81
105
|
strategiesProducingOutput(collection) {
|
|
82
106
|
return this._byOutput.get(collection) ?? [];
|
|
83
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The #1253-pattern typo guard for match fields (#1249): a misspelt
|
|
110
|
+
* `to`/`from` silently matches nothing forever, so validate against the
|
|
111
|
+
* collection's enumerable field set at the earliest point it exists.
|
|
112
|
+
* `schemaFieldKeys(schema) === undefined` (TS-generic collection,
|
|
113
|
+
* unreadable validator) is DELIBERATELY silent — those fields are real
|
|
114
|
+
* and unenumerable. `configKeys` folds in the collection's other
|
|
115
|
+
* non-schema field declarations (fieldMeta/moneyFields/dictKeyFields/
|
|
116
|
+
* refs/computed); `denormExempt` is derived here from this registry's
|
|
117
|
+
* own strategies rather than taken as a parameter — a field this
|
|
118
|
+
* collection's own derivations write via `denorm` is never a typo.
|
|
119
|
+
*/
|
|
120
|
+
validateFieldsFor(collectionName, schema, configKeys, viaSources = void 0) {
|
|
121
|
+
const targets = matchTargetFieldNames(viaSources ?? {});
|
|
122
|
+
const virtual = new Set(targets.virtual);
|
|
123
|
+
const shapeKeys = schemaFieldKeys(schema);
|
|
124
|
+
if (shapeKeys === void 0 && virtual.size === 0) return;
|
|
125
|
+
const keys = shapeKeys === void 0 ? void 0 : /* @__PURE__ */ new Set([...shapeKeys, ...configKeys, ...targets.declared]);
|
|
126
|
+
const denormExempt = /* @__PURE__ */ new Set();
|
|
127
|
+
const regs = new Set([...this._bySource.values()].flat());
|
|
128
|
+
for (const reg of regs) {
|
|
129
|
+
if (reg.spec.source !== collectionName) continue;
|
|
130
|
+
for (const out of Object.values(reg.spec.outputs)) {
|
|
131
|
+
if (out.collection === collectionName) for (const d of out.denorm ?? []) denormExempt.add(d);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const unknown = (f) => keys !== void 0 && !keys.has(f) && !denormExempt.has(f);
|
|
135
|
+
for (const reg of regs) {
|
|
136
|
+
const name = reg.spec.name ?? reg.spec.source;
|
|
137
|
+
for (const t of reg.triggers) {
|
|
138
|
+
for (const p of t.match) {
|
|
139
|
+
if (reg.spec.source === collectionName) {
|
|
140
|
+
if (virtual.has(p.to)) throw new ValidationError(this._virtualMatchMessage(name, collectionName, p.to, "to"));
|
|
141
|
+
if (unknown(p.to)) {
|
|
142
|
+
throw new ValidationError(
|
|
143
|
+
`derivation "${name}": triggerBy match names source field "${p.to}", which "${collectionName}" does not declare \u2014 a typo here silently matches nothing forever`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (t.collection === collectionName && p.from !== "id") {
|
|
148
|
+
if (virtual.has(p.from)) throw new ValidationError(this._virtualMatchMessage(name, collectionName, p.from, "from"));
|
|
149
|
+
if (unknown(p.from)) {
|
|
150
|
+
throw new ValidationError(
|
|
151
|
+
`derivation "${name}": triggerBy match reads "${p.from}" from written "${collectionName}" records, which that collection does not declare \u2014 a typo here silently matches nothing forever`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** #1266 — one message shape for both match sides; see {@link matchTargetFieldNames}. */
|
|
160
|
+
_virtualMatchMessage(name, collectionName, field, side) {
|
|
161
|
+
const where = side === "to" ? `names source field "${field}", which "${collectionName}"` : `reads "${field}" from written "${collectionName}" records, which that collection`;
|
|
162
|
+
return `derivation "${name}": triggerBy match ${where} declares as a VIRTUAL computed field \u2014 virtual fields are evaluated on read and never stored, so the matcher would read every candidate record and match nothing, forever. Declare it as \`mode: 'materialized'\` (stored, indexable) or match on a stored field.`;
|
|
163
|
+
}
|
|
84
164
|
/**
|
|
85
165
|
* All registered strategies as a flat, deduplicated array.
|
|
86
166
|
* Each strategy is indexed once per source (not once per output key),
|
|
@@ -172,4 +252,4 @@ var DerivationRegistry = class {
|
|
|
172
252
|
export {
|
|
173
253
|
DerivationRegistry
|
|
174
254
|
};
|
|
175
|
-
//# sourceMappingURL=chunk-
|
|
255
|
+
//# sourceMappingURL=chunk-AJQY44CE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-formula/derivations/strategy-hash.ts","../src/with-formula/derivations/registry.ts"],"sourcesContent":["/**\n * Deterministic hash of a derivation strategy's \"shape\": source\n * collection, declared sibling sources, output keys, derive function\n * source. Used to detect\n * strategy drift: a record whose `_derivedFrom.strategyHash` doesn't\n * match the current strategy is considered stale.\n *\n * Web Crypto SHA-256 — no extra deps.\n */\nimport { sha256Hex } from '../../kernel/enclave/index.js'\n\nexport async function computeStrategyHash(\n source: string,\n outputKeys: readonly string[],\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n derive: (...args: any[]) => any,\n sources?: ReadonlyArray<string>,\n): Promise<string> {\n const canonical = JSON.stringify({\n source,\n outputs: [...outputKeys].sort(),\n derive: derive.toString(),\n // Declared sibling sources — adding/removing a trigger\n // collection invalidates cached derived records. Omitted when empty\n // so strategies without siblings keep their existing hash.\n ...(sources?.length ? { sources: [...sources].sort() } : {}),\n })\n const bytes = new TextEncoder().encode(canonical)\n return sha256Hex(bytes)\n}\n","import { computedEntryParts, type ComputedEntryParts } from '../../kernel/collection-config.js'\nimport { DerivationCycleError, DuplicateBehaviorNameError, ValidationError } from '../../kernel/errors.js'\nimport { ViaGraph, type FieldRef, type EdgeKind, type Grain } from '../../kernel/via/graph.js'\nimport { schemaFieldKeys } from '../../with-shape/introspection/describe.js'\nimport { computeStrategyHash } from './strategy-hash.js'\nimport { normalizeTriggerBy, type NormalizedTrigger } from './trigger-match.js'\nimport type { DerivationSpec } from './types.js'\n\n/**\n * Whole-record artifact-grain field marker (#638 Task 2). A derivation's\n * `derive()`/a rollup's `compute()` reads the WHOLE source record — there is\n * no single declared field to point at — so a trigger/output collection is\n * modelled as one artifact node per collection (`kernel/via/graph.ts`'s\n * \"Artifact-grain targets... modelled as a field node whose field is the\n * artifact key\" convention). MUST match `materialized-views/registry.ts`'s\n * and `vault.ts`'s overlay-edge marker so cross-registry edges (a derivation\n * feeding into / out of an MV or overlay collection) resolve to the SAME\n * graph node.\n */\nconst WHOLE_RECORD = '*'\n\n/** Strip this registry's `.${WHOLE_RECORD}` artifact suffix off a graph cycle\n * path entry, recovering the bare collection name — preserves the exact\n * pre-#638 `DerivationCycleError` message shape (behavior lock) for a pure\n * record-shape derivation cycle. A rollup's real-field target (not suffixed\n * `.${WHOLE_RECORD}`) is left untouched. */\nfunction stripArtifactSuffix(displayId: string): string {\n const suffix = `.${WHOLE_RECORD}`\n return displayId.endsWith(suffix) ? displayId.slice(0, -suffix.length) : displayId\n}\n\ninterface RegisteredStrategy {\n // Type-erased to allow the registry to hold heterogeneous strategies.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: DerivationSpec<any, any>\n strategyHash: string\n readonly triggers: ReadonlyArray<NormalizedTrigger>\n}\n\n/**\n * Vault-internal registry of derivation strategies. Owned by `Vault`;\n * not exported.\n *\n * @internal\n */\n/**\n * Field names a `triggerBy` match may NOT target, and the ones it may (#1266).\n *\n * Lives HERE, not in `kernel/collection-config.ts` where computed-mode logic\n * otherwise sits, because that module is in the FLOOR bundle: putting twenty\n * lines there cost every consumer 5.2% of the floor whether or not they use\n * derivations. Registry code is already behind the derivations opt-in.\n *\n * A derivation matcher reads the STORED record, so a match target must be a\n * field that is stored. `mode: 'virtual'` computed fields are evaluated on the\n * READ path and never persisted — but they appear in `computed:` (and in\n * `via(computed(...))`) exactly like materialized ones, so the registration\n * guard accepted them and the fan-out then matched nothing, forever: the guard's\n * own stated failure mode, reached through the guard rather than around it.\n *\n * Rejected rather than supported. Matching a virtual field means running user\n * code for every candidate row, which turns an indexed narrow into a full scan\n * of the collection; `mode: 'materialized'` is stored, already works, and is\n * what the error points the caller at.\n *\n * `declared` also folds in `viaFields`, which the guard's key set previously\n * omitted entirely — a `via()`-declared MATERIALIZED field is a perfectly good\n * match target and was being rejected as a typo. An over-firing guard teaches\n * people to stop trusting it, so both directions are fixed together.\n */\nfunction matchTargetFieldNames(opts: {\n // Deliberately `unknown`-valued: the caller is generic over the record type\n // (`ComputedFields<T>`), and this reads only `mode`, so narrowing the value\n // type here would force a variance cast at every call site instead of one\n // here. Both shapes are duck-checked below.\n readonly computed?: Readonly<Record<string, unknown>> | undefined\n readonly viaFields?: Readonly<Record<string, unknown>> | undefined\n}): { readonly declared: readonly string[]; readonly virtual: readonly string[] } {\n const declared: string[] = []\n const virtual: string[] = []\n for (const [field, entry] of Object.entries(opts.computed ?? {})) {\n declared.push(field)\n const parts: ComputedEntryParts = computedEntryParts(entry as Parameters<typeof computedEntryParts>[0])\n if (parts.mode === 'virtual') virtual.push(field)\n }\n for (const [field, spec] of Object.entries(opts.viaFields ?? {})) {\n declared.push(field)\n const descriptors = (spec as { descriptors?: readonly unknown[] }).descriptors ?? []\n for (const d of descriptors) {\n if ((d as { _viaBrand?: unknown })._viaBrand === 'computed'\n && (d as { mode?: unknown }).mode === 'virtual') virtual.push(field)\n }\n }\n return { declared, virtual }\n}\n\nexport class DerivationRegistry {\n private readonly _bySource = new Map<string, RegisteredStrategy[]>()\n private readonly _byOutput = new Map<string, RegisteredStrategy[]>()\n private readonly _byName = new Map<string, RegisteredStrategy>()\n\n /**\n * Register a derivation strategy. If `spec.name` is given and already\n * registered by another derivation in this vault, throws\n * {@link DuplicateBehaviorNameError} before indexing.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async register(spec: DerivationSpec<any, any>): Promise<void> {\n if (spec.name !== undefined && this._byName.has(spec.name)) {\n throw new DuplicateBehaviorNameError(spec.name, 'derivation')\n }\n\n const outputKeys = Object.keys(spec.outputs)\n const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive, spec.sources)\n const triggers = normalizeTriggerBy(spec.triggerBy)\n const reg: RegisteredStrategy = { spec, strategyHash, triggers }\n\n if (spec.name !== undefined) this._byName.set(spec.name, reg)\n\n const fromSource = this._bySource.get(spec.source)\n if (fromSource) fromSource.push(reg)\n else this._bySource.set(spec.source, [reg])\n\n // Declared sibling sources index the SAME `reg` under each\n // extra collection so `strategiesForSource(extra)` returns it and a\n // sibling write re-fires the derivation. Sibling keys also enter\n // `_bySource`, so `validate()`'s cycle DFS walks them automatically.\n for (const extra of spec.sources ?? []) {\n const fromExtra = this._bySource.get(extra)\n if (fromExtra) fromExtra.push(reg)\n else this._bySource.set(extra, [reg])\n }\n\n // FK triggers index the SAME `reg` under each parent collection so\n // a parent write re-fires the derivation (fanned out to matching source\n // records in `dispatchDerivations`). Like sources[], these keys enter\n // `_bySource` so the cycle DFS walks the trigger→output edge.\n for (const t of triggers) {\n const fromTrigger = this._bySource.get(t.collection)\n if (fromTrigger) fromTrigger.push(reg)\n else this._bySource.set(t.collection, [reg])\n }\n\n // Rollup: a write to the child `from` collection recomputes\n // the parent (= spec.source = into) at id child[key]. Index under `from`\n // so a child write fires it; spec.source (into) is already indexed above,\n // so a parent write also recomputes its own aggregate (covers the\n // parent-created-after-children case). The from→into edge enters the cycle\n // DFS automatically.\n if (spec.rollup) {\n const fromRollup = this._bySource.get(spec.rollup.from)\n if (fromRollup) fromRollup.push(reg)\n else this._bySource.set(spec.rollup.from, [reg])\n }\n\n for (const key of outputKeys) {\n const output = spec.outputs[key]\n if (!output) continue\n const outputCollection = output.collection\n const arr = this._byOutput.get(outputCollection)\n if (arr) arr.push(reg)\n else this._byOutput.set(outputCollection, [reg])\n }\n }\n\n strategiesForSource(source: string): ReadonlyArray<RegisteredStrategy> {\n return this._bySource.get(source) ?? []\n }\n\n strategiesProducingOutput(collection: string): ReadonlyArray<RegisteredStrategy> {\n return this._byOutput.get(collection) ?? []\n }\n\n /**\n * The #1253-pattern typo guard for match fields (#1249): a misspelt\n * `to`/`from` silently matches nothing forever, so validate against the\n * collection's enumerable field set at the earliest point it exists.\n * `schemaFieldKeys(schema) === undefined` (TS-generic collection,\n * unreadable validator) is DELIBERATELY silent — those fields are real\n * and unenumerable. `configKeys` folds in the collection's other\n * non-schema field declarations (fieldMeta/moneyFields/dictKeyFields/\n * refs/computed); `denormExempt` is derived here from this registry's\n * own strategies rather than taken as a parameter — a field this\n * collection's own derivations write via `denorm` is never a typo.\n */\n validateFieldsFor(\n collectionName: string,\n schema: unknown,\n configKeys: ReadonlyArray<string>,\n viaSources: {\n readonly computed?: Readonly<Record<string, unknown>> | undefined\n readonly viaFields?: Readonly<Record<string, unknown>> | undefined\n } | undefined = undefined,\n ): void {\n const targets = matchTargetFieldNames(viaSources ?? {})\n const virtual = new Set(targets.virtual)\n const shapeKeys = schemaFieldKeys(schema)\n // The two checks have DIFFERENT preconditions, which is why one loop runs\n // both rather than an early return covering both. The typo check needs a\n // readable field list and must stay silent without one — a false \"unknown\n // field\" on a TS-generic collection would be worse than no check. The\n // virtual check needs nothing but the declaration in hand: \"declared, and\n // never stored\" is provable from the declaration alone (#1266).\n if (shapeKeys === undefined && virtual.size === 0) return\n const keys = shapeKeys === undefined ? undefined\n : new Set([...shapeKeys, ...configKeys, ...targets.declared])\n const denormExempt = new Set<string>()\n const regs = new Set([...this._bySource.values()].flat())\n for (const reg of regs) {\n if (reg.spec.source !== collectionName) continue\n for (const out of Object.values(reg.spec.outputs) as Array<{ collection?: string; denorm?: readonly string[] }>) {\n if (out.collection === collectionName) for (const d of out.denorm ?? []) denormExempt.add(d)\n }\n }\n const unknown = (f: string): boolean =>\n keys !== undefined && !keys.has(f) && !denormExempt.has(f)\n for (const reg of regs) {\n const name = reg.spec.name ?? reg.spec.source\n for (const t of reg.triggers) {\n for (const p of t.match) {\n // `to` reads the SOURCE record; `from` reads the WRITTEN record.\n // Both are the stored shape, so both refuse a virtual target.\n if (reg.spec.source === collectionName) {\n if (virtual.has(p.to)) throw new ValidationError(this._virtualMatchMessage(name, collectionName, p.to, 'to'))\n if (unknown(p.to)) {\n throw new ValidationError(\n `derivation \"${name}\": triggerBy match names source field \"${p.to}\", which \"${collectionName}\" does not declare — a typo here silently matches nothing forever`)\n }\n }\n if (t.collection === collectionName && p.from !== 'id') {\n if (virtual.has(p.from)) throw new ValidationError(this._virtualMatchMessage(name, collectionName, p.from, 'from'))\n if (unknown(p.from)) {\n throw new ValidationError(\n `derivation \"${name}\": triggerBy match reads \"${p.from}\" from written \"${collectionName}\" records, which that collection does not declare — a typo here silently matches nothing forever`)\n }\n }\n }\n }\n }\n }\n\n /** #1266 — one message shape for both match sides; see {@link matchTargetFieldNames}. */\n private _virtualMatchMessage(name: string, collectionName: string, field: string, side: 'to' | 'from'): string {\n const where = side === 'to'\n ? `names source field \"${field}\", which \"${collectionName}\"`\n : `reads \"${field}\" from written \"${collectionName}\" records, which that collection`\n return `derivation \"${name}\": triggerBy match ${where} declares as a VIRTUAL computed field — virtual fields are evaluated on read and never stored, so the matcher would read every candidate record and match nothing, forever. Declare it as \\`mode: 'materialized'\\` (stored, indexable) or match on a stored field.`\n }\n\n /**\n * All registered strategies as a flat, deduplicated array.\n * Each strategy is indexed once per source (not once per output key),\n * so iterating `_bySource.values()` naturally yields each strategy\n * exactly once per source — deduplication is handled by flattening\n * the per-source arrays and collecting into a Set by identity.\n *\n * Used by `dumpSchema()` / `describeDerivations()` in the introspection\n * walker to populate the derivations map.\n */\n all(): ReadonlyArray<RegisteredStrategy> {\n const seen = new Set<RegisteredStrategy>()\n for (const strategies of this._bySource.values()) {\n for (const s of strategies) seen.add(s)\n }\n return [...seen]\n }\n\n /**\n * Graph edges for #638 Task 2: one `'derivation'`/`'record'` edge per\n * (non-self-write) output key — target = the output collection (a\n * `WHOLE_RECORD` artifact node), sources = every collection that can\n * trigger this strategy (`source`, `sources[]`, `triggerBy[].collection`,\n * and — for a rollup — `rollup.from`), mirroring EXACTLY the trigger keys\n * `register()` indexes under `_bySource`. The self-write reverse-denorm\n * output (`output.collection === source` with `denorm` declared) is\n * skipped — the SAME condition the old local DFS used; it is not a cycle\n * (see the comment `validate()` used to carry, now on this skip).\n * Rollups ADDITIONALLY get a dedicated `'rollup'`/`'aggregate'` edge\n * targeting the real `rollup.field` on the parent (`source`) — the\n * self-write output above only covers the cycle-DFS skip, not\n * dispatch/taint (Tasks 3/4/6), which need the real field.\n */\n edges(): ReadonlyArray<{ readonly target: FieldRef; readonly sources: readonly FieldRef[]; readonly kind: EdgeKind; readonly grain: Grain }> {\n const out: Array<{ target: FieldRef; sources: FieldRef[]; kind: EdgeKind; grain: Grain }> = []\n for (const reg of this.all()) {\n const spec = reg.spec\n const triggerCollections = [\n spec.source,\n ...(spec.sources ?? []),\n ...(spec.triggerBy ?? []).map((t) => t.collection),\n ...(spec.rollup ? [spec.rollup.from] : []),\n ]\n const sources: FieldRef[] = triggerCollections.map((c) => ({ collection: c, field: WHOLE_RECORD }))\n for (const key of Object.keys(spec.outputs)) {\n const output = spec.outputs[key]\n if (!output) continue\n if (output.shape === 'record' && output.collection === spec.source && output.denorm !== undefined) continue\n out.push({ target: { collection: output.collection, field: WHOLE_RECORD }, sources, kind: 'derivation', grain: 'record' })\n }\n if (spec.rollup) {\n out.push({\n target: { collection: spec.source, field: spec.rollup.field },\n sources: [{ collection: spec.rollup.from, field: WHOLE_RECORD }],\n kind: 'rollup',\n grain: 'aggregate',\n })\n }\n }\n return out\n }\n\n /**\n * Cycle detection, delegated to `ViaGraph.assertAcyclic()` (#638 Task 2 —\n * retires the local DFS). Call after all `register()` calls complete (i.e.\n * at vault open). Throws `DerivationCycleError` on the first cycle found —\n * SAME class/timing as before; the message/path is byte-identical to the\n * pre-#638 shape for a pure record-shape derivation cycle (the\n * `WHOLE_RECORD` artifact suffix is stripped back to a bare collection\n * name before re-throwing).\n *\n * `graph` is the caller's shared per-vault graph, ALREADY carrying this\n * registry's `edges()` (the caller registers them — see `Vault._initDerivations`)\n * — omit it (e.g. this registry's own unit tests) to validate this\n * registry's edges in isolation against a throwaway graph.\n */\n validate(graph?: ViaGraph): void {\n const g = graph ?? new ViaGraph()\n if (!graph) {\n for (const edge of this.edges()) g.registerDerived(edge.target, edge.sources, edge.kind, edge.grain)\n }\n try {\n g.assertAcyclic()\n } catch (e) {\n if (e instanceof DerivationCycleError) throw new DerivationCycleError(e.path.map(stripArtifactSuffix))\n throw e\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createNoydb
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZVCC36XQ.js";
|
|
4
4
|
import {
|
|
5
5
|
deriveSchemaManifest
|
|
6
6
|
} from "./chunk-BBWVXF57.js";
|
|
@@ -286,4 +286,4 @@ export {
|
|
|
286
286
|
followRedirects,
|
|
287
287
|
open
|
|
288
288
|
};
|
|
289
|
-
//# sourceMappingURL=chunk-
|
|
289
|
+
//# sourceMappingURL=chunk-BMELYFHU.js.map
|
|
@@ -38,11 +38,34 @@ function withDerivation(spec) {
|
|
|
38
38
|
`withDerivation: triggerBy.collection must not equal the source "${spec.source}" (use sources[] for same-id triggers)`
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const hasOn = typeof t.on === "string";
|
|
42
|
+
const hasMatch = Array.isArray(t.match);
|
|
43
|
+
if (hasOn === hasMatch) {
|
|
44
|
+
throw new ValidationError(
|
|
45
|
+
`withDerivation: triggerBy on "${t.collection}" needs exactly one of \`on\` or \`match\``
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (hasOn && t.on.length === 0) {
|
|
42
49
|
throw new ValidationError(
|
|
43
50
|
`withDerivation: triggerBy on "${t.collection}" needs a non-empty \`on\` (the FK field on the source)`
|
|
44
51
|
);
|
|
45
52
|
}
|
|
53
|
+
if (hasMatch) {
|
|
54
|
+
const match = t.match;
|
|
55
|
+
if (match.length === 0) {
|
|
56
|
+
throw new ValidationError(`withDerivation: triggerBy match on "${t.collection}" must be non-empty`);
|
|
57
|
+
}
|
|
58
|
+
const seen = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const p of match) {
|
|
60
|
+
if (typeof p?.from !== "string" || p.from.length === 0 || typeof p?.to !== "string" || p.to.length === 0) {
|
|
61
|
+
throw new ValidationError(`withDerivation: triggerBy match on "${t.collection}" needs non-empty \`from\` and \`to\` in every pair`);
|
|
62
|
+
}
|
|
63
|
+
if (seen.has(p.to)) {
|
|
64
|
+
throw new ValidationError(`withDerivation: triggerBy match on "${t.collection}" repeats \`to: "${p.to}"\` \u2014 two pairs constraining one source field is a contradiction, not a wider match`);
|
|
65
|
+
}
|
|
66
|
+
seen.add(p.to);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
46
69
|
if (t.maxFanout !== void 0 && (!Number.isInteger(t.maxFanout) || t.maxFanout < 1)) {
|
|
47
70
|
throw new ValidationError(
|
|
48
71
|
`withDerivation: triggerBy maxFanout on "${t.collection}" must be a positive integer (got ${String(t.maxFanout)}).`
|
|
@@ -126,4 +149,4 @@ export {
|
|
|
126
149
|
withDerivation,
|
|
127
150
|
withRollup
|
|
128
151
|
};
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
152
|
+
//# sourceMappingURL=chunk-BUE2D7PX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-formula/derivations/with-derivation.ts","../src/with-formula/derivations/with-rollup.ts"],"sourcesContent":["import { ValidationError } from '../../kernel/errors.js'\nimport type { DerivationSpec, DerivationStrategy } from './types.js'\n\n/**\n * Register a deterministic derivation: one source collection → one or\n * more typed outputs, computed by the user's `derive` function on\n * plaintext after DEK unwrap. Outputs are encrypted with the same DEK\n * as the source and written via the standard `Collection.put` path.\n *\n * See design-history/2026-05-01-dim14-derivation-v1-design.md.\n */\nexport function withDerivation<\n TSource extends Record<string, unknown>,\n TOutputs extends Record<string, Record<string, unknown> | ReadonlyArray<Record<string, unknown>>>,\n>(spec: DerivationSpec<TSource, TOutputs>): DerivationStrategy {\n if (!spec.source || spec.source.length === 0) {\n throw new ValidationError('withDerivation: source collection name is required')\n }\n if (!spec.outputs || Object.keys(spec.outputs).length === 0) {\n throw new ValidationError('withDerivation: outputs map must declare at least one output')\n }\n if (spec.deterministic !== true) {\n throw new ValidationError('withDerivation: v1 only supports deterministic derivations')\n }\n if (typeof spec.derive !== 'function') {\n throw new ValidationError('withDerivation: derive must be a function')\n }\n\n // Validate declared sibling sources. Each must be a non-empty\n // string and must differ from the primary source — a self-reference\n // would double-register the strategy under the same `_bySource` key.\n if (spec.sources !== undefined) {\n for (const extra of spec.sources) {\n if (typeof extra !== 'string' || extra.length === 0) {\n throw new ValidationError('withDerivation: each entry in sources[] must be a non-empty string')\n }\n if (extra === spec.source) {\n throw new ValidationError(\n `withDerivation: sources[] must not contain the primary source \"${spec.source}\"`,\n )\n }\n }\n }\n\n // Validate FK triggers. Each `collection` must be a non-empty\n // string differing from the primary source, and `on` a non-empty field.\n if (spec.triggerBy !== undefined) {\n for (const t of spec.triggerBy) {\n if (typeof t?.collection !== 'string' || t.collection.length === 0) {\n throw new ValidationError('withDerivation: each triggerBy entry needs a non-empty `collection`')\n }\n if (t.collection === spec.source) {\n throw new ValidationError(\n `withDerivation: triggerBy.collection must not equal the source \"${spec.source}\" (use sources[] for same-id triggers)`,\n )\n }\n const hasOn = typeof (t as { on?: unknown }).on === 'string'\n const hasMatch = Array.isArray((t as { match?: unknown }).match)\n if (hasOn === hasMatch) {\n throw new ValidationError(\n `withDerivation: triggerBy on \"${t.collection}\" needs exactly one of \\`on\\` or \\`match\\``,\n )\n }\n if (hasOn && (t as { on: string }).on.length === 0) {\n throw new ValidationError(\n `withDerivation: triggerBy on \"${t.collection}\" needs a non-empty \\`on\\` (the FK field on the source)`,\n )\n }\n if (hasMatch) {\n const match = (t as { match: ReadonlyArray<{ from: string; to: string }> }).match\n if (match.length === 0) {\n throw new ValidationError(`withDerivation: triggerBy match on \"${t.collection}\" must be non-empty`)\n }\n const seen = new Set<string>()\n for (const p of match) {\n if (typeof p?.from !== 'string' || p.from.length === 0 || typeof p?.to !== 'string' || p.to.length === 0) {\n throw new ValidationError(`withDerivation: triggerBy match on \"${t.collection}\" needs non-empty \\`from\\` and \\`to\\` in every pair`)\n }\n if (seen.has(p.to)) {\n throw new ValidationError(`withDerivation: triggerBy match on \"${t.collection}\" repeats \\`to: \"${p.to}\"\\` — two pairs constraining one source field is a contradiction, not a wider match`)\n }\n seen.add(p.to)\n }\n }\n if (t.maxFanout !== undefined && (!Number.isInteger(t.maxFanout) || t.maxFanout < 1)) {\n throw new ValidationError(\n `withDerivation: triggerBy maxFanout on \"${t.collection}\" must be a positive integer (got ${String(t.maxFanout)}).`,\n )\n }\n }\n }\n\n // Validate array-shape outputs.\n const lifecycleMode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {\n // Self-write output (collection === source): reverse-denorm must declare\n // `denorm` (the fields it owns) — field-level provenance.\n if (outputSpec.shape === 'record' && outputSpec.collection === spec.source) {\n if (!outputSpec.denorm || outputSpec.denorm.length === 0) {\n throw new ValidationError(\n `withDerivation: self-write output \"${outputKey}\" (collection === source \"${spec.source}\") `\n + 'must declare `denorm: [...]` naming the fields it maintains.',\n )\n }\n }\n if (outputSpec.shape === 'array') {\n if (lifecycleMode !== 'eager') {\n throw new ValidationError(\n `withDerivation: shape 'array' supports lifecycle 'eager' only in this release `\n + `Output \"${outputKey}\" declared lifecycle '${lifecycleMode}'. `\n + 'Switch to `lifecycle: \"eager\"` or use shape: \"record\".',\n )\n }\n if (typeof outputSpec.key !== 'function') {\n throw new ValidationError(\n `withDerivation: shape 'array' output \"${outputKey}\" requires \\`key: (out) => string\\`.`,\n )\n }\n if (outputSpec.maxFanout !== undefined) {\n if (!Number.isInteger(outputSpec.maxFanout) || outputSpec.maxFanout < 1) {\n throw new ValidationError(\n `withDerivation: maxFanout for output \"${outputKey}\" must be a positive integer `\n + `(got ${String(outputSpec.maxFanout)}).`,\n )\n }\n }\n }\n }\n\n return {\n __noydb_strategy: 'derivation',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: spec as DerivationSpec<any, any>,\n }\n}\n","import { ValidationError } from '../../kernel/errors.js'\nimport type { DerivationSpec, DerivationStrategy } from './types.js'\n\n/** Options for {@link withRollup} (#844b — was an inline literal, so unnameable). */\nexport interface WithRollupOptions<\n TChild extends Record<string, unknown> = Record<string, unknown>,\n TParent extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Child collection — the trigger. */\n readonly from: string\n /** FK field on the child pointing at the parent id. */\n readonly key: keyof TChild & string\n /** Parent collection carrying the maintained summary. */\n readonly into: string\n /** Field on the parent to maintain. */\n readonly field: keyof TParent & string\n /** Reduces the matching children to the value written at `field`. */\n readonly compute: (children: TChild[]) => unknown\n}\n\n/**\n * `withRollup` — aggregate many child records onto a single field of their\n * parent. The reverse of a join: instead of reading children\n * on demand, the parent carries a maintained summary.\n *\n * ```ts\n * withRollup<Sale, Buyer>({\n * from: 'sales', // child collection (the trigger)\n * key: 'buyerId', // FK on the child → parent id\n * into: 'buyers', // parent collection\n * field: 'revenueByYear', // field on the parent to maintain\n * compute: (sales) => groupSumByYear(sales, 'total'),\n * })\n * ```\n *\n * On every write OR delete of a `from` record, the parent at id `child[key]`\n * is recomputed: `compute(allChildren where child[key] === parentId)` is\n * patched onto `parent[field]`. A parent write also recomputes its own\n * aggregate (so a parent created after its children still fills in). Only the\n * `field` is touched — the rest of the parent record is never clobbered — and\n * a value-equality guard suppresses no-op writes. The aggregate is gap-free\n * with respect to child inserts, updates, and deletes.\n *\n * Desugars to a `withDerivation` strategy carrying a `rollup` marker; dispatch\n * handles it without invoking the executor. Eager-only in this slice.\n */\nexport function withRollup<\n TChild extends Record<string, unknown> = Record<string, unknown>,\n TParent extends Record<string, unknown> = Record<string, unknown>,\n>(opts: WithRollupOptions<TChild, TParent>): DerivationStrategy {\n const { from, key, into, field, compute } = opts\n if (!from || from.length === 0) {\n throw new ValidationError('withRollup: `from` (child collection) is required')\n }\n if (!into || into.length === 0) {\n throw new ValidationError('withRollup: `into` (parent collection) is required')\n }\n if (from === into) {\n throw new ValidationError('withRollup: `from` and `into` must be different collections')\n }\n if (!key || key.length === 0) {\n throw new ValidationError('withRollup: `key` (FK field on the child) is required')\n }\n if (!field || field.length === 0) {\n throw new ValidationError('withRollup: `field` (target field on the parent) is required')\n }\n if (typeof compute !== 'function') {\n throw new ValidationError('withRollup: `compute` must be a function')\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const spec: DerivationSpec<any, any> = {\n source: into, // the parent record is what carries the rolled-up field\n deterministic: true,\n rollup: { from, key, field, compute: compute as (children: unknown[]) => unknown },\n // Synthetic self-write output for registry / cycle bookkeeping. Dispatch\n // patches `field` directly (value-equality guarded); the executor is not run.\n outputs: { value: { shape: 'record', collection: into, denorm: [field] } },\n derive: () => ({ value: {} }), // never invoked for a rollup strategy\n lifecycle: 'eager',\n }\n\n return { __noydb_strategy: 'derivation', spec }\n}\n"],"mappings":"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","names":[]}
|