@noy-db/hub 0.2.0-pre.17 → 0.2.0-pre.19
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/dist/aggregate/index.cjs +227 -3
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +3 -3
- package/dist/aggregate/index.d.ts +3 -3
- package/dist/aggregate/index.js +5 -4
- package/dist/aggregate/index.js.map +1 -1
- package/dist/attestation/index.cjs.map +1 -1
- package/dist/attestation/index.d.cts +5 -5
- package/dist/attestation/index.d.ts +5 -5
- package/dist/attestation/index.js +6 -6
- package/dist/blobs/index.cjs +4 -10
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +6 -6
- package/dist/blobs/index.d.ts +6 -6
- package/dist/blobs/index.js +6 -6
- package/dist/bundle/index.cjs +1587 -392
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +7 -7
- package/dist/bundle/index.d.ts +7 -7
- package/dist/bundle/index.js +10 -10
- package/dist/{chunk-NBBMMJ2H.js → chunk-3FSMVWBN.js} +4 -4
- package/dist/{chunk-HGVSHKZW.js → chunk-3Q2AOPLT.js} +100 -29
- package/dist/chunk-3Q2AOPLT.js.map +1 -0
- package/dist/{chunk-SHX5QBCI.js → chunk-4ULLGYPA.js} +3 -3
- package/dist/{chunk-CD2AVTEM.js → chunk-5IGWRMEC.js} +5 -5
- package/dist/{chunk-QO6RGLLD.js → chunk-6KESZO5D.js} +35 -7
- package/dist/chunk-6KESZO5D.js.map +1 -0
- package/dist/{chunk-GP3SDSH2.js → chunk-6OSOE6BY.js} +15 -2
- package/dist/chunk-6OSOE6BY.js.map +1 -0
- package/dist/{chunk-F4G63NTZ.js → chunk-7C6VFNIY.js} +2 -2
- package/dist/{chunk-XJV6OB4D.js → chunk-7HD67R6U.js} +2 -2
- package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
- package/dist/chunk-ADB7GPM3.js.map +1 -0
- package/dist/{chunk-NYSYPFXJ.js → chunk-B6E5IRPJ.js} +3 -3
- package/dist/chunk-CYNTFU2D.js +129 -0
- package/dist/chunk-CYNTFU2D.js.map +1 -0
- package/dist/{chunk-ZEGSDPB7.js → chunk-DJF3FXW5.js} +35 -1
- package/dist/chunk-DJF3FXW5.js.map +1 -0
- package/dist/{chunk-3G3W65EQ.js → chunk-DY3EOJEN.js} +2 -2
- package/dist/{chunk-YYVZYTWW.js → chunk-E66DSTJP.js} +3 -3
- package/dist/{chunk-5LIROIDM.js → chunk-FBLAWK6A.js} +2 -2
- package/dist/{chunk-E77UKJYL.js → chunk-FPHRTW2Z.js} +5 -5
- package/dist/{state-vault-W2OEABNO.js → chunk-G4PYA575.js} +24 -7
- package/dist/chunk-G4PYA575.js.map +1 -0
- package/dist/{chunk-U5QCMH3W.js → chunk-GKQAU52M.js} +4 -4
- package/dist/{chunk-2FU2FTXD.js → chunk-GYAWXHFO.js} +2 -2
- package/dist/{chunk-ROPJVUG3.js → chunk-H42KZXNV.js} +5 -210
- package/dist/chunk-H42KZXNV.js.map +1 -0
- package/dist/{chunk-XPIHJ34I.js → chunk-IBVTH4JR.js} +4 -4
- package/dist/{chunk-C3HYQPV4.js → chunk-IVP5IVON.js} +2 -2
- package/dist/{chunk-BL5GYANC.js → chunk-KEDJDWWQ.js} +3 -3
- package/dist/{chunk-I5IUYN7B.js → chunk-KNKNOJFS.js} +3 -3
- package/dist/chunk-KNKNOJFS.js.map +1 -0
- package/dist/{chunk-D77ZQSQQ.js → chunk-KYGGXXT6.js} +829 -170
- package/dist/chunk-KYGGXXT6.js.map +1 -0
- package/dist/{chunk-J7RWBXFY.js → chunk-LSIIPKYT.js} +2 -2
- package/dist/{chunk-BSZOCSDZ.js → chunk-M3FPNTO2.js} +4 -4
- package/dist/{chunk-XMVHEWF6.js → chunk-MI36HL5G.js} +4 -4
- package/dist/{chunk-ROVO6NPJ.js → chunk-NN6IISZO.js} +58 -3
- package/dist/chunk-NN6IISZO.js.map +1 -0
- package/dist/{chunk-7H2GEJ3O.js → chunk-OBMYMKGO.js} +29 -6
- package/dist/{chunk-7H2GEJ3O.js.map → chunk-OBMYMKGO.js.map} +1 -1
- package/dist/{chunk-UNTGHX5A.js → chunk-OKOKPYWH.js} +2 -2
- package/dist/{chunk-WV7WV6JO.js → chunk-OY7RX2VL.js} +9 -15
- package/dist/chunk-OY7RX2VL.js.map +1 -0
- package/dist/{chunk-H2MRGONI.js → chunk-PTGQPWMV.js} +2 -2
- package/dist/{chunk-BJSLBUJ7.js → chunk-PWFTQHYX.js} +2 -2
- package/dist/{chunk-5AXTH4QZ.js → chunk-Q5MCHUXZ.js} +2 -2
- package/dist/{chunk-QHM6XEAH.js → chunk-S22UOMHM.js} +6 -6
- package/dist/{chunk-WIAOUFFB.js → chunk-S3XA7G35.js} +2 -2
- package/dist/{chunk-SISBMAPO.js → chunk-SHIUFIPW.js} +1 -1
- package/dist/chunk-SHIUFIPW.js.map +1 -0
- package/dist/{chunk-KCEHMDZF.js → chunk-U7JNBSS3.js} +3 -3
- package/dist/{chunk-ZNGPEV5J.js → chunk-V3VIRTTE.js} +3 -3
- package/dist/{chunk-TIDXB5DF.js → chunk-V5FZWQNN.js} +4 -4
- package/dist/chunk-VEIVAYJ7.js +361 -0
- package/dist/chunk-VEIVAYJ7.js.map +1 -0
- package/dist/{chunk-AEIKD3PP.js → chunk-VNUE6FHP.js} +3 -3
- package/dist/{chunk-DYYYUW5D.js → chunk-WFK2EVYU.js} +10 -2
- package/dist/chunk-WFK2EVYU.js.map +1 -0
- package/dist/{chunk-XMHUK5PN.js → chunk-X7FJMKT3.js} +2 -2
- package/dist/{chunk-FEJDVE3Z.js → chunk-XPH3FWME.js} +7 -2
- package/dist/{chunk-FEJDVE3Z.js.map → chunk-XPH3FWME.js.map} +1 -1
- package/dist/{chunk-SNMJ7SB3.js → chunk-Y5J63SMF.js} +5 -5
- package/dist/{chunk-M476FOQ7.js → chunk-YLRRU72W.js} +2 -2
- package/dist/{chunk-DWEBTE2W.js → chunk-YX333DPS.js} +4 -4
- package/dist/{chunk-BH3X5L6A.js → chunk-YZE6C3TQ.js} +3 -3
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +6 -6
- package/dist/consent/index.d.ts +6 -6
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-7BN2HDWG.js → crypto-B46VNH6X.js} +3 -3
- package/dist/{delegation-MGH5SODX.js → delegation-5HON72PV.js} +5 -5
- package/dist/derivations/index.cjs +82 -2
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +7 -7
- package/dist/derivations/index.d.ts +7 -7
- package/dist/derivations/index.js +8 -6
- package/dist/{dev-unlock-iXbYFAWl.d.cts → dev-unlock-BR1rMOS-.d.cts} +1 -1
- package/dist/{dev-unlock-CI1ijTML.d.ts → dev-unlock-whL49sxV.d.ts} +1 -1
- package/dist/{errors-Dz64FA65.d.cts → errors-DL-zTrrF.d.cts} +29 -1
- package/dist/{errors-Dz64FA65.d.ts → errors-DL-zTrrF.d.ts} +29 -1
- package/dist/executor-44R5CUS2.js +12 -0
- package/dist/executor-AOACUK7Z.js +8 -0
- package/dist/executor-OKFLQCDW.js +8 -0
- package/dist/{fanout-sidecar-FIJJ46YG.js → fanout-sidecar-DCQWJQ6S.js} +2 -2
- package/dist/forget/index.cjs.map +1 -1
- package/dist/forget/index.d.cts +1 -1
- package/dist/forget/index.d.ts +1 -1
- package/dist/forget/index.js +4 -4
- package/dist/guards/index.cjs +80 -3
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +7 -7
- package/dist/guards/index.d.ts +7 -7
- package/dist/guards/index.js +8 -4
- package/dist/{hash-tEcM5fnv.d.cts → hash-BEUBmmI4.d.cts} +1 -1
- package/dist/{hash-blk7Bkes.d.ts → hash-Dtb7FwWd.d.ts} +1 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +7 -7
- package/dist/history/index.d.ts +7 -7
- package/dist/history/index.js +5 -5
- package/dist/i18n/index.cjs +149 -132
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +6 -6
- package/dist/i18n/index.d.ts +6 -6
- package/dist/i18n/index.js +14 -14
- package/dist/{index-u-kWzSrL.d.cts → index-BM7O48Ur.d.cts} +85 -9
- package/dist/{index-C-SSRIxP.d.cts → index-BMmajblo.d.cts} +14 -0
- package/dist/{index-C-SSRIxP.d.ts → index-BMmajblo.d.ts} +14 -0
- package/dist/{index-DpU6KWof.d.ts → index-BelbyUwz.d.ts} +85 -9
- package/dist/index.cjs +2206 -992
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -16
- package/dist/index.d.ts +29 -16
- package/dist/index.js +76 -54
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +4 -4
- package/dist/issue-EPA2PSWP.js +12 -0
- package/dist/{ledger-LFVLHE5H.js → ledger-LS6GXCBP.js} +5 -5
- package/dist/materialized-views/index.cjs +257 -4
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +7 -7
- package/dist/materialized-views/index.d.ts +7 -7
- package/dist/materialized-views/index.js +8 -7
- package/dist/noydb-BVKFP74P.js +38 -0
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +7 -7
- package/dist/overlay-views/index.d.ts +7 -7
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +6 -6
- package/dist/periods/index.d.ts +6 -6
- package/dist/periods/index.js +5 -5
- package/dist/{public-envelope-RXZNP3V6.js → public-envelope-AGU6SS4Z.js} +4 -4
- package/dist/query/index.cjs +320 -28
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +3 -3
- package/dist/query/index.d.ts +3 -3
- package/dist/query/index.js +7 -6
- package/dist/read-only-facade-EX6WZZBP.js +7 -0
- package/dist/registry-ERNAMRDE.js +8 -0
- package/dist/registry-EXTHSXQW.js +8 -0
- package/dist/{registry-SECUWSGY.js → registry-RDPTFXQ7.js} +3 -3
- package/dist/{revoke-B54H2S2W.js → revoke-IFLXEZA5.js} +6 -6
- package/dist/sealed-record/index.cjs.map +1 -1
- package/dist/sealed-record/index.d.cts +1 -1
- package/dist/sealed-record/index.d.ts +1 -1
- package/dist/sealed-record/index.js +2 -2
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +7 -7
- package/dist/session/index.d.ts +7 -7
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +6 -6
- package/dist/shadow/index.d.ts +6 -6
- package/dist/shadow/index.js +2 -2
- package/dist/{signer-YSXZT574.js → signer-UNWOUJAK.js} +5 -5
- package/dist/snapshots/index.cjs.map +1 -1
- package/dist/snapshots/index.d.cts +6 -6
- package/dist/snapshots/index.d.ts +6 -6
- package/dist/snapshots/index.js +4 -4
- package/dist/{stale-TOA36SRK.js → stale-NTEV5SLX.js} +2 -2
- package/dist/state-vault-TUTFRTOA.js +14 -0
- package/dist/state-vault-TUTFRTOA.js.map +1 -0
- package/dist/store/index.cjs +8 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +13 -6
- package/dist/store/index.d.ts +13 -6
- package/dist/store/index.js +2 -2
- package/dist/{strategy-4M9jo172.d.ts → strategy-BDxQnnTX.d.ts} +315 -4
- package/dist/{strategy-CLC1j79g.d.cts → strategy-C5ol6NdV.d.cts} +315 -4
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +5 -5
- package/dist/sync/index.d.ts +5 -5
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +6 -6
- package/dist/team/index.d.ts +6 -6
- package/dist/team/index.js +8 -8
- package/dist/transition-guard-B1N82hMf.d.cts +165 -0
- package/dist/transition-guard-C__YeF3_.d.ts +165 -0
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +6 -6
- package/dist/tx/index.d.ts +6 -6
- package/dist/tx/index.js +3 -3
- package/dist/{types-CljIHm_J.d.ts → types-CraiZOyO.d.ts} +609 -305
- package/dist/{types-CrSpRDuG.d.cts → types-D-gr5t0G.d.cts} +609 -305
- package/dist/{ulid-CrI7PPbA.d.cts → ulid-DQnSAP5W.d.cts} +1 -1
- package/dist/{ulid-CWfL2Vfv.d.ts → ulid-FFRRHkVf.d.ts} +1 -1
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/{vault-group-DHAHFX2A.js → vault-group-27EV7KB4.js} +205 -8
- package/dist/vault-group-27EV7KB4.js.map +1 -0
- package/dist/{with-materialized-view-NzF71cG_.d.cts → with-materialized-view-BboqxyV3.d.cts} +1 -1
- package/dist/{with-materialized-view-B892zYZV.d.ts → with-materialized-view-CguCeVcT.d.ts} +1 -1
- package/dist/{with-overlayed-view-CR6m7CHe.d.ts → with-overlayed-view-DO08u_tx.d.ts} +1 -1
- package/dist/{with-overlayed-view-UI8qSGL4.d.cts → with-overlayed-view-mmsg5Of3.d.cts} +1 -1
- package/dist/with-rollup-_TyBzz3T.d.ts +47 -0
- package/dist/with-rollup-aaxOZcIb.d.cts +47 -0
- package/package.json +3 -3
- package/dist/chunk-D77ZQSQQ.js.map +0 -1
- package/dist/chunk-DYYYUW5D.js.map +0 -1
- package/dist/chunk-GP3SDSH2.js.map +0 -1
- package/dist/chunk-HGVSHKZW.js.map +0 -1
- package/dist/chunk-I5IUYN7B.js.map +0 -1
- package/dist/chunk-JDWE6JMX.js +0 -139
- package/dist/chunk-JDWE6JMX.js.map +0 -1
- package/dist/chunk-PDULVIBY.js +0 -63
- package/dist/chunk-PDULVIBY.js.map +0 -1
- package/dist/chunk-QO6RGLLD.js.map +0 -1
- package/dist/chunk-ROPJVUG3.js.map +0 -1
- package/dist/chunk-ROVO6NPJ.js.map +0 -1
- package/dist/chunk-SISBMAPO.js.map +0 -1
- package/dist/chunk-UMLVJTYV.js.map +0 -1
- package/dist/chunk-WV7WV6JO.js.map +0 -1
- package/dist/chunk-ZEGSDPB7.js.map +0 -1
- package/dist/executor-3W63Y44O.js +0 -11
- package/dist/executor-CFFWPWBJ.js +0 -8
- package/dist/executor-VDQQOR4F.js +0 -8
- package/dist/immutable-guard-B5M95nbq.d.ts +0 -82
- package/dist/immutable-guard-qN3zF8o1.d.cts +0 -82
- package/dist/issue-TTMGHQ2J.js +0 -12
- package/dist/noydb-36S6GQNC.js +0 -37
- package/dist/read-only-facade-ITU6L7BL.js +0 -7
- package/dist/registry-3YFLZ7WD.js +0 -8
- package/dist/registry-TGZISEWC.js +0 -8
- package/dist/state-vault-W2OEABNO.js.map +0 -1
- package/dist/vault-group-DHAHFX2A.js.map +0 -1
- package/dist/with-derivation-BZ2y4bzF.d.ts +0 -13
- package/dist/with-derivation-Bozs8DmD.d.cts +0 -13
- /package/dist/{chunk-NBBMMJ2H.js.map → chunk-3FSMVWBN.js.map} +0 -0
- /package/dist/{chunk-SHX5QBCI.js.map → chunk-4ULLGYPA.js.map} +0 -0
- /package/dist/{chunk-CD2AVTEM.js.map → chunk-5IGWRMEC.js.map} +0 -0
- /package/dist/{chunk-F4G63NTZ.js.map → chunk-7C6VFNIY.js.map} +0 -0
- /package/dist/{chunk-XJV6OB4D.js.map → chunk-7HD67R6U.js.map} +0 -0
- /package/dist/{chunk-NYSYPFXJ.js.map → chunk-B6E5IRPJ.js.map} +0 -0
- /package/dist/{chunk-3G3W65EQ.js.map → chunk-DY3EOJEN.js.map} +0 -0
- /package/dist/{chunk-YYVZYTWW.js.map → chunk-E66DSTJP.js.map} +0 -0
- /package/dist/{chunk-5LIROIDM.js.map → chunk-FBLAWK6A.js.map} +0 -0
- /package/dist/{chunk-E77UKJYL.js.map → chunk-FPHRTW2Z.js.map} +0 -0
- /package/dist/{chunk-U5QCMH3W.js.map → chunk-GKQAU52M.js.map} +0 -0
- /package/dist/{chunk-2FU2FTXD.js.map → chunk-GYAWXHFO.js.map} +0 -0
- /package/dist/{chunk-XPIHJ34I.js.map → chunk-IBVTH4JR.js.map} +0 -0
- /package/dist/{chunk-C3HYQPV4.js.map → chunk-IVP5IVON.js.map} +0 -0
- /package/dist/{chunk-BL5GYANC.js.map → chunk-KEDJDWWQ.js.map} +0 -0
- /package/dist/{chunk-J7RWBXFY.js.map → chunk-LSIIPKYT.js.map} +0 -0
- /package/dist/{chunk-BSZOCSDZ.js.map → chunk-M3FPNTO2.js.map} +0 -0
- /package/dist/{chunk-XMVHEWF6.js.map → chunk-MI36HL5G.js.map} +0 -0
- /package/dist/{chunk-UNTGHX5A.js.map → chunk-OKOKPYWH.js.map} +0 -0
- /package/dist/{chunk-H2MRGONI.js.map → chunk-PTGQPWMV.js.map} +0 -0
- /package/dist/{chunk-BJSLBUJ7.js.map → chunk-PWFTQHYX.js.map} +0 -0
- /package/dist/{chunk-5AXTH4QZ.js.map → chunk-Q5MCHUXZ.js.map} +0 -0
- /package/dist/{chunk-QHM6XEAH.js.map → chunk-S22UOMHM.js.map} +0 -0
- /package/dist/{chunk-WIAOUFFB.js.map → chunk-S3XA7G35.js.map} +0 -0
- /package/dist/{chunk-KCEHMDZF.js.map → chunk-U7JNBSS3.js.map} +0 -0
- /package/dist/{chunk-ZNGPEV5J.js.map → chunk-V3VIRTTE.js.map} +0 -0
- /package/dist/{chunk-TIDXB5DF.js.map → chunk-V5FZWQNN.js.map} +0 -0
- /package/dist/{chunk-AEIKD3PP.js.map → chunk-VNUE6FHP.js.map} +0 -0
- /package/dist/{chunk-XMHUK5PN.js.map → chunk-X7FJMKT3.js.map} +0 -0
- /package/dist/{chunk-SNMJ7SB3.js.map → chunk-Y5J63SMF.js.map} +0 -0
- /package/dist/{chunk-M476FOQ7.js.map → chunk-YLRRU72W.js.map} +0 -0
- /package/dist/{chunk-DWEBTE2W.js.map → chunk-YX333DPS.js.map} +0 -0
- /package/dist/{chunk-BH3X5L6A.js.map → chunk-YZE6C3TQ.js.map} +0 -0
- /package/dist/{crypto-7BN2HDWG.js.map → crypto-B46VNH6X.js.map} +0 -0
- /package/dist/{delegation-MGH5SODX.js.map → delegation-5HON72PV.js.map} +0 -0
- /package/dist/{executor-3W63Y44O.js.map → executor-44R5CUS2.js.map} +0 -0
- /package/dist/{executor-CFFWPWBJ.js.map → executor-AOACUK7Z.js.map} +0 -0
- /package/dist/{executor-VDQQOR4F.js.map → executor-OKFLQCDW.js.map} +0 -0
- /package/dist/{fanout-sidecar-FIJJ46YG.js.map → fanout-sidecar-DCQWJQ6S.js.map} +0 -0
- /package/dist/{issue-TTMGHQ2J.js.map → issue-EPA2PSWP.js.map} +0 -0
- /package/dist/{ledger-LFVLHE5H.js.map → ledger-LS6GXCBP.js.map} +0 -0
- /package/dist/{noydb-36S6GQNC.js.map → noydb-BVKFP74P.js.map} +0 -0
- /package/dist/{public-envelope-RXZNP3V6.js.map → public-envelope-AGU6SS4Z.js.map} +0 -0
- /package/dist/{read-only-facade-ITU6L7BL.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
- /package/dist/{registry-3YFLZ7WD.js.map → registry-ERNAMRDE.js.map} +0 -0
- /package/dist/{registry-SECUWSGY.js.map → registry-EXTHSXQW.js.map} +0 -0
- /package/dist/{registry-TGZISEWC.js.map → registry-RDPTFXQ7.js.map} +0 -0
- /package/dist/{revoke-B54H2S2W.js.map → revoke-IFLXEZA5.js.map} +0 -0
- /package/dist/{signer-YSXZT574.js.map → signer-UNWOUJAK.js.map} +0 -0
- /package/dist/{stale-TOA36SRK.js.map → stale-NTEV5SLX.js.map} +0 -0
package/dist/bundle/index.cjs
CHANGED
|
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// src/errors.ts
|
|
34
|
-
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, RecordCekNotFoundError;
|
|
34
|
+
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, DataResidencyError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, RecordCekNotFoundError;
|
|
35
35
|
var init_errors = __esm({
|
|
36
36
|
"src/errors.ts"() {
|
|
37
37
|
"use strict";
|
|
@@ -776,6 +776,21 @@ Resolutions:
|
|
|
776
776
|
this.vaultId = vaultId;
|
|
777
777
|
}
|
|
778
778
|
};
|
|
779
|
+
DataResidencyError = class extends NoydbError {
|
|
780
|
+
vaultId;
|
|
781
|
+
requiredRegion;
|
|
782
|
+
backendRegion;
|
|
783
|
+
constructor(vaultId, requiredRegion, backendRegion) {
|
|
784
|
+
super(
|
|
785
|
+
"DATA_RESIDENCY",
|
|
786
|
+
`Shard "${vaultId}" requires region "${requiredRegion}" but its placement backend declares region ${backendRegion === void 0 ? "(none)" : `"${backendRegion}"`}. Refusing to provision \u2014 route this shard to a region-correct backend via routeStore({ vaultRoutes }) (e.g. a region-encoded partition key) before retrying.`
|
|
787
|
+
);
|
|
788
|
+
this.name = "DataResidencyError";
|
|
789
|
+
this.vaultId = vaultId;
|
|
790
|
+
this.requiredRegion = requiredRegion;
|
|
791
|
+
this.backendRegion = backendRegion;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
779
794
|
CrossShardJoinError = class extends NoydbError {
|
|
780
795
|
constructor(message) {
|
|
781
796
|
super("CROSS_SHARD_JOIN", message);
|
|
@@ -3893,7 +3908,146 @@ var init_policy = __esm({
|
|
|
3893
3908
|
}
|
|
3894
3909
|
});
|
|
3895
3910
|
|
|
3911
|
+
// src/i18n/script.ts
|
|
3912
|
+
function inferScripts(locale) {
|
|
3913
|
+
const parts = locale.split("-");
|
|
3914
|
+
const subtag = parts.find((t) => /^[A-Z][a-z]{3}$/.test(t));
|
|
3915
|
+
if (subtag && SUBTAG_SCRIPTS[subtag]) return SUBTAG_SCRIPTS[subtag];
|
|
3916
|
+
const base = (parts[0] ?? "").toLowerCase();
|
|
3917
|
+
if (LATIN_BASE.has(base)) return ["Latin"];
|
|
3918
|
+
const primary = SCRIPT_TABLE[base];
|
|
3919
|
+
if (primary) return [...primary, "Latin"];
|
|
3920
|
+
return ["Latin"];
|
|
3921
|
+
}
|
|
3922
|
+
var LATIN_BASE, SCRIPT_TABLE, SUBTAG_SCRIPTS, BASELINE;
|
|
3923
|
+
var init_script = __esm({
|
|
3924
|
+
"src/i18n/script.ts"() {
|
|
3925
|
+
"use strict";
|
|
3926
|
+
LATIN_BASE = /* @__PURE__ */ new Set([
|
|
3927
|
+
"en",
|
|
3928
|
+
"fr",
|
|
3929
|
+
"de",
|
|
3930
|
+
"es",
|
|
3931
|
+
"it",
|
|
3932
|
+
"pt",
|
|
3933
|
+
"nl",
|
|
3934
|
+
"sv",
|
|
3935
|
+
"no",
|
|
3936
|
+
"da",
|
|
3937
|
+
"fi",
|
|
3938
|
+
"is",
|
|
3939
|
+
"pl",
|
|
3940
|
+
"cs",
|
|
3941
|
+
"sk",
|
|
3942
|
+
"hu",
|
|
3943
|
+
"ro",
|
|
3944
|
+
"hr",
|
|
3945
|
+
"sl",
|
|
3946
|
+
"et",
|
|
3947
|
+
"lv",
|
|
3948
|
+
"lt",
|
|
3949
|
+
"tr",
|
|
3950
|
+
"vi",
|
|
3951
|
+
"id",
|
|
3952
|
+
"ms",
|
|
3953
|
+
"tl",
|
|
3954
|
+
"sw",
|
|
3955
|
+
"af",
|
|
3956
|
+
"ca",
|
|
3957
|
+
"gl",
|
|
3958
|
+
"eu",
|
|
3959
|
+
"cy",
|
|
3960
|
+
"ga"
|
|
3961
|
+
]);
|
|
3962
|
+
SCRIPT_TABLE = {
|
|
3963
|
+
th: ["Thai"],
|
|
3964
|
+
ko: ["Hangul", "Han"],
|
|
3965
|
+
ja: ["Han", "Hiragana", "Katakana"],
|
|
3966
|
+
zh: ["Han"],
|
|
3967
|
+
ar: ["Arabic"],
|
|
3968
|
+
fa: ["Arabic"],
|
|
3969
|
+
ur: ["Arabic"],
|
|
3970
|
+
ru: ["Cyrillic"],
|
|
3971
|
+
uk: ["Cyrillic"],
|
|
3972
|
+
bg: ["Cyrillic"],
|
|
3973
|
+
sr: ["Cyrillic"],
|
|
3974
|
+
he: ["Hebrew"],
|
|
3975
|
+
el: ["Greek"],
|
|
3976
|
+
hi: ["Devanagari"],
|
|
3977
|
+
ta: ["Tamil"],
|
|
3978
|
+
km: ["Khmer"],
|
|
3979
|
+
lo: ["Lao"],
|
|
3980
|
+
my: ["Myanmar"]
|
|
3981
|
+
};
|
|
3982
|
+
SUBTAG_SCRIPTS = {
|
|
3983
|
+
Latn: ["Latin"],
|
|
3984
|
+
Cyrl: ["Cyrillic", "Latin"],
|
|
3985
|
+
Hans: ["Han", "Latin"],
|
|
3986
|
+
Hant: ["Han", "Latin"],
|
|
3987
|
+
Thai: ["Thai", "Latin"],
|
|
3988
|
+
Arab: ["Arabic", "Latin"]
|
|
3989
|
+
};
|
|
3990
|
+
BASELINE = String.raw`\p{White_Space}\p{Script=Common}\p{Script=Inherited}\p{Mark}`;
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3896
3994
|
// src/i18n/core.ts
|
|
3995
|
+
function toChain(fallback) {
|
|
3996
|
+
return Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
3997
|
+
}
|
|
3998
|
+
function pickFromChain(value, chain) {
|
|
3999
|
+
for (const fb of chain) {
|
|
4000
|
+
if (fb === "any") {
|
|
4001
|
+
const any = Object.values(value).find((v) => v !== "");
|
|
4002
|
+
if (any !== void 0) return any;
|
|
4003
|
+
} else if (value[fb] !== void 0 && value[fb] !== "") {
|
|
4004
|
+
return value[fb];
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return void 0;
|
|
4008
|
+
}
|
|
4009
|
+
function resolveI18nText(value, locale, fallback, field, opts) {
|
|
4010
|
+
if (locale === "raw") {
|
|
4011
|
+
return value;
|
|
4012
|
+
}
|
|
4013
|
+
if (!locale) {
|
|
4014
|
+
throw new LocaleNotSpecifiedError(field ?? "<unknown>");
|
|
4015
|
+
}
|
|
4016
|
+
if (value[locale] !== void 0 && value[locale] !== "") {
|
|
4017
|
+
return value[locale];
|
|
4018
|
+
}
|
|
4019
|
+
const policy = opts?.policy ?? "throw";
|
|
4020
|
+
const callerChain = toChain(fallback);
|
|
4021
|
+
const callerHit = pickFromChain(value, callerChain);
|
|
4022
|
+
if (callerHit !== void 0) return callerHit;
|
|
4023
|
+
if (policy === "substitute") {
|
|
4024
|
+
const subHit = pickFromChain(value, toChain(opts?.substitute));
|
|
4025
|
+
if (subHit !== void 0) return subHit;
|
|
4026
|
+
if (opts?.smartSubstitute) {
|
|
4027
|
+
const smartHit = pickNearestScript(value, locale);
|
|
4028
|
+
if (smartHit !== void 0) return smartHit;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (policy === "throw") {
|
|
4032
|
+
throw new LocaleNotSpecifiedError(
|
|
4033
|
+
field ?? "<unknown>",
|
|
4034
|
+
`No translation available for locale "${locale}"` + (callerChain.length > 0 ? ` or fallback chain [${callerChain.join(", ")}]` : "") + "."
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
return null;
|
|
4038
|
+
}
|
|
4039
|
+
function pickNearestScript(value, target) {
|
|
4040
|
+
const targetScript = inferScripts(target)[0] ?? "Latin";
|
|
4041
|
+
let best;
|
|
4042
|
+
for (const [loc, v] of Object.entries(value)) {
|
|
4043
|
+
if (typeof v !== "string" || v === "") continue;
|
|
4044
|
+
const s = inferScripts(loc)[0] ?? "Latin";
|
|
4045
|
+
const score = s === targetScript ? 0 : s === "Latin" ? 1 : 2;
|
|
4046
|
+
if (best === void 0 || score < best.score) best = { score, v };
|
|
4047
|
+
if (score === 0) break;
|
|
4048
|
+
}
|
|
4049
|
+
return best?.v;
|
|
4050
|
+
}
|
|
3897
4051
|
function getAtPath(obj, path) {
|
|
3898
4052
|
const arrayIdx = path.indexOf("[].");
|
|
3899
4053
|
if (arrayIdx !== -1) {
|
|
@@ -3929,9 +4083,61 @@ function setAtPathInPlace(obj, path, value) {
|
|
|
3929
4083
|
}
|
|
3930
4084
|
obj[path] = value;
|
|
3931
4085
|
}
|
|
4086
|
+
function applyAtPath(obj, path, locale, fallback, opts) {
|
|
4087
|
+
const arrayIdx = path.indexOf("[].");
|
|
4088
|
+
if (arrayIdx !== -1) {
|
|
4089
|
+
const arrayKey = path.slice(0, arrayIdx);
|
|
4090
|
+
const restPath = path.slice(arrayIdx + 3);
|
|
4091
|
+
const arr = obj[arrayKey];
|
|
4092
|
+
if (!Array.isArray(arr)) return obj;
|
|
4093
|
+
return {
|
|
4094
|
+
...obj,
|
|
4095
|
+
[arrayKey]: arr.map((item) => {
|
|
4096
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
4097
|
+
return applyAtPath(item, restPath, locale, fallback, opts);
|
|
4098
|
+
})
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
const dotIdx = path.indexOf(".");
|
|
4102
|
+
if (dotIdx !== -1) {
|
|
4103
|
+
const head = path.slice(0, dotIdx);
|
|
4104
|
+
const rest = path.slice(dotIdx + 1);
|
|
4105
|
+
const nested = obj[head];
|
|
4106
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) return obj;
|
|
4107
|
+
return {
|
|
4108
|
+
...obj,
|
|
4109
|
+
[head]: applyAtPath(nested, rest, locale, fallback, opts)
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
const raw = obj[path];
|
|
4113
|
+
if (raw === void 0 || raw === null) return obj;
|
|
4114
|
+
if (typeof raw !== "object" || Array.isArray(raw)) return obj;
|
|
4115
|
+
return {
|
|
4116
|
+
...obj,
|
|
4117
|
+
[path]: resolveI18nText(raw, locale, fallback, path, opts)
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
function applyI18nLocale(record, i18nFields, locale, fallback, layer = "read") {
|
|
4121
|
+
const fieldNames = Object.keys(i18nFields);
|
|
4122
|
+
if (fieldNames.length === 0) return record;
|
|
4123
|
+
let result = record;
|
|
4124
|
+
for (const [field, descriptor] of Object.entries(i18nFields)) {
|
|
4125
|
+
const { onMissing, substitute, smartSubstitute } = descriptor.options;
|
|
4126
|
+
const opts = {
|
|
4127
|
+
policy: resolvePolicy(onMissing, layer),
|
|
4128
|
+
...substitute !== void 0 ? { substitute } : {},
|
|
4129
|
+
...smartSubstitute ? { smartSubstitute } : {}
|
|
4130
|
+
};
|
|
4131
|
+
result = applyAtPath(result, field, locale, fallback, opts);
|
|
4132
|
+
}
|
|
4133
|
+
return result;
|
|
4134
|
+
}
|
|
3932
4135
|
var init_core = __esm({
|
|
3933
4136
|
"src/i18n/core.ts"() {
|
|
3934
4137
|
"use strict";
|
|
4138
|
+
init_errors();
|
|
4139
|
+
init_policy();
|
|
4140
|
+
init_script();
|
|
3935
4141
|
}
|
|
3936
4142
|
});
|
|
3937
4143
|
|
|
@@ -4343,6 +4549,21 @@ function formatCurrency(decimal, currency, scale, locale) {
|
|
|
4343
4549
|
});
|
|
4344
4550
|
return fmt.format(decimal);
|
|
4345
4551
|
}
|
|
4552
|
+
function moneyScaledValue(stored, desc) {
|
|
4553
|
+
let raw;
|
|
4554
|
+
if (desc.mode === "fixed") {
|
|
4555
|
+
raw = stored;
|
|
4556
|
+
} else {
|
|
4557
|
+
if (!isMoneyValueObject(stored)) return null;
|
|
4558
|
+
raw = stored.amount;
|
|
4559
|
+
}
|
|
4560
|
+
if (typeof raw !== "string" && typeof raw !== "number") return null;
|
|
4561
|
+
try {
|
|
4562
|
+
return BigInt(String(raw));
|
|
4563
|
+
} catch {
|
|
4564
|
+
return null;
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4346
4567
|
function decodeValue(stored, desc) {
|
|
4347
4568
|
let currency;
|
|
4348
4569
|
let scaledIntString;
|
|
@@ -4853,15 +5074,15 @@ function warnCeilingApproaching(target, side, rows, maxRows) {
|
|
|
4853
5074
|
`[noy-db] .join() ${side} side is at ${pct}% of the ${maxRows}-row ceiling for target "${target}" (${rows} rows). Streaming joins over scan() are not yet supported for collections that need to exceed this.`
|
|
4854
5075
|
);
|
|
4855
5076
|
}
|
|
4856
|
-
function applyJoins(rows, joins, context) {
|
|
5077
|
+
function applyJoins(rows, joins, context, locale) {
|
|
4857
5078
|
if (joins.length === 0) return [...rows];
|
|
4858
5079
|
let result = [...rows];
|
|
4859
5080
|
for (const leg of joins) {
|
|
4860
|
-
result = applyOneJoin(result, leg, context);
|
|
5081
|
+
result = applyOneJoin(result, leg, context, locale);
|
|
4861
5082
|
}
|
|
4862
5083
|
return result;
|
|
4863
5084
|
}
|
|
4864
|
-
function applyOneJoin(leftRows, leg, context) {
|
|
5085
|
+
function applyOneJoin(leftRows, leg, context, locale) {
|
|
4865
5086
|
if (leg.isDictJoin) {
|
|
4866
5087
|
const dictSource = context.resolveDictSource?.(leg.field);
|
|
4867
5088
|
if (!dictSource) {
|
|
@@ -4916,24 +5137,27 @@ function applyOneJoin(leftRows, leg, context) {
|
|
|
4916
5137
|
if (rightSnapshot.length > maxRows * JOIN_WARN_FRACTION) {
|
|
4917
5138
|
warnCeilingApproaching(leg.target, "right", rightSnapshot.length, maxRows);
|
|
4918
5139
|
}
|
|
5140
|
+
const effLocale = locale ?? context.defaultLocale;
|
|
5141
|
+
const i18nResolve = effLocale !== void 0 && source.i18nFields !== void 0 ? (right) => right !== null && typeof right === "object" ? applyI18nLocale(right, source.i18nFields, effLocale, void 0, "join") : right : void 0;
|
|
4919
5142
|
const strategy = leg.strategy ?? (source.lookupById ? "nested" : "hash");
|
|
4920
5143
|
if (strategy === "nested" && source.lookupById) {
|
|
4921
5144
|
const lookup = (id) => source.lookupById?.(id);
|
|
4922
|
-
return nestedLoopJoin(leftRows, leg, lookup);
|
|
5145
|
+
return nestedLoopJoin(leftRows, leg, lookup, i18nResolve);
|
|
4923
5146
|
}
|
|
4924
|
-
return hashJoin(leftRows, leg, rightSnapshot);
|
|
5147
|
+
return hashJoin(leftRows, leg, rightSnapshot, i18nResolve);
|
|
4925
5148
|
}
|
|
4926
|
-
function nestedLoopJoin(leftRows, leg, lookupById) {
|
|
5149
|
+
function nestedLoopJoin(leftRows, leg, lookupById, i18nResolve) {
|
|
4927
5150
|
const out = [];
|
|
4928
5151
|
for (const left of leftRows) {
|
|
4929
5152
|
const rawId = readPath(left, leg.field);
|
|
4930
5153
|
const key = coerceRefKey(rawId);
|
|
4931
|
-
|
|
5154
|
+
let right = key === null ? void 0 : lookupById(key);
|
|
5155
|
+
if (i18nResolve && right !== void 0) right = i18nResolve(right);
|
|
4932
5156
|
out.push(attachJoin(left, leg, right, rawId));
|
|
4933
5157
|
}
|
|
4934
5158
|
return out;
|
|
4935
5159
|
}
|
|
4936
|
-
function hashJoin(leftRows, leg, rightSnapshot) {
|
|
5160
|
+
function hashJoin(leftRows, leg, rightSnapshot, i18nResolve) {
|
|
4937
5161
|
const rightMap = /* @__PURE__ */ new Map();
|
|
4938
5162
|
for (const record of rightSnapshot) {
|
|
4939
5163
|
const rawId = readPath(record, "id");
|
|
@@ -4946,7 +5170,8 @@ function hashJoin(leftRows, leg, rightSnapshot) {
|
|
|
4946
5170
|
for (const left of leftRows) {
|
|
4947
5171
|
const rawId = readPath(left, leg.field);
|
|
4948
5172
|
const key = coerceRefKey(rawId);
|
|
4949
|
-
|
|
5173
|
+
let right = key === null ? void 0 : rightMap.get(key);
|
|
5174
|
+
if (i18nResolve && right !== void 0) right = i18nResolve(right);
|
|
4950
5175
|
out.push(attachJoin(left, leg, right, rawId));
|
|
4951
5176
|
}
|
|
4952
5177
|
return out;
|
|
@@ -4981,6 +5206,7 @@ var init_join = __esm({
|
|
|
4981
5206
|
"use strict";
|
|
4982
5207
|
init_predicate();
|
|
4983
5208
|
init_errors();
|
|
5209
|
+
init_core();
|
|
4984
5210
|
DEFAULT_JOIN_MAX_ROWS = 5e4;
|
|
4985
5211
|
JOIN_WARN_FRACTION = 0.8;
|
|
4986
5212
|
warnedDanglingKeys = /* @__PURE__ */ new Set();
|
|
@@ -5285,7 +5511,7 @@ var init_money_reducer = __esm({
|
|
|
5285
5511
|
});
|
|
5286
5512
|
|
|
5287
5513
|
// src/query/builder.ts
|
|
5288
|
-
function executePlanWithSource(source, plan, joinContext) {
|
|
5514
|
+
function executePlanWithSource(source, plan, joinContext, locale) {
|
|
5289
5515
|
const hasCrossJoins = plan.clauses.some((c) => c.type === "crossJoin");
|
|
5290
5516
|
let result;
|
|
5291
5517
|
if (hasCrossJoins) {
|
|
@@ -5300,7 +5526,8 @@ function executePlanWithSource(source, plan, joinContext) {
|
|
|
5300
5526
|
result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
|
|
5301
5527
|
}
|
|
5302
5528
|
if (plan.orderBy.length > 0) {
|
|
5303
|
-
|
|
5529
|
+
const labelMaps = buildOrderLabelMaps(plan.orderBy, joinContext, locale);
|
|
5530
|
+
result = sortRecords(result, plan.orderBy, source.moneyFields, labelMaps);
|
|
5304
5531
|
}
|
|
5305
5532
|
if (plan.offset > 0) {
|
|
5306
5533
|
result = result.slice(plan.offset);
|
|
@@ -5439,17 +5666,55 @@ function applyCrossJoin(leftRel, clause, rightSource) {
|
|
|
5439
5666
|
}
|
|
5440
5667
|
return expanded;
|
|
5441
5668
|
}
|
|
5442
|
-
function sortRecords(records, orderBy) {
|
|
5669
|
+
function sortRecords(records, orderBy, moneyFields, labelMaps) {
|
|
5443
5670
|
return [...records].sort((a, b) => {
|
|
5444
|
-
for (const { field, direction } of orderBy) {
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
const
|
|
5671
|
+
for (const { field, direction, by } of orderBy) {
|
|
5672
|
+
let av = readField(a, field);
|
|
5673
|
+
let bv = readField(b, field);
|
|
5674
|
+
const labelMap = by === "label" ? labelMaps?.get(field) : void 0;
|
|
5675
|
+
if (labelMap) {
|
|
5676
|
+
av = (typeof av === "string" ? labelMap.get(av) : void 0) ?? av;
|
|
5677
|
+
bv = (typeof bv === "string" ? labelMap.get(bv) : void 0) ?? bv;
|
|
5678
|
+
const cmp2 = compareValues(av, bv);
|
|
5679
|
+
if (cmp2 !== 0) return direction === "asc" ? cmp2 : -cmp2;
|
|
5680
|
+
continue;
|
|
5681
|
+
}
|
|
5682
|
+
const desc = moneyFields?.[field];
|
|
5683
|
+
const cmp = desc ? compareMoney(av, bv, desc) : compareValues(av, bv);
|
|
5448
5684
|
if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
|
|
5449
5685
|
}
|
|
5450
5686
|
return 0;
|
|
5451
5687
|
});
|
|
5452
5688
|
}
|
|
5689
|
+
function buildOrderLabelMaps(orderBy, joinContext, locale) {
|
|
5690
|
+
if (!joinContext?.resolveDictSource) return void 0;
|
|
5691
|
+
const resolveDict = joinContext.resolveDictSource.bind(joinContext);
|
|
5692
|
+
let maps;
|
|
5693
|
+
for (const { field, by } of orderBy) {
|
|
5694
|
+
if (by !== "label") continue;
|
|
5695
|
+
const dictSource = resolveDict(field);
|
|
5696
|
+
if (!dictSource) continue;
|
|
5697
|
+
const loc = locale ?? dictSource.displayLocale;
|
|
5698
|
+
if (loc === void 0) continue;
|
|
5699
|
+
const codeToLabel = /* @__PURE__ */ new Map();
|
|
5700
|
+
for (const entry of dictSource.snapshot()) {
|
|
5701
|
+
const k = entry["key"];
|
|
5702
|
+
const labels = entry["labels"];
|
|
5703
|
+
const label = labels?.[loc];
|
|
5704
|
+
if (typeof k === "string" && typeof label === "string") codeToLabel.set(k, label);
|
|
5705
|
+
}
|
|
5706
|
+
;
|
|
5707
|
+
(maps ??= /* @__PURE__ */ new Map()).set(field, codeToLabel);
|
|
5708
|
+
}
|
|
5709
|
+
return maps;
|
|
5710
|
+
}
|
|
5711
|
+
function compareMoney(a, b, desc) {
|
|
5712
|
+
const av = moneyScaledValue(a, desc);
|
|
5713
|
+
const bv = moneyScaledValue(b, desc);
|
|
5714
|
+
if (av === null) return bv === null ? 0 : 1;
|
|
5715
|
+
if (bv === null) return -1;
|
|
5716
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
5717
|
+
}
|
|
5453
5718
|
function readField(record, field) {
|
|
5454
5719
|
if (record === null || record === void 0) return void 0;
|
|
5455
5720
|
if (!field.includes(".")) {
|
|
@@ -5743,11 +6008,16 @@ var init_builder = __esm({
|
|
|
5743
6008
|
this.predicates
|
|
5744
6009
|
);
|
|
5745
6010
|
}
|
|
5746
|
-
/**
|
|
5747
|
-
|
|
6011
|
+
/**
|
|
6012
|
+
* Sort by a field. Subsequent calls are tie-breakers. Pass
|
|
6013
|
+
* `{ by: 'label' }` to sort a `dictKey`/`staticDict` field by its resolved
|
|
6014
|
+
* label at the query locale instead of the stored code (#285).
|
|
6015
|
+
*/
|
|
6016
|
+
orderBy(field, direction = "asc", opts) {
|
|
6017
|
+
const entry = opts?.by === "label" ? { field, direction, by: "label" } : { field, direction };
|
|
5748
6018
|
return new _Query(
|
|
5749
6019
|
this.source,
|
|
5750
|
-
{ ...this.plan, orderBy: [...this.plan.orderBy,
|
|
6020
|
+
{ ...this.plan, orderBy: [...this.plan.orderBy, entry] },
|
|
5751
6021
|
this.joinContext,
|
|
5752
6022
|
this.aggregateStrategy,
|
|
5753
6023
|
this.predicates
|
|
@@ -5950,16 +6220,21 @@ var init_builder = __esm({
|
|
|
5950
6220
|
* carries any join legs, they are applied after `where` / `orderBy`
|
|
5951
6221
|
* / `limit` / `offset` narrow the left set. See the `.join()` doc
|
|
5952
6222
|
* for the ordering rationale.
|
|
6223
|
+
*
|
|
6224
|
+
* `opts.locale` (#285 §3) resolves JOINED right-side i18n fields at the
|
|
6225
|
+
* `join` layer to that locale; without it, the owning collection's default
|
|
6226
|
+
* locale applies, and a locale-less query leaves joined i18n fields raw.
|
|
6227
|
+
* (Left/base i18n fields are resolved by `get`/`list`, not here.)
|
|
5953
6228
|
*/
|
|
5954
|
-
toArray() {
|
|
5955
|
-
const base = this.decodeMoney(executePlanWithSource(this.source, this.plan, this.joinContext));
|
|
6229
|
+
toArray(opts) {
|
|
6230
|
+
const base = this.decodeMoney(executePlanWithSource(this.source, this.plan, this.joinContext, opts?.locale));
|
|
5956
6231
|
if (this.plan.joins.length === 0) return base;
|
|
5957
6232
|
if (!this.joinContext) {
|
|
5958
6233
|
throw new Error(
|
|
5959
6234
|
`Query.toArray(): plan carries ${this.plan.joins.length} join leg(s) but no JoinContext is attached. This usually means the Query was constructed via the raw Query constructor with a plan that had joins pre-populated. Use collection.query().join(...) instead.`
|
|
5960
6235
|
);
|
|
5961
6236
|
}
|
|
5962
|
-
return applyJoins(base, this.plan.joins, this.joinContext);
|
|
6237
|
+
return applyJoins(base, this.plan.joins, this.joinContext, opts?.locale);
|
|
5963
6238
|
}
|
|
5964
6239
|
/**
|
|
5965
6240
|
* Decode this source's money fields on read (stored scaled-int → canonical
|
|
@@ -5978,9 +6253,9 @@ var init_builder = __esm({
|
|
|
5978
6253
|
if (!moneyFields || Object.keys(moneyFields).length === 0) return records;
|
|
5979
6254
|
return records.map((r) => decodeMoneyFields(r, moneyFields, "raw"));
|
|
5980
6255
|
}
|
|
5981
|
-
/** Return the first matching record, or null. Joins are applied. */
|
|
5982
|
-
first() {
|
|
5983
|
-
const arr = this.limit(1).toArray();
|
|
6256
|
+
/** Return the first matching record, or null. Joins are applied. `opts.locale` resolves joined i18n fields (#285 §3). */
|
|
6257
|
+
first(opts) {
|
|
6258
|
+
const arr = this.limit(1).toArray(opts);
|
|
5984
6259
|
return arr[0] ?? null;
|
|
5985
6260
|
}
|
|
5986
6261
|
/**
|
|
@@ -6994,6 +7269,95 @@ var init_strategy5 = __esm({
|
|
|
6994
7269
|
}
|
|
6995
7270
|
});
|
|
6996
7271
|
|
|
7272
|
+
// src/search/tokenize.ts
|
|
7273
|
+
var WORD, tokenize;
|
|
7274
|
+
var init_tokenize = __esm({
|
|
7275
|
+
"src/search/tokenize.ts"() {
|
|
7276
|
+
"use strict";
|
|
7277
|
+
WORD = /[\p{L}\p{N}]+/gu;
|
|
7278
|
+
tokenize = (text) => {
|
|
7279
|
+
if (!text) return [];
|
|
7280
|
+
return text.normalize("NFKC").toLowerCase().match(WORD) ?? [];
|
|
7281
|
+
};
|
|
7282
|
+
}
|
|
7283
|
+
});
|
|
7284
|
+
|
|
7285
|
+
// src/search/scan.ts
|
|
7286
|
+
function fieldText(record, field) {
|
|
7287
|
+
const v = record[field];
|
|
7288
|
+
if (typeof v === "string") return v;
|
|
7289
|
+
if (v === null || v === void 0) return "";
|
|
7290
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
7291
|
+
return "";
|
|
7292
|
+
}
|
|
7293
|
+
function searchScan(entries, field, query, opts = {}, tokenizer = tokenize) {
|
|
7294
|
+
const queryTerms = tokenizer(query);
|
|
7295
|
+
if (queryTerms.length === 0) return [];
|
|
7296
|
+
const match = opts.match ?? "any";
|
|
7297
|
+
const usePrefix = opts.prefix ?? false;
|
|
7298
|
+
const exactTerms = usePrefix ? queryTerms.slice(0, -1) : queryTerms;
|
|
7299
|
+
const prefixTerm = usePrefix ? queryTerms[queryTerms.length - 1] : void 0;
|
|
7300
|
+
const docs = entries.map((e) => ({ id: e.id, record: e.record, terms: tokenizer(fieldText(e.record, field)) }));
|
|
7301
|
+
const N = docs.length || 1;
|
|
7302
|
+
const df = /* @__PURE__ */ new Map();
|
|
7303
|
+
let totalLen = 0;
|
|
7304
|
+
for (const d of docs) {
|
|
7305
|
+
totalLen += d.terms.length;
|
|
7306
|
+
for (const t of new Set(d.terms)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
7307
|
+
}
|
|
7308
|
+
const avgdl = totalLen / N || 1;
|
|
7309
|
+
let prefixDf = 0;
|
|
7310
|
+
if (prefixTerm !== void 0) {
|
|
7311
|
+
for (const d of docs) {
|
|
7312
|
+
if (d.terms.some((t) => t.startsWith(prefixTerm))) prefixDf++;
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
const requiredCount = exactTerms.length + (prefixTerm !== void 0 ? 1 : 0);
|
|
7316
|
+
const results = [];
|
|
7317
|
+
for (const d of docs) {
|
|
7318
|
+
const tf = /* @__PURE__ */ new Map();
|
|
7319
|
+
for (const t of d.terms) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
7320
|
+
const matched = [];
|
|
7321
|
+
for (const qt of exactTerms) {
|
|
7322
|
+
const c = tf.get(qt) ?? 0;
|
|
7323
|
+
if (c > 0) matched.push({ tf: c, df: df.get(qt) ?? 0 });
|
|
7324
|
+
}
|
|
7325
|
+
if (prefixTerm !== void 0) {
|
|
7326
|
+
let ptf = 0;
|
|
7327
|
+
for (const [t, c] of tf) if (t.startsWith(prefixTerm)) ptf += c;
|
|
7328
|
+
if (ptf > 0) matched.push({ tf: ptf, df: prefixDf });
|
|
7329
|
+
}
|
|
7330
|
+
if (matched.length === 0) continue;
|
|
7331
|
+
if (match === "all" && matched.length < requiredCount) continue;
|
|
7332
|
+
let score = 0;
|
|
7333
|
+
for (const m of matched) {
|
|
7334
|
+
const idf = Math.log(1 + (N - m.df + 0.5) / (m.df + 0.5));
|
|
7335
|
+
const denom = m.tf + K1 * (1 - B + B * (d.terms.length / avgdl));
|
|
7336
|
+
score += idf * (m.tf * (K1 + 1) / (denom || 1));
|
|
7337
|
+
}
|
|
7338
|
+
results.push({ id: d.id, score, record: d.record });
|
|
7339
|
+
}
|
|
7340
|
+
results.sort((a, b) => b.score - a.score);
|
|
7341
|
+
return opts.limit !== void 0 ? results.slice(0, opts.limit) : results;
|
|
7342
|
+
}
|
|
7343
|
+
var K1, B;
|
|
7344
|
+
var init_scan = __esm({
|
|
7345
|
+
"src/search/scan.ts"() {
|
|
7346
|
+
"use strict";
|
|
7347
|
+
init_tokenize();
|
|
7348
|
+
K1 = 1.2;
|
|
7349
|
+
B = 0.75;
|
|
7350
|
+
}
|
|
7351
|
+
});
|
|
7352
|
+
|
|
7353
|
+
// src/search/index.ts
|
|
7354
|
+
var init_search = __esm({
|
|
7355
|
+
"src/search/index.ts"() {
|
|
7356
|
+
"use strict";
|
|
7357
|
+
init_scan();
|
|
7358
|
+
}
|
|
7359
|
+
});
|
|
7360
|
+
|
|
6997
7361
|
// src/indexing/unique-constraints.ts
|
|
6998
7362
|
function buildUniqueConstraintSet(collectionName, indexes, mode) {
|
|
6999
7363
|
const uniqueDefs = [];
|
|
@@ -8011,12 +8375,13 @@ var executor_exports2 = {};
|
|
|
8011
8375
|
__export(executor_exports2, {
|
|
8012
8376
|
MaterializedViewExecutor: () => MaterializedViewExecutor
|
|
8013
8377
|
});
|
|
8014
|
-
async function materializeQueryResult(q, mvName) {
|
|
8378
|
+
async function materializeQueryResult(q, mvName, i18nLocale, i18nFields) {
|
|
8015
8379
|
if (typeof q?.toArray === "function") {
|
|
8016
8380
|
return await q.toArray();
|
|
8017
8381
|
}
|
|
8018
8382
|
if (typeof q?.run === "function") {
|
|
8019
|
-
const
|
|
8383
|
+
const runOpts = i18nLocale !== void 0 ? { locale: i18nLocale, i18nFields } : void 0;
|
|
8384
|
+
const result = await Promise.resolve(q.run(runOpts));
|
|
8020
8385
|
if (Array.isArray(result)) {
|
|
8021
8386
|
return result;
|
|
8022
8387
|
}
|
|
@@ -8045,6 +8410,29 @@ async function materializeUnionResult(spec, db) {
|
|
|
8045
8410
|
}
|
|
8046
8411
|
if (!spec.groupBy) return unified;
|
|
8047
8412
|
const groupFields = typeof spec.groupBy === "string" ? [spec.groupBy] : spec.groupBy;
|
|
8413
|
+
if (spec.i18nLocale !== void 0 && spec.i18nFields !== void 0) {
|
|
8414
|
+
const groupI18n = {};
|
|
8415
|
+
for (const f of groupFields) {
|
|
8416
|
+
const d = spec.i18nFields[f];
|
|
8417
|
+
if (d !== void 0) groupI18n[f] = d;
|
|
8418
|
+
}
|
|
8419
|
+
if (Object.keys(groupI18n).length > 0) {
|
|
8420
|
+
for (let i = 0; i < unified.length; i++) {
|
|
8421
|
+
unified[i] = applyI18nLocale(unified[i], groupI18n, spec.i18nLocale, void 0, "mv");
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
for (const f of groupFields) {
|
|
8426
|
+
for (const row of unified) {
|
|
8427
|
+
const v = row[f];
|
|
8428
|
+
if (v !== null && typeof v === "object") {
|
|
8429
|
+
throw new LocaleNotSpecifiedError(
|
|
8430
|
+
f,
|
|
8431
|
+
`Materialized view "${spec.name}" groups by "${f}", whose value is a raw i18n locale map \u2014 an unstable object group key. Declare { i18nLocale, i18nFields } on the MV to resolve it at the 'mv' layer, or group by a dictKey/staticDict code (the stable key) and resolve the label at read time.`
|
|
8432
|
+
);
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8048
8436
|
if (!spec.aggregate) {
|
|
8049
8437
|
const seen = /* @__PURE__ */ new Map();
|
|
8050
8438
|
for (const row of unified) {
|
|
@@ -8076,6 +8464,7 @@ var init_executor2 = __esm({
|
|
|
8076
8464
|
init_registry();
|
|
8077
8465
|
init_groupby();
|
|
8078
8466
|
init_canonical_key();
|
|
8467
|
+
init_core();
|
|
8079
8468
|
DEFAULT_MAX_ROWS = 1e5;
|
|
8080
8469
|
MaterializedViewExecutor = {
|
|
8081
8470
|
async refresh(reg, accessor) {
|
|
@@ -8091,7 +8480,7 @@ var init_executor2 = __esm({
|
|
|
8091
8480
|
rows = await materializeUnionResult(spec, ctxForQuery);
|
|
8092
8481
|
} else {
|
|
8093
8482
|
const q = spec.query(ctxForQuery);
|
|
8094
|
-
rows = await materializeQueryResult(q, spec.name);
|
|
8483
|
+
rows = await materializeQueryResult(q, spec.name, spec.i18nLocale, spec.i18nFields);
|
|
8095
8484
|
}
|
|
8096
8485
|
if (rows.length > maxRows) {
|
|
8097
8486
|
throw new MaterializedViewTooLargeError(spec.name, rows.length, maxRows);
|
|
@@ -8272,6 +8661,15 @@ var init_fanout_sidecar = __esm({
|
|
|
8272
8661
|
});
|
|
8273
8662
|
|
|
8274
8663
|
// src/collection.ts
|
|
8664
|
+
function selfWriteFieldEqual(a, b) {
|
|
8665
|
+
if (a === b) return true;
|
|
8666
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
8667
|
+
try {
|
|
8668
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
8669
|
+
} catch {
|
|
8670
|
+
return false;
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8275
8673
|
function warnOnceFallback(adapterName) {
|
|
8276
8674
|
if (fallbackWarned.has(adapterName)) return;
|
|
8277
8675
|
fallbackWarned.add(adapterName);
|
|
@@ -8341,6 +8739,7 @@ var init_collection = __esm({
|
|
|
8341
8739
|
init_persisted_indexes();
|
|
8342
8740
|
init_lazy_builder();
|
|
8343
8741
|
init_strategy5();
|
|
8742
|
+
init_search();
|
|
8344
8743
|
init_errors();
|
|
8345
8744
|
init_unique_constraints();
|
|
8346
8745
|
init_cache();
|
|
@@ -9276,6 +9675,111 @@ var init_collection = __esm({
|
|
|
9276
9675
|
* output (carries `_derivedFrom`) — defensive guard against missed
|
|
9277
9676
|
* cycle detection.
|
|
9278
9677
|
*/
|
|
9678
|
+
/**
|
|
9679
|
+
* @internal #376 — the RAW stored record (canonical-money form, i18n maps
|
|
9680
|
+
* intact), WITHOUT the locale resolution `get()` applies. Used as the
|
|
9681
|
+
* patch base for self-write reverse-denorm so writing back never clobbers
|
|
9682
|
+
* an i18n map or re-quantizes money incorrectly. Returns null for
|
|
9683
|
+
* missing / tombstoned records.
|
|
9684
|
+
*/
|
|
9685
|
+
async _getStoredRecord(id) {
|
|
9686
|
+
let raw;
|
|
9687
|
+
if (this.lazy && this.lru) {
|
|
9688
|
+
const cached = this.lru.get(id);
|
|
9689
|
+
if (cached) raw = cached.record;
|
|
9690
|
+
else {
|
|
9691
|
+
const env = await this.adapter.get(this.vault, this.name, id);
|
|
9692
|
+
if (!env || isTombstone(env, this.encrypted)) return null;
|
|
9693
|
+
raw = await this.decryptRecord(env, { id });
|
|
9694
|
+
if (raw === null) return null;
|
|
9695
|
+
this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
|
|
9696
|
+
}
|
|
9697
|
+
} else {
|
|
9698
|
+
await this.ensureHydrated();
|
|
9699
|
+
raw = this.cache.get(id)?.record ?? null;
|
|
9700
|
+
}
|
|
9701
|
+
if (raw === null) return null;
|
|
9702
|
+
return canonicalizeStoredMoney(raw, this.moneyFields);
|
|
9703
|
+
}
|
|
9704
|
+
/**
|
|
9705
|
+
* @internal #376 — ids of records whose top-level `field` equals `value`.
|
|
9706
|
+
* Uses the FK index when the field is indexed (O(matches)); otherwise a
|
|
9707
|
+
* linear scan (O(N) — fine for small child sets; index the FK to scale).
|
|
9708
|
+
*/
|
|
9709
|
+
async _findMatchingIds(field, value) {
|
|
9710
|
+
const hit = this.getIndexes()?.lookupEqual(field, value);
|
|
9711
|
+
if (hit) return [...hit];
|
|
9712
|
+
const target = String(value);
|
|
9713
|
+
const matches = (rec) => {
|
|
9714
|
+
const fv = rec[field];
|
|
9715
|
+
return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
|
|
9716
|
+
};
|
|
9717
|
+
if (!this.lazy) {
|
|
9718
|
+
await this.ensureHydrated();
|
|
9719
|
+
const out2 = [];
|
|
9720
|
+
for (const [rid, e] of this.cache) {
|
|
9721
|
+
if (matches(e.record)) out2.push(rid);
|
|
9722
|
+
}
|
|
9723
|
+
return out2;
|
|
9724
|
+
}
|
|
9725
|
+
const ids = await this.adapter.list(this.vault, this.name);
|
|
9726
|
+
const out = [];
|
|
9727
|
+
for (const rid of ids) {
|
|
9728
|
+
const raw = await this._getStoredRecord(rid);
|
|
9729
|
+
if (raw !== null && matches(raw)) out.push(rid);
|
|
9730
|
+
}
|
|
9731
|
+
return out;
|
|
9732
|
+
}
|
|
9733
|
+
/**
|
|
9734
|
+
* @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
|
|
9735
|
+
* Gathers every child of `parentId`, runs `compute`, and patches only the
|
|
9736
|
+
* rollup `field` onto the parent's raw stored record (value-equality
|
|
9737
|
+
* guarded). No-op when the parent record does not exist.
|
|
9738
|
+
*/
|
|
9739
|
+
async recomputeRollup(spec, parentId) {
|
|
9740
|
+
if (this.derivationSource === void 0 || spec.rollup === void 0) return;
|
|
9741
|
+
const { from, key, field, compute } = spec.rollup;
|
|
9742
|
+
const into = spec.source;
|
|
9743
|
+
const intoColl = this.derivationSource.getCollection(into);
|
|
9744
|
+
const base = await intoColl._getStoredRecord(parentId);
|
|
9745
|
+
if (base === null) return;
|
|
9746
|
+
const fromColl = this.derivationSource.getCollection(from);
|
|
9747
|
+
const childIds = await fromColl._findMatchingIds(key, parentId);
|
|
9748
|
+
const children = [];
|
|
9749
|
+
for (const cid of childIds) {
|
|
9750
|
+
const c = await fromColl.get(cid);
|
|
9751
|
+
if (c !== null && c !== void 0) children.push(c);
|
|
9752
|
+
}
|
|
9753
|
+
const newValue = compute(children);
|
|
9754
|
+
if (selfWriteFieldEqual(base[field], newValue)) return;
|
|
9755
|
+
const patched = { ...base, [field]: newValue };
|
|
9756
|
+
const txCtx = this.derivationSource.getActiveTxContext();
|
|
9757
|
+
if (txCtx !== null) {
|
|
9758
|
+
const prior = await this.adapter.get(this.vault, into, parentId);
|
|
9759
|
+
txCtx._executed.push({
|
|
9760
|
+
op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
|
|
9761
|
+
priorEnvelope: prior
|
|
9762
|
+
});
|
|
9763
|
+
}
|
|
9764
|
+
await intoColl.put(parentId, patched);
|
|
9765
|
+
}
|
|
9766
|
+
/**
|
|
9767
|
+
* @internal #376 slice 2 — fire any rollups for which THIS collection is the
|
|
9768
|
+
* child `from`, recomputing the affected parent after a child delete. Called
|
|
9769
|
+
* from the delete path with the just-removed record's key value. Other
|
|
9770
|
+
* derivation kinds do not react to deletes (unchanged).
|
|
9771
|
+
*/
|
|
9772
|
+
async dispatchRollupsOnDelete(deleted) {
|
|
9773
|
+
if (this.derivationSource === void 0) return;
|
|
9774
|
+
const registry = this.derivationSource.registry();
|
|
9775
|
+
const rec = deleted;
|
|
9776
|
+
for (const { spec } of registry.strategiesForSource(this.name)) {
|
|
9777
|
+
if (!spec.rollup || spec.rollup.from !== this.name) continue;
|
|
9778
|
+
const kv = rec[spec.rollup.key];
|
|
9779
|
+
if (typeof kv !== "string" && typeof kv !== "number") continue;
|
|
9780
|
+
await this.recomputeRollup(spec, String(kv));
|
|
9781
|
+
}
|
|
9782
|
+
}
|
|
9279
9783
|
async dispatchDerivations(id, record, version) {
|
|
9280
9784
|
if (this.derivationSource === void 0) return;
|
|
9281
9785
|
const incoming = canonicalizeStoredMoney(record, this.moneyFields);
|
|
@@ -9286,29 +9790,60 @@ var init_collection = __esm({
|
|
|
9286
9790
|
let DerivationExecutor2 = null;
|
|
9287
9791
|
for (const { spec, strategyHash } of strategies) {
|
|
9288
9792
|
const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
|
|
9289
|
-
if (
|
|
9290
|
-
if (
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
if (spec.source === this.name) {
|
|
9296
|
-
sourceWithId = { ...incoming, id };
|
|
9793
|
+
if (spec.rollup) {
|
|
9794
|
+
if (mode !== "eager") continue;
|
|
9795
|
+
let parentId;
|
|
9796
|
+
if (this.name === spec.rollup.from) {
|
|
9797
|
+
const kv = incoming[spec.rollup.key];
|
|
9798
|
+
parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
|
|
9297
9799
|
} else {
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9800
|
+
parentId = id;
|
|
9801
|
+
}
|
|
9802
|
+
if (parentId !== null) await this.recomputeRollup(spec, parentId);
|
|
9803
|
+
continue;
|
|
9804
|
+
}
|
|
9805
|
+
const isSource = spec.source === this.name;
|
|
9806
|
+
const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
|
|
9807
|
+
const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
|
|
9808
|
+
const runs = [];
|
|
9809
|
+
if (isSource) {
|
|
9810
|
+
runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
|
|
9811
|
+
} else if (isSibling) {
|
|
9812
|
+
const p = await this.derivationSource.getCollection(spec.source).get(id);
|
|
9813
|
+
if (p !== null && p !== void 0) {
|
|
9814
|
+
const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
|
|
9815
|
+
runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
|
|
9816
|
+
}
|
|
9817
|
+
} else if (trigger) {
|
|
9818
|
+
const srcColl = this.derivationSource.getCollection(spec.source);
|
|
9819
|
+
const ids = await srcColl._findMatchingIds(trigger.on, id);
|
|
9820
|
+
if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
|
|
9821
|
+
throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
|
|
9302
9822
|
}
|
|
9823
|
+
for (const sid of ids) {
|
|
9824
|
+
const raw = await srcColl._getStoredRecord(sid);
|
|
9825
|
+
if (raw === null) continue;
|
|
9826
|
+
runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
if (runs.length === 0) continue;
|
|
9830
|
+
if (mode !== "eager") {
|
|
9831
|
+
for (const run of runs) await markStale(registry, spec, run.runId);
|
|
9832
|
+
continue;
|
|
9833
|
+
}
|
|
9834
|
+
if (DerivationExecutor2 === null) {
|
|
9835
|
+
({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports)));
|
|
9836
|
+
}
|
|
9837
|
+
for (const run of runs) {
|
|
9303
9838
|
const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
|
|
9304
|
-
const result = await DerivationExecutor2.run(spec,
|
|
9839
|
+
const result = await DerivationExecutor2.run(spec, run.input, run.version, strategyHash, ctx);
|
|
9305
9840
|
for (const key of Object.keys(spec.outputs)) {
|
|
9306
9841
|
const out = result.outputs[key];
|
|
9307
9842
|
if (!out) continue;
|
|
9308
9843
|
if (out.kind === "failed") {
|
|
9309
9844
|
const err = out.error;
|
|
9310
9845
|
if (spec.strict) throw err;
|
|
9311
|
-
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${
|
|
9846
|
+
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
|
|
9312
9847
|
continue;
|
|
9313
9848
|
}
|
|
9314
9849
|
const outSpec = spec.outputs[key];
|
|
@@ -9321,7 +9856,7 @@ var init_collection = __esm({
|
|
|
9321
9856
|
this.adapter,
|
|
9322
9857
|
this.vault,
|
|
9323
9858
|
spec.source,
|
|
9324
|
-
|
|
9859
|
+
run.runId,
|
|
9325
9860
|
key
|
|
9326
9861
|
);
|
|
9327
9862
|
const prevKeys = new Set(prior?.keys ?? []);
|
|
@@ -9348,7 +9883,7 @@ var init_collection = __esm({
|
|
|
9348
9883
|
}
|
|
9349
9884
|
await saveFanoutSidecar2(this.adapter, this.vault, {
|
|
9350
9885
|
source: spec.source,
|
|
9351
|
-
sourceId:
|
|
9886
|
+
sourceId: run.runId,
|
|
9352
9887
|
outputKey: key,
|
|
9353
9888
|
outputCollection: outSpec.collection,
|
|
9354
9889
|
keys: newKeysList
|
|
@@ -9356,25 +9891,44 @@ var init_collection = __esm({
|
|
|
9356
9891
|
continue;
|
|
9357
9892
|
}
|
|
9358
9893
|
if (out.skipped === true) {
|
|
9359
|
-
await outputCollection._internalDelete(
|
|
9894
|
+
await outputCollection._internalDelete(run.runId, txCtx);
|
|
9895
|
+
continue;
|
|
9896
|
+
}
|
|
9897
|
+
if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
|
|
9898
|
+
const value = out.value;
|
|
9899
|
+
const patched = { ...run.base };
|
|
9900
|
+
let changed = false;
|
|
9901
|
+
for (const f of outSpec.denorm) {
|
|
9902
|
+
if (!selfWriteFieldEqual(run.base[f], value[f])) {
|
|
9903
|
+
patched[f] = value[f];
|
|
9904
|
+
changed = true;
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
if (!changed) continue;
|
|
9908
|
+
if (txCtx !== null) {
|
|
9909
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
9910
|
+
txCtx._executed.push({
|
|
9911
|
+
op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
|
|
9912
|
+
priorEnvelope: prior
|
|
9913
|
+
});
|
|
9914
|
+
}
|
|
9915
|
+
await outputCollection.put(run.runId, patched);
|
|
9360
9916
|
continue;
|
|
9361
9917
|
}
|
|
9362
9918
|
if (txCtx !== null) {
|
|
9363
|
-
const prior = await this.adapter.get(this.vault, outSpec.collection,
|
|
9919
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
9364
9920
|
txCtx._executed.push({
|
|
9365
9921
|
op: {
|
|
9366
9922
|
type: "put",
|
|
9367
9923
|
vaultName: this.vault,
|
|
9368
9924
|
collectionName: outSpec.collection,
|
|
9369
|
-
id
|
|
9925
|
+
id: run.runId
|
|
9370
9926
|
},
|
|
9371
9927
|
priorEnvelope: prior
|
|
9372
9928
|
});
|
|
9373
9929
|
}
|
|
9374
|
-
await outputCollection.put(
|
|
9930
|
+
await outputCollection.put(run.runId, out.value);
|
|
9375
9931
|
}
|
|
9376
|
-
} else {
|
|
9377
|
-
await markStale(registry, spec, id);
|
|
9378
9932
|
}
|
|
9379
9933
|
}
|
|
9380
9934
|
}
|
|
@@ -9591,6 +10145,7 @@ var init_collection = __esm({
|
|
|
9591
10145
|
if (!internal) {
|
|
9592
10146
|
await this.dispatchMaterializedViewsOnDelete(id);
|
|
9593
10147
|
await this.dispatchArrayDerivationsOnDelete(id);
|
|
10148
|
+
if (existing) await this.dispatchRollupsOnDelete(existing.record);
|
|
9594
10149
|
}
|
|
9595
10150
|
}
|
|
9596
10151
|
/**
|
|
@@ -9747,6 +10302,29 @@ var init_collection = __esm({
|
|
|
9747
10302
|
hasReadTransforms() {
|
|
9748
10303
|
return this.moneyFields !== void 0 && Object.keys(this.moneyFields).length > 0 || this.i18nFields !== void 0 && Object.keys(this.i18nFields).length > 0 || this.dictKeyFields !== void 0 && Object.keys(this.dictKeyFields).length > 0;
|
|
9749
10304
|
}
|
|
10305
|
+
/**
|
|
10306
|
+
* Scan-mode full-text search over a plain-text `field` (#308). Decrypts the
|
|
10307
|
+
* collection in memory and ranks records by BM25 against the tokenized query.
|
|
10308
|
+
* **Zero added store leakage** — pure client-side scan; nothing searchable is
|
|
10309
|
+
* written to the store. (A store-usable blind index for at-scale search is a
|
|
10310
|
+
* separate, gated opt-in — see the #308 design note.) Eager mode only.
|
|
10311
|
+
*
|
|
10312
|
+
* `opts.match` (`'any'` default | `'all'`), `opts.prefix` (last query term as
|
|
10313
|
+
* a prefix → typeahead), `opts.limit` (top-N). Returns `{ id, score, record }`
|
|
10314
|
+
* ranked by descending score. The default tokenizer is word-boundary based —
|
|
10315
|
+
* see `src/search/tokenize.ts` for the Thai/CJK caveat.
|
|
10316
|
+
*/
|
|
10317
|
+
async search(field, query, opts = {}) {
|
|
10318
|
+
if (this.lazy) {
|
|
10319
|
+
throw new Error(
|
|
10320
|
+
`Collection "${this.name}": search() (scan mode) requires eager mode (prefetch: true). A store-usable blind index for lazy / at-scale search is a separate gated opt-in (#308).`
|
|
10321
|
+
);
|
|
10322
|
+
}
|
|
10323
|
+
await this.ensureHydrated();
|
|
10324
|
+
const entries = [];
|
|
10325
|
+
for (const [id, e] of this.cache) entries.push({ id, record: e.record });
|
|
10326
|
+
return searchScan(entries, field, query, opts);
|
|
10327
|
+
}
|
|
9750
10328
|
// ─── Bulk operations ─────────────────────────────────────
|
|
9751
10329
|
/**
|
|
9752
10330
|
* Put many records in one call. Each item is processed sequentially
|
|
@@ -9924,6 +10502,10 @@ var init_collection = __esm({
|
|
|
9924
10502
|
leftCollection,
|
|
9925
10503
|
resolveRef: (field) => resolver.resolveRef(leftCollection, field),
|
|
9926
10504
|
resolveSource: (collectionName) => resolver.resolveSource(collectionName),
|
|
10505
|
+
// #285 §3 — flow the vault/collection default locale to joins so a
|
|
10506
|
+
// joined i18n field resolves like get()/list() when no per-call
|
|
10507
|
+
// locale is given; toArray({ locale }) overrides it.
|
|
10508
|
+
...this.defaultLocale !== void 0 ? { defaultLocale: this.defaultLocale } : {},
|
|
9927
10509
|
...resolver.resolveDictSource ? { resolveDictSource: (field) => resolver.resolveDictSource(leftCollection, field) } : {}
|
|
9928
10510
|
} : void 0;
|
|
9929
10511
|
return new Query(source, void 0, joinContext, this.aggregateStrategy);
|
|
@@ -10012,7 +10594,10 @@ var init_collection = __esm({
|
|
|
10012
10594
|
};
|
|
10013
10595
|
this.emitter.on("change", handler);
|
|
10014
10596
|
return () => this.emitter.off("change", handler);
|
|
10015
|
-
}
|
|
10597
|
+
},
|
|
10598
|
+
// #285 §3 — expose this (right-side) collection's i18nText descriptors so
|
|
10599
|
+
// the join executor can resolve joined i18n fields at the `join` layer.
|
|
10600
|
+
...this.i18nFields !== void 0 ? { i18nFields: this.i18nFields } : {}
|
|
10016
10601
|
};
|
|
10017
10602
|
}
|
|
10018
10603
|
/**
|
|
@@ -10245,6 +10830,10 @@ var init_collection = __esm({
|
|
|
10245
10830
|
leftCollection,
|
|
10246
10831
|
resolveRef: (field) => resolver.resolveRef(leftCollection, field),
|
|
10247
10832
|
resolveSource: (collectionName) => resolver.resolveSource(collectionName),
|
|
10833
|
+
// #285 §3 — flow the vault/collection default locale to joins so a
|
|
10834
|
+
// joined i18n field resolves like get()/list() when no per-call
|
|
10835
|
+
// locale is given; toArray({ locale }) overrides it.
|
|
10836
|
+
...this.defaultLocale !== void 0 ? { defaultLocale: this.defaultLocale } : {},
|
|
10248
10837
|
...resolver.resolveDictSource ? { resolveDictSource: (field) => resolver.resolveDictSource(leftCollection, field) } : {}
|
|
10249
10838
|
} : void 0;
|
|
10250
10839
|
return new ScanBuilder(
|
|
@@ -10645,14 +11234,15 @@ var init_collection = __esm({
|
|
|
10645
11234
|
(d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
|
|
10646
11235
|
);
|
|
10647
11236
|
if (!locale && !hasStaticDisplay) return result;
|
|
11237
|
+
const layer = localeOpts?._layer ?? "read";
|
|
10648
11238
|
if (locale && hasI18n && this.i18nFields) {
|
|
10649
|
-
result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
|
|
11239
|
+
result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
|
|
10650
11240
|
}
|
|
10651
11241
|
if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
|
|
10652
11242
|
const withLabels = { ...result };
|
|
10653
11243
|
const resolver = this.dictLabelResolver;
|
|
10654
11244
|
for (const [field, desc] of Object.entries(this.dictKeyFields)) {
|
|
10655
|
-
const policy = desc.onMissing ? resolvePolicy(desc.onMissing,
|
|
11245
|
+
const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
|
|
10656
11246
|
const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
|
|
10657
11247
|
const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
|
|
10658
11248
|
const resolveKey = async (key) => {
|
|
@@ -10801,6 +11391,34 @@ var init_collection = __esm({
|
|
|
10801
11391
|
}
|
|
10802
11392
|
}
|
|
10803
11393
|
}
|
|
11394
|
+
/**
|
|
11395
|
+
* @internal — hard-delete this record's persisted `_idx/<field>/<recordId>`
|
|
11396
|
+
* side-cars for the erasure path (#401). `forget()` crypto-shreds the body but
|
|
11397
|
+
* keeps the collection DEK, under which these side-cars are encrypted — so
|
|
11398
|
+
* without this they leave the indexed field VALUES readable after a "forget".
|
|
11399
|
+
*
|
|
11400
|
+
* Content-free: the side-car id is `encodeIdxId(def.key, id)`, so it needs no
|
|
11401
|
+
* body decode (the body is being shredded). Eager mode has no durable side-car
|
|
11402
|
+
* → no-op. The in-memory mirror is left as-is: it is ephemeral (rebuilt from
|
|
11403
|
+
* the now-deleted side-cars on reopen) and live reads skip the tombstone, so a
|
|
11404
|
+
* stale mirror hit cannot surface the erased record. Returns the count deleted
|
|
11405
|
+
* + the `def.key`s whose delete FAILED (residue that still leaks the value).
|
|
11406
|
+
*/
|
|
11407
|
+
async _purgePersistedIndexes(id) {
|
|
11408
|
+
const persisted = this.persistedIndexes;
|
|
11409
|
+
if (!persisted) return { purged: 0, residue: [] };
|
|
11410
|
+
let purged = 0;
|
|
11411
|
+
const residue = [];
|
|
11412
|
+
for (const def of persisted.definitions()) {
|
|
11413
|
+
try {
|
|
11414
|
+
await this.adapter.delete(this.vault, this.name, encodeIdxId(def.key, id));
|
|
11415
|
+
purged++;
|
|
11416
|
+
} catch {
|
|
11417
|
+
residue.push(def.key);
|
|
11418
|
+
}
|
|
11419
|
+
}
|
|
11420
|
+
return { purged, residue };
|
|
11421
|
+
}
|
|
10804
11422
|
/**
|
|
10805
11423
|
* Bulk-load the persisted-index mirror from `_idx/<field>/*` side-cars
|
|
10806
11424
|
* on first lazy-mode query. Idempotent — subsequent calls short-circuit
|
|
@@ -11672,6 +12290,35 @@ var init_archive = __esm({
|
|
|
11672
12290
|
});
|
|
11673
12291
|
|
|
11674
12292
|
// src/sequence/index.ts
|
|
12293
|
+
function compileSequenceFormat(format, series, partition) {
|
|
12294
|
+
const parts = partition ?? [];
|
|
12295
|
+
for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
|
|
12296
|
+
const token = m[1] ?? "";
|
|
12297
|
+
if (token === "seq") continue;
|
|
12298
|
+
if (SEQ_PAD_TOKEN.test(token)) continue;
|
|
12299
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
12300
|
+
if (partMatch) {
|
|
12301
|
+
const idx = Number(partMatch[1]);
|
|
12302
|
+
if (idx >= parts.length) {
|
|
12303
|
+
throw new ValidationError(
|
|
12304
|
+
`sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
|
|
12305
|
+
);
|
|
12306
|
+
}
|
|
12307
|
+
continue;
|
|
12308
|
+
}
|
|
12309
|
+
throw new ValidationError(
|
|
12310
|
+
`sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
|
|
12311
|
+
);
|
|
12312
|
+
}
|
|
12313
|
+
return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
|
|
12314
|
+
if (token === "seq") return String(serial);
|
|
12315
|
+
const padMatch = SEQ_PAD_TOKEN.exec(token);
|
|
12316
|
+
if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
|
|
12317
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
12318
|
+
if (partMatch) return String(parts[Number(partMatch[1])]);
|
|
12319
|
+
return full;
|
|
12320
|
+
});
|
|
12321
|
+
}
|
|
11675
12322
|
function resolveSequenceKey(series, opts) {
|
|
11676
12323
|
const partition = opts?.partition;
|
|
11677
12324
|
if (!partition || partition.length === 0) return series;
|
|
@@ -11692,7 +12339,7 @@ async function sleepBackoff2(attempt) {
|
|
|
11692
12339
|
const ms = Math.floor(Math.random() * ceil);
|
|
11693
12340
|
await new Promise((r) => setTimeout(r, ms));
|
|
11694
12341
|
}
|
|
11695
|
-
var SEQUENCE_COLLECTION, MAX_NEXT_ATTEMPTS, SequenceStore;
|
|
12342
|
+
var SEQUENCE_COLLECTION, MAX_NEXT_ATTEMPTS, SEQ_FORMAT_TOKEN, SEQ_PAD_TOKEN, SEQ_PARTITION_TOKEN, SequenceStore;
|
|
11696
12343
|
var init_sequence = __esm({
|
|
11697
12344
|
"src/sequence/index.ts"() {
|
|
11698
12345
|
"use strict";
|
|
@@ -11701,6 +12348,9 @@ var init_sequence = __esm({
|
|
|
11701
12348
|
init_errors();
|
|
11702
12349
|
SEQUENCE_COLLECTION = "_sequences";
|
|
11703
12350
|
MAX_NEXT_ATTEMPTS = 16;
|
|
12351
|
+
SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
|
|
12352
|
+
SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
|
|
12353
|
+
SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
|
|
11704
12354
|
SequenceStore = class {
|
|
11705
12355
|
adapter;
|
|
11706
12356
|
vault;
|
|
@@ -12145,6 +12795,9 @@ var init_strategy10 = __esm({
|
|
|
12145
12795
|
});
|
|
12146
12796
|
|
|
12147
12797
|
// src/refs.ts
|
|
12798
|
+
function isRefArray(desc) {
|
|
12799
|
+
return desc.isArray === true;
|
|
12800
|
+
}
|
|
12148
12801
|
var RefIntegrityError, RefRegistry;
|
|
12149
12802
|
var init_refs = __esm({
|
|
12150
12803
|
"src/refs.ts"() {
|
|
@@ -12190,7 +12843,7 @@ var init_refs = __esm({
|
|
|
12190
12843
|
for (const k of existingKeys) {
|
|
12191
12844
|
const a = existing[k];
|
|
12192
12845
|
const b = refs[k];
|
|
12193
|
-
if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
|
|
12846
|
+
if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
|
|
12194
12847
|
throw new Error(
|
|
12195
12848
|
`RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
|
|
12196
12849
|
);
|
|
@@ -12201,7 +12854,7 @@ var init_refs = __esm({
|
|
|
12201
12854
|
this.outbound.set(collection, { ...refs });
|
|
12202
12855
|
for (const [field, desc] of Object.entries(refs)) {
|
|
12203
12856
|
const list = this.inbound.get(desc.target) ?? [];
|
|
12204
|
-
list.push({ collection, field, mode: desc.mode });
|
|
12857
|
+
list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
|
|
12205
12858
|
this.inbound.set(desc.target, list);
|
|
12206
12859
|
}
|
|
12207
12860
|
}
|
|
@@ -12231,57 +12884,201 @@ var init_refs = __esm({
|
|
|
12231
12884
|
}
|
|
12232
12885
|
});
|
|
12233
12886
|
|
|
12234
|
-
// src/
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
var
|
|
12245
|
-
|
|
12887
|
+
// src/links/link-set.ts
|
|
12888
|
+
function linkCollectionName(name) {
|
|
12889
|
+
return `${LINK_COLLECTION_PREFIX}${name}`;
|
|
12890
|
+
}
|
|
12891
|
+
function isLinkCollectionName(name) {
|
|
12892
|
+
return name.startsWith(LINK_COLLECTION_PREFIX);
|
|
12893
|
+
}
|
|
12894
|
+
function linkRowKey(aId, bId) {
|
|
12895
|
+
return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
|
|
12896
|
+
}
|
|
12897
|
+
var LINK_COLLECTION_PREFIX, LinkSet, LinkEndpointError, LinkIntegrityError;
|
|
12898
|
+
var init_link_set = __esm({
|
|
12899
|
+
"src/links/link-set.ts"() {
|
|
12246
12900
|
"use strict";
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
}
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12901
|
+
init_types();
|
|
12902
|
+
init_crypto();
|
|
12903
|
+
init_errors();
|
|
12904
|
+
LINK_COLLECTION_PREFIX = "_links_";
|
|
12905
|
+
LinkSet = class {
|
|
12906
|
+
constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
|
|
12907
|
+
this.adapter = adapter;
|
|
12908
|
+
this.vault = vault;
|
|
12909
|
+
this.name = name;
|
|
12910
|
+
this.spec = spec;
|
|
12911
|
+
this.encrypted = encrypted;
|
|
12912
|
+
this.getDEK = getDEK;
|
|
12913
|
+
this.actor = actor;
|
|
12914
|
+
this.emitter = emitter;
|
|
12915
|
+
this.endpointExists = endpointExists;
|
|
12916
|
+
this.collName = linkCollectionName(name);
|
|
12917
|
+
}
|
|
12918
|
+
adapter;
|
|
12919
|
+
vault;
|
|
12920
|
+
name;
|
|
12921
|
+
spec;
|
|
12922
|
+
encrypted;
|
|
12923
|
+
getDEK;
|
|
12924
|
+
actor;
|
|
12925
|
+
emitter;
|
|
12926
|
+
endpointExists;
|
|
12927
|
+
collName;
|
|
12928
|
+
dekPromise = null;
|
|
12929
|
+
dek() {
|
|
12930
|
+
if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
|
|
12931
|
+
return this.dekPromise;
|
|
12932
|
+
}
|
|
12933
|
+
async encryptEntry(entry, version) {
|
|
12934
|
+
const json = JSON.stringify(entry);
|
|
12935
|
+
const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
|
|
12936
|
+
if (!this.encrypted) return { ...base, _iv: "", _data: json };
|
|
12937
|
+
const { iv, data } = await encrypt(json, await this.dek());
|
|
12938
|
+
return { ...base, _iv: iv, _data: data };
|
|
12939
|
+
}
|
|
12940
|
+
async decryptEntry(env) {
|
|
12941
|
+
const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
|
|
12942
|
+
return JSON.parse(json);
|
|
12943
|
+
}
|
|
12944
|
+
async connect(aId, bId, meta) {
|
|
12945
|
+
if (!await this.endpointExists(this.spec.a, aId)) {
|
|
12946
|
+
throw new LinkEndpointError(this.name, this.spec.a, aId);
|
|
12947
|
+
}
|
|
12948
|
+
if (!await this.endpointExists(this.spec.b, bId)) {
|
|
12949
|
+
throw new LinkEndpointError(this.name, this.spec.b, bId);
|
|
12950
|
+
}
|
|
12951
|
+
const key = linkRowKey(aId, bId);
|
|
12952
|
+
const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
|
|
12953
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
12954
|
+
const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
|
|
12955
|
+
await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
|
|
12956
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
|
|
12957
|
+
}
|
|
12958
|
+
async disconnect(aId, bId) {
|
|
12959
|
+
const key = linkRowKey(aId, bId);
|
|
12960
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
12961
|
+
if (!existing) return;
|
|
12962
|
+
await this.adapter.delete(this.vault, this.collName, key);
|
|
12963
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
|
|
12964
|
+
}
|
|
12965
|
+
async has(aId, bId) {
|
|
12966
|
+
return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
|
|
12967
|
+
}
|
|
12968
|
+
async of(id) {
|
|
12969
|
+
const rows = await this.list();
|
|
12970
|
+
return rows.filter((r) => r.a === id || r.b === id);
|
|
12971
|
+
}
|
|
12972
|
+
async list() {
|
|
12973
|
+
const keys = await this.adapter.list(this.vault, this.collName);
|
|
12974
|
+
const out = [];
|
|
12975
|
+
for (const key of keys) {
|
|
12976
|
+
const env = await this.adapter.get(this.vault, this.collName, key);
|
|
12977
|
+
if (!env) continue;
|
|
12978
|
+
const e = await this.decryptEntry(env);
|
|
12979
|
+
out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
|
|
12980
|
+
}
|
|
12981
|
+
return out;
|
|
12982
|
+
}
|
|
12983
|
+
// ── Vault-internal cascade helpers ──────────────────────────────────
|
|
12984
|
+
/** @internal — rows where the deleted endpoint id matches the relevant slot. */
|
|
12985
|
+
async _rowsTouchingEndpoint(collection, id) {
|
|
12986
|
+
const rows = await this.list();
|
|
12987
|
+
return rows.filter(
|
|
12988
|
+
(r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
|
|
12989
|
+
);
|
|
12990
|
+
}
|
|
12991
|
+
/** @internal — the storage collection name (for tx pre-image capture). */
|
|
12992
|
+
get _collectionName() {
|
|
12993
|
+
return this.collName;
|
|
12994
|
+
}
|
|
12995
|
+
};
|
|
12996
|
+
LinkEndpointError = class extends NoydbError {
|
|
12997
|
+
link;
|
|
12998
|
+
endpoint;
|
|
12999
|
+
missingId;
|
|
13000
|
+
constructor(link, endpoint, missingId) {
|
|
13001
|
+
super(
|
|
13002
|
+
"LINK_ENDPOINT",
|
|
13003
|
+
`link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
|
|
13004
|
+
);
|
|
13005
|
+
this.name = "LinkEndpointError";
|
|
13006
|
+
this.link = link;
|
|
13007
|
+
this.endpoint = endpoint;
|
|
13008
|
+
this.missingId = missingId;
|
|
13009
|
+
}
|
|
13010
|
+
};
|
|
13011
|
+
LinkIntegrityError = class extends NoydbError {
|
|
13012
|
+
link;
|
|
13013
|
+
endpoint;
|
|
13014
|
+
id;
|
|
13015
|
+
count;
|
|
13016
|
+
constructor(link, endpoint, id, count) {
|
|
13017
|
+
super(
|
|
13018
|
+
"LINK_INTEGRITY",
|
|
13019
|
+
`Cannot delete "${endpoint}"/"${id}": ${count} link(s) in "${link}" still reference it (onDelete: 'strict').`
|
|
13020
|
+
);
|
|
13021
|
+
this.name = "LinkIntegrityError";
|
|
13022
|
+
this.link = link;
|
|
13023
|
+
this.endpoint = endpoint;
|
|
13024
|
+
this.id = id;
|
|
13025
|
+
this.count = count;
|
|
13026
|
+
}
|
|
13027
|
+
};
|
|
13028
|
+
}
|
|
13029
|
+
});
|
|
13030
|
+
|
|
13031
|
+
// src/periods/periods.ts
|
|
13032
|
+
var PERIODS_COLLECTION;
|
|
13033
|
+
var init_periods = __esm({
|
|
13034
|
+
"src/periods/periods.ts"() {
|
|
13035
|
+
"use strict";
|
|
13036
|
+
PERIODS_COLLECTION = "_periods";
|
|
13037
|
+
}
|
|
13038
|
+
});
|
|
13039
|
+
|
|
13040
|
+
// src/periods/index.ts
|
|
13041
|
+
var init_periods2 = __esm({
|
|
13042
|
+
"src/periods/index.ts"() {
|
|
13043
|
+
"use strict";
|
|
13044
|
+
init_periods();
|
|
13045
|
+
}
|
|
13046
|
+
});
|
|
13047
|
+
|
|
13048
|
+
// src/blobs/export-blobs.ts
|
|
13049
|
+
function createExportBlobsHandle(actor, listAccessibleCollections, getCollection, writeAudit, options) {
|
|
13050
|
+
let aborted = false;
|
|
13051
|
+
const abort = () => {
|
|
13052
|
+
aborted = true;
|
|
13053
|
+
};
|
|
13054
|
+
if (options.signal) {
|
|
13055
|
+
if (options.signal.aborted) aborted = true;
|
|
13056
|
+
options.signal.addEventListener("abort", () => {
|
|
13057
|
+
aborted = true;
|
|
13058
|
+
});
|
|
13059
|
+
}
|
|
13060
|
+
function assertLive() {
|
|
13061
|
+
if (aborted) throw new ExportBlobsAbortedError("aborted by caller");
|
|
13062
|
+
}
|
|
13063
|
+
const allowlist = options.collections ? new Set(options.collections) : null;
|
|
13064
|
+
let auditPromise = null;
|
|
13065
|
+
function writeAuditOnce() {
|
|
13066
|
+
if (!auditPromise) {
|
|
13067
|
+
auditPromise = writeAudit({
|
|
13068
|
+
id: generateBatchId(),
|
|
13069
|
+
mechanism: "exportBlobs",
|
|
13070
|
+
actor,
|
|
13071
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13072
|
+
collections: options.collections ?? null,
|
|
13073
|
+
predicate: Boolean(options.where),
|
|
13074
|
+
afterBlobId: options.afterBlobId ?? null
|
|
13075
|
+
});
|
|
13076
|
+
}
|
|
13077
|
+
return auditPromise;
|
|
13078
|
+
}
|
|
13079
|
+
async function* generate() {
|
|
13080
|
+
await writeAuditOnce();
|
|
13081
|
+
assertLive();
|
|
12285
13082
|
const allCollections = await listAccessibleCollections();
|
|
12286
13083
|
const targets = allCollections.filter((name) => {
|
|
12287
13084
|
if (name.startsWith("_")) return false;
|
|
@@ -13906,14 +14703,17 @@ var init_read_only_facade = __esm({
|
|
|
13906
14703
|
"use strict";
|
|
13907
14704
|
ReadOnlyVaultFacade = class {
|
|
13908
14705
|
_vault;
|
|
13909
|
-
|
|
14706
|
+
_layer;
|
|
14707
|
+
constructor(vault, layer = "read") {
|
|
13910
14708
|
this._vault = vault;
|
|
14709
|
+
this._layer = layer;
|
|
13911
14710
|
}
|
|
13912
14711
|
collection(name) {
|
|
13913
14712
|
const c = this._vault.collection(name);
|
|
14713
|
+
const layer = this._layer;
|
|
13914
14714
|
return {
|
|
13915
|
-
get: (id) => c.get(id),
|
|
13916
|
-
list: () => c.list(),
|
|
14715
|
+
get: (id) => c.get(id, { _layer: layer }),
|
|
14716
|
+
list: () => c.list({ _layer: layer }),
|
|
13917
14717
|
query: () => c.query()
|
|
13918
14718
|
};
|
|
13919
14719
|
}
|
|
@@ -13969,6 +14769,16 @@ var init_registry3 = __esm({
|
|
|
13969
14769
|
if (fromExtra) fromExtra.push(reg);
|
|
13970
14770
|
else this._bySource.set(extra, [reg]);
|
|
13971
14771
|
}
|
|
14772
|
+
for (const t of spec.triggerBy ?? []) {
|
|
14773
|
+
const fromTrigger = this._bySource.get(t.collection);
|
|
14774
|
+
if (fromTrigger) fromTrigger.push(reg);
|
|
14775
|
+
else this._bySource.set(t.collection, [reg]);
|
|
14776
|
+
}
|
|
14777
|
+
if (spec.rollup) {
|
|
14778
|
+
const fromRollup = this._bySource.get(spec.rollup.from);
|
|
14779
|
+
if (fromRollup) fromRollup.push(reg);
|
|
14780
|
+
else this._bySource.set(spec.rollup.from, [reg]);
|
|
14781
|
+
}
|
|
13972
14782
|
for (const key of outputKeys) {
|
|
13973
14783
|
const output = spec.outputs[key];
|
|
13974
14784
|
if (!output) continue;
|
|
@@ -14022,6 +14832,9 @@ var init_registry3 = __esm({
|
|
|
14022
14832
|
for (const key of Object.keys(s.spec.outputs)) {
|
|
14023
14833
|
const output = s.spec.outputs[key];
|
|
14024
14834
|
if (!output) continue;
|
|
14835
|
+
if (output.shape === "record" && output.collection === s.spec.source && output.denorm !== void 0) {
|
|
14836
|
+
continue;
|
|
14837
|
+
}
|
|
14025
14838
|
visit(output.collection);
|
|
14026
14839
|
}
|
|
14027
14840
|
}
|
|
@@ -14237,6 +15050,7 @@ var init_vault = __esm({
|
|
|
14237
15050
|
init_strategy10();
|
|
14238
15051
|
init_refs();
|
|
14239
15052
|
init_dictionary();
|
|
15053
|
+
init_link_set();
|
|
14240
15054
|
init_core();
|
|
14241
15055
|
init_strategy2();
|
|
14242
15056
|
init_sync_strategy();
|
|
@@ -14329,13 +15143,17 @@ var init_vault = __esm({
|
|
|
14329
15143
|
*/
|
|
14330
15144
|
overlayedViewRegistry = null;
|
|
14331
15145
|
/**
|
|
14332
|
-
* Cached read-only
|
|
14333
|
-
* and to derivation callbacks via `derive(source, ctx)`.
|
|
14334
|
-
*
|
|
15146
|
+
* Cached read-only facades handed to guard callbacks via `ctx.vault`
|
|
15147
|
+
* and to derivation callbacks via `derive(source, ctx)`. Split by
|
|
15148
|
+
* resolution layer (#285): the guard facade reads at `layer:'guard'`,
|
|
15149
|
+
* the derivation facade at `layer:'derivation'`, so i18nText / dictKey
|
|
15150
|
+
* fields resolve under that layer's `onMissing` policy. Allocated
|
|
15151
|
+
* eagerly inside `_initGuards()` / `_initDerivations()` so read
|
|
14335
15152
|
* accessors stay synchronous (callers in `tx/transaction.ts` rely on
|
|
14336
|
-
* that).
|
|
15153
|
+
* that). Each stays `null` for vaults without that subsystem.
|
|
14337
15154
|
*/
|
|
14338
|
-
|
|
15155
|
+
guardFacade = null;
|
|
15156
|
+
derivationFacade = null;
|
|
14339
15157
|
getDEK;
|
|
14340
15158
|
/**
|
|
14341
15159
|
* Per-principal user envelope API.
|
|
@@ -14497,6 +15315,10 @@ var init_vault = __esm({
|
|
|
14497
15315
|
i18nFieldRegistry = /* @__PURE__ */ new Map();
|
|
14498
15316
|
/** Cache of DictionaryHandle instances, one per dictionary name. */
|
|
14499
15317
|
dictionaryCache = /* @__PURE__ */ new Map();
|
|
15318
|
+
/** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
|
|
15319
|
+
linkRegistry = /* @__PURE__ */ new Map();
|
|
15320
|
+
/** Cache of LinkSet handles, one per link name. */
|
|
15321
|
+
linkSetCache = /* @__PURE__ */ new Map();
|
|
14500
15322
|
/** — subscribers for cross-tier access events. */
|
|
14501
15323
|
crossTierSubs = /* @__PURE__ */ new Set();
|
|
14502
15324
|
/** — currently-active elevation, or null. One per vault. */
|
|
@@ -14613,6 +15435,9 @@ var init_vault = __esm({
|
|
|
14613
15435
|
if (collectionName === SEQUENCE_COLLECTION) {
|
|
14614
15436
|
throw new ReservedCollectionNameError(collectionName);
|
|
14615
15437
|
}
|
|
15438
|
+
if (isLinkCollectionName(collectionName)) {
|
|
15439
|
+
throw new ReservedCollectionNameError(collectionName);
|
|
15440
|
+
}
|
|
14616
15441
|
let coll = this.collectionCache.get(collectionName);
|
|
14617
15442
|
if (coll && options?.moneyFields) {
|
|
14618
15443
|
coll._applyMoneyFields(options.moneyFields);
|
|
@@ -14684,6 +15509,7 @@ var init_vault = __esm({
|
|
|
14684
15509
|
}));
|
|
14685
15510
|
schemaUpdateGate = new SchemaUpdateGate(work);
|
|
14686
15511
|
}
|
|
15512
|
+
const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
|
|
14687
15513
|
const collOpts = {
|
|
14688
15514
|
adapter: this.adapter,
|
|
14689
15515
|
vault: this.name,
|
|
@@ -14699,7 +15525,7 @@ var init_vault = __esm({
|
|
|
14699
15525
|
schemaFence: this.schemaFence,
|
|
14700
15526
|
getDEK: this.getDEK,
|
|
14701
15527
|
onDirty: this.onDirty,
|
|
14702
|
-
historyConfig:
|
|
15528
|
+
historyConfig: effectiveHistoryConfig,
|
|
14703
15529
|
// thread the vault-wide blob strategy into every
|
|
14704
15530
|
// collection. `undefined` is intentionally preserved so the
|
|
14705
15531
|
// Collection constructor uses its NO_BLOBS default.
|
|
@@ -14710,7 +15536,11 @@ var init_vault = __esm({
|
|
|
14710
15536
|
historyStrategy: this.historyStrategy,
|
|
14711
15537
|
i18nStrategy: this.i18nStrategy,
|
|
14712
15538
|
syncStrategy: this.syncStrategy,
|
|
14713
|
-
ledger
|
|
15539
|
+
// Per-collection ledger opt-out (#361): when this collection sets
|
|
15540
|
+
// `historyConfig.ledger: false`, withhold the ledger reference so all
|
|
15541
|
+
// four `if (this.ledger)` append sites in Collection no-op. The chain
|
|
15542
|
+
// stays valid — it simply never receives this collection's entries.
|
|
15543
|
+
ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
|
|
14714
15544
|
refEnforcer: this,
|
|
14715
15545
|
joinResolver: this,
|
|
14716
15546
|
defaultLocale: this.locale,
|
|
@@ -15071,6 +15901,68 @@ var init_vault = __esm({
|
|
|
15071
15901
|
}
|
|
15072
15902
|
return handle;
|
|
15073
15903
|
}
|
|
15904
|
+
/**
|
|
15905
|
+
* Declare a managed many-to-many link set (#377-B). Registers a
|
|
15906
|
+
* `_links_<name>` junction between two endpoint collections; access its
|
|
15907
|
+
* rows via `vault.links(name)`. Idempotent for an identical re-declaration;
|
|
15908
|
+
* a conflicting one throws. See {@link links}.
|
|
15909
|
+
*
|
|
15910
|
+
* ```ts
|
|
15911
|
+
* vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
|
|
15912
|
+
* ```
|
|
15913
|
+
*
|
|
15914
|
+
* `a` / `b` accept either a collection name or a `ref(target)` descriptor
|
|
15915
|
+
* (only its `target` is used — links manage their own integrity). `onDelete`
|
|
15916
|
+
* governs what happens to link rows when an endpoint record is deleted
|
|
15917
|
+
* (`'cascade'` default, `'strict'`, `'warn'`).
|
|
15918
|
+
*/
|
|
15919
|
+
link(name, spec) {
|
|
15920
|
+
const a = typeof spec.a === "string" ? spec.a : spec.a.target;
|
|
15921
|
+
const b = typeof spec.b === "string" ? spec.b : spec.b.target;
|
|
15922
|
+
for (const [slot, target] of [["a", a], ["b", b]]) {
|
|
15923
|
+
if (!target || target.startsWith("_") || target.includes("/")) {
|
|
15924
|
+
throw new ValidationError(
|
|
15925
|
+
`vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
|
|
15926
|
+
);
|
|
15927
|
+
}
|
|
15928
|
+
}
|
|
15929
|
+
const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
|
|
15930
|
+
const existing = this.linkRegistry.get(name);
|
|
15931
|
+
if (existing) {
|
|
15932
|
+
if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
|
|
15933
|
+
throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
|
|
15934
|
+
}
|
|
15935
|
+
return;
|
|
15936
|
+
}
|
|
15937
|
+
this.linkRegistry.set(name, resolved);
|
|
15938
|
+
}
|
|
15939
|
+
/**
|
|
15940
|
+
* Access a declared link set (#377-B). Throws if `name` was not first
|
|
15941
|
+
* declared via {@link link}. Returns a cached {@link LinkSetHandle}:
|
|
15942
|
+
* `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
|
|
15943
|
+
*/
|
|
15944
|
+
links(name) {
|
|
15945
|
+
let handle = this.linkSetCache.get(name);
|
|
15946
|
+
if (!handle) {
|
|
15947
|
+
const spec = this.linkRegistry.get(name);
|
|
15948
|
+
if (!spec) {
|
|
15949
|
+
throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
|
|
15950
|
+
}
|
|
15951
|
+
handle = new LinkSet(
|
|
15952
|
+
this.adapter,
|
|
15953
|
+
this.name,
|
|
15954
|
+
name,
|
|
15955
|
+
spec,
|
|
15956
|
+
this.encrypted,
|
|
15957
|
+
this.getDEK,
|
|
15958
|
+
this.keyring.userId,
|
|
15959
|
+
this.emitter,
|
|
15960
|
+
async (collection, id) => await this.collection(collection).get(id) !== null
|
|
15961
|
+
);
|
|
15962
|
+
this.linkSetCache.set(name, handle);
|
|
15963
|
+
}
|
|
15964
|
+
return handle;
|
|
15965
|
+
}
|
|
15074
15966
|
/**
|
|
15075
15967
|
* Build a `JoinableSource` for a dictKey field, for use in dict joins
|
|
15076
15968
|
*. Returns a source whose snapshot contains `{ key, ...labels }`
|
|
@@ -15230,65 +16122,16 @@ var init_vault = __esm({
|
|
|
15230
16122
|
});
|
|
15231
16123
|
}
|
|
15232
16124
|
}
|
|
15233
|
-
/**
|
|
15234
|
-
* Bulk blob extraction primitive.
|
|
15235
|
-
*
|
|
15236
|
-
* Returns an async-iterable handle over every blob attached to
|
|
15237
|
-
* records in the vault. Single capability check (`plaintext/blob`)
|
|
15238
|
-
* at handle creation; single audit entry to `_export_audit` before
|
|
15239
|
-
* the first yield. Per-blob decryption happens lazily as the
|
|
15240
|
-
* consumer pulls tuples.
|
|
15241
|
-
*
|
|
15242
|
-
* ```ts
|
|
15243
|
-
* const handle = vault.exportBlobs({
|
|
15244
|
-
* collections: ['invoiceScans'],
|
|
15245
|
-
* where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
|
|
15246
|
-
* })
|
|
15247
|
-
* for await (const { bytes, meta, recordRef } of handle) {
|
|
15248
|
-
* await uploadToColdStorage(bytes, recordRef)
|
|
15249
|
-
* }
|
|
15250
|
-
* ```
|
|
15251
|
-
*
|
|
15252
|
-
* @see `@noy-db/hub/store/export-blobs` for the full option surface.
|
|
15253
|
-
*/
|
|
15254
|
-
/**
|
|
15255
|
-
* Evict blob slots per the per-collection `blobFields` retention
|
|
15256
|
-
* policy.
|
|
15257
|
-
*
|
|
15258
|
-
* Iterates every collection declared with `{ blobFields: {...} }`.
|
|
15259
|
-
* For each record, checks every configured slot against its
|
|
15260
|
-
* policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
|
|
15261
|
-
* (predicate) — and evicts matching slots. Every eviction writes
|
|
15262
|
-
* one entry to `_blob_eviction_audit` (actor + eTag + reason +
|
|
15263
|
-
* timestamp, no plaintext). Consumer-scheduled; noy-db never runs
|
|
15264
|
-
* this on its own.
|
|
15265
|
-
*
|
|
15266
|
-
* ```ts
|
|
15267
|
-
* await vault.compact() // run full pass
|
|
15268
|
-
* await vault.compact({ dryRun: true }) // preview counts
|
|
15269
|
-
* await vault.compact({ maxEvictions: 1000 }) // cap batch
|
|
15270
|
-
* ```
|
|
15271
|
-
*/
|
|
15272
|
-
/**
|
|
15273
|
-
* Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
|
|
15274
|
-
* returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
|
|
15275
|
-
* an optimistic-CAS counter at `_sequences/<name>`. Each name is an
|
|
15276
|
-
* independent sequence.
|
|
15277
|
-
*
|
|
15278
|
-
* **Online-only:** `next()` throws `SequenceOfflineError` unless the
|
|
15279
|
-
* store advertises `capabilities.casAtomic` — gap-free numbering cannot
|
|
15280
|
-
* be serialized by an offline / non-CAS writer.
|
|
15281
|
-
*
|
|
15282
|
-
* ```ts
|
|
15283
|
-
* const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
|
|
15284
|
-
* const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
|
|
15285
|
-
* ```
|
|
15286
|
-
*/
|
|
15287
16125
|
sequence(series, opts) {
|
|
15288
16126
|
if (series.includes("\0")) {
|
|
15289
16127
|
throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
|
|
15290
16128
|
}
|
|
15291
16129
|
if (this.numberingConfigs.has(series)) {
|
|
16130
|
+
if (opts?.format !== void 0) {
|
|
16131
|
+
throw new ValidationError(
|
|
16132
|
+
`sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
|
|
16133
|
+
);
|
|
16134
|
+
}
|
|
15292
16135
|
const eng = this.deferred();
|
|
15293
16136
|
return {
|
|
15294
16137
|
next: async (nextOpts) => {
|
|
@@ -15312,7 +16155,17 @@ var init_vault = __esm({
|
|
|
15312
16155
|
actor: this.keyring.userId
|
|
15313
16156
|
});
|
|
15314
16157
|
}
|
|
15315
|
-
|
|
16158
|
+
const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
|
|
16159
|
+
if (opts?.format === void 0) return handle;
|
|
16160
|
+
const render = compileSequenceFormat(opts.format, series, opts.partition);
|
|
16161
|
+
return {
|
|
16162
|
+
next: async (nextOpts) => {
|
|
16163
|
+
const serial = await handle.next(nextOpts);
|
|
16164
|
+
return { serial, formatted: render(serial) };
|
|
16165
|
+
},
|
|
16166
|
+
peek: () => handle.peek(),
|
|
16167
|
+
seedTo: (n) => handle.seedTo(n)
|
|
16168
|
+
};
|
|
15316
16169
|
}
|
|
15317
16170
|
/** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
|
|
15318
16171
|
deferred() {
|
|
@@ -15539,6 +16392,43 @@ var init_vault = __esm({
|
|
|
15539
16392
|
if (descriptor.mode !== "strict") continue;
|
|
15540
16393
|
const rawId = obj[field];
|
|
15541
16394
|
if (rawId === null || rawId === void 0) continue;
|
|
16395
|
+
if (isRefArray(descriptor)) {
|
|
16396
|
+
if (!Array.isArray(rawId)) {
|
|
16397
|
+
throw new RefIntegrityError({
|
|
16398
|
+
collection: collectionName,
|
|
16399
|
+
id: obj["id"] ?? "<unknown>",
|
|
16400
|
+
field,
|
|
16401
|
+
refTo: descriptor.target,
|
|
16402
|
+
refId: null,
|
|
16403
|
+
message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
|
|
16404
|
+
});
|
|
16405
|
+
}
|
|
16406
|
+
const arrTarget = this.collection(descriptor.target);
|
|
16407
|
+
for (const el of rawId) {
|
|
16408
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
16409
|
+
throw new RefIntegrityError({
|
|
16410
|
+
collection: collectionName,
|
|
16411
|
+
id: obj["id"] ?? "<unknown>",
|
|
16412
|
+
field,
|
|
16413
|
+
refTo: descriptor.target,
|
|
16414
|
+
refId: null,
|
|
16415
|
+
message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
|
|
16416
|
+
});
|
|
16417
|
+
}
|
|
16418
|
+
const elId = String(el);
|
|
16419
|
+
if (!await arrTarget.get(elId)) {
|
|
16420
|
+
throw new RefIntegrityError({
|
|
16421
|
+
collection: collectionName,
|
|
16422
|
+
id: obj["id"] ?? "<unknown>",
|
|
16423
|
+
field,
|
|
16424
|
+
refTo: descriptor.target,
|
|
16425
|
+
refId: elId,
|
|
16426
|
+
message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
|
|
16427
|
+
});
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
continue;
|
|
16431
|
+
}
|
|
15542
16432
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
15543
16433
|
throw new RefIntegrityError({
|
|
15544
16434
|
collection: collectionName,
|
|
@@ -15588,6 +16478,11 @@ var init_vault = __esm({
|
|
|
15588
16478
|
const allRecords = await fromCollection.list();
|
|
15589
16479
|
const matches = allRecords.filter((rec) => {
|
|
15590
16480
|
const raw = rec[rule.field];
|
|
16481
|
+
if (rule.isArray) {
|
|
16482
|
+
return Array.isArray(raw) && raw.some(
|
|
16483
|
+
(el) => (typeof el === "string" || typeof el === "number") && String(el) === id
|
|
16484
|
+
);
|
|
16485
|
+
}
|
|
15591
16486
|
if (typeof raw !== "string" && typeof raw !== "number") return false;
|
|
15592
16487
|
return String(raw) === id;
|
|
15593
16488
|
});
|
|
@@ -15626,10 +16521,45 @@ var init_vault = __esm({
|
|
|
15626
16521
|
}
|
|
15627
16522
|
}
|
|
15628
16523
|
}
|
|
16524
|
+
await this.enforceLinksOnDelete(collectionName, id);
|
|
15629
16525
|
} finally {
|
|
15630
16526
|
this.cascadeInProgress.delete(key);
|
|
15631
16527
|
}
|
|
15632
16528
|
}
|
|
16529
|
+
/**
|
|
16530
|
+
* @internal — apply link `onDelete` policy when an endpoint record is
|
|
16531
|
+
* deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
|
|
16532
|
+
* removes the touching link rows (tx-atomic when a transaction is active),
|
|
16533
|
+
* `'warn'` leaves orphans for `checkIntegrity()`.
|
|
16534
|
+
*/
|
|
16535
|
+
async enforceLinksOnDelete(collectionName, id) {
|
|
16536
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
16537
|
+
if (spec.a !== collectionName && spec.b !== collectionName) continue;
|
|
16538
|
+
const handle = this.links(name);
|
|
16539
|
+
const touching = await handle._rowsTouchingEndpoint(collectionName, id);
|
|
16540
|
+
if (touching.length === 0) continue;
|
|
16541
|
+
const mode = spec.onDelete ?? "cascade";
|
|
16542
|
+
if (mode === "warn") continue;
|
|
16543
|
+
if (mode === "strict") {
|
|
16544
|
+
throw new LinkIntegrityError(name, collectionName, id, touching.length);
|
|
16545
|
+
}
|
|
16546
|
+
const linkColl = handle._collectionName;
|
|
16547
|
+
const txCtx = this.noydb._activeTxContextOrNull;
|
|
16548
|
+
for (const row of touching) {
|
|
16549
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
16550
|
+
if (txCtx !== null) {
|
|
16551
|
+
const prior = await this.adapter.get(this.name, linkColl, rowKey);
|
|
16552
|
+
if (prior !== null) {
|
|
16553
|
+
txCtx._executed.push({
|
|
16554
|
+
op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
|
|
16555
|
+
priorEnvelope: prior
|
|
16556
|
+
});
|
|
16557
|
+
}
|
|
16558
|
+
}
|
|
16559
|
+
await handle.disconnect(row.a, row.b);
|
|
16560
|
+
}
|
|
16561
|
+
}
|
|
16562
|
+
}
|
|
15633
16563
|
// ─── Join resolver) ────────────────────
|
|
15634
16564
|
/**
|
|
15635
16565
|
* Look up the `RefDescriptor` the left collection declared for a
|
|
@@ -15690,6 +16620,23 @@ var init_vault = __esm({
|
|
|
15690
16620
|
for (const [field, descriptor] of Object.entries(refs)) {
|
|
15691
16621
|
const rawId = record[field];
|
|
15692
16622
|
if (rawId === null || rawId === void 0) continue;
|
|
16623
|
+
const target = this.collection(descriptor.target);
|
|
16624
|
+
if (isRefArray(descriptor)) {
|
|
16625
|
+
if (!Array.isArray(rawId)) {
|
|
16626
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
|
|
16627
|
+
continue;
|
|
16628
|
+
}
|
|
16629
|
+
for (const el of rawId) {
|
|
16630
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
16631
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
16632
|
+
continue;
|
|
16633
|
+
}
|
|
16634
|
+
if (!await target.get(String(el))) {
|
|
16635
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
16636
|
+
}
|
|
16637
|
+
}
|
|
16638
|
+
continue;
|
|
16639
|
+
}
|
|
15693
16640
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
15694
16641
|
violations.push({
|
|
15695
16642
|
collection: collectionName,
|
|
@@ -15702,7 +16649,6 @@ var init_vault = __esm({
|
|
|
15702
16649
|
continue;
|
|
15703
16650
|
}
|
|
15704
16651
|
const refId = String(rawId);
|
|
15705
|
-
const target = this.collection(descriptor.target);
|
|
15706
16652
|
const exists = await target.get(refId);
|
|
15707
16653
|
if (!exists) {
|
|
15708
16654
|
violations.push({
|
|
@@ -15717,6 +16663,19 @@ var init_vault = __esm({
|
|
|
15717
16663
|
}
|
|
15718
16664
|
}
|
|
15719
16665
|
}
|
|
16666
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
16667
|
+
const linkColl = linkCollectionName(name);
|
|
16668
|
+
const rows = await this.links(name).list();
|
|
16669
|
+
for (const row of rows) {
|
|
16670
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
16671
|
+
if (await this.collection(spec.a).get(row.a) === null) {
|
|
16672
|
+
violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
|
|
16673
|
+
}
|
|
16674
|
+
if (await this.collection(spec.b).get(row.b) === null) {
|
|
16675
|
+
violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
|
|
16676
|
+
}
|
|
16677
|
+
}
|
|
16678
|
+
}
|
|
15720
16679
|
return { violations };
|
|
15721
16680
|
}
|
|
15722
16681
|
/**
|
|
@@ -15823,6 +16782,8 @@ var init_vault = __esm({
|
|
|
15823
16782
|
const blobResidueCollections = /* @__PURE__ */ new Set();
|
|
15824
16783
|
let blobsShredded = 0;
|
|
15825
16784
|
let blobsRetainedShared = 0;
|
|
16785
|
+
let indexPostingsPurged = 0;
|
|
16786
|
+
const indexResidue = [];
|
|
15826
16787
|
const blobsEnabled = this.blobStrategy !== void 0;
|
|
15827
16788
|
const actor = this.keyring.userId;
|
|
15828
16789
|
for (const ref of refs) {
|
|
@@ -15844,6 +16805,9 @@ var init_vault = __esm({
|
|
|
15844
16805
|
ref.id,
|
|
15845
16806
|
actor
|
|
15846
16807
|
);
|
|
16808
|
+
const idxPurge = await coll._purgePersistedIndexes(ref.id);
|
|
16809
|
+
indexPostingsPurged += idxPurge.purged;
|
|
16810
|
+
for (const field of idxPurge.residue) indexResidue.push(`${ref.collection}:${ref.id}:${field}`);
|
|
15847
16811
|
if (blobsEnabled) {
|
|
15848
16812
|
const r = await this.collection(ref.collection).blob(ref.id).shredAllForRecord();
|
|
15849
16813
|
blobsShredded += r.shredded.length;
|
|
@@ -15879,7 +16843,9 @@ var init_vault = __esm({
|
|
|
15879
16843
|
unmigratedCount: unmigratedRecords.length,
|
|
15880
16844
|
blobsShredded,
|
|
15881
16845
|
blobsRetainedShared,
|
|
15882
|
-
blobResidueCollections: [...blobResidueCollections]
|
|
16846
|
+
blobResidueCollections: [...blobResidueCollections],
|
|
16847
|
+
indexPostingsPurged,
|
|
16848
|
+
indexResidueCount: indexResidue.length
|
|
15883
16849
|
})
|
|
15884
16850
|
});
|
|
15885
16851
|
return {
|
|
@@ -15891,6 +16857,8 @@ var init_vault = __esm({
|
|
|
15891
16857
|
blobsShredded,
|
|
15892
16858
|
blobsRetainedShared,
|
|
15893
16859
|
blobResidueCollections: [...blobResidueCollections],
|
|
16860
|
+
indexPostingsPurged,
|
|
16861
|
+
indexResidue,
|
|
15894
16862
|
ledgerEntry
|
|
15895
16863
|
};
|
|
15896
16864
|
}
|
|
@@ -15992,7 +16960,7 @@ var init_vault = __esm({
|
|
|
15992
16960
|
const registry = new GuardRegistry2();
|
|
15993
16961
|
for (const h of handles) registry.register(h.spec);
|
|
15994
16962
|
this.guardRegistry = registry;
|
|
15995
|
-
this.
|
|
16963
|
+
this.guardFacade = new ReadOnlyVaultFacade2(this, "guard");
|
|
15996
16964
|
}
|
|
15997
16965
|
/**
|
|
15998
16966
|
* @internal — The gate handler in Noydb.#registerGuardGate calls into
|
|
@@ -16023,8 +16991,8 @@ var init_vault = __esm({
|
|
|
16023
16991
|
}
|
|
16024
16992
|
registry.validate();
|
|
16025
16993
|
this.derivationRegistry = registry;
|
|
16026
|
-
if (this.
|
|
16027
|
-
this.
|
|
16994
|
+
if (this.derivationFacade === null) {
|
|
16995
|
+
this.derivationFacade = new ReadOnlyVaultFacade2(this, "derivation");
|
|
16028
16996
|
}
|
|
16029
16997
|
}
|
|
16030
16998
|
/**
|
|
@@ -16141,7 +17109,7 @@ var init_vault = __esm({
|
|
|
16141
17109
|
const { DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports));
|
|
16142
17110
|
const sourceColl = this.collection(sourceCollection);
|
|
16143
17111
|
const records = await sourceColl.list();
|
|
16144
|
-
const ctx = { vault: this.
|
|
17112
|
+
const ctx = { vault: this.derivationFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this, "derivation") };
|
|
16145
17113
|
let derived = 0;
|
|
16146
17114
|
let failed = 0;
|
|
16147
17115
|
for (const record of records) {
|
|
@@ -16206,17 +17174,18 @@ var init_vault = __esm({
|
|
|
16206
17174
|
* never see null).
|
|
16207
17175
|
*/
|
|
16208
17176
|
_getReadOnlyFacade() {
|
|
16209
|
-
return this.
|
|
17177
|
+
return this.guardFacade;
|
|
16210
17178
|
}
|
|
16211
17179
|
/**
|
|
16212
|
-
* Internal lazy-allocator for the read-only facade
|
|
16213
|
-
* defensive fallback; in practice
|
|
16214
|
-
* instantiates this, so the lazy path is
|
|
17180
|
+
* Internal lazy-allocator for the derivation read-only facade
|
|
17181
|
+
* (`layer:'derivation'`). Used as a defensive fallback; in practice
|
|
17182
|
+
* `_initDerivations()` eagerly instantiates this, so the lazy path is
|
|
17183
|
+
* a no-op.
|
|
16215
17184
|
*/
|
|
16216
17185
|
_ensureReadOnlyFacade() {
|
|
16217
|
-
if (this.
|
|
17186
|
+
if (this.derivationFacade !== null) return this.derivationFacade;
|
|
16218
17187
|
throw new Error(
|
|
16219
|
-
"Vault:
|
|
17188
|
+
"Vault: derivation hook fired before _initDerivations() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
|
|
16220
17189
|
);
|
|
16221
17190
|
}
|
|
16222
17191
|
/**
|
|
@@ -17152,6 +18121,8 @@ var init_vault = __esm({
|
|
|
17152
18121
|
*/
|
|
17153
18122
|
async *exportStream(opts = {}) {
|
|
17154
18123
|
const granularity = opts.granularity ?? "collection";
|
|
18124
|
+
const exportLocale = opts.resolveLabels;
|
|
18125
|
+
const localeOpts = exportLocale !== void 0 ? { locale: exportLocale, _layer: "export" } : void 0;
|
|
17155
18126
|
const snapshot = await this.adapter.loadAll(this.name);
|
|
17156
18127
|
const collectionNames = Object.keys(snapshot).sort();
|
|
17157
18128
|
const ledgerHead = opts.withLedgerHead ? await (async () => {
|
|
@@ -17161,19 +18132,21 @@ var init_vault = __esm({
|
|
|
17161
18132
|
return head ? { hash: head.hash, index: head.entry.index, ts: head.entry.ts } : void 0;
|
|
17162
18133
|
})() : void 0;
|
|
17163
18134
|
const dictSnapshotCache = /* @__PURE__ */ new Map();
|
|
17164
|
-
|
|
17165
|
-
const
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
const
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
18135
|
+
if (exportLocale === void 0) {
|
|
18136
|
+
for (const collectionName of collectionNames) {
|
|
18137
|
+
const dictFields = this.dictKeyFieldRegistry.get(collectionName);
|
|
18138
|
+
if (dictFields && Object.keys(dictFields).length > 0) {
|
|
18139
|
+
const snap = {};
|
|
18140
|
+
for (const [fieldName, dictName] of Object.entries(dictFields)) {
|
|
18141
|
+
const entries = await this.dictionary(dictName).list();
|
|
18142
|
+
const keyMap = {};
|
|
18143
|
+
for (const entry of entries) {
|
|
18144
|
+
keyMap[entry.key] = entry.labels;
|
|
18145
|
+
}
|
|
18146
|
+
snap[fieldName] = keyMap;
|
|
17173
18147
|
}
|
|
17174
|
-
snap
|
|
18148
|
+
dictSnapshotCache.set(collectionName, snap);
|
|
17175
18149
|
}
|
|
17176
|
-
dictSnapshotCache.set(collectionName, snap);
|
|
17177
18150
|
}
|
|
17178
18151
|
}
|
|
17179
18152
|
for (const collectionName of collectionNames) {
|
|
@@ -17186,7 +18159,7 @@ var init_vault = __esm({
|
|
|
17186
18159
|
if (granularity === "collection") {
|
|
17187
18160
|
const records = [];
|
|
17188
18161
|
for (const id of ids) {
|
|
17189
|
-
const record = await coll.get(id);
|
|
18162
|
+
const record = await coll.get(id, localeOpts);
|
|
17190
18163
|
if (record !== null) records.push(record);
|
|
17191
18164
|
}
|
|
17192
18165
|
const chunk = {
|
|
@@ -17200,7 +18173,7 @@ var init_vault = __esm({
|
|
|
17200
18173
|
yield chunk;
|
|
17201
18174
|
} else {
|
|
17202
18175
|
for (const id of ids) {
|
|
17203
|
-
const record = await coll.get(id);
|
|
18176
|
+
const record = await coll.get(id, localeOpts);
|
|
17204
18177
|
if (record === null) continue;
|
|
17205
18178
|
const chunk = {
|
|
17206
18179
|
collection: collectionName,
|
|
@@ -17304,7 +18277,10 @@ var init_vault = __esm({
|
|
|
17304
18277
|
const allDictionaries = {};
|
|
17305
18278
|
for await (const chunk of this.exportStream({
|
|
17306
18279
|
granularity: "collection",
|
|
17307
|
-
withLedgerHead: opts.withLedgerHead === true
|
|
18280
|
+
withLedgerHead: opts.withLedgerHead === true,
|
|
18281
|
+
// #285 export layer: thread the export locale so records are read at the
|
|
18282
|
+
// `export` layer (i18nText collapsed + dictKey/staticDict labels resolved).
|
|
18283
|
+
...opts.resolveLabels !== void 0 ? { resolveLabels: opts.resolveLabels } : {}
|
|
17308
18284
|
})) {
|
|
17309
18285
|
collections[chunk.collection] = {
|
|
17310
18286
|
schema: null,
|
|
@@ -18615,43 +19591,214 @@ var init_executor3 = __esm({
|
|
|
18615
19591
|
}
|
|
18616
19592
|
});
|
|
18617
19593
|
|
|
18618
|
-
// src/federation/
|
|
18619
|
-
function
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
init_errors();
|
|
18626
|
-
}
|
|
18627
|
-
});
|
|
18628
|
-
|
|
18629
|
-
// src/federation/cross-shard-join.ts
|
|
18630
|
-
function coerceKey(value) {
|
|
18631
|
-
if (value === null || value === void 0) return null;
|
|
18632
|
-
if (typeof value === "string") return value;
|
|
18633
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
18634
|
-
return null;
|
|
18635
|
-
}
|
|
18636
|
-
function warnOnceBroadcastMiss(field, as, key) {
|
|
18637
|
-
const dedup = `${field}\u2192${as}:${key}`;
|
|
18638
|
-
if (warnedBroadcastKeys.has(dedup)) return;
|
|
18639
|
-
warnedBroadcastKeys.add(dedup);
|
|
18640
|
-
console.warn(
|
|
18641
|
-
`[noy-db] broadcastJoin: no "${as}" dimension row for ${field}="${key}". Attaching null. Use mode: 'cascade' to silence.`
|
|
18642
|
-
);
|
|
18643
|
-
}
|
|
18644
|
-
async function applyBroadcastLegs(rows, legs) {
|
|
18645
|
-
if (legs.length === 0) return [...rows];
|
|
18646
|
-
const indexes = [];
|
|
18647
|
-
for (const leg of legs) {
|
|
18648
|
-
const map = /* @__PURE__ */ new Map();
|
|
18649
|
-
for (const rec of await leg.from.list()) {
|
|
18650
|
-
const k = coerceKey(readPath(rec, leg.on));
|
|
18651
|
-
if (k !== null && !map.has(k)) map.set(k, rec);
|
|
19594
|
+
// src/federation/schema-manifest.ts
|
|
19595
|
+
function captureBlueprint(configure) {
|
|
19596
|
+
const recorded = [];
|
|
19597
|
+
const collectionStub = new Proxy(
|
|
19598
|
+
{},
|
|
19599
|
+
{
|
|
19600
|
+
get: () => () => collectionStub
|
|
18652
19601
|
}
|
|
18653
|
-
|
|
18654
|
-
|
|
19602
|
+
);
|
|
19603
|
+
const proxy = new Proxy(
|
|
19604
|
+
{},
|
|
19605
|
+
{
|
|
19606
|
+
get: (_t, prop) => {
|
|
19607
|
+
if (prop === "collection") {
|
|
19608
|
+
return (name, opts) => {
|
|
19609
|
+
recorded.push({
|
|
19610
|
+
name,
|
|
19611
|
+
indexes: opts?.indexes ?? [],
|
|
19612
|
+
persistJsonSchema: !!opts?.persistJsonSchema
|
|
19613
|
+
});
|
|
19614
|
+
return collectionStub;
|
|
19615
|
+
};
|
|
19616
|
+
}
|
|
19617
|
+
return () => proxy;
|
|
19618
|
+
}
|
|
19619
|
+
}
|
|
19620
|
+
);
|
|
19621
|
+
configure(proxy);
|
|
19622
|
+
const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
|
|
19623
|
+
const indexes = {};
|
|
19624
|
+
const persistJsonSchema = [];
|
|
19625
|
+
for (const c of sorted) {
|
|
19626
|
+
indexes[c.name] = c.indexes;
|
|
19627
|
+
if (c.persistJsonSchema) persistJsonSchema.push(c.name);
|
|
19628
|
+
}
|
|
19629
|
+
return {
|
|
19630
|
+
// `persistJsonSchema` is already name-sorted: it is populated while
|
|
19631
|
+
// iterating `sorted` (collections in name order).
|
|
19632
|
+
collections: sorted.map((c) => c.name),
|
|
19633
|
+
indexes,
|
|
19634
|
+
persistJsonSchema
|
|
19635
|
+
};
|
|
19636
|
+
}
|
|
19637
|
+
function canonical(value) {
|
|
19638
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
19639
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
19640
|
+
const obj = value;
|
|
19641
|
+
const keys = Object.keys(obj).sort();
|
|
19642
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
19643
|
+
}
|
|
19644
|
+
async function fingerprintBlueprint(bp) {
|
|
19645
|
+
return sha256Hex2(new TextEncoder().encode(canonical(bp)));
|
|
19646
|
+
}
|
|
19647
|
+
var init_schema_manifest = __esm({
|
|
19648
|
+
"src/federation/schema-manifest.ts"() {
|
|
19649
|
+
"use strict";
|
|
19650
|
+
init_crypto();
|
|
19651
|
+
}
|
|
19652
|
+
});
|
|
19653
|
+
|
|
19654
|
+
// src/federation/state-vault.ts
|
|
19655
|
+
var state_vault_exports = {};
|
|
19656
|
+
__export(state_vault_exports, {
|
|
19657
|
+
STATE_VAULT_NAME: () => STATE_VAULT_NAME,
|
|
19658
|
+
StateManagementVault: () => StateManagementVault
|
|
19659
|
+
});
|
|
19660
|
+
var REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, StateManagementVault;
|
|
19661
|
+
var init_state_vault = __esm({
|
|
19662
|
+
"src/federation/state-vault.ts"() {
|
|
19663
|
+
"use strict";
|
|
19664
|
+
init_schema_manifest();
|
|
19665
|
+
init_constants2();
|
|
19666
|
+
init_ulid();
|
|
19667
|
+
init_constants2();
|
|
19668
|
+
REGISTRY = "vaultRegistry";
|
|
19669
|
+
MANIFEST = "schemaManifest";
|
|
19670
|
+
EVENTS = "deploymentEvents";
|
|
19671
|
+
MIGRATION_STATUS = "migrationStatus";
|
|
19672
|
+
StateManagementVault = class _StateManagementVault {
|
|
19673
|
+
constructor(registry, schemaManifest, events, migrationStatus) {
|
|
19674
|
+
this.registry = registry;
|
|
19675
|
+
this.schemaManifest = schemaManifest;
|
|
19676
|
+
this.#events = events;
|
|
19677
|
+
this.#migrationStatus = migrationStatus;
|
|
19678
|
+
}
|
|
19679
|
+
registry;
|
|
19680
|
+
schemaManifest;
|
|
19681
|
+
/**
|
|
19682
|
+
* The append-only deployment-events log is kept truly private so the raw
|
|
19683
|
+
* mutable Collection is never surfaced — events may only be written via
|
|
19684
|
+
* `appendEvent` and read via `queryEvents`. (`registry` and
|
|
19685
|
+
* `schemaManifest` are deliberately public: consumers read and write them.)
|
|
19686
|
+
*/
|
|
19687
|
+
#events;
|
|
19688
|
+
/** Per-shard fleet-migration progress (#271). Surfaced via typed methods only. */
|
|
19689
|
+
#migrationStatus;
|
|
19690
|
+
/** Idempotently open the reserved state vault and bind the control-plane collections. */
|
|
19691
|
+
static async open(db) {
|
|
19692
|
+
const vault = await db.openVault(STATE_VAULT_NAME);
|
|
19693
|
+
return new _StateManagementVault(
|
|
19694
|
+
vault.collection(REGISTRY),
|
|
19695
|
+
vault.collection(MANIFEST),
|
|
19696
|
+
vault.collection(EVENTS),
|
|
19697
|
+
vault.collection(MIGRATION_STATUS)
|
|
19698
|
+
);
|
|
19699
|
+
}
|
|
19700
|
+
/** Read one shard's migration status (or null). */
|
|
19701
|
+
async getMigrationStatus(vaultId) {
|
|
19702
|
+
return this.#migrationStatus.get(vaultId);
|
|
19703
|
+
}
|
|
19704
|
+
/** All migration-status rows (hydrates first). */
|
|
19705
|
+
async listMigrationStatus() {
|
|
19706
|
+
await this.#migrationStatus.list();
|
|
19707
|
+
return this.#migrationStatus.query().toArray();
|
|
19708
|
+
}
|
|
19709
|
+
/** Upsert one shard's migration status (keyed by vaultId). */
|
|
19710
|
+
async upsertMigrationStatus(row) {
|
|
19711
|
+
await this.#migrationStatus.put(row.vaultId, row);
|
|
19712
|
+
}
|
|
19713
|
+
/** Read-only query over the append-only deployment-events log. */
|
|
19714
|
+
queryEvents() {
|
|
19715
|
+
return this.#events.query();
|
|
19716
|
+
}
|
|
19717
|
+
/**
|
|
19718
|
+
* Append a deployment event with a fresh unique (ULID) id. This is the
|
|
19719
|
+
* only write path to the events log; no update/delete is exposed.
|
|
19720
|
+
* Callers should treat failures as non-fatal — this method does not
|
|
19721
|
+
* swallow errors, so wrap the call site in try/catch where appropriate.
|
|
19722
|
+
*/
|
|
19723
|
+
async appendEvent(event) {
|
|
19724
|
+
const ts = event.ts ?? Date.now();
|
|
19725
|
+
const id = generateULID();
|
|
19726
|
+
await this.#events.put(id, { ...event, id, ts });
|
|
19727
|
+
}
|
|
19728
|
+
/**
|
|
19729
|
+
* Ensure a manifest row exists for `(templateName, template.version)`.
|
|
19730
|
+
* Safe to call repeatedly: the `fingerprint` is a deterministic hash of
|
|
19731
|
+
* the template's declared shape (stable across calls), though each call
|
|
19732
|
+
* refreshes `recordedAt`.
|
|
19733
|
+
*/
|
|
19734
|
+
async recordManifest(templateName, template) {
|
|
19735
|
+
const bp = captureBlueprint(template.configure);
|
|
19736
|
+
const fingerprint = await fingerprintBlueprint(bp);
|
|
19737
|
+
await this.schemaManifest.put(`${templateName}:${template.version}`, {
|
|
19738
|
+
templateName,
|
|
19739
|
+
version: template.version,
|
|
19740
|
+
collections: bp.collections,
|
|
19741
|
+
indexes: bp.indexes,
|
|
19742
|
+
persistJsonSchema: bp.persistJsonSchema,
|
|
19743
|
+
fingerprint,
|
|
19744
|
+
recordedAt: Date.now()
|
|
19745
|
+
});
|
|
19746
|
+
return fingerprint;
|
|
19747
|
+
}
|
|
19748
|
+
/**
|
|
19749
|
+
* True when `template`'s current declared shape does not match the recorded
|
|
19750
|
+
* manifest for `(templateName, template.version)`. Because shards carry no
|
|
19751
|
+
* schema state independent of their template, this catches "a template's
|
|
19752
|
+
* shape changed without bumping `version`" — not independent per-shard drift.
|
|
19753
|
+
* A missing manifest is treated as drift (nothing to verify against).
|
|
19754
|
+
*/
|
|
19755
|
+
async detectDrift(templateName, template) {
|
|
19756
|
+
const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
|
|
19757
|
+
if (!row) return true;
|
|
19758
|
+
const current = await fingerprintBlueprint(captureBlueprint(template.configure));
|
|
19759
|
+
return current !== row.fingerprint;
|
|
19760
|
+
}
|
|
19761
|
+
};
|
|
19762
|
+
}
|
|
19763
|
+
});
|
|
19764
|
+
|
|
19765
|
+
// src/federation/classify-skip.ts
|
|
19766
|
+
function classifyShardSkip(err) {
|
|
19767
|
+
return err instanceof NoAccessError ? "no-grant" : "error";
|
|
19768
|
+
}
|
|
19769
|
+
var init_classify_skip = __esm({
|
|
19770
|
+
"src/federation/classify-skip.ts"() {
|
|
19771
|
+
"use strict";
|
|
19772
|
+
init_errors();
|
|
19773
|
+
}
|
|
19774
|
+
});
|
|
19775
|
+
|
|
19776
|
+
// src/federation/cross-shard-join.ts
|
|
19777
|
+
function coerceKey(value) {
|
|
19778
|
+
if (value === null || value === void 0) return null;
|
|
19779
|
+
if (typeof value === "string") return value;
|
|
19780
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
19781
|
+
return null;
|
|
19782
|
+
}
|
|
19783
|
+
function warnOnceBroadcastMiss(field, as, key) {
|
|
19784
|
+
const dedup = `${field}\u2192${as}:${key}`;
|
|
19785
|
+
if (warnedBroadcastKeys.has(dedup)) return;
|
|
19786
|
+
warnedBroadcastKeys.add(dedup);
|
|
19787
|
+
console.warn(
|
|
19788
|
+
`[noy-db] broadcastJoin: no "${as}" dimension row for ${field}="${key}". Attaching null. Use mode: 'cascade' to silence.`
|
|
19789
|
+
);
|
|
19790
|
+
}
|
|
19791
|
+
async function applyBroadcastLegs(rows, legs) {
|
|
19792
|
+
if (legs.length === 0) return [...rows];
|
|
19793
|
+
const indexes = [];
|
|
19794
|
+
for (const leg of legs) {
|
|
19795
|
+
const map = /* @__PURE__ */ new Map();
|
|
19796
|
+
for (const rec of await leg.from.list()) {
|
|
19797
|
+
const k = coerceKey(readPath(rec, leg.on));
|
|
19798
|
+
if (k !== null && !map.has(k)) map.set(k, rec);
|
|
19799
|
+
}
|
|
19800
|
+
indexes.push({ leg, map });
|
|
19801
|
+
}
|
|
18655
19802
|
return rows.map((row) => {
|
|
18656
19803
|
const out = { ...row };
|
|
18657
19804
|
for (const { leg, map } of indexes) {
|
|
@@ -18899,6 +20046,7 @@ var SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQ
|
|
|
18899
20046
|
var init_vault_group = __esm({
|
|
18900
20047
|
"src/federation/vault-group.ts"() {
|
|
18901
20048
|
"use strict";
|
|
20049
|
+
init_state_vault();
|
|
18902
20050
|
init_errors();
|
|
18903
20051
|
init_constants2();
|
|
18904
20052
|
init_classify_skip();
|
|
@@ -18908,12 +20056,13 @@ var init_vault_group = __esm({
|
|
|
18908
20056
|
SHARD_SEPARATOR = "--";
|
|
18909
20057
|
SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
|
|
18910
20058
|
VaultGroup = class {
|
|
18911
|
-
constructor(db, name, registry, sharding, template) {
|
|
20059
|
+
constructor(db, name, registry, sharding, template, migrateOnOpen = false) {
|
|
18912
20060
|
this.db = db;
|
|
18913
20061
|
this.name = name;
|
|
18914
20062
|
this.registry = registry;
|
|
18915
20063
|
this.sharding = sharding;
|
|
18916
20064
|
this.template = template;
|
|
20065
|
+
this.migrateOnOpen = migrateOnOpen;
|
|
18917
20066
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
18918
20067
|
throw new ValidationError(
|
|
18919
20068
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
@@ -18925,6 +20074,7 @@ var init_vault_group = __esm({
|
|
|
18925
20074
|
registry;
|
|
18926
20075
|
sharding;
|
|
18927
20076
|
template;
|
|
20077
|
+
migrateOnOpen;
|
|
18928
20078
|
/** @internal — set when the group is managed (no explicit registry). */
|
|
18929
20079
|
stateVault;
|
|
18930
20080
|
/** @internal */
|
|
@@ -18958,8 +20108,22 @@ var init_vault_group = __esm({
|
|
|
18958
20108
|
const rows = this.registry.query().toArray();
|
|
18959
20109
|
return rows.filter((r) => r.group === this.name);
|
|
18960
20110
|
}
|
|
18961
|
-
/**
|
|
20111
|
+
/**
|
|
20112
|
+
* Open an existing shard and apply the template. When `migrateOnOpen` is set
|
|
20113
|
+
* (#271) and the shard's registry version is behind the template, its cutover
|
|
20114
|
+
* runs inline first — so a behind shard never surfaces a stale handle.
|
|
20115
|
+
*/
|
|
18962
20116
|
async openShard(partitionKey) {
|
|
20117
|
+
if (this.migrateOnOpen) {
|
|
20118
|
+
const row = await this.registry.get(this.registryId(partitionKey));
|
|
20119
|
+
if (row && row.schemaVersion < this.template.version) {
|
|
20120
|
+
await this.migrateShard(partitionKey);
|
|
20121
|
+
}
|
|
20122
|
+
}
|
|
20123
|
+
return this._openShardRaw(partitionKey);
|
|
20124
|
+
}
|
|
20125
|
+
/** @internal — open + configure with no migrate-on-open hook (used by the migration path itself to avoid recursion). */
|
|
20126
|
+
async _openShardRaw(partitionKey) {
|
|
18963
20127
|
const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
|
|
18964
20128
|
this.template.configure(vault);
|
|
18965
20129
|
return vault;
|
|
@@ -18971,13 +20135,21 @@ var init_vault_group = __esm({
|
|
|
18971
20135
|
* - row + vault present → no-op, return handle
|
|
18972
20136
|
* - row present, vault gone → ShardProvisioningError
|
|
18973
20137
|
* - row absent (vault present or not) → open-or-create, configure, write row
|
|
20138
|
+
*
|
|
20139
|
+
* When `region` is given (the routing `put` passes `sharding.regionOf(record)`),
|
|
20140
|
+
* the candidate backend's `capabilities.region` must match or this throws
|
|
20141
|
+
* `DataResidencyError` BEFORE provisioning (#271 data-residency guard).
|
|
18974
20142
|
*/
|
|
18975
|
-
async createShard(partitionKey) {
|
|
20143
|
+
async createShard(partitionKey, region) {
|
|
18976
20144
|
const vaultId = this.shardVaultId(partitionKey);
|
|
18977
20145
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
18978
20146
|
const provisioned = await this.db._shardVaultProvisioned(vaultId);
|
|
18979
20147
|
if (row && !provisioned) throw new ShardProvisioningError(vaultId, partitionKey);
|
|
18980
20148
|
if (row && provisioned) return this.openShard(partitionKey);
|
|
20149
|
+
if (region !== void 0) {
|
|
20150
|
+
const backendRegion = this.db._resolveBackend(vaultId).capabilities?.region;
|
|
20151
|
+
if (backendRegion !== region) throw new DataResidencyError(vaultId, region, backendRegion);
|
|
20152
|
+
}
|
|
18981
20153
|
const vault = await this.db.openVault(vaultId);
|
|
18982
20154
|
this.template.configure(vault);
|
|
18983
20155
|
await this.registry.put(this.registryId(partitionKey), {
|
|
@@ -19037,6 +20209,172 @@ var init_vault_group = __esm({
|
|
|
19037
20209
|
});
|
|
19038
20210
|
return { eligible, skipped };
|
|
19039
20211
|
}
|
|
20212
|
+
/** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
|
|
20213
|
+
crossVaultDerivations = [];
|
|
20214
|
+
/**
|
|
20215
|
+
* Register a push-model cross-vault derivation — the Insight Vault pattern
|
|
20216
|
+
* (#271, Layer 4). Drive it with {@link refreshInsights}.
|
|
20217
|
+
*
|
|
20218
|
+
* For each shard, `derive(records, ctx)` runs on that shard's `source`
|
|
20219
|
+
* records and its return value is written into the analytics
|
|
20220
|
+
* (`target.vault` / `target.collection`) vault, keyed by partition key —
|
|
20221
|
+
* one summary row per shard. The derivation runs in-process under THIS
|
|
20222
|
+
* group's `Noydb` (which already holds both the shard and Insight Vault
|
|
20223
|
+
* keyrings); the shard's decrypted records are reduced to a summary that is
|
|
20224
|
+
* re-encrypted under the Insight Vault's own DEK, so no shard ciphertext
|
|
20225
|
+
* crosses a DEK boundary.
|
|
20226
|
+
*
|
|
20227
|
+
* **Zero-knowledge note:** the Insight Vault backend sees aggregated
|
|
20228
|
+
* structure (totals, counts, timestamps) drawn from many shards — a weaker
|
|
20229
|
+
* ZK profile than the per-shard vaults. Opt-in; keep summaries to aggregate
|
|
20230
|
+
* scalars (no embeddings / no raw records).
|
|
20231
|
+
*
|
|
20232
|
+
* v1 is explicit-refresh (no write-path push); call `refreshInsights()`
|
|
20233
|
+
* after a batch of writes, or on a schedule.
|
|
20234
|
+
*
|
|
20235
|
+
* The `target.vault` must NOT be the group itself or one of its shards —
|
|
20236
|
+
* a summary writing back into client-shard data would breach the Insight
|
|
20237
|
+
* Vault's separate-DEK-boundary contract. Such a target throws a
|
|
20238
|
+
* `ValidationError` at registration (#271 Insight-write isolation).
|
|
20239
|
+
*/
|
|
20240
|
+
withCrossVaultDerivation(spec) {
|
|
20241
|
+
const target = spec.target.vault;
|
|
20242
|
+
if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
|
|
20243
|
+
throw new ValidationError(
|
|
20244
|
+
`withCrossVaultDerivation: target.vault "${target}" is the "${this.name}" group itself or one of its shards \u2014 an Insight summary must target a SEPARATE analytics vault, never write back into client-shard data (it would breach the per-shard DEK boundary). Use a distinct vault name.`
|
|
20245
|
+
);
|
|
20246
|
+
}
|
|
20247
|
+
this.crossVaultDerivations.push(spec);
|
|
20248
|
+
}
|
|
20249
|
+
/**
|
|
20250
|
+
* Run every registered {@link withCrossVaultDerivation}: read each eligible
|
|
20251
|
+
* shard's source records, derive a per-shard summary, and write it into the
|
|
20252
|
+
* Insight Vault keyed by partition key. Shards behind `minVersion`,
|
|
20253
|
+
* unprovisioned, or whose read errors are reported in `skippedVaults` and
|
|
20254
|
+
* are not written (a stale summary is never left behind for a failed shard).
|
|
20255
|
+
*/
|
|
20256
|
+
async refreshInsights(options = {}) {
|
|
20257
|
+
if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
|
|
20258
|
+
const { eligible, skipped } = await this.resolveEligible(
|
|
20259
|
+
options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
|
|
20260
|
+
);
|
|
20261
|
+
let written = 0;
|
|
20262
|
+
for (const spec of this.crossVaultDerivations) {
|
|
20263
|
+
const results = await this.db.queryAcross(
|
|
20264
|
+
eligible.map((r) => r.vaultId),
|
|
20265
|
+
async (vault) => {
|
|
20266
|
+
this.template.configure(vault);
|
|
20267
|
+
return vault.collection(spec.source).list();
|
|
20268
|
+
},
|
|
20269
|
+
{ create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
|
|
20270
|
+
);
|
|
20271
|
+
const insight = await this.db.openVault(spec.target.vault);
|
|
20272
|
+
const out = insight.collection(spec.target.collection);
|
|
20273
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
20274
|
+
const row = eligible[i];
|
|
20275
|
+
const res = results[i];
|
|
20276
|
+
if (!res || res.result === void 0) {
|
|
20277
|
+
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
20278
|
+
continue;
|
|
20279
|
+
}
|
|
20280
|
+
const ctx = {
|
|
20281
|
+
vaultId: row.vaultId,
|
|
20282
|
+
partitionKey: row.partitionKey,
|
|
20283
|
+
schemaVersion: row.schemaVersion
|
|
20284
|
+
};
|
|
20285
|
+
const summary = spec.derive(res.result, ctx);
|
|
20286
|
+
await out.put(row.partitionKey, summary);
|
|
20287
|
+
written++;
|
|
20288
|
+
}
|
|
20289
|
+
}
|
|
20290
|
+
return { written, skippedVaults: skipped };
|
|
20291
|
+
}
|
|
20292
|
+
/** @internal — the control-plane vault for migration status; lazily opened. */
|
|
20293
|
+
async ensureStateVault() {
|
|
20294
|
+
if (!this.stateVault) this.stateVault = await StateManagementVault.open(this.db);
|
|
20295
|
+
return this.stateVault;
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Migrate ONE shard to the template's current version (#271 fleet runner,
|
|
20299
|
+
* per-shard step). Opens the shard (applying the template, which arms the
|
|
20300
|
+
* M12 cutover), drains schema-write detection, runs `vault.runSchemaCutover()`
|
|
20301
|
+
* (the per-vault drain-barrier-transform protocol), then advances the
|
|
20302
|
+
* registry row's `schemaVersion` and records `migration-status`. A shard
|
|
20303
|
+
* already at the template version is a no-op (`status: 'done'`, migrated 0).
|
|
20304
|
+
* Never throws on a cutover failure — it records `status: 'failed'` and
|
|
20305
|
+
* returns the row, so a fleet run continues past a bad shard.
|
|
20306
|
+
*/
|
|
20307
|
+
async migrateShard(partitionKey) {
|
|
20308
|
+
const vaultId = this.shardVaultId(partitionKey);
|
|
20309
|
+
const row = await this.registry.get(this.registryId(partitionKey));
|
|
20310
|
+
if (!row) throw new UnknownShardError(partitionKey, this.name);
|
|
20311
|
+
const target = this.template.version;
|
|
20312
|
+
const sv = await this.ensureStateVault();
|
|
20313
|
+
const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
|
|
20314
|
+
if (row.schemaVersion >= target) {
|
|
20315
|
+
const done = { ...base, status: "done", migrated: 0, finishedAt: Date.now() };
|
|
20316
|
+
await sv.upsertMigrationStatus(done);
|
|
20317
|
+
return done;
|
|
20318
|
+
}
|
|
20319
|
+
await sv.upsertMigrationStatus({ ...base, status: "running", startedAt: Date.now() });
|
|
20320
|
+
try {
|
|
20321
|
+
await sv.appendEvent({ type: "migration-started", group: this.name, vaultId, version: target });
|
|
20322
|
+
} catch {
|
|
20323
|
+
}
|
|
20324
|
+
try {
|
|
20325
|
+
const vault = await this._openShardRaw(partitionKey);
|
|
20326
|
+
await vault._drainPendingSchemaWrites();
|
|
20327
|
+
const { migrated } = await vault.runSchemaCutover();
|
|
20328
|
+
await this.registry.put(this.registryId(partitionKey), { ...row, schemaVersion: target });
|
|
20329
|
+
const done = { ...base, currentVersion: target, status: "done", migrated, finishedAt: Date.now() };
|
|
20330
|
+
await sv.upsertMigrationStatus(done);
|
|
20331
|
+
try {
|
|
20332
|
+
await sv.appendEvent({ type: "migration-completed", group: this.name, vaultId, version: target });
|
|
20333
|
+
} catch {
|
|
20334
|
+
}
|
|
20335
|
+
return done;
|
|
20336
|
+
} catch (err) {
|
|
20337
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
20338
|
+
const failed = { ...base, status: "failed", error, finishedAt: Date.now() };
|
|
20339
|
+
await sv.upsertMigrationStatus(failed);
|
|
20340
|
+
try {
|
|
20341
|
+
await sv.appendEvent({ type: "migration-failed", group: this.name, vaultId, version: target, detail: error });
|
|
20342
|
+
} catch {
|
|
20343
|
+
}
|
|
20344
|
+
return failed;
|
|
20345
|
+
}
|
|
20346
|
+
}
|
|
20347
|
+
/**
|
|
20348
|
+
* Active batch runner (#271): migrate every shard behind the template version
|
|
20349
|
+
* to it, in controlled batches. **Resumable + crash-safe** — shards already at
|
|
20350
|
+
* the target are skipped (the registry version is the source of truth), so a
|
|
20351
|
+
* re-run after a crash only picks up the unfinished + previously-failed shards.
|
|
20352
|
+
*
|
|
20353
|
+
* - `cohort` — restrict to these partition keys (the staged / canary rollout:
|
|
20354
|
+
* migrate a small cohort, verify the Insight Vault, then run the rest).
|
|
20355
|
+
* - `batchSize` — max shards migrated concurrently per batch (back-pressure).
|
|
20356
|
+
* Default 4. Batches run sequentially; shards within a batch run in parallel.
|
|
20357
|
+
*/
|
|
20358
|
+
async migrateFleet(options = {}) {
|
|
20359
|
+
const target = this.template.version;
|
|
20360
|
+
const rows = await this.allRows();
|
|
20361
|
+
const cohort = options.cohort;
|
|
20362
|
+
const todo = rows.filter(
|
|
20363
|
+
(r) => r.schemaVersion < target && (cohort === void 0 || cohort.includes(r.partitionKey))
|
|
20364
|
+
);
|
|
20365
|
+
const batchSize = Math.max(1, options.batchSize ?? 4);
|
|
20366
|
+
const migrated = [];
|
|
20367
|
+
const failed = [];
|
|
20368
|
+
for (let i = 0; i < todo.length; i += batchSize) {
|
|
20369
|
+
const batch = todo.slice(i, i + batchSize);
|
|
20370
|
+
const settled = await Promise.all(batch.map((r) => this.migrateShard(r.partitionKey)));
|
|
20371
|
+
for (const res of settled) {
|
|
20372
|
+
if (res.status === "done") migrated.push(res.vaultId);
|
|
20373
|
+
else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
return { target, migrated, failed };
|
|
20377
|
+
}
|
|
19040
20378
|
};
|
|
19041
20379
|
ShardedCollection = class {
|
|
19042
20380
|
constructor(group, collectionName) {
|
|
@@ -19054,7 +20392,7 @@ var init_vault_group = __esm({
|
|
|
19054
20392
|
if (this.group.sharding.autoCreate === false) {
|
|
19055
20393
|
throw new UnknownShardError(key, this.group.name);
|
|
19056
20394
|
}
|
|
19057
|
-
vault = await this.group.createShard(key);
|
|
20395
|
+
vault = await this.group.createShard(key, this.group.sharding.regionOf?.(record));
|
|
19058
20396
|
} else {
|
|
19059
20397
|
vault = await this.group.openShard(key);
|
|
19060
20398
|
}
|
|
@@ -19244,159 +20582,6 @@ var init_vault_group = __esm({
|
|
|
19244
20582
|
}
|
|
19245
20583
|
});
|
|
19246
20584
|
|
|
19247
|
-
// src/federation/schema-manifest.ts
|
|
19248
|
-
function captureBlueprint(configure) {
|
|
19249
|
-
const recorded = [];
|
|
19250
|
-
const collectionStub = new Proxy(
|
|
19251
|
-
{},
|
|
19252
|
-
{
|
|
19253
|
-
get: () => () => collectionStub
|
|
19254
|
-
}
|
|
19255
|
-
);
|
|
19256
|
-
const proxy = new Proxy(
|
|
19257
|
-
{},
|
|
19258
|
-
{
|
|
19259
|
-
get: (_t, prop) => {
|
|
19260
|
-
if (prop === "collection") {
|
|
19261
|
-
return (name, opts) => {
|
|
19262
|
-
recorded.push({
|
|
19263
|
-
name,
|
|
19264
|
-
indexes: opts?.indexes ?? [],
|
|
19265
|
-
persistJsonSchema: !!opts?.persistJsonSchema
|
|
19266
|
-
});
|
|
19267
|
-
return collectionStub;
|
|
19268
|
-
};
|
|
19269
|
-
}
|
|
19270
|
-
return () => proxy;
|
|
19271
|
-
}
|
|
19272
|
-
}
|
|
19273
|
-
);
|
|
19274
|
-
configure(proxy);
|
|
19275
|
-
const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
|
|
19276
|
-
const indexes = {};
|
|
19277
|
-
const persistJsonSchema = [];
|
|
19278
|
-
for (const c of sorted) {
|
|
19279
|
-
indexes[c.name] = c.indexes;
|
|
19280
|
-
if (c.persistJsonSchema) persistJsonSchema.push(c.name);
|
|
19281
|
-
}
|
|
19282
|
-
return {
|
|
19283
|
-
// `persistJsonSchema` is already name-sorted: it is populated while
|
|
19284
|
-
// iterating `sorted` (collections in name order).
|
|
19285
|
-
collections: sorted.map((c) => c.name),
|
|
19286
|
-
indexes,
|
|
19287
|
-
persistJsonSchema
|
|
19288
|
-
};
|
|
19289
|
-
}
|
|
19290
|
-
function canonical(value) {
|
|
19291
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
19292
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
19293
|
-
const obj = value;
|
|
19294
|
-
const keys = Object.keys(obj).sort();
|
|
19295
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
19296
|
-
}
|
|
19297
|
-
async function fingerprintBlueprint(bp) {
|
|
19298
|
-
return sha256Hex2(new TextEncoder().encode(canonical(bp)));
|
|
19299
|
-
}
|
|
19300
|
-
var init_schema_manifest = __esm({
|
|
19301
|
-
"src/federation/schema-manifest.ts"() {
|
|
19302
|
-
"use strict";
|
|
19303
|
-
init_crypto();
|
|
19304
|
-
}
|
|
19305
|
-
});
|
|
19306
|
-
|
|
19307
|
-
// src/federation/state-vault.ts
|
|
19308
|
-
var state_vault_exports = {};
|
|
19309
|
-
__export(state_vault_exports, {
|
|
19310
|
-
STATE_VAULT_NAME: () => STATE_VAULT_NAME,
|
|
19311
|
-
StateManagementVault: () => StateManagementVault
|
|
19312
|
-
});
|
|
19313
|
-
var REGISTRY, MANIFEST, EVENTS, StateManagementVault;
|
|
19314
|
-
var init_state_vault = __esm({
|
|
19315
|
-
"src/federation/state-vault.ts"() {
|
|
19316
|
-
"use strict";
|
|
19317
|
-
init_schema_manifest();
|
|
19318
|
-
init_constants2();
|
|
19319
|
-
init_ulid();
|
|
19320
|
-
init_constants2();
|
|
19321
|
-
REGISTRY = "vaultRegistry";
|
|
19322
|
-
MANIFEST = "schemaManifest";
|
|
19323
|
-
EVENTS = "deploymentEvents";
|
|
19324
|
-
StateManagementVault = class _StateManagementVault {
|
|
19325
|
-
constructor(registry, schemaManifest, events) {
|
|
19326
|
-
this.registry = registry;
|
|
19327
|
-
this.schemaManifest = schemaManifest;
|
|
19328
|
-
this.#events = events;
|
|
19329
|
-
}
|
|
19330
|
-
registry;
|
|
19331
|
-
schemaManifest;
|
|
19332
|
-
/**
|
|
19333
|
-
* The append-only deployment-events log is kept truly private so the raw
|
|
19334
|
-
* mutable Collection is never surfaced — events may only be written via
|
|
19335
|
-
* `appendEvent` and read via `queryEvents`. (`registry` and
|
|
19336
|
-
* `schemaManifest` are deliberately public: consumers read and write them.)
|
|
19337
|
-
*/
|
|
19338
|
-
#events;
|
|
19339
|
-
/** Idempotently open the reserved state vault and bind the three control-plane collections. */
|
|
19340
|
-
static async open(db) {
|
|
19341
|
-
const vault = await db.openVault(STATE_VAULT_NAME);
|
|
19342
|
-
return new _StateManagementVault(
|
|
19343
|
-
vault.collection(REGISTRY),
|
|
19344
|
-
vault.collection(MANIFEST),
|
|
19345
|
-
vault.collection(EVENTS)
|
|
19346
|
-
);
|
|
19347
|
-
}
|
|
19348
|
-
/** Read-only query over the append-only deployment-events log. */
|
|
19349
|
-
queryEvents() {
|
|
19350
|
-
return this.#events.query();
|
|
19351
|
-
}
|
|
19352
|
-
/**
|
|
19353
|
-
* Append a deployment event with a fresh unique (ULID) id. This is the
|
|
19354
|
-
* only write path to the events log; no update/delete is exposed.
|
|
19355
|
-
* Callers should treat failures as non-fatal — this method does not
|
|
19356
|
-
* swallow errors, so wrap the call site in try/catch where appropriate.
|
|
19357
|
-
*/
|
|
19358
|
-
async appendEvent(event) {
|
|
19359
|
-
const ts = event.ts ?? Date.now();
|
|
19360
|
-
const id = generateULID();
|
|
19361
|
-
await this.#events.put(id, { ...event, id, ts });
|
|
19362
|
-
}
|
|
19363
|
-
/**
|
|
19364
|
-
* Ensure a manifest row exists for `(templateName, template.version)`.
|
|
19365
|
-
* Safe to call repeatedly: the `fingerprint` is a deterministic hash of
|
|
19366
|
-
* the template's declared shape (stable across calls), though each call
|
|
19367
|
-
* refreshes `recordedAt`.
|
|
19368
|
-
*/
|
|
19369
|
-
async recordManifest(templateName, template) {
|
|
19370
|
-
const bp = captureBlueprint(template.configure);
|
|
19371
|
-
const fingerprint = await fingerprintBlueprint(bp);
|
|
19372
|
-
await this.schemaManifest.put(`${templateName}:${template.version}`, {
|
|
19373
|
-
templateName,
|
|
19374
|
-
version: template.version,
|
|
19375
|
-
collections: bp.collections,
|
|
19376
|
-
indexes: bp.indexes,
|
|
19377
|
-
persistJsonSchema: bp.persistJsonSchema,
|
|
19378
|
-
fingerprint,
|
|
19379
|
-
recordedAt: Date.now()
|
|
19380
|
-
});
|
|
19381
|
-
return fingerprint;
|
|
19382
|
-
}
|
|
19383
|
-
/**
|
|
19384
|
-
* True when `template`'s current declared shape does not match the recorded
|
|
19385
|
-
* manifest for `(templateName, template.version)`. Because shards carry no
|
|
19386
|
-
* schema state independent of their template, this catches "a template's
|
|
19387
|
-
* shape changed without bumping `version`" — not independent per-shard drift.
|
|
19388
|
-
* A missing manifest is treated as drift (nothing to verify against).
|
|
19389
|
-
*/
|
|
19390
|
-
async detectDrift(templateName, template) {
|
|
19391
|
-
const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
|
|
19392
|
-
if (!row) return true;
|
|
19393
|
-
const current = await fingerprintBlueprint(captureBlueprint(template.configure));
|
|
19394
|
-
return current !== row.fingerprint;
|
|
19395
|
-
}
|
|
19396
|
-
};
|
|
19397
|
-
}
|
|
19398
|
-
});
|
|
19399
|
-
|
|
19400
20585
|
// src/noydb.ts
|
|
19401
20586
|
var noydb_exports = {};
|
|
19402
20587
|
__export(noydb_exports, {
|
|
@@ -20266,7 +21451,7 @@ var init_noydb = __esm({
|
|
|
20266
21451
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
20267
21452
|
const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(this);
|
|
20268
21453
|
const registry = opts.registry ?? stateVault.registry;
|
|
20269
|
-
const group = new VaultGroup2(this, name, registry, opts.sharding, template);
|
|
21454
|
+
const group = new VaultGroup2(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
|
|
20270
21455
|
if (stateVault) {
|
|
20271
21456
|
group._attachStateVault(stateVault);
|
|
20272
21457
|
await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
|
|
@@ -20300,6 +21485,16 @@ var init_noydb = __esm({
|
|
|
20300
21485
|
async _shardVaultProvisioned(vaultId) {
|
|
20301
21486
|
return (await this.options.store.list(vaultId, "_keyring")).length > 0;
|
|
20302
21487
|
}
|
|
21488
|
+
/**
|
|
21489
|
+
* @internal — the physical backend store a vault id maps to. A
|
|
21490
|
+
* `routeStore` resolves the vault-prefix route via its `resolveBackend`;
|
|
21491
|
+
* a plain store is its own backend. Used by the federation data-residency
|
|
21492
|
+
* guard to read the placement backend's `capabilities.region` (#271).
|
|
21493
|
+
*/
|
|
21494
|
+
_resolveBackend(vaultId) {
|
|
21495
|
+
const store = this.options.store;
|
|
21496
|
+
return store.resolveBackend ? store.resolveBackend(vaultId) : this.options.store;
|
|
21497
|
+
}
|
|
20303
21498
|
/**
|
|
20304
21499
|
* Change the current user's passphrase for a vault.
|
|
20305
21500
|
*
|