@noy-db/hub 0.2.0-pre.10 → 0.2.0-pre.12
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/README.md +126 -0
- package/dist/aggregate/index.cjs +289 -12
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +2 -2
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/aggregate/index.js +7 -7
- package/dist/aggregate/index.js.map +1 -1
- package/dist/attestation/index.cjs.map +1 -1
- package/dist/attestation/index.d.cts +3 -3
- package/dist/attestation/index.d.ts +3 -3
- package/dist/attestation/index.js +6 -6
- package/dist/blobs/index.cjs +28 -0
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +4 -4
- package/dist/blobs/index.d.ts +4 -4
- package/dist/blobs/index.js +5 -5
- package/dist/bundle/index.cjs +1468 -19
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +5 -5
- package/dist/bundle/index.d.ts +5 -5
- package/dist/bundle/index.js +9 -9
- package/dist/{chunk-7CEGU63S.js → chunk-4BHFNKTP.js} +2 -2
- package/dist/{chunk-5OEJ6GOT.js → chunk-5ARRXIVR.js} +2 -2
- package/dist/{chunk-DRXIZOFV.js → chunk-6AD5TBF2.js} +31 -3
- package/dist/chunk-6AD5TBF2.js.map +1 -0
- package/dist/{chunk-YM7LFCG7.js → chunk-6BYBVRZU.js} +3 -3
- package/dist/{chunk-5IXJGFF2.js → chunk-7JJE3OMJ.js} +5 -5
- package/dist/{chunk-HHOO7HGH.js → chunk-7LVRIW4G.js} +4 -4
- package/dist/{chunk-O6EJ6WTI.js → chunk-AGRC7NQQ.js} +62 -2
- package/dist/chunk-AGRC7NQQ.js.map +1 -0
- package/dist/{chunk-IMYKDWB4.js → chunk-B7GGYNKQ.js} +2 -2
- package/dist/{chunk-BDV7INMP.js → chunk-BXOUVUES.js} +4 -4
- package/dist/{chunk-FO3UEG4S.js → chunk-C2CIIQRG.js} +2 -2
- package/dist/{chunk-ZROPXHJY.js → chunk-CHBXWJZQ.js} +2 -2
- package/dist/{chunk-RYIL3PI2.js → chunk-CILT6V3V.js} +2 -2
- package/dist/{chunk-PXTQPZO4.js → chunk-DLTU4M2I.js} +6 -6
- package/dist/{chunk-GAUEWM7D.js → chunk-EKNUBIIQ.js} +4 -4
- package/dist/{chunk-HQSQC2XL.js → chunk-GFPR7VJS.js} +17 -4
- package/dist/chunk-GFPR7VJS.js.map +1 -0
- package/dist/{chunk-6EOXTJS2.js → chunk-HBAJDI2N.js} +5 -5
- package/dist/{chunk-PVUUIWHY.js → chunk-HLGDYFWR.js} +10 -3
- package/dist/chunk-HLGDYFWR.js.map +1 -0
- package/dist/{chunk-RRNA5GKT.js → chunk-IEPT7HVP.js} +2 -2
- package/dist/{chunk-R233SLY3.js → chunk-IUBHXEPJ.js} +2 -2
- package/dist/{chunk-CH22FZHT.js → chunk-L6BYRCYB.js} +2 -2
- package/dist/{chunk-5OX6XVNS.js → chunk-LOA2VCMS.js} +5 -5
- package/dist/{chunk-BB27JMWB.js → chunk-LSEW3ZZ2.js} +3 -3
- package/dist/{chunk-Y26YV5R3.js → chunk-LWSD4QPT.js} +3 -3
- package/dist/{chunk-WIRRPTFH.js → chunk-LYNNZEQD.js} +1 -1
- package/dist/chunk-LYNNZEQD.js.map +1 -0
- package/dist/{chunk-26NK23DZ.js → chunk-M45IRXDM.js} +3 -3
- package/dist/{chunk-CXJG63MA.js → chunk-NP6EZT44.js} +20 -6
- package/dist/chunk-NP6EZT44.js.map +1 -0
- package/dist/{chunk-GNHAC43Q.js → chunk-O53RIZCC.js} +5 -5
- package/dist/chunk-OPDTLHFA.js +783 -0
- package/dist/chunk-OPDTLHFA.js.map +1 -0
- package/dist/{chunk-LSTBFLL2.js → chunk-P3Z5Y2TS.js} +2 -2
- package/dist/{chunk-QSOYKKMD.js → chunk-P4EDT5ZP.js} +2 -2
- package/dist/{chunk-PC6ZEDRL.js → chunk-RHQYVHFH.js} +2 -2
- package/dist/{chunk-3LPV6BXR.js → chunk-RRDWXNBQ.js} +3 -3
- package/dist/{chunk-4CLICFEY.js → chunk-SJJQKNMP.js} +4 -4
- package/dist/{chunk-TY32C732.js → chunk-SZ4N3IL5.js} +5 -5
- package/dist/{chunk-4USCAEDT.js → chunk-TMHJEYW7.js} +502 -60
- package/dist/chunk-TMHJEYW7.js.map +1 -0
- package/dist/{chunk-2N62W5YP.js → chunk-UA6G45ME.js} +3 -3
- package/dist/{chunk-6YLPHBKR.js → chunk-UOC7JMZO.js} +13 -4
- package/dist/chunk-UOC7JMZO.js.map +1 -0
- package/dist/{chunk-DAP2XL7Q.js → chunk-VOXMU6LB.js} +2 -2
- package/dist/chunk-WNRGOVLG.js +64 -0
- package/dist/chunk-WNRGOVLG.js.map +1 -0
- package/dist/{chunk-DJRWA3Q5.js → chunk-WUG3E423.js} +4 -4
- package/dist/{chunk-PM3QYWUU.js → chunk-XHM2SARW.js} +3 -3
- package/dist/{chunk-RC6SU5NO.js → chunk-XSIFXX54.js} +2 -2
- package/dist/{chunk-CXFOITNS.js → chunk-ZC7MNVYN.js} +2 -2
- package/dist/{chunk-6T2UDBKG.js → chunk-ZCFS7U4J.js} +2 -2
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +4 -4
- package/dist/consent/index.d.ts +4 -4
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-2CRLG4F4.js → crypto-AJB72OKN.js} +3 -3
- package/dist/{delegation-ZTRT2PRV.js → delegation-6FCWDRUS.js} +5 -5
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +5 -5
- package/dist/derivations/index.d.ts +5 -5
- package/dist/derivations/index.js +4 -4
- package/dist/{dev-unlock-BOEYl1xl.d.ts → dev-unlock-D3mpVFRc.d.ts} +1 -1
- package/dist/{dev-unlock-AglVnkPY.d.cts → dev-unlock-ckqa_Nso.d.cts} +1 -1
- package/dist/executor-7KSCEIFA.js +8 -0
- package/dist/executor-D2QMNGRJ.js +8 -0
- package/dist/executor-O5AZK7UW.js +11 -0
- package/dist/{fanout-sidecar-OKPMMPLG.js → fanout-sidecar-ZSKEQ6NI.js} +2 -2
- package/dist/guards/index.cjs +53 -1
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +12 -6
- package/dist/guards/index.d.ts +12 -6
- package/dist/guards/index.js +5 -3
- package/dist/{hash-B9m3_fhj.d.ts → hash-CTZVkXLx.d.ts} +1 -1
- package/dist/{hash-RVqz2zi8.d.cts → hash-rDSSd_oW.d.cts} +1 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +5 -5
- package/dist/history/index.d.ts +5 -5
- package/dist/history/index.js +5 -5
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +4 -4
- package/dist/i18n/index.d.ts +4 -4
- package/dist/i18n/index.js +6 -6
- package/dist/immutable-guard-C51vAHuh.d.cts +67 -0
- package/dist/immutable-guard-DyD0qg2k.d.ts +67 -0
- package/dist/index-CkFHr4OP.d.ts +1190 -0
- package/dist/index-Cmop06zJ.d.cts +1190 -0
- package/dist/index.cjs +1636 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -13
- package/dist/index.d.ts +46 -13
- package/dist/index.js +76 -44
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +2 -2
- package/dist/issue-YIYG4OW5.js +12 -0
- package/dist/{ledger-O7FXOG3D.js → ledger-5JMVF7PY.js} +5 -5
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +5 -6
- package/dist/materialized-views/index.d.ts +5 -6
- package/dist/materialized-views/index.js +6 -6
- package/dist/noydb-D5SLAJ6V.js +34 -0
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +5 -5
- package/dist/overlay-views/index.d.ts +5 -5
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +4 -4
- package/dist/periods/index.d.ts +4 -4
- package/dist/periods/index.js +5 -5
- package/dist/{public-envelope-HMYHZIRH.js → public-envelope-PFLZI5MO.js} +4 -4
- package/dist/query/index.cjs +293 -10
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +4 -4
- package/dist/registry-BVQ5ITMF.js +8 -0
- package/dist/registry-JLP3QOLD.js +8 -0
- package/dist/{registry-ST2VNFZC.js → registry-NCY445U5.js} +3 -3
- package/dist/{revoke-S6JMSLUN.js → revoke-7RLGQWZ7.js} +6 -6
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +5 -5
- package/dist/session/index.d.ts +5 -5
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +4 -4
- package/dist/shadow/index.d.ts +4 -4
- package/dist/shadow/index.js +2 -2
- package/dist/{signer-7NPTB3SQ.js → signer-6JF44I4A.js} +5 -5
- package/dist/snapshots/index.cjs.map +1 -1
- package/dist/snapshots/index.d.cts +4 -4
- package/dist/snapshots/index.d.ts +4 -4
- package/dist/snapshots/index.js +4 -4
- package/dist/{stale-VKXSXJF4.js → stale-UBLP3RJ3.js} +2 -2
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +4 -4
- package/dist/store/index.d.ts +4 -4
- package/dist/store/index.js +2 -2
- package/dist/strategy-rtpKDfTC.d.cts +2029 -0
- package/dist/strategy-rtpKDfTC.d.ts +2029 -0
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +3 -3
- package/dist/sync/index.d.ts +3 -3
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +4 -4
- package/dist/team/index.d.ts +4 -4
- package/dist/team/index.js +8 -8
- package/dist/tx/index.cjs +8 -1
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +4 -4
- package/dist/tx/index.d.ts +4 -4
- package/dist/tx/index.js +3 -3
- package/dist/{types-n2_IfwlQ.d.cts → types-BGwjsDef.d.cts} +520 -6
- package/dist/{types-CaNQm4i8.d.ts → types-DRdfwgTG.d.ts} +520 -6
- package/dist/{ulid-CLMjmyhG.d.cts → ulid-D4d0Xto3.d.cts} +1 -1
- package/dist/{ulid-B9SMWj5i.d.ts → ulid-DOTPZ5_h.d.ts} +1 -1
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/vault-group-Z4KB75ZH.js +450 -0
- package/dist/vault-group-Z4KB75ZH.js.map +1 -0
- package/dist/{with-derivation-CVIOPTUf.d.ts → with-derivation-B082Y_WQ.d.ts} +1 -1
- package/dist/{with-derivation-aKrtS7Jj.d.cts → with-derivation-CB1EdcFF.d.cts} +1 -1
- package/dist/{with-materialized-view-C1eA1_T_.d.cts → with-materialized-view-CzRg1Dpr.d.cts} +1 -1
- package/dist/{with-materialized-view-DaYaE8-Q.d.ts → with-materialized-view-Dw4SwjKl.d.ts} +1 -1
- package/dist/{with-overlayed-view-DleJfKcV.d.cts → with-overlayed-view-C9YFKXzn.d.cts} +1 -1
- package/dist/{with-overlayed-view-DQsh2p8H.d.ts → with-overlayed-view-CaCXeW26.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/chunk-2LPPNWF6.js +0 -340
- package/dist/chunk-2LPPNWF6.js.map +0 -1
- package/dist/chunk-4USCAEDT.js.map +0 -1
- package/dist/chunk-6YLPHBKR.js.map +0 -1
- package/dist/chunk-C3WE6UJY.js +0 -19
- package/dist/chunk-C3WE6UJY.js.map +0 -1
- package/dist/chunk-CXJG63MA.js.map +0 -1
- package/dist/chunk-DRXIZOFV.js.map +0 -1
- package/dist/chunk-HQSQC2XL.js.map +0 -1
- package/dist/chunk-O6EJ6WTI.js.map +0 -1
- package/dist/chunk-PVUUIWHY.js.map +0 -1
- package/dist/chunk-WIRRPTFH.js.map +0 -1
- package/dist/executor-S76VN45G.js +0 -8
- package/dist/executor-UCXLIGLW.js +0 -11
- package/dist/executor-ZCNZJMGR.js +0 -8
- package/dist/index-B8bjExET.d.cts +0 -2434
- package/dist/index-DfUbNad8.d.ts +0 -2434
- package/dist/issue-3W6IVLKH.js +0 -12
- package/dist/noydb-YAZNH5TI.js +0 -34
- package/dist/registry-UFIK7CSR.js +0 -8
- package/dist/registry-ZGYYSM5I.js +0 -8
- package/dist/strategy-CT2LCKAX.d.cts +0 -613
- package/dist/strategy-CT2LCKAX.d.ts +0 -613
- package/dist/with-guard-DZQbPzoP.d.cts +0 -18
- package/dist/with-guard-DseETUrF.d.ts +0 -18
- /package/dist/{chunk-7CEGU63S.js.map → chunk-4BHFNKTP.js.map} +0 -0
- /package/dist/{chunk-5OEJ6GOT.js.map → chunk-5ARRXIVR.js.map} +0 -0
- /package/dist/{chunk-YM7LFCG7.js.map → chunk-6BYBVRZU.js.map} +0 -0
- /package/dist/{chunk-5IXJGFF2.js.map → chunk-7JJE3OMJ.js.map} +0 -0
- /package/dist/{chunk-HHOO7HGH.js.map → chunk-7LVRIW4G.js.map} +0 -0
- /package/dist/{chunk-IMYKDWB4.js.map → chunk-B7GGYNKQ.js.map} +0 -0
- /package/dist/{chunk-BDV7INMP.js.map → chunk-BXOUVUES.js.map} +0 -0
- /package/dist/{chunk-FO3UEG4S.js.map → chunk-C2CIIQRG.js.map} +0 -0
- /package/dist/{chunk-ZROPXHJY.js.map → chunk-CHBXWJZQ.js.map} +0 -0
- /package/dist/{chunk-RYIL3PI2.js.map → chunk-CILT6V3V.js.map} +0 -0
- /package/dist/{chunk-PXTQPZO4.js.map → chunk-DLTU4M2I.js.map} +0 -0
- /package/dist/{chunk-GAUEWM7D.js.map → chunk-EKNUBIIQ.js.map} +0 -0
- /package/dist/{chunk-6EOXTJS2.js.map → chunk-HBAJDI2N.js.map} +0 -0
- /package/dist/{chunk-RRNA5GKT.js.map → chunk-IEPT7HVP.js.map} +0 -0
- /package/dist/{chunk-R233SLY3.js.map → chunk-IUBHXEPJ.js.map} +0 -0
- /package/dist/{chunk-CH22FZHT.js.map → chunk-L6BYRCYB.js.map} +0 -0
- /package/dist/{chunk-5OX6XVNS.js.map → chunk-LOA2VCMS.js.map} +0 -0
- /package/dist/{chunk-BB27JMWB.js.map → chunk-LSEW3ZZ2.js.map} +0 -0
- /package/dist/{chunk-Y26YV5R3.js.map → chunk-LWSD4QPT.js.map} +0 -0
- /package/dist/{chunk-26NK23DZ.js.map → chunk-M45IRXDM.js.map} +0 -0
- /package/dist/{chunk-GNHAC43Q.js.map → chunk-O53RIZCC.js.map} +0 -0
- /package/dist/{chunk-LSTBFLL2.js.map → chunk-P3Z5Y2TS.js.map} +0 -0
- /package/dist/{chunk-QSOYKKMD.js.map → chunk-P4EDT5ZP.js.map} +0 -0
- /package/dist/{chunk-PC6ZEDRL.js.map → chunk-RHQYVHFH.js.map} +0 -0
- /package/dist/{chunk-3LPV6BXR.js.map → chunk-RRDWXNBQ.js.map} +0 -0
- /package/dist/{chunk-4CLICFEY.js.map → chunk-SJJQKNMP.js.map} +0 -0
- /package/dist/{chunk-TY32C732.js.map → chunk-SZ4N3IL5.js.map} +0 -0
- /package/dist/{chunk-2N62W5YP.js.map → chunk-UA6G45ME.js.map} +0 -0
- /package/dist/{chunk-DAP2XL7Q.js.map → chunk-VOXMU6LB.js.map} +0 -0
- /package/dist/{chunk-DJRWA3Q5.js.map → chunk-WUG3E423.js.map} +0 -0
- /package/dist/{chunk-PM3QYWUU.js.map → chunk-XHM2SARW.js.map} +0 -0
- /package/dist/{chunk-RC6SU5NO.js.map → chunk-XSIFXX54.js.map} +0 -0
- /package/dist/{chunk-CXFOITNS.js.map → chunk-ZC7MNVYN.js.map} +0 -0
- /package/dist/{chunk-6T2UDBKG.js.map → chunk-ZCFS7U4J.js.map} +0 -0
- /package/dist/{crypto-2CRLG4F4.js.map → crypto-AJB72OKN.js.map} +0 -0
- /package/dist/{delegation-ZTRT2PRV.js.map → delegation-6FCWDRUS.js.map} +0 -0
- /package/dist/{executor-S76VN45G.js.map → executor-7KSCEIFA.js.map} +0 -0
- /package/dist/{executor-UCXLIGLW.js.map → executor-D2QMNGRJ.js.map} +0 -0
- /package/dist/{executor-ZCNZJMGR.js.map → executor-O5AZK7UW.js.map} +0 -0
- /package/dist/{fanout-sidecar-OKPMMPLG.js.map → fanout-sidecar-ZSKEQ6NI.js.map} +0 -0
- /package/dist/{issue-3W6IVLKH.js.map → issue-YIYG4OW5.js.map} +0 -0
- /package/dist/{ledger-O7FXOG3D.js.map → ledger-5JMVF7PY.js.map} +0 -0
- /package/dist/{noydb-YAZNH5TI.js.map → noydb-D5SLAJ6V.js.map} +0 -0
- /package/dist/{public-envelope-HMYHZIRH.js.map → public-envelope-PFLZI5MO.js.map} +0 -0
- /package/dist/{registry-ST2VNFZC.js.map → registry-BVQ5ITMF.js.map} +0 -0
- /package/dist/{registry-UFIK7CSR.js.map → registry-JLP3QOLD.js.map} +0 -0
- /package/dist/{registry-ZGYYSM5I.js.map → registry-NCY445U5.js.map} +0 -0
- /package/dist/{revoke-S6JMSLUN.js.map → revoke-7RLGQWZ7.js.map} +0 -0
- /package/dist/{signer-7NPTB3SQ.js.map → signer-6JF44I4A.js.map} +0 -0
- /package/dist/{stale-VKXSXJF4.js.map → stale-UBLP3RJ3.js.map} +0 -0
package/README.md
CHANGED
|
@@ -178,6 +178,132 @@ Core has zero `node:` imports — it runs unchanged in browsers, Node, Bun, Deno
|
|
|
178
178
|
|
|
179
179
|
CSV, XML, xlsx, and the rest of the plaintext tier — plus encrypted `.noydb` bundles under the `as-noydb` encrypted tier — all live in the [`@noy-db/as-*`](https://www.npmjs.com/search?q=%40noy-db%2Fas-) family. Every invocation is gated by the two-tier authorization model (`canExportPlaintext` default off, `canExportBundle` default on for owner/admin) and lands in the audit ledger.
|
|
180
180
|
|
|
181
|
+
## Money fields
|
|
182
|
+
|
|
183
|
+
`money()` is a schema-layer field descriptor (a sibling of `i18nText()` / `dictKey()`) for currency-safe, exact decimal values. Money is stored as a scaled integer encoded as a **digit string**, so it is exact for any magnitude — past `Number.MAX_SAFE_INTEGER` included (a JSON number would silently truncate at 2^53).
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
vault.collection('invoices', {
|
|
187
|
+
schema: z.object({ id: z.string(), total: z.union([z.number(), z.string()]) }),
|
|
188
|
+
moneyFields: { total: money({ currency: 'EUR', scale: 2 }) }, // scale optional — ISO-4217 default
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
await invoices.put('a', { id: 'a', total: '123.45' }) // stored as '12345'
|
|
192
|
+
const inv = await invoices.get('a', { locale: 'de-DE' })
|
|
193
|
+
// inv.total → '123.45' (exact decimal string)
|
|
194
|
+
// inv.totalFormatted → '123,45 €' (Intl, full precision)
|
|
195
|
+
// inv.totalNumber → 123.45 (convenience JS number; lossy past 2^53)
|
|
196
|
+
|
|
197
|
+
// Exact aggregation — sum/min/max run in BigInt, no float drift:
|
|
198
|
+
invoices.query().aggregate({ total: sum('total') }).run() // → '0.60', never 0.6000000000000001
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- **Rounding:** excess precision is **rejected** by default; opt in per field with `money({ ..., rounding: 'half-even' })` (`half-up` / `half-even` / `half-down` / `up` / `down` / `ceil` / `floor`).
|
|
202
|
+
- **Multi-currency:** opt in with `money({ currencies: 'any' | ['EUR','USD'] })` — currency travels per record as `{ amount, currency }`; `sum` returns an exact per-currency map (`{ EUR: '15.50', USD: '3.00' }`), or one figure with `sum('total', { convertTo: 'EUR', fx })`.
|
|
203
|
+
- Money `sum`/`min`/`max` implement incremental `remove()`, so they stay exact under live aggregation and materialized-view maintenance.
|
|
204
|
+
|
|
205
|
+
## Computed fields
|
|
206
|
+
|
|
207
|
+
`computed` declares schema-owned scalar fields derived on write — keeping the arithmetic next to the schema instead of scattered across handlers. Each function is pure and synchronous; they run **first** in the write pipeline (before schema validation), in declaration order, so a later field can read an earlier one. The result is **materialized** on the record — stored, queryable, and `aggregate(sum())`-able like any field.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
vault.collection('lines', {
|
|
211
|
+
schema: z.object({
|
|
212
|
+
id: z.string(), unitPrice: z.number(), qty: z.number(),
|
|
213
|
+
netAmount: z.number().optional(), taxAmount: z.number().optional(), total: z.number().optional(),
|
|
214
|
+
}),
|
|
215
|
+
computed: {
|
|
216
|
+
netAmount: (r) => r.unitPrice * r.qty,
|
|
217
|
+
taxAmount: (r) => r.netAmount * 0.22, // reads the field computed above
|
|
218
|
+
total: (r) => r.netAmount + r.taxAmount,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
await lines.put('a', { id: 'a', unitPrice: 10, qty: 3 }) // computed fields not supplied
|
|
223
|
+
const line = await lines.get('a') // → { …, netAmount: 30, taxAmount: 6.6, total: 36.6 }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- A computed field **overwrites** any user-supplied value of the same name (the field is schema-owned); a throwing function rejects the write with `ComputedFieldError`.
|
|
227
|
+
- **Composes with `money()`** — declare a computed field as a money field too and it's quantized after evaluation, so `sum()` over it is exact.
|
|
228
|
+
|
|
229
|
+
## Immutable collections (WORM)
|
|
230
|
+
|
|
231
|
+
`immutableGuard` makes a collection write-once after a condition holds — issued invoices/DDTs that must never change. It's declarative sugar over `guards`: it generates the block-on-`check`/`onDelete` + ledgered admin-`amendment` strategy, so it reuses the whole guard machinery (and composes with `periods`/`history`).
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { createNoydb, immutableGuard } from '@noy-db/hub'
|
|
235
|
+
|
|
236
|
+
await createNoydb({
|
|
237
|
+
store, user, secret,
|
|
238
|
+
guardStrategies: [
|
|
239
|
+
immutableGuard({ collection: 'invoices', after: (r) => r.status === 'issued' }),
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
await invoices.put('a', { id: 'a', status: 'draft', total: 100 }) // ok
|
|
244
|
+
await invoices.put('a', { id: 'a', status: 'issued', total: 100 }) // ok — the transition write
|
|
245
|
+
await invoices.put('a', { id: 'a', status: 'issued', total: 999 }) // ✗ RecordLockedError
|
|
246
|
+
await invoices.delete('a') // ✗ RecordLockedError
|
|
247
|
+
|
|
248
|
+
// the sanctioned, ledgered override:
|
|
249
|
+
await db.transaction({ amendment: true, reason: 'correct issued total' }, async (tx) => {
|
|
250
|
+
tx.vault('books').collection('invoices').put('a', { id: 'a', status: 'issued', total: 110 })
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- `after(record)` is evaluated on the **existing** record, so inserts and the write that *first* makes a record immutable are allowed; everything after is blocked.
|
|
255
|
+
- `appendOnly: true` is shorthand for `after: () => true` — immutable from creation.
|
|
256
|
+
- The admin/owner `amendment` path is the only way through, and every amendment is appended to the audit ledger.
|
|
257
|
+
|
|
258
|
+
## Retention, legal-hold & archival
|
|
259
|
+
|
|
260
|
+
For retention-bound data (e.g. 10-year fiscal records), two facilities share one rule — **a legal hold blocks eviction**.
|
|
261
|
+
|
|
262
|
+
**Blob retention** (`vault.compact()`) gains a hold and a period-bound floor:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
vault.collection('invoices', {
|
|
266
|
+
blobFields: {
|
|
267
|
+
pdf: {
|
|
268
|
+
retainDays: 3650, // base TTL
|
|
269
|
+
legalHold: (r) => r.underLitigation === true, // never evict while held
|
|
270
|
+
retainUntil:(r) => r.fiscalYearEnd, // floor: keep until period obligation ends
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
const { evicted, held } = await vault.compact() // held = retained-by-hold count
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Record archival** (`withArchive`) relocates sealed records to a cold store — envelope-level, no re-encryption — and restores on demand:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { createNoydb, withArchive } from '@noy-db/hub'
|
|
281
|
+
|
|
282
|
+
const db = await createNoydb({ store: primary, archiveStrategy: withArchive({ store: coldStore }) })
|
|
283
|
+
vault.collection('invoices', {
|
|
284
|
+
archive: { archiveWhen: (r) => r.fiscalYear <= thisYear - 1, legalHold: (r) => r.underHold },
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
await vault.archive() // → { archived, held, scanned }
|
|
288
|
+
await vault.listArchived('invoices') // → [{ collection, id }, …]
|
|
289
|
+
await vault.restore('invoices', 'inv-2020') // relocate back to primary (decryptable)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Archival uses low-level relocation, so it **bypasses guards** (issued/immutable records over a sealed period can still be archived) and doesn't recompute finalized aggregates. Archived records read `null` from the primary store until restored; a `legalHold` predicate blocks archival entirely.
|
|
293
|
+
|
|
294
|
+
## Atomic sequences
|
|
295
|
+
|
|
296
|
+
`vault.sequence(name)` gives gap-free, exactly-once numbering — the primitive fiscal/ERP/ticketing apps need for invoice or DDT numbers — backed by an optimistic compare-and-swap counter.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const n = await vault.sequence('invoice-2026').next() // 1, then 2, 3, … no gaps, no duplicates
|
|
300
|
+
const cur = await vault.sequence('invoice-2026').peek() // read current value without allocating
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- **Independent per name** — `sequence('invoice-2026')` and `sequence('ddt-2026')` are separate counters.
|
|
304
|
+
- **Concurrency-safe** — concurrent `next()` calls retry on CAS contention (jittered backoff); a genuine burst beyond the retry budget surfaces `SequenceContentionError` so the caller can retry or queue.
|
|
305
|
+
- **Online-only — by design.** Gap-free numbering needs single-authority serialization, which an offline writer can't provide. `next()` throws `SequenceOfflineError` unless the store advertises `capabilities.casAtomic`. This is the honest wall: assign each `next()` value to its record in the same operation (a discarded value is a gap in *usage*, not in the sequence).
|
|
306
|
+
|
|
181
307
|
## Status
|
|
182
308
|
|
|
183
309
|
**Pre-release** (`0.1.0-pre.1`). API may change before `1.0`. Install from the `next` dist-tag:
|
package/dist/aggregate/index.cjs
CHANGED
|
@@ -218,6 +218,263 @@ var GroupCardinalityError = class extends NoydbError {
|
|
|
218
218
|
}
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
+
// src/money/fixed-point.ts
|
|
222
|
+
function formatScaledInt(value, scale) {
|
|
223
|
+
const negative = value < 0n;
|
|
224
|
+
const abs = (negative ? -value : value).toString();
|
|
225
|
+
if (scale === 0) return (negative ? "-" : "") + abs;
|
|
226
|
+
const padded = abs.padStart(scale + 1, "0");
|
|
227
|
+
const cut = padded.length - scale;
|
|
228
|
+
const intPart = padded.slice(0, cut);
|
|
229
|
+
const fracPart = padded.slice(cut);
|
|
230
|
+
return (negative ? "-" : "") + intPart + "." + fracPart;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/money/iso4217.ts
|
|
234
|
+
var MINOR_UNITS = {
|
|
235
|
+
// 2-decimal majors
|
|
236
|
+
EUR: 2,
|
|
237
|
+
USD: 2,
|
|
238
|
+
GBP: 2,
|
|
239
|
+
CHF: 2,
|
|
240
|
+
CAD: 2,
|
|
241
|
+
AUD: 2,
|
|
242
|
+
NZD: 2,
|
|
243
|
+
SGD: 2,
|
|
244
|
+
HKD: 2,
|
|
245
|
+
CNY: 2,
|
|
246
|
+
INR: 2,
|
|
247
|
+
BRL: 2,
|
|
248
|
+
MXN: 2,
|
|
249
|
+
ZAR: 2,
|
|
250
|
+
RUB: 2,
|
|
251
|
+
TRY: 2,
|
|
252
|
+
PLN: 2,
|
|
253
|
+
SEK: 2,
|
|
254
|
+
NOK: 2,
|
|
255
|
+
DKK: 2,
|
|
256
|
+
CZK: 2,
|
|
257
|
+
HUF: 2,
|
|
258
|
+
RON: 2,
|
|
259
|
+
ILS: 2,
|
|
260
|
+
THB: 2,
|
|
261
|
+
PHP: 2,
|
|
262
|
+
MYR: 2,
|
|
263
|
+
IDR: 2,
|
|
264
|
+
AED: 2,
|
|
265
|
+
SAR: 2,
|
|
266
|
+
QAR: 2,
|
|
267
|
+
EGP: 2,
|
|
268
|
+
// 0-decimal
|
|
269
|
+
JPY: 0,
|
|
270
|
+
KRW: 0,
|
|
271
|
+
ISK: 0,
|
|
272
|
+
CLP: 0,
|
|
273
|
+
VND: 0,
|
|
274
|
+
XOF: 0,
|
|
275
|
+
XAF: 0,
|
|
276
|
+
PYG: 0,
|
|
277
|
+
// 3-decimal
|
|
278
|
+
BHD: 3,
|
|
279
|
+
KWD: 3,
|
|
280
|
+
OMR: 3,
|
|
281
|
+
TND: 3,
|
|
282
|
+
JOD: 3,
|
|
283
|
+
IQD: 3,
|
|
284
|
+
LYD: 3
|
|
285
|
+
};
|
|
286
|
+
function scaleForCurrency(code) {
|
|
287
|
+
const v = MINOR_UNITS[code];
|
|
288
|
+
return v === void 0 ? null : v;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/money/descriptor.ts
|
|
292
|
+
var MoneyUnsupportedError = class extends NoydbError {
|
|
293
|
+
constructor(field, message) {
|
|
294
|
+
super(
|
|
295
|
+
"MONEY_UNSUPPORTED",
|
|
296
|
+
message ?? `money: operation is not supported on field "${field}" \u2014 use sum() and count() and divide at the boundary`
|
|
297
|
+
);
|
|
298
|
+
this.field = field;
|
|
299
|
+
this.name = "MoneyUnsupportedError";
|
|
300
|
+
}
|
|
301
|
+
field;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/money/money-reducer.ts
|
|
305
|
+
function toScaledInt(v) {
|
|
306
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "bigint") {
|
|
307
|
+
try {
|
|
308
|
+
return BigInt(v);
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function readMoney(record, field, desc) {
|
|
316
|
+
const raw = readPath(record, field);
|
|
317
|
+
if (raw === null || raw === void 0) return null;
|
|
318
|
+
if (desc.mode === "fixed") {
|
|
319
|
+
const value2 = toScaledInt(raw);
|
|
320
|
+
return value2 === null ? null : { currency: desc.fixedCurrency, value: value2 };
|
|
321
|
+
}
|
|
322
|
+
if (typeof raw !== "object") return null;
|
|
323
|
+
const o = raw;
|
|
324
|
+
if (typeof o.currency !== "string") return null;
|
|
325
|
+
const value = toScaledInt(o.amount);
|
|
326
|
+
return value === null ? null : { currency: o.currency, value };
|
|
327
|
+
}
|
|
328
|
+
function targetScaleFor(desc, currency) {
|
|
329
|
+
if (desc.allows(currency)) return desc.scaleFor(currency);
|
|
330
|
+
const s = scaleForCurrency(currency);
|
|
331
|
+
if (s === null) {
|
|
332
|
+
throw new Error(`money: cannot determine scale for conversion target "${currency}"`);
|
|
333
|
+
}
|
|
334
|
+
return s;
|
|
335
|
+
}
|
|
336
|
+
function parseRate(rate) {
|
|
337
|
+
const s = String(rate).trim();
|
|
338
|
+
const neg = s.startsWith("-");
|
|
339
|
+
const body = neg ? s.slice(1) : s;
|
|
340
|
+
const dot = body.indexOf(".");
|
|
341
|
+
const intPart = dot === -1 ? body : body.slice(0, dot);
|
|
342
|
+
const fracPart = dot === -1 ? "" : body.slice(dot + 1);
|
|
343
|
+
const int = BigInt((intPart === "" ? "0" : intPart) + fracPart);
|
|
344
|
+
return { int: neg ? -int : int, scale: fracPart.length };
|
|
345
|
+
}
|
|
346
|
+
function divRoundHalfEven(n, d) {
|
|
347
|
+
const q = n / d;
|
|
348
|
+
const r = n % d;
|
|
349
|
+
const twiceR = (r < 0n ? -r : r) * 2n;
|
|
350
|
+
if (twiceR < d) return q;
|
|
351
|
+
if (twiceR > d) return q + (n < 0n ? -1n : 1n);
|
|
352
|
+
return q % 2n === 0n ? q : q + (n < 0n ? -1n : 1n);
|
|
353
|
+
}
|
|
354
|
+
function convertScaled(value, srcScale, rate, targetScale) {
|
|
355
|
+
const { int: rateInt, scale: rateScale } = parseRate(rate);
|
|
356
|
+
const product = value * rateInt;
|
|
357
|
+
const curScale = srcScale + rateScale;
|
|
358
|
+
if (curScale === targetScale) return product;
|
|
359
|
+
if (curScale < targetScale) return product * 10n ** BigInt(targetScale - curScale);
|
|
360
|
+
return divRoundHalfEven(product, 10n ** BigInt(curScale - targetScale));
|
|
361
|
+
}
|
|
362
|
+
function finalizeSum(state, desc, convertTo, fx) {
|
|
363
|
+
if (convertTo !== void 0) {
|
|
364
|
+
if (fx === void 0) {
|
|
365
|
+
throw new Error(`money: sum convertTo "${convertTo}" requires an fx rate map`);
|
|
366
|
+
}
|
|
367
|
+
const targetScale = targetScaleFor(desc, convertTo);
|
|
368
|
+
let total = 0n;
|
|
369
|
+
for (const [cur, v] of state) {
|
|
370
|
+
if (cur === convertTo) {
|
|
371
|
+
total += convertScaled(v, desc.scaleFor(cur), 1, targetScale);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const rate = fx[`${cur}->${convertTo}`];
|
|
375
|
+
if (rate === void 0) {
|
|
376
|
+
throw new Error(`money: no fx rate for "${cur}->${convertTo}"`);
|
|
377
|
+
}
|
|
378
|
+
total += convertScaled(v, desc.scaleFor(cur), rate, targetScale);
|
|
379
|
+
}
|
|
380
|
+
return formatScaledInt(total, targetScale);
|
|
381
|
+
}
|
|
382
|
+
if (desc.mode === "fixed") {
|
|
383
|
+
const cur = desc.fixedCurrency;
|
|
384
|
+
return formatScaledInt(state.get(cur) ?? 0n, desc.scaleFor(cur));
|
|
385
|
+
}
|
|
386
|
+
const out = {};
|
|
387
|
+
for (const [cur, v] of state) out[cur] = formatScaledInt(v, desc.scaleFor(cur));
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function moneySumReducer(field, desc, convertTo, fx) {
|
|
391
|
+
return {
|
|
392
|
+
op: "sum",
|
|
393
|
+
field,
|
|
394
|
+
init: () => /* @__PURE__ */ new Map(),
|
|
395
|
+
step: (state, record) => {
|
|
396
|
+
const m = readMoney(record, field, desc);
|
|
397
|
+
if (m) state.set(m.currency, (state.get(m.currency) ?? 0n) + m.value);
|
|
398
|
+
return state;
|
|
399
|
+
},
|
|
400
|
+
remove: (state, record) => {
|
|
401
|
+
const m = readMoney(record, field, desc);
|
|
402
|
+
if (m) state.set(m.currency, (state.get(m.currency) ?? 0n) - m.value);
|
|
403
|
+
return state;
|
|
404
|
+
},
|
|
405
|
+
finalize: (state) => finalizeSum(state, desc, convertTo, fx)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function extremum(values, op) {
|
|
409
|
+
let out = values[0];
|
|
410
|
+
for (let i = 1; i < values.length; i++) {
|
|
411
|
+
const v = values[i];
|
|
412
|
+
if (op === "min" ? v < out : v > out) out = v;
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
function moneyMinMaxReducer(op, field, desc) {
|
|
417
|
+
return {
|
|
418
|
+
op,
|
|
419
|
+
field,
|
|
420
|
+
init: () => /* @__PURE__ */ new Map(),
|
|
421
|
+
step: (state, record) => {
|
|
422
|
+
const m = readMoney(record, field, desc);
|
|
423
|
+
if (m) {
|
|
424
|
+
const arr = state.get(m.currency);
|
|
425
|
+
if (arr) arr.push(m.value);
|
|
426
|
+
else state.set(m.currency, [m.value]);
|
|
427
|
+
}
|
|
428
|
+
return state;
|
|
429
|
+
},
|
|
430
|
+
remove: (state, record) => {
|
|
431
|
+
const m = readMoney(record, field, desc);
|
|
432
|
+
if (m) {
|
|
433
|
+
const arr = state.get(m.currency);
|
|
434
|
+
if (arr) {
|
|
435
|
+
const idx = arr.indexOf(m.value);
|
|
436
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return state;
|
|
440
|
+
},
|
|
441
|
+
finalize: (state) => {
|
|
442
|
+
if (desc.mode === "fixed") {
|
|
443
|
+
const cur = desc.fixedCurrency;
|
|
444
|
+
const arr = state.get(cur);
|
|
445
|
+
if (!arr || arr.length === 0) return null;
|
|
446
|
+
return formatScaledInt(extremum(arr, op), desc.scaleFor(cur));
|
|
447
|
+
}
|
|
448
|
+
const out = {};
|
|
449
|
+
for (const [cur, arr] of state) {
|
|
450
|
+
if (arr.length > 0) out[cur] = formatScaledInt(extremum(arr, op), desc.scaleFor(cur));
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function wrapMoneyReducers(spec, moneyFields) {
|
|
457
|
+
let changed = false;
|
|
458
|
+
const out = {};
|
|
459
|
+
for (const [key, reducer] of Object.entries(spec)) {
|
|
460
|
+
const field = reducer.field;
|
|
461
|
+
const desc = field ? moneyFields[field] : void 0;
|
|
462
|
+
if (desc && reducer.op === "avg") {
|
|
463
|
+
throw new MoneyUnsupportedError(
|
|
464
|
+
field,
|
|
465
|
+
`avg() is not supported on money field "${field}" in v1 \u2014 use sum() and count() and divide at the boundary.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (desc && (reducer.op === "sum" || reducer.op === "min" || reducer.op === "max")) {
|
|
469
|
+
changed = true;
|
|
470
|
+
out[key] = reducer.op === "sum" ? moneySumReducer(field, desc, reducer.convertTo, reducer.fx) : moneyMinMaxReducer(reducer.op, field, desc);
|
|
471
|
+
} else {
|
|
472
|
+
out[key] = reducer;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return changed ? out : spec;
|
|
476
|
+
}
|
|
477
|
+
|
|
221
478
|
// src/aggregate/groupby.ts
|
|
222
479
|
var GROUPBY_WARN_CARDINALITY = 1e4;
|
|
223
480
|
var GROUPBY_MAX_CARDINALITY = 1e5;
|
|
@@ -235,15 +492,17 @@ function resetGroupByWarnings() {
|
|
|
235
492
|
warnedCardinalityFields.clear();
|
|
236
493
|
}
|
|
237
494
|
var GroupedQueryBase = class {
|
|
238
|
-
constructor(executeRecords, fieldOrFields, upstreams, dictLabelResolver) {
|
|
495
|
+
constructor(executeRecords, fieldOrFields, upstreams, dictLabelResolver, moneyFields) {
|
|
239
496
|
this.executeRecords = executeRecords;
|
|
240
497
|
this.upstreams = upstreams;
|
|
241
498
|
this.dictLabelResolver = dictLabelResolver;
|
|
499
|
+
this.moneyFields = moneyFields;
|
|
242
500
|
this.fields = typeof fieldOrFields === "string" ? [fieldOrFields] : [...fieldOrFields];
|
|
243
501
|
}
|
|
244
502
|
executeRecords;
|
|
245
503
|
upstreams;
|
|
246
504
|
dictLabelResolver;
|
|
505
|
+
moneyFields;
|
|
247
506
|
/**
|
|
248
507
|
* Field set this grouped query buckets on. Stored in declaration
|
|
249
508
|
* order — the same order is preserved on every result row by
|
|
@@ -251,6 +510,10 @@ var GroupedQueryBase = class {
|
|
|
251
510
|
* `[field]`.
|
|
252
511
|
*/
|
|
253
512
|
fields;
|
|
513
|
+
/** Apply money-aware reducer rewriting when money fields are declared. */
|
|
514
|
+
wrapSpec(spec) {
|
|
515
|
+
return this.moneyFields ? wrapMoneyReducers(spec, this.moneyFields) : spec;
|
|
516
|
+
}
|
|
254
517
|
};
|
|
255
518
|
var GroupedQuery = class extends GroupedQueryBase {
|
|
256
519
|
/**
|
|
@@ -263,7 +526,7 @@ var GroupedQuery = class extends GroupedQueryBase {
|
|
|
263
526
|
return new GroupedAggregation(
|
|
264
527
|
this.executeRecords,
|
|
265
528
|
this.fields,
|
|
266
|
-
spec,
|
|
529
|
+
this.wrapSpec(spec),
|
|
267
530
|
this.upstreams,
|
|
268
531
|
this.dictLabelResolver
|
|
269
532
|
);
|
|
@@ -274,7 +537,7 @@ var GroupedQueryN = class extends GroupedQueryBase {
|
|
|
274
537
|
return new GroupedAggregation(
|
|
275
538
|
this.executeRecords,
|
|
276
539
|
this.fields,
|
|
277
|
-
spec,
|
|
540
|
+
this.wrapSpec(spec),
|
|
278
541
|
this.upstreams,
|
|
279
542
|
this.dictLabelResolver
|
|
280
543
|
);
|
|
@@ -408,11 +671,11 @@ function withAggregate() {
|
|
|
408
671
|
aggregate(executeRecords, spec, upstreams) {
|
|
409
672
|
return new Aggregation(executeRecords, spec, upstreams);
|
|
410
673
|
},
|
|
411
|
-
groupBy(executeRecords, field, upstreams, dictLabelResolver) {
|
|
412
|
-
return new GroupedQuery(executeRecords, field, upstreams, dictLabelResolver);
|
|
674
|
+
groupBy(executeRecords, field, upstreams, dictLabelResolver, moneyFields) {
|
|
675
|
+
return new GroupedQuery(executeRecords, field, upstreams, dictLabelResolver, moneyFields);
|
|
413
676
|
},
|
|
414
|
-
groupByN(executeRecords, fields, upstreams) {
|
|
415
|
-
return new GroupedQueryN(executeRecords, fields, upstreams);
|
|
677
|
+
groupByN(executeRecords, fields, upstreams, moneyFields) {
|
|
678
|
+
return new GroupedQueryN(executeRecords, fields, upstreams, void 0, moneyFields);
|
|
416
679
|
},
|
|
417
680
|
async scanAggregate(iter, spec) {
|
|
418
681
|
const collected = [];
|
|
@@ -431,7 +694,8 @@ function count(opts) {
|
|
|
431
694
|
init: () => 0,
|
|
432
695
|
step: (state) => state + 1,
|
|
433
696
|
remove: (state) => state - 1,
|
|
434
|
-
finalize: (state) => state
|
|
697
|
+
finalize: (state) => state,
|
|
698
|
+
merge: (a, b) => a + b
|
|
435
699
|
};
|
|
436
700
|
}
|
|
437
701
|
function sum(field, opts) {
|
|
@@ -440,10 +704,15 @@ function sum(field, opts) {
|
|
|
440
704
|
return {
|
|
441
705
|
op: "sum",
|
|
442
706
|
field,
|
|
707
|
+
// Money-only metadata, read by `wrapMoneyReducers`. No effect on a
|
|
708
|
+
// generic numeric sum.
|
|
709
|
+
...opts?.convertTo !== void 0 ? { convertTo: opts.convertTo } : {},
|
|
710
|
+
...opts?.fx !== void 0 ? { fx: opts.fx } : {},
|
|
443
711
|
init: () => 0,
|
|
444
712
|
step: (state, record) => state + readNumber(record, field),
|
|
445
713
|
remove: (state, record) => state - readNumber(record, field),
|
|
446
|
-
finalize: (state) => state
|
|
714
|
+
finalize: (state) => state,
|
|
715
|
+
merge: (a, b) => a + b
|
|
447
716
|
};
|
|
448
717
|
}
|
|
449
718
|
function avg(field, opts) {
|
|
@@ -461,7 +730,8 @@ function avg(field, opts) {
|
|
|
461
730
|
sum: state.sum - readNumber(record, field),
|
|
462
731
|
count: state.count - 1
|
|
463
732
|
}),
|
|
464
|
-
finalize: (state) => state.count === 0 ? null : state.sum / state.count
|
|
733
|
+
finalize: (state) => state.count === 0 ? null : state.sum / state.count,
|
|
734
|
+
merge: (a, b) => ({ sum: a.sum + b.sum, count: a.count + b.count })
|
|
465
735
|
};
|
|
466
736
|
}
|
|
467
737
|
function pushValue(state, value) {
|
|
@@ -491,7 +761,8 @@ function min(field, opts) {
|
|
|
491
761
|
if (v < out) out = v;
|
|
492
762
|
}
|
|
493
763
|
return out;
|
|
494
|
-
}
|
|
764
|
+
},
|
|
765
|
+
merge: (a, b) => ({ values: [...a.values, ...b.values] })
|
|
495
766
|
};
|
|
496
767
|
}
|
|
497
768
|
function max(field, opts) {
|
|
@@ -511,11 +782,17 @@ function max(field, opts) {
|
|
|
511
782
|
if (v > out) out = v;
|
|
512
783
|
}
|
|
513
784
|
return out;
|
|
514
|
-
}
|
|
785
|
+
},
|
|
786
|
+
merge: (a, b) => ({ values: [...a.values, ...b.values] })
|
|
515
787
|
};
|
|
516
788
|
}
|
|
517
789
|
function readNumber(record, field) {
|
|
518
790
|
const value = readPath(record, field);
|
|
791
|
+
if (typeof value === "object" && value !== null && "amount" in value && "currency" in value) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
`aggregate: field "${field}" holds a money value but was not money-aware \u2014 declare it in the collection's moneyFields so sum/min/max stay exact`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
519
796
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
520
797
|
}
|
|
521
798
|
// Annotate the CommonJS export names for ESM import in node:
|