@noy-db/hub 0.4.0-pre.5 → 0.4.0-pre.7
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/api-3NKVNZPM.js +20 -0
- package/dist/attestation/index.js +20 -20
- package/dist/{backup-YCHAC575.js → backup-PTSVXDDW.js} +13 -13
- package/dist/blobs/index.js +15 -12
- package/dist/blobs/index.js.map +1 -1
- package/dist/broker/index.js +12 -12
- package/dist/cargo/index.js +36 -36
- package/dist/{chunk-5WLDZZ6S.js → chunk-35NMVJ7I.js} +3 -3
- package/dist/{chunk-TNATF3HB.js → chunk-3HUYRN24.js} +4 -4
- package/dist/chunk-3HUYRN24.js.map +1 -0
- package/dist/{chunk-TNJVDG2B.js → chunk-3XKESW5E.js} +3 -3
- package/dist/{chunk-MUPZ6IZX.js → chunk-4JQK3L4V.js} +2 -2
- package/dist/{chunk-Z4D3YGAB.js → chunk-4ZKNC6A6.js} +2 -2
- package/dist/{chunk-ONB627HU.js → chunk-55LX6FSX.js} +2 -2
- package/dist/{chunk-QMZ5A6FQ.js → chunk-56IIVAEO.js} +2 -2
- package/dist/chunk-5D2ALSM5.js +19 -0
- package/dist/chunk-5D2ALSM5.js.map +1 -0
- package/dist/{chunk-FHHYCBBP.js → chunk-5DRROK5W.js} +2 -2
- package/dist/{chunk-GTNRMJYH.js → chunk-5GQEU4DN.js} +2 -2
- package/dist/{chunk-YQQPNXNO.js → chunk-6L3DKWRY.js} +6 -6
- package/dist/chunk-6LJY7X5A.js +18 -0
- package/dist/chunk-6LJY7X5A.js.map +1 -0
- package/dist/{chunk-M6QA2KK2.js → chunk-6M2I3DKF.js} +2 -2
- package/dist/{chunk-FTCGEUOY.js → chunk-6R6Q3MRN.js} +7 -7
- package/dist/{chunk-ZUDAK436.js → chunk-6UBZ6I5Z.js} +2 -2
- package/dist/{chunk-SEU6QHH4.js → chunk-6ZNSB3H6.js} +5 -17
- package/dist/chunk-6ZNSB3H6.js.map +1 -0
- package/dist/chunk-AFHQATY4.js +47 -0
- package/dist/chunk-AFHQATY4.js.map +1 -0
- package/dist/{chunk-7U6AB7DZ.js → chunk-AKKRXT23.js} +2 -2
- package/dist/{chunk-2PDBYF2O.js → chunk-AT2JJEKJ.js} +4 -4
- package/dist/chunk-AT2JJEKJ.js.map +1 -0
- package/dist/{chunk-OWLZONGX.js → chunk-BLBAQRBM.js} +2 -2
- package/dist/{chunk-QBOQI6PG.js → chunk-BN4HRYAZ.js} +2 -2
- package/dist/{chunk-QVGEVZ5F.js → chunk-BQ6DHN4I.js} +3 -3
- package/dist/{chunk-GP54LU5N.js → chunk-BYFXLDVV.js} +9 -9
- package/dist/{chunk-FIP7KI2P.js → chunk-C25JXSOR.js} +337 -21
- package/dist/chunk-C25JXSOR.js.map +1 -0
- package/dist/chunk-CADUYBQD.js +151 -0
- package/dist/chunk-CADUYBQD.js.map +1 -0
- package/dist/{chunk-HAPY7WFP.js → chunk-CCDP6EPB.js} +13 -355
- package/dist/chunk-CCDP6EPB.js.map +1 -0
- package/dist/{chunk-QRHEAZ2Q.js → chunk-CUZLNGD5.js} +1 -1
- package/dist/{chunk-YXIVUWDU.js → chunk-DGBIHDVK.js} +5 -5
- package/dist/chunk-DSDXAKIP.js +50 -0
- package/dist/chunk-DSDXAKIP.js.map +1 -0
- package/dist/{chunk-QZJEYZ4I.js → chunk-DZAF24PG.js} +5 -5
- package/dist/chunk-DZFQVICS.js +23 -0
- package/dist/chunk-DZFQVICS.js.map +1 -0
- package/dist/chunk-E23UGFVC.js +63 -0
- package/dist/chunk-E23UGFVC.js.map +1 -0
- package/dist/{chunk-X3MWEFPT.js → chunk-EBKCYQ2D.js} +14 -14
- package/dist/chunk-EBKCYQ2D.js.map +1 -0
- package/dist/chunk-EP5L47XV.js +22 -0
- package/dist/chunk-EP5L47XV.js.map +1 -0
- package/dist/{chunk-TMR64XPL.js → chunk-EZNTORXE.js} +2 -2
- package/dist/{chunk-JAMDDLIM.js → chunk-FMNU7IUG.js} +3 -3
- package/dist/{chunk-YXZNGHID.js → chunk-FO6RNKKW.js} +8 -46
- package/dist/chunk-FO6RNKKW.js.map +1 -0
- package/dist/{chunk-YMUGBZN2.js → chunk-FQ5CHJR7.js} +1 -1
- package/dist/chunk-FQ5CHJR7.js.map +1 -0
- package/dist/{chunk-YZN6NRTR.js → chunk-FRMW23UK.js} +13 -11
- package/dist/chunk-FRMW23UK.js.map +1 -0
- package/dist/chunk-GJW4HX2O.js +1 -0
- package/dist/{chunk-7BIJS6II.js → chunk-GP3UWP22.js} +4 -4
- package/dist/{chunk-YTW6ZIPD.js → chunk-GZACYEOB.js} +1 -1
- package/dist/chunk-GZACYEOB.js.map +1 -0
- package/dist/{chunk-JIHH26WQ.js → chunk-HFUAFR6B.js} +6 -6
- package/dist/{chunk-J5TEU77Q.js → chunk-IF6WNGAK.js} +59 -4
- package/dist/chunk-IF6WNGAK.js.map +1 -0
- package/dist/{chunk-ZX5FNWH7.js → chunk-IG6KUWBX.js} +2 -2
- package/dist/chunk-IG6KUWBX.js.map +1 -0
- package/dist/{chunk-CKUYQO2P.js → chunk-IMRB2C4O.js} +7 -5
- package/dist/chunk-IMRB2C4O.js.map +1 -0
- package/dist/{chunk-RWW3I3PO.js → chunk-ISRKW4YN.js} +3 -3
- package/dist/{chunk-P4NUJVWL.js → chunk-IXNCZTWA.js} +2 -2
- package/dist/{chunk-YFFEF6HR.js → chunk-JIDLU57U.js} +3 -3
- package/dist/{chunk-PTT2NB46.js → chunk-JYJFGD2H.js} +2 -2
- package/dist/{chunk-3OWYOECI.js → chunk-JYV4JJV2.js} +5 -5
- package/dist/{chunk-OGVE4IJW.js → chunk-KBPU5M2E.js} +7 -7
- package/dist/{chunk-I2XEBEEF.js → chunk-KFRPNHWR.js} +4 -4
- package/dist/chunk-KFRPNHWR.js.map +1 -0
- package/dist/{chunk-3LJYHE7S.js → chunk-KPXM4WO6.js} +64 -57
- package/dist/chunk-KPXM4WO6.js.map +1 -0
- package/dist/{chunk-QMMOMBH6.js → chunk-KRCBZYTZ.js} +26 -177
- package/dist/chunk-KRCBZYTZ.js.map +1 -0
- package/dist/{chunk-UTEATWHO.js → chunk-KW42W6SV.js} +6 -6
- package/dist/{chunk-BY5YBSXA.js → chunk-L3YYLT4E.js} +15 -2
- package/dist/chunk-L3YYLT4E.js.map +1 -0
- package/dist/{chunk-UVBHM7AK.js → chunk-MANUDU7I.js} +6 -6
- package/dist/{chunk-YJ3TREAC.js → chunk-MHMLXJ3I.js} +2 -2
- package/dist/chunk-MHMLXJ3I.js.map +1 -0
- package/dist/{chunk-D4LAJ737.js → chunk-MSMJJB4J.js} +2 -2
- package/dist/chunk-MSMJJB4J.js.map +1 -0
- package/dist/{chunk-DYZ5CCVM.js → chunk-MXUT7FTE.js} +2 -2
- package/dist/chunk-MXUT7FTE.js.map +1 -0
- package/dist/{chunk-7ICV4ASA.js → chunk-N5PLUFH2.js} +27 -5
- package/dist/chunk-N5PLUFH2.js.map +1 -0
- package/dist/{chunk-K7BADAOS.js → chunk-NEX7JCMO.js} +4 -4
- package/dist/{chunk-5HSSTOOG.js → chunk-NFZGZ4AK.js} +2 -2
- package/dist/{chunk-XUFAFHQM.js → chunk-NJ4DYYO5.js} +7 -4
- package/dist/chunk-NJ4DYYO5.js.map +1 -0
- package/dist/{chunk-VEGQYPCP.js → chunk-NQSO4UAH.js} +8 -8
- package/dist/chunk-NQSO4UAH.js.map +1 -0
- package/dist/{chunk-GHEMAVTN.js → chunk-NYEFP7DY.js} +4 -4
- package/dist/chunk-NYEFP7DY.js.map +1 -0
- package/dist/{chunk-S4YERMGJ.js → chunk-OBPVWOSQ.js} +4 -4
- package/dist/{chunk-OHV6T5JB.js → chunk-OLIWCCFM.js} +2 -5
- package/dist/{chunk-2CRZOVDI.js → chunk-OLSARBZU.js} +2 -2
- package/dist/chunk-PEOZ34EM.js +13 -0
- package/dist/chunk-PEOZ34EM.js.map +1 -0
- package/dist/{chunk-KJTIAECI.js → chunk-PT4XLP6I.js} +171 -12
- package/dist/chunk-PT4XLP6I.js.map +1 -0
- package/dist/{chunk-LC7DSMHR.js → chunk-PTHKZWEH.js} +2 -14
- package/dist/{chunk-LC7DSMHR.js.map → chunk-PTHKZWEH.js.map} +1 -1
- package/dist/chunk-PVEWA2DU.js +23 -0
- package/dist/chunk-PVEWA2DU.js.map +1 -0
- package/dist/chunk-QADFHRQM.js +367 -0
- package/dist/chunk-QADFHRQM.js.map +1 -0
- package/dist/{chunk-7LP7FFJ2.js → chunk-QD46HPZG.js} +2 -2
- package/dist/{chunk-G4MFQXYX.js → chunk-QJNCB5SK.js} +2 -2
- package/dist/{chunk-6SYOHUHM.js → chunk-QZWTRENR.js} +2 -2
- package/dist/chunk-QZWTRENR.js.map +1 -0
- package/dist/{chunk-TSNRZSRC.js → chunk-RO5NM6UI.js} +2 -2
- package/dist/chunk-RO5NM6UI.js.map +1 -0
- package/dist/{chunk-GEEJPID6.js → chunk-RZPOZPZU.js} +8 -8
- package/dist/chunk-SGDPRCC4.js +1075 -0
- package/dist/chunk-SGDPRCC4.js.map +1 -0
- package/dist/{chunk-YCAJDK4O.js → chunk-SGQ4N5FS.js} +5 -5
- package/dist/{chunk-522MBPE5.js → chunk-SHEEBRZ4.js} +2 -2
- package/dist/{chunk-YWZEDG7F.js → chunk-SHYIA73K.js} +6 -6
- package/dist/{chunk-L2CAWCQL.js → chunk-SKM35Y4Q.js} +17 -26
- package/dist/chunk-SKM35Y4Q.js.map +1 -0
- package/dist/chunk-SMY5X46Z.js +14 -0
- package/dist/chunk-SMY5X46Z.js.map +1 -0
- package/dist/{chunk-K4BWPNJO.js → chunk-TE6LH6J2.js} +5 -5
- package/dist/{chunk-AFAPBZAA.js → chunk-TG634KCO.js} +11 -2
- package/dist/chunk-TG634KCO.js.map +1 -0
- package/dist/{chunk-V5JUAW4M.js → chunk-TIVK6PRK.js} +9 -10
- package/dist/chunk-TIVK6PRK.js.map +1 -0
- package/dist/{chunk-JN3SGXJE.js → chunk-UADPR6F4.js} +3 -3
- package/dist/{chunk-JN3SGXJE.js.map → chunk-UADPR6F4.js.map} +1 -1
- package/dist/{chunk-SBOHNJUD.js → chunk-V6UQZA2H.js} +2 -2
- package/dist/chunk-VG2ZSFYD.js +32 -0
- package/dist/chunk-VG2ZSFYD.js.map +1 -0
- package/dist/{chunk-SC4JLMZI.js → chunk-VH6SOCXH.js} +2 -2
- package/dist/chunk-VKQCVU2Z.js +21 -0
- package/dist/chunk-VKQCVU2Z.js.map +1 -0
- package/dist/chunk-VSRSQ2CR.js +654 -0
- package/dist/chunk-VSRSQ2CR.js.map +1 -0
- package/dist/{chunk-WQN5SM7P.js → chunk-W3G5REIU.js} +2 -2
- package/dist/{chunk-EGU4PWQW.js → chunk-W66AM5IB.js} +2 -2
- package/dist/chunk-W66AM5IB.js.map +1 -0
- package/dist/{chunk-HQKRZIX6.js → chunk-WBKZUO5V.js} +26 -43
- package/dist/chunk-WBKZUO5V.js.map +1 -0
- package/dist/{chunk-OMSKGLYA.js → chunk-WO4T4OD5.js} +2 -2
- package/dist/{chunk-6RPWILRH.js → chunk-WZ3NCMK6.js} +6 -5
- package/dist/{chunk-BAAUFCJK.js → chunk-X4MVWPHV.js} +3 -3
- package/dist/{chunk-R2N37AA6.js → chunk-XIDQKFZF.js} +3 -3
- package/dist/{chunk-EEZCB74X.js → chunk-XOBHRD2M.js} +575 -651
- package/dist/chunk-XOBHRD2M.js.map +1 -0
- package/dist/chunk-XZHEOSQ2.js +16 -0
- package/dist/chunk-XZHEOSQ2.js.map +1 -0
- package/dist/chunk-Y2NBBT6K.js +18 -0
- package/dist/chunk-Y2NBBT6K.js.map +1 -0
- package/dist/{chunk-OWOT7CFC.js → chunk-Y6B6HVKC.js} +2 -2
- package/dist/{chunk-5ATANZSW.js → chunk-Y6VZQGGH.js} +6 -6
- package/dist/chunk-YMUFFJCO.js +23 -0
- package/dist/chunk-YMUFFJCO.js.map +1 -0
- package/dist/{chunk-YZNKFYDC.js → chunk-ZFUFAHZ3.js} +4 -4
- package/dist/{chunk-PWWWMRT7.js → chunk-ZTLOXFPB.js} +2 -2
- package/dist/chunk-ZTLOXFPB.js.map +1 -0
- package/dist/{walk-E3OVLBFR.js → chunk-ZWBKDKXC.js} +4 -13
- package/dist/{walk-E3OVLBFR.js.map → chunk-ZWBKDKXC.js.map} +1 -1
- package/dist/classified/index.js +5 -5
- package/dist/{classified-marker-52P3D72W.js → classified-marker-EOOSMWFC.js} +3 -3
- package/dist/collection-facade-TTRF5NFJ.js +45 -0
- package/dist/{computed-33DRJDOC.js → computed-AB45PS4V.js} +3 -3
- package/dist/consent/index.js +13 -9
- package/dist/consent/index.js.map +1 -1
- package/dist/{cover-5BSNJREH.js → cover/index.js} +7 -6
- package/dist/crdt/index.js +4 -0
- package/dist/crdt/index.js.map +1 -1
- package/dist/custody/index.js +26 -0
- package/dist/{dead-filter-JXVCKS6W.js → dead-filter-6DYY7S64.js} +2 -2
- package/dist/{chunk-ILM6X6CP.js → delegation-ONOZVPIP.js} +12 -6
- package/dist/{chunk-ILM6X6CP.js.map → delegation-ONOZVPIP.js.map} +1 -1
- package/dist/derivations/index.js +12 -12
- package/dist/derive-IGGVSUPH.js +22 -0
- package/dist/directory/index.js +46 -0
- package/dist/{dispatch-PKQQFDBT.js → dispatch-7T45NN7C.js} +19 -18
- package/dist/{dispatch-PKQQFDBT.js.map → dispatch-7T45NN7C.js.map} +1 -1
- package/dist/{dispatch-SMXUO7W5.js → dispatch-RPTLK7LJ.js} +5 -5
- package/dist/{enclave-I5H3GINP.js → enclave-CLJVSDFG.js} +9 -9
- package/dist/executor-IEBLTBCW.js +9 -0
- package/dist/executor-RM7XWQDU.js +9 -0
- package/dist/executor-XZYM4KHT.js +28 -0
- package/dist/export-accessible-24XCCD3A.js +23 -0
- package/dist/extract-partition-H2TPEEOF.js +38 -0
- package/dist/{fanout-sidecar-WWXVHHUH.js → fanout-sidecar-TV7CXAJ6.js} +9 -9
- package/dist/find-OZMTRIK3.js +11 -0
- package/dist/forget/index.js +11 -11
- package/dist/forget/index.js.map +1 -1
- package/dist/guards/index.js +4 -4
- package/dist/history/index.js +16 -12
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.js +16 -14
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.d.ts +18 -24
- package/dist/index.js +212 -2045
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.js +4 -2
- package/dist/indexing/index.js.map +1 -1
- package/dist/introspection/index.js +28 -0
- package/dist/issue-55HJE7JF.js +19 -0
- package/dist/kernel/collection-config.d.ts +1 -1
- package/dist/kernel/collection.d.ts +2 -2
- package/dist/kernel/errors.d.ts +2 -2
- package/dist/kernel/lazy.d.ts +30 -0
- package/dist/kernel/noydb.d.ts +22 -11
- package/dist/kernel/query/builder.d.ts +10 -10
- package/dist/kernel/query/index.d.ts +0 -6
- package/dist/kernel/query/scan-builder.d.ts +4 -4
- package/dist/kernel/types.d.ts +25 -25
- package/dist/kernel/vault.d.ts +10 -10
- package/dist/{ledger-M2BKCNVS.js → ledger-XY6NP3J2.js} +11 -11
- package/dist/liberate-LXOJ2EMB.js +24 -0
- package/dist/{link-set-ZU4E5J5S.js → link-set-POQB7BU5.js} +11 -11
- package/dist/materialized-views/index.js +22 -19
- package/dist/money/index.js +31 -0
- package/dist/noydb-KXS35UJS.js +82 -0
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.js +15 -13
- package/dist/pod/index.js +11 -12
- package/dist/{policy-IY2YEA3A.js → policy/index.js} +6 -6
- package/dist/port/with/blob-strategy.d.ts +15 -5
- package/dist/port/with/collection-registries.d.ts +62 -0
- package/dist/port/with/strategies.d.ts +11 -11
- package/dist/port/with/team-strategy.d.ts +2 -1
- package/dist/portability/index.js +8 -8
- package/dist/{post-register-XIAHB6KF.js → post-register-URQANI3B.js} +6 -6
- package/dist/query/index.js +6 -46
- package/dist/{aggregate → reduce}/index.js +24 -17
- package/dist/reduce/index.js.map +1 -0
- package/dist/register-5W6CG4RM.js +25 -0
- package/dist/registry-7Y5WF7GS.js +9 -0
- package/dist/{registry-VW6P6A3J.js → registry-BWD2ED52.js} +2 -2
- package/dist/registry-MCSGBZXI.js +18 -0
- package/dist/registry-UJFP3NWF.js +19 -0
- package/dist/request-withdrawal-IWYDM4SO.js +31 -0
- package/dist/{reveal-VWXRV2XU.js → reveal-BXZGWIOP.js} +6 -6
- package/dist/revoke-TINYPHYY.js +24 -0
- package/dist/satellites/index.js +1 -1
- package/dist/schema-update/index.js +26 -0
- package/dist/sealed-record/index.js +12 -12
- package/dist/search/index.js +36 -0
- package/dist/{seed-CDBFOTIK.js → seed-F7YYQTRC.js} +11 -11
- package/dist/sequence/index.js +30 -0
- package/dist/session/index.js +13 -9
- package/dist/session/index.js.map +1 -1
- package/dist/shadow/index.js +6 -2
- package/dist/shadow/index.js.map +1 -1
- package/dist/signer-WMZKCNKC.js +25 -0
- package/dist/snapshots/index.js +14 -10
- package/dist/snapshots/index.js.map +1 -1
- package/dist/{stale-NBROTA66.js → stale-JOE7GBAE.js} +11 -10
- package/dist/storage-B3WBKXKT.js +23 -0
- package/dist/store/index.js +22 -0
- package/dist/{store-coordination-provider-P3IU5JHD.js → store-coordination-provider-5LX2UFU2.js} +3 -3
- package/dist/sync/index.js +14 -10
- package/dist/sync/index.js.map +1 -1
- package/dist/team/index.js +36 -29
- package/dist/tiers/index.js +10 -10
- package/dist/to/index.js +1 -1
- package/dist/{tx → transactions}/index.js +7 -3
- package/dist/transactions/index.js.map +1 -0
- package/dist/util/index.js +1 -1
- package/dist/{verify-5X37OXRY.js → verify-7XWHJWUN.js} +5 -5
- package/dist/via/blob/active.d.ts +7 -18
- package/dist/via/blob/binding.d.ts +1 -1
- package/dist/via/blob/index.d.ts +4 -2
- package/dist/via/i18n/core.d.ts +1 -1
- package/dist/via/i18n/index.d.ts +2 -0
- package/dist/via/money/money-reducer.d.ts +2 -2
- package/dist/walk-6OHAWGBS.js +21 -0
- package/dist/with-audit/consent/index.d.ts +2 -0
- package/dist/with-audit/forget/active.d.ts +3 -3
- package/dist/with-audit/forget/index.d.ts +4 -4
- package/dist/with-audit/forget/strategy.d.ts +3 -3
- package/dist/with-audit/guards/executor.d.ts +4 -4
- package/dist/with-audit/guards/immutable-guard.d.ts +3 -3
- package/dist/with-audit/guards/index.d.ts +1 -1
- package/dist/with-audit/guards/registry.d.ts +3 -3
- package/dist/with-audit/guards/transition-guard.d.ts +3 -3
- package/dist/with-audit/guards/types.d.ts +10 -10
- package/dist/with-audit/guards/with-guard.d.ts +2 -2
- package/dist/with-audit/periods/index.d.ts +2 -0
- package/dist/with-cargo/floor.d.ts +3 -3
- package/dist/with-commit/crdt/index.d.ts +2 -0
- package/dist/with-commit/history/index.d.ts +2 -0
- package/dist/with-commit/numbering/descriptor.d.ts +16 -6
- package/dist/with-commit/tx/active.d.ts +5 -5
- package/dist/with-commit/tx/index.d.ts +2 -2
- package/dist/with-commit/tx/invariants.d.ts +1 -1
- package/dist/with-commit/tx/strategy.d.ts +3 -3
- package/dist/with-fork/shadow/index.d.ts +2 -0
- package/dist/with-fork/snapshots/active.d.ts +2 -2
- package/dist/with-fork/snapshots/index.d.ts +3 -1
- package/dist/with-fork/snapshots/noydb-facade.d.ts +3 -3
- package/dist/with-fork/snapshots/strategy.d.ts +2 -2
- package/dist/with-formula/derivations/executor.d.ts +2 -2
- package/dist/with-formula/derivations/index.d.ts +2 -1
- package/dist/with-formula/derivations/registry.d.ts +3 -3
- package/dist/with-formula/derivations/stale.d.ts +2 -2
- package/dist/with-formula/derivations/types.d.ts +3 -3
- package/dist/with-formula/derivations/with-derivation.d.ts +2 -2
- package/dist/with-formula/derivations/with-rollup.d.ts +15 -8
- package/dist/with-formula/materialized-views/dependency-analyzer.d.ts +3 -3
- package/dist/with-formula/materialized-views/index.d.ts +1 -1
- package/dist/with-formula/materialized-views/registry.d.ts +4 -4
- package/dist/with-formula/materialized-views/types.d.ts +11 -11
- package/dist/with-formula/materialized-views/with-materialized-view.d.ts +2 -2
- package/dist/with-formula/overlay-views/index.d.ts +1 -1
- package/dist/with-formula/overlay-views/registry.d.ts +4 -4
- package/dist/with-formula/overlay-views/types.d.ts +3 -3
- package/dist/with-formula/overlay-views/virtual-collection.d.ts +2 -2
- package/dist/with-formula/overlay-views/with-overlayed-view.d.ts +2 -2
- package/dist/with-lookup/indexing/active.d.ts +5 -5
- package/dist/with-lookup/indexing/index.d.ts +3 -1
- package/dist/with-lookup/indexing/strategy.d.ts +3 -3
- package/dist/with-lookup/{aggregate → reduce}/active.d.ts +8 -8
- package/dist/with-lookup/{aggregate → reduce}/groupby.d.ts +21 -21
- package/dist/with-lookup/reduce/index.d.ts +34 -0
- package/dist/with-lookup/{aggregate → reduce}/reducers.d.ts +10 -10
- package/dist/with-lookup/{aggregate/aggregation.d.ts → reduce/reduction.d.ts} +21 -21
- package/dist/with-lookup/{aggregate → reduce}/strategy.d.ts +15 -15
- package/dist/with-lookup/search/build-docs.d.ts +7 -2
- package/dist/with-lookup/search/index.d.ts +1 -1
- package/dist/with-party/directory/cover/index.d.ts +1 -0
- package/dist/with-party/directory/index.d.ts +2 -0
- package/dist/with-party/session/index.d.ts +2 -0
- package/dist/with-party/sync/index.d.ts +2 -0
- package/dist/with-party/team/index.d.ts +2 -0
- package/dist/with-party/team/keyring.d.ts +68 -20
- package/dist/with-party/team/noydb-facade.d.ts +2 -1
- package/dist/with-shape/blobs/blob-set.d.ts +1 -1
- package/dist/with-shape/introspection/index.d.ts +6 -0
- package/dist/with-shape/schema-update/index.d.ts +1 -0
- package/dist/with-store/index.d.ts +3 -2
- package/dist/withdraw-accessible-DEUCOO4F.js +28 -0
- package/dist/withdraw-accessible-DEUCOO4F.js.map +1 -0
- package/package.json +42 -30
- package/dist/aggregate/index.js.map +0 -1
- package/dist/api-22ES33N3.js +0 -20
- package/dist/as/index.js +0 -30
- package/dist/at/index.js +0 -1
- package/dist/by/index.js +0 -11
- package/dist/chunk-2PDBYF2O.js.map +0 -1
- package/dist/chunk-2YELV6TV.js +0 -1
- package/dist/chunk-3LJYHE7S.js.map +0 -1
- package/dist/chunk-6SYOHUHM.js.map +0 -1
- package/dist/chunk-7ICV4ASA.js.map +0 -1
- package/dist/chunk-AFAPBZAA.js.map +0 -1
- package/dist/chunk-BY5YBSXA.js.map +0 -1
- package/dist/chunk-CJVEAVYH.js +0 -205
- package/dist/chunk-CJVEAVYH.js.map +0 -1
- package/dist/chunk-CKUYQO2P.js.map +0 -1
- package/dist/chunk-D4LAJ737.js.map +0 -1
- package/dist/chunk-DYZ5CCVM.js.map +0 -1
- package/dist/chunk-EEZCB74X.js.map +0 -1
- package/dist/chunk-EGU4PWQW.js.map +0 -1
- package/dist/chunk-FIP7KI2P.js.map +0 -1
- package/dist/chunk-GHEMAVTN.js.map +0 -1
- package/dist/chunk-HAPY7WFP.js.map +0 -1
- package/dist/chunk-HQKRZIX6.js.map +0 -1
- package/dist/chunk-I2XEBEEF.js.map +0 -1
- package/dist/chunk-IGLU4ME7.js +0 -255
- package/dist/chunk-IGLU4ME7.js.map +0 -1
- package/dist/chunk-J5TEU77Q.js.map +0 -1
- package/dist/chunk-KJTIAECI.js.map +0 -1
- package/dist/chunk-L2CAWCQL.js.map +0 -1
- package/dist/chunk-LB7FD65R.js +0 -163
- package/dist/chunk-LB7FD65R.js.map +0 -1
- package/dist/chunk-PWWWMRT7.js.map +0 -1
- package/dist/chunk-QMMOMBH6.js.map +0 -1
- package/dist/chunk-SEU6QHH4.js.map +0 -1
- package/dist/chunk-TNATF3HB.js.map +0 -1
- package/dist/chunk-TSNRZSRC.js.map +0 -1
- package/dist/chunk-V5JUAW4M.js.map +0 -1
- package/dist/chunk-VEGQYPCP.js.map +0 -1
- package/dist/chunk-X3MWEFPT.js.map +0 -1
- package/dist/chunk-XUFAFHQM.js.map +0 -1
- package/dist/chunk-YJ3TREAC.js.map +0 -1
- package/dist/chunk-YMUGBZN2.js.map +0 -1
- package/dist/chunk-YTW6ZIPD.js.map +0 -1
- package/dist/chunk-YXZNGHID.js.map +0 -1
- package/dist/chunk-YZN6NRTR.js.map +0 -1
- package/dist/chunk-ZX5FNWH7.js.map +0 -1
- package/dist/collection-facade-XKCFHSLR.js +0 -44
- package/dist/delegation-WWZPEDK2.js +0 -25
- package/dist/derive-CWWFEQ4N.js +0 -21
- package/dist/executor-FCJEH3FU.js +0 -9
- package/dist/executor-OHET7TF7.js +0 -9
- package/dist/executor-SHYLIBOM.js +0 -26
- package/dist/export-accessible-FBL6TDSK.js +0 -23
- package/dist/extract-partition-ZYIRYMTI.js +0 -38
- package/dist/find-LEBUXVL7.js +0 -11
- package/dist/in/index.js +0 -1
- package/dist/issue-F76PAGIR.js +0 -19
- package/dist/liberate-ZP7UB57K.js +0 -24
- package/dist/noydb-BNDGEK5X.js +0 -71
- package/dist/on/index.js +0 -17
- package/dist/port/as/index.d.ts +0 -26
- package/dist/port/at/index.d.ts +0 -18
- package/dist/port/in/index.d.ts +0 -21
- package/dist/port/on/index.d.ts +0 -27
- package/dist/port/ui/index.d.ts +0 -21
- package/dist/port/with/index.d.ts +0 -18
- package/dist/register-HB4TQZSV.js +0 -23
- package/dist/registry-6BMBFEXT.js +0 -19
- package/dist/registry-7CZWF3EQ.js +0 -9
- package/dist/registry-JOFO7RAH.js +0 -18
- package/dist/request-withdrawal-UN3TRHWI.js +0 -31
- package/dist/revoke-OLO2OVR7.js +0 -24
- package/dist/signer-MGGKPYAN.js +0 -25
- package/dist/storage-3R37QZLA.js +0 -23
- package/dist/tx/index.js.map +0 -1
- package/dist/ui/index.js +0 -1
- package/dist/with/index.js +0 -31
- package/dist/with-lookup/aggregate/index.d.ts +0 -23
- package/dist/withdraw-accessible-Q2EMATCU.js +0 -28
- /package/dist/{api-22ES33N3.js.map → api-3NKVNZPM.js.map} +0 -0
- /package/dist/{backup-YCHAC575.js.map → backup-PTSVXDDW.js.map} +0 -0
- /package/dist/{chunk-5WLDZZ6S.js.map → chunk-35NMVJ7I.js.map} +0 -0
- /package/dist/{chunk-TNJVDG2B.js.map → chunk-3XKESW5E.js.map} +0 -0
- /package/dist/{chunk-MUPZ6IZX.js.map → chunk-4JQK3L4V.js.map} +0 -0
- /package/dist/{chunk-Z4D3YGAB.js.map → chunk-4ZKNC6A6.js.map} +0 -0
- /package/dist/{chunk-ONB627HU.js.map → chunk-55LX6FSX.js.map} +0 -0
- /package/dist/{chunk-QMZ5A6FQ.js.map → chunk-56IIVAEO.js.map} +0 -0
- /package/dist/{chunk-FHHYCBBP.js.map → chunk-5DRROK5W.js.map} +0 -0
- /package/dist/{chunk-GTNRMJYH.js.map → chunk-5GQEU4DN.js.map} +0 -0
- /package/dist/{chunk-YQQPNXNO.js.map → chunk-6L3DKWRY.js.map} +0 -0
- /package/dist/{chunk-M6QA2KK2.js.map → chunk-6M2I3DKF.js.map} +0 -0
- /package/dist/{chunk-FTCGEUOY.js.map → chunk-6R6Q3MRN.js.map} +0 -0
- /package/dist/{chunk-ZUDAK436.js.map → chunk-6UBZ6I5Z.js.map} +0 -0
- /package/dist/{chunk-7U6AB7DZ.js.map → chunk-AKKRXT23.js.map} +0 -0
- /package/dist/{chunk-OWLZONGX.js.map → chunk-BLBAQRBM.js.map} +0 -0
- /package/dist/{chunk-QBOQI6PG.js.map → chunk-BN4HRYAZ.js.map} +0 -0
- /package/dist/{chunk-QVGEVZ5F.js.map → chunk-BQ6DHN4I.js.map} +0 -0
- /package/dist/{chunk-GP54LU5N.js.map → chunk-BYFXLDVV.js.map} +0 -0
- /package/dist/{chunk-QRHEAZ2Q.js.map → chunk-CUZLNGD5.js.map} +0 -0
- /package/dist/{chunk-YXIVUWDU.js.map → chunk-DGBIHDVK.js.map} +0 -0
- /package/dist/{chunk-QZJEYZ4I.js.map → chunk-DZAF24PG.js.map} +0 -0
- /package/dist/{chunk-TMR64XPL.js.map → chunk-EZNTORXE.js.map} +0 -0
- /package/dist/{chunk-JAMDDLIM.js.map → chunk-FMNU7IUG.js.map} +0 -0
- /package/dist/{as/index.js.map → chunk-GJW4HX2O.js.map} +0 -0
- /package/dist/{chunk-7BIJS6II.js.map → chunk-GP3UWP22.js.map} +0 -0
- /package/dist/{chunk-JIHH26WQ.js.map → chunk-HFUAFR6B.js.map} +0 -0
- /package/dist/{chunk-RWW3I3PO.js.map → chunk-ISRKW4YN.js.map} +0 -0
- /package/dist/{chunk-P4NUJVWL.js.map → chunk-IXNCZTWA.js.map} +0 -0
- /package/dist/{chunk-YFFEF6HR.js.map → chunk-JIDLU57U.js.map} +0 -0
- /package/dist/{chunk-PTT2NB46.js.map → chunk-JYJFGD2H.js.map} +0 -0
- /package/dist/{chunk-3OWYOECI.js.map → chunk-JYV4JJV2.js.map} +0 -0
- /package/dist/{chunk-OGVE4IJW.js.map → chunk-KBPU5M2E.js.map} +0 -0
- /package/dist/{chunk-UTEATWHO.js.map → chunk-KW42W6SV.js.map} +0 -0
- /package/dist/{chunk-UVBHM7AK.js.map → chunk-MANUDU7I.js.map} +0 -0
- /package/dist/{chunk-K7BADAOS.js.map → chunk-NEX7JCMO.js.map} +0 -0
- /package/dist/{chunk-5HSSTOOG.js.map → chunk-NFZGZ4AK.js.map} +0 -0
- /package/dist/{chunk-S4YERMGJ.js.map → chunk-OBPVWOSQ.js.map} +0 -0
- /package/dist/{chunk-OHV6T5JB.js.map → chunk-OLIWCCFM.js.map} +0 -0
- /package/dist/{chunk-2CRZOVDI.js.map → chunk-OLSARBZU.js.map} +0 -0
- /package/dist/{chunk-7LP7FFJ2.js.map → chunk-QD46HPZG.js.map} +0 -0
- /package/dist/{chunk-G4MFQXYX.js.map → chunk-QJNCB5SK.js.map} +0 -0
- /package/dist/{chunk-GEEJPID6.js.map → chunk-RZPOZPZU.js.map} +0 -0
- /package/dist/{chunk-YCAJDK4O.js.map → chunk-SGQ4N5FS.js.map} +0 -0
- /package/dist/{chunk-522MBPE5.js.map → chunk-SHEEBRZ4.js.map} +0 -0
- /package/dist/{chunk-YWZEDG7F.js.map → chunk-SHYIA73K.js.map} +0 -0
- /package/dist/{chunk-K4BWPNJO.js.map → chunk-TE6LH6J2.js.map} +0 -0
- /package/dist/{chunk-SBOHNJUD.js.map → chunk-V6UQZA2H.js.map} +0 -0
- /package/dist/{chunk-SC4JLMZI.js.map → chunk-VH6SOCXH.js.map} +0 -0
- /package/dist/{chunk-WQN5SM7P.js.map → chunk-W3G5REIU.js.map} +0 -0
- /package/dist/{chunk-OMSKGLYA.js.map → chunk-WO4T4OD5.js.map} +0 -0
- /package/dist/{chunk-6RPWILRH.js.map → chunk-WZ3NCMK6.js.map} +0 -0
- /package/dist/{chunk-BAAUFCJK.js.map → chunk-X4MVWPHV.js.map} +0 -0
- /package/dist/{chunk-R2N37AA6.js.map → chunk-XIDQKFZF.js.map} +0 -0
- /package/dist/{chunk-OWOT7CFC.js.map → chunk-Y6B6HVKC.js.map} +0 -0
- /package/dist/{chunk-5ATANZSW.js.map → chunk-Y6VZQGGH.js.map} +0 -0
- /package/dist/{chunk-YZNKFYDC.js.map → chunk-ZFUFAHZ3.js.map} +0 -0
- /package/dist/{classified-marker-52P3D72W.js.map → classified-marker-EOOSMWFC.js.map} +0 -0
- /package/dist/{at/index.js.map → collection-facade-TTRF5NFJ.js.map} +0 -0
- /package/dist/{by/index.js.map → computed-AB45PS4V.js.map} +0 -0
- /package/dist/{in → cover}/index.js.map +0 -0
- /package/dist/{on → custody}/index.js.map +0 -0
- /package/dist/{dead-filter-JXVCKS6W.js.map → dead-filter-6DYY7S64.js.map} +0 -0
- /package/dist/{chunk-2YELV6TV.js.map → derive-IGGVSUPH.js.map} +0 -0
- /package/dist/{ui → directory}/index.js.map +0 -0
- /package/dist/{dispatch-SMXUO7W5.js.map → dispatch-RPTLK7LJ.js.map} +0 -0
- /package/dist/{collection-facade-XKCFHSLR.js.map → enclave-CLJVSDFG.js.map} +0 -0
- /package/dist/{computed-33DRJDOC.js.map → executor-IEBLTBCW.js.map} +0 -0
- /package/dist/{cover-5BSNJREH.js.map → executor-RM7XWQDU.js.map} +0 -0
- /package/dist/{delegation-WWZPEDK2.js.map → executor-XZYM4KHT.js.map} +0 -0
- /package/dist/{derive-CWWFEQ4N.js.map → export-accessible-24XCCD3A.js.map} +0 -0
- /package/dist/{enclave-I5H3GINP.js.map → extract-partition-H2TPEEOF.js.map} +0 -0
- /package/dist/{fanout-sidecar-WWXVHHUH.js.map → fanout-sidecar-TV7CXAJ6.js.map} +0 -0
- /package/dist/{executor-FCJEH3FU.js.map → find-OZMTRIK3.js.map} +0 -0
- /package/dist/{with → introspection}/index.js.map +0 -0
- /package/dist/{executor-OHET7TF7.js.map → issue-55HJE7JF.js.map} +0 -0
- /package/dist/{executor-SHYLIBOM.js.map → ledger-XY6NP3J2.js.map} +0 -0
- /package/dist/{export-accessible-FBL6TDSK.js.map → liberate-LXOJ2EMB.js.map} +0 -0
- /package/dist/{extract-partition-ZYIRYMTI.js.map → link-set-POQB7BU5.js.map} +0 -0
- /package/dist/{find-LEBUXVL7.js.map → money/index.js.map} +0 -0
- /package/dist/{issue-F76PAGIR.js.map → noydb-KXS35UJS.js.map} +0 -0
- /package/dist/{ledger-M2BKCNVS.js.map → policy/index.js.map} +0 -0
- /package/dist/{post-register-XIAHB6KF.js.map → post-register-URQANI3B.js.map} +0 -0
- /package/dist/{liberate-ZP7UB57K.js.map → register-5W6CG4RM.js.map} +0 -0
- /package/dist/{link-set-ZU4E5J5S.js.map → registry-7Y5WF7GS.js.map} +0 -0
- /package/dist/{noydb-BNDGEK5X.js.map → registry-BWD2ED52.js.map} +0 -0
- /package/dist/{policy-IY2YEA3A.js.map → registry-MCSGBZXI.js.map} +0 -0
- /package/dist/{register-HB4TQZSV.js.map → registry-UJFP3NWF.js.map} +0 -0
- /package/dist/{registry-6BMBFEXT.js.map → request-withdrawal-IWYDM4SO.js.map} +0 -0
- /package/dist/{reveal-VWXRV2XU.js.map → reveal-BXZGWIOP.js.map} +0 -0
- /package/dist/{registry-7CZWF3EQ.js.map → revoke-TINYPHYY.js.map} +0 -0
- /package/dist/{registry-JOFO7RAH.js.map → schema-update/index.js.map} +0 -0
- /package/dist/{registry-VW6P6A3J.js.map → search/index.js.map} +0 -0
- /package/dist/{seed-CDBFOTIK.js.map → seed-F7YYQTRC.js.map} +0 -0
- /package/dist/{request-withdrawal-UN3TRHWI.js.map → sequence/index.js.map} +0 -0
- /package/dist/{revoke-OLO2OVR7.js.map → signer-WMZKCNKC.js.map} +0 -0
- /package/dist/{signer-MGGKPYAN.js.map → stale-JOE7GBAE.js.map} +0 -0
- /package/dist/{stale-NBROTA66.js.map → storage-B3WBKXKT.js.map} +0 -0
- /package/dist/{storage-3R37QZLA.js.map → store/index.js.map} +0 -0
- /package/dist/{store-coordination-provider-P3IU5JHD.js.map → store-coordination-provider-5LX2UFU2.js.map} +0 -0
- /package/dist/{verify-5X37OXRY.js.map → verify-7XWHJWUN.js.map} +0 -0
- /package/dist/{withdraw-accessible-Q2EMATCU.js.map → walk-6OHAWGBS.js.map} +0 -0
- /package/dist/with-lookup/{aggregate → reduce}/canonical-key.d.ts +0 -0
package/dist/chunk-LB7FD65R.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
diff
|
|
3
|
-
} from "./chunk-SVLR4POP.js";
|
|
4
|
-
|
|
5
|
-
// src/with-cargo/vault-diff.ts
|
|
6
|
-
async function diffVault(vault, candidate, options = {}) {
|
|
7
|
-
const idKey = options.idKey ?? "id";
|
|
8
|
-
const filter = options.collections ? new Set(options.collections) : null;
|
|
9
|
-
const compareFn = options.compareFn ?? ((a, b) => diff(a, b).length === 0);
|
|
10
|
-
const live = /* @__PURE__ */ new Map();
|
|
11
|
-
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
12
|
-
if (filter && !filter.has(chunk.collection)) continue;
|
|
13
|
-
const collection = live.get(chunk.collection) ?? /* @__PURE__ */ new Map();
|
|
14
|
-
for (const record of chunk.records) {
|
|
15
|
-
const id = readIdField(record, idKey);
|
|
16
|
-
if (!id) continue;
|
|
17
|
-
collection.set(id, record);
|
|
18
|
-
}
|
|
19
|
-
live.set(chunk.collection, collection);
|
|
20
|
-
}
|
|
21
|
-
const cand = await normaliseCandidate(candidate, idKey, filter);
|
|
22
|
-
const added = [];
|
|
23
|
-
const modified = [];
|
|
24
|
-
const deleted = [];
|
|
25
|
-
const unchanged = options.includeUnchanged ? [] : void 0;
|
|
26
|
-
const collectionNames = /* @__PURE__ */ new Set([...live.keys(), ...cand.keys()]);
|
|
27
|
-
for (const collection of [...collectionNames].sort()) {
|
|
28
|
-
const liveColl = live.get(collection) ?? /* @__PURE__ */ new Map();
|
|
29
|
-
const candColl = cand.get(collection) ?? /* @__PURE__ */ new Map();
|
|
30
|
-
const allIds = /* @__PURE__ */ new Set([...liveColl.keys(), ...candColl.keys()]);
|
|
31
|
-
for (const id of [...allIds].sort()) {
|
|
32
|
-
const before = liveColl.get(id);
|
|
33
|
-
const after = candColl.get(id);
|
|
34
|
-
if (before === void 0 && after !== void 0) {
|
|
35
|
-
added.push({ collection, id, record: after });
|
|
36
|
-
} else if (before !== void 0 && after === void 0) {
|
|
37
|
-
const metadata = options.includeMetadata ? await vault.collection(collection).getMetadata(id) ?? void 0 : void 0;
|
|
38
|
-
deleted.push({ collection, id, record: before, ...metadata !== void 0 ? { metadata } : {} });
|
|
39
|
-
} else if (before !== void 0 && after !== void 0) {
|
|
40
|
-
if (compareFn(before, after)) {
|
|
41
|
-
unchanged?.push({ collection, id, record: after });
|
|
42
|
-
} else {
|
|
43
|
-
const metadata = options.includeMetadata ? await vault.collection(collection).getMetadata(id) ?? void 0 : void 0;
|
|
44
|
-
const fieldDiffs = diff(before, after);
|
|
45
|
-
const fieldsChanged = uniqueTopLevelKeys(fieldDiffs);
|
|
46
|
-
modified.push({
|
|
47
|
-
collection,
|
|
48
|
-
id,
|
|
49
|
-
record: after,
|
|
50
|
-
before,
|
|
51
|
-
fieldsChanged,
|
|
52
|
-
fieldDiffs,
|
|
53
|
-
...metadata !== void 0 ? { metadata } : {}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const summary = {
|
|
60
|
-
add: added.length,
|
|
61
|
-
modify: modified.length,
|
|
62
|
-
delete: deleted.length,
|
|
63
|
-
total: added.length + modified.length + deleted.length
|
|
64
|
-
};
|
|
65
|
-
return {
|
|
66
|
-
added,
|
|
67
|
-
modified,
|
|
68
|
-
deleted,
|
|
69
|
-
unchanged,
|
|
70
|
-
summary,
|
|
71
|
-
format(opts) {
|
|
72
|
-
return formatDiff(opts?.detail ?? "full", { added, modified, deleted, summary });
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
async function normaliseCandidate(candidate, idKey, filter) {
|
|
77
|
-
const out = /* @__PURE__ */ new Map();
|
|
78
|
-
if (typeof candidate === "object" && candidate !== null && "exportStream" in candidate && typeof candidate.exportStream === "function") {
|
|
79
|
-
for await (const chunk of candidate.exportStream({ granularity: "collection" })) {
|
|
80
|
-
if (filter && !filter.has(chunk.collection)) continue;
|
|
81
|
-
const collection = out.get(chunk.collection) ?? /* @__PURE__ */ new Map();
|
|
82
|
-
for (const record of chunk.records) {
|
|
83
|
-
const id = readIdField(record, idKey);
|
|
84
|
-
if (!id) continue;
|
|
85
|
-
collection.set(id, record);
|
|
86
|
-
}
|
|
87
|
-
out.set(chunk.collection, collection);
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
if (typeof candidate === "string") {
|
|
92
|
-
let parsed;
|
|
93
|
-
try {
|
|
94
|
-
parsed = JSON.parse(candidate);
|
|
95
|
-
} catch (err) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`diffVault: candidate string is not valid JSON (${err.message})`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return collectionsFromObject(parsed, idKey, filter);
|
|
101
|
-
}
|
|
102
|
-
return collectionsFromObject(candidate, idKey, filter);
|
|
103
|
-
}
|
|
104
|
-
function collectionsFromObject(raw, idKey, filter) {
|
|
105
|
-
const out = /* @__PURE__ */ new Map();
|
|
106
|
-
if (raw === null || typeof raw !== "object") {
|
|
107
|
-
throw new Error("diffVault: candidate must be a Vault, an object, or a JSON string");
|
|
108
|
-
}
|
|
109
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
110
|
-
if (key.startsWith("_")) continue;
|
|
111
|
-
if (filter && !filter.has(key)) continue;
|
|
112
|
-
if (!Array.isArray(value)) continue;
|
|
113
|
-
const collection = /* @__PURE__ */ new Map();
|
|
114
|
-
for (const record of value) {
|
|
115
|
-
if (record === null || typeof record !== "object") continue;
|
|
116
|
-
const id = readIdField(record, idKey);
|
|
117
|
-
if (!id) continue;
|
|
118
|
-
collection.set(id, record);
|
|
119
|
-
}
|
|
120
|
-
out.set(key, collection);
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
function uniqueTopLevelKeys(diffs) {
|
|
125
|
-
const keys = /* @__PURE__ */ new Set();
|
|
126
|
-
for (const d of diffs) {
|
|
127
|
-
const m = /^[^.[]+/.exec(d.path);
|
|
128
|
-
if (m) keys.add(m[0]);
|
|
129
|
-
}
|
|
130
|
-
return [...keys];
|
|
131
|
-
}
|
|
132
|
-
function readIdField(record, idKey) {
|
|
133
|
-
if (record === null || typeof record !== "object") return "";
|
|
134
|
-
const v = record[idKey];
|
|
135
|
-
if (typeof v === "string") return v;
|
|
136
|
-
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
137
|
-
return "";
|
|
138
|
-
}
|
|
139
|
-
function formatDiff(detail, b) {
|
|
140
|
-
const head = `${b.summary.add} added \xB7 ${b.summary.modify} modified \xB7 ${b.summary.delete} deleted`;
|
|
141
|
-
if (detail === "count") return head;
|
|
142
|
-
if (b.summary.total === 0) return head + "\n(no changes)";
|
|
143
|
-
if (detail === "one-line") return head;
|
|
144
|
-
const rows = [head, ""];
|
|
145
|
-
for (const e of b.added) rows.push(`${e.collection}/${e.id} added`);
|
|
146
|
-
for (const e of b.modified) {
|
|
147
|
-
const fields = e.fieldDiffs.map((f) => `${f.path}: ${shortJSON(f.from)} \u2192 ${shortJSON(f.to)}`).join(", ");
|
|
148
|
-
rows.push(`${e.collection}/${e.id} modified ${fields}`);
|
|
149
|
-
}
|
|
150
|
-
for (const e of b.deleted) rows.push(`${e.collection}/${e.id} deleted`);
|
|
151
|
-
return rows.join("\n");
|
|
152
|
-
}
|
|
153
|
-
function shortJSON(value) {
|
|
154
|
-
if (value === void 0) return "undefined";
|
|
155
|
-
const s = JSON.stringify(value);
|
|
156
|
-
if (typeof s !== "string") return "<unrepresentable>";
|
|
157
|
-
return s.length > 60 ? s.slice(0, 57) + "..." : s;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export {
|
|
161
|
-
diffVault
|
|
162
|
-
};
|
|
163
|
-
//# sourceMappingURL=chunk-LB7FD65R.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-cargo/vault-diff.ts"],"sourcesContent":["/**\n * Vault-level diff orchestrator.\n *\n * Compares a live `Vault`'s plaintext state against a candidate state\n * (another vault, a plain `{ collection: records[] }` map, or a vault\n * dump JSON) and returns a structured `VaultDiff` plan listing the\n * records that would be added, modified, or deleted to bring the live\n * vault into the candidate's shape.\n *\n * Builds on two existing record-level helpers:\n *\n * 1. `diff(a, b)` from `./history/diff.ts` — emits dot-pathed\n * `DiffEntry[]` with `type: 'added' | 'removed' | 'changed'` for\n * each changed field of two records. Used here for the\n * `fieldDiffs` of every `modified` entry, and (with empty result)\n * as the default deep-equal check.\n *\n * 2. `Vault.exportStream()` from `./vault.ts` — the canonical\n * decrypt-and-stream-records iterator. Used to walk both sides\n * when the candidate is itself a `Vault`. ACL-scoped: collections\n * the caller can't read silently drop out, the same way every\n * other plaintext-emitting export pipeline filters them.\n *\n * The new orchestration is the **vault-level** enumeration: bucket\n * each record id into added (only in candidate), deleted (only in\n * vault), or modified (in both with field changes); leave the\n * field-level granularity to the existing `diff()`.\n *\n * Use cases:\n *\n * - Import preview (`@noy-db/as-*` `fromString` returns a plan\n * whose body is a `VaultDiff`).\n * - Backup verification (\"does this `.noydb` bundle from yesterday\n * match the current vault?\").\n * - Two-vault reconciliation (\"what's different between Office A\n * and Office B before we sync?\").\n * - Test assertions (golden-file testing with one-liner\n * `expect(plan.summary).toEqual(...)`).\n *\n * @module\n */\n\nimport type { Vault } from '../kernel/vault.js'\nimport { diff as fieldDiff, type DiffEntry as FieldDiffEntry } from '../with-commit/history/diff.js'\n\n// ─── Public types ──────────────────────────────────────────────────────\n\n/** Per-record entry shape — added and deleted records carry only the record value. */\nexport interface VaultDiffEntry<T = unknown> {\n readonly collection: string\n readonly id: string\n readonly record: T\n /**\n * Unencrypted envelope metadata for the RECEIVER-side record.\n * Populated only when `diffVault` is called with `{includeMetadata: true}`.\n * Default: `undefined` (zero extra reads, no behavior change).\n *\n * Populated for `modified` (the CURRENT receiver state, before the candidate\n * is applied) and `deleted` (the receiver envelope of the record being\n * removed). `added` entries have no receiver envelope at diff time, so their\n * `metadata` is always absent.\n */\n readonly metadata?: {\n readonly version: number\n readonly timestamp: string\n readonly by?: string\n readonly source?: string\n readonly sourceTs?: string\n }\n}\n\n/** Modified records carry both halves of the diff plus the field-level breakdown. */\nexport interface VaultDiffModifiedEntry<T = unknown> extends VaultDiffEntry<T> {\n /** The record as it stands in the live vault. */\n readonly before: T\n /** Top-level keys whose values differ between `before` and `record`. */\n readonly fieldsChanged: readonly string[]\n /**\n * Field-level diff entries from `diff(before, record)`. Reuses the\n * existing per-record diff helper so consumers can render git-style\n * `path: from → to` rows without re-walking the records.\n */\n readonly fieldDiffs: readonly FieldDiffEntry[]\n}\n\nexport interface VaultDiff<T = unknown> {\n readonly added: readonly VaultDiffEntry<T>[]\n readonly modified: readonly VaultDiffModifiedEntry<T>[]\n readonly deleted: readonly VaultDiffEntry<T>[]\n /** Only populated when `options.includeUnchanged: true`. */\n readonly unchanged: readonly VaultDiffEntry<T>[] | undefined\n readonly summary: {\n readonly add: number\n readonly modify: number\n readonly delete: number\n readonly total: number\n }\n /**\n * Format the diff as a human-readable string.\n *\n * - `'count'` — one line, just the numbers (`12 added · 3 modified · 0 deleted`)\n * - `'one-line'` — count plus a single overview line\n * - `'full'` — count + one row per added/modified/deleted record (default)\n */\n format(opts?: { detail?: 'count' | 'one-line' | 'full' }): string\n}\n\nexport interface DiffOptions {\n /** Restrict the diff to a subset of collections. */\n readonly collections?: readonly string[]\n /** Field on each record that carries its id. Defaults to `'id'`. */\n readonly idKey?: string\n /** Override the default deep-equal check for \"modified vs unchanged\". */\n readonly compareFn?: (a: unknown, b: unknown) => boolean\n /** If true, include unchanged records in the diff (off by default to save memory). */\n readonly includeUnchanged?: boolean\n /**\n * When `true`, each entry in `added`, `modified`, and `deleted` will carry a\n * `metadata` field with the RECEIVER-side envelope metadata\n * (`version`, `timestamp`, `by?`, `source?`, `sourceTs?`).\n *\n * Off by default — no extra adapter reads, no behavior change for existing callers.\n */\n readonly includeMetadata?: boolean\n}\n\n/**\n * Candidate state to diff the vault against:\n *\n * - A `Vault` instance — both sides are walked via `exportStream()`.\n * - A `Record<collection, records[]>` map — same shape `as-json.toObject()`\n * produces. Useful for diffing parsed file content against the live vault.\n * - A `VaultDump` (output of `vault.dump()`) — a JSON string carrying the\n * full vault state. Parsed and reduced to the map shape above.\n */\nexport type DiffCandidate<T = unknown> =\n | Vault\n | Record<string, readonly T[]>\n | string\n\n// ─── Implementation ────────────────────────────────────────────────────\n\n/**\n * Compute the diff between a live vault and a candidate state.\n *\n * Ungated shared import/merge infrastructure: the `as-*` exporters call this\n * internally from their `fromString`/import merge logic, so it is always\n * available (not behind `withCargo()`). Only `extractPartition` is a gated\n * cargo capability.\n *\n * Returns a fully buffered `VaultDiff` — no streaming. Memory cost is\n * O(n + m) in the row count of vault + candidate. For documented\n * 1K-50K-record vaults this is fine; a streaming variant lands as a\n * follow-up if a > 100K-record consumer arrives.\n */\nexport async function diffVault<T = unknown>(\n vault: Vault,\n candidate: DiffCandidate<T>,\n options: DiffOptions = {},\n): Promise<VaultDiff<T>> {\n const idKey = options.idKey ?? 'id'\n const filter = options.collections ? new Set(options.collections) : null\n const compareFn =\n options.compareFn ?? ((a: unknown, b: unknown) => fieldDiff(a, b).length === 0)\n\n // Side A — walk the live vault via exportStream(). Each chunk arrives\n // already decrypted and ACL-scoped, so collections the caller can't\n // read silently drop out. exportStream's records are typed `unknown[]`\n // — diffVault is the type-erasure boundary; the caller asserts the\n // record shape via the function's `<T>` generic.\n const live = new Map<string, Map<string, T>>()\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (filter && !filter.has(chunk.collection)) continue\n const collection = live.get(chunk.collection) ?? new Map<string, T>()\n for (const record of chunk.records) {\n const id = readIdField(record, idKey)\n if (!id) continue\n collection.set(id, record as T)\n }\n live.set(chunk.collection, collection)\n }\n\n // Side B — normalise the candidate into the same shape.\n const cand = await normaliseCandidate<T>(candidate, idKey, filter)\n\n // Walk every (collection, id) on either side and bucket.\n const added: VaultDiffEntry<T>[] = []\n const modified: VaultDiffModifiedEntry<T>[] = []\n const deleted: VaultDiffEntry<T>[] = []\n const unchanged: VaultDiffEntry<T>[] | undefined = options.includeUnchanged ? [] : undefined\n\n const collectionNames = new Set([...live.keys(), ...cand.keys()])\n for (const collection of [...collectionNames].sort()) {\n const liveColl = live.get(collection) ?? new Map<string, T>()\n const candColl = cand.get(collection) ?? new Map<string, T>()\n const allIds = new Set([...liveColl.keys(), ...candColl.keys()])\n\n for (const id of [...allIds].sort()) {\n const before = liveColl.get(id)\n const after = candColl.get(id)\n\n if (before === undefined && after !== undefined) {\n // added: record only in candidate — no receiver envelope\n added.push({ collection, id, record: after })\n } else if (before !== undefined && after === undefined) {\n // deleted: record only in receiver — attach metadata if requested\n const metadata = options.includeMetadata\n ? (await vault.collection(collection).getMetadata(id)) ?? undefined\n : undefined\n deleted.push({ collection, id, record: before, ...(metadata !== undefined ? { metadata } : {}) })\n } else if (before !== undefined && after !== undefined) {\n if (compareFn(before, after)) {\n unchanged?.push({ collection, id, record: after })\n } else {\n // modified: record in both — attach receiver-side metadata if requested\n const metadata = options.includeMetadata\n ? (await vault.collection(collection).getMetadata(id)) ?? undefined\n : undefined\n const fieldDiffs = fieldDiff(before, after)\n const fieldsChanged = uniqueTopLevelKeys(fieldDiffs)\n modified.push({\n collection,\n id,\n record: after,\n before,\n fieldsChanged,\n fieldDiffs,\n ...(metadata !== undefined ? { metadata } : {}),\n })\n }\n }\n }\n }\n\n const summary = {\n add: added.length,\n modify: modified.length,\n delete: deleted.length,\n total: added.length + modified.length + deleted.length,\n }\n\n return {\n added,\n modified,\n deleted,\n unchanged,\n summary,\n format(opts) {\n return formatDiff(opts?.detail ?? 'full', { added, modified, deleted, summary })\n },\n }\n}\n\n// ─── Internals ─────────────────────────────────────────────────────────\n\nasync function normaliseCandidate<T>(\n candidate: DiffCandidate<T>,\n idKey: string,\n filter: Set<string> | null,\n): Promise<Map<string, Map<string, T>>> {\n const out = new Map<string, Map<string, T>>()\n\n // Vault instance — duck-type via the exportStream method (matches\n // vault.ts's structural shape without forcing a runtime instanceof check\n // that would import the class and risk circular deps).\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n 'exportStream' in candidate &&\n typeof (candidate as Vault).exportStream === 'function'\n ) {\n for await (const chunk of (candidate as Vault).exportStream({ granularity: 'collection' })) {\n if (filter && !filter.has(chunk.collection)) continue\n const collection = out.get(chunk.collection) ?? new Map<string, T>()\n for (const record of chunk.records) {\n const id = readIdField(record, idKey)\n if (!id) continue\n collection.set(id, record as T)\n }\n out.set(chunk.collection, collection)\n }\n return out\n }\n\n // String — assume a vault.dump() JSON string. Parse and reduce to the map shape.\n if (typeof candidate === 'string') {\n let parsed: unknown\n try {\n parsed = JSON.parse(candidate)\n } catch (err) {\n throw new Error(\n `diffVault: candidate string is not valid JSON (${(err as Error).message})`,\n )\n }\n return collectionsFromObject<T>(parsed, idKey, filter)\n }\n\n // Plain object — `Record<collection, records[]>` (same shape as-json.toObject() returns).\n return collectionsFromObject<T>(candidate, idKey, filter)\n}\n\nfunction collectionsFromObject<T>(\n raw: unknown,\n idKey: string,\n filter: Set<string> | null,\n): Map<string, Map<string, T>> {\n const out = new Map<string, Map<string, T>>()\n if (raw === null || typeof raw !== 'object') {\n throw new Error('diffVault: candidate must be a Vault, an object, or a JSON string')\n }\n // A vault dump JSON has a top-level shape like { _compartment, _keyring, <coll>: <records[]> }.\n // We accept both: keys starting with `_` are skipped (they're metadata), the rest are collections.\n for (const [key, value] of Object.entries(raw)) {\n if (key.startsWith('_')) continue\n if (filter && !filter.has(key)) continue\n if (!Array.isArray(value)) continue\n const collection = new Map<string, T>()\n for (const record of value as readonly T[]) {\n if (record === null || typeof record !== 'object') continue\n const id = readIdField(record, idKey)\n if (!id) continue\n collection.set(id, record)\n }\n out.set(key, collection)\n }\n return out\n}\n\nfunction uniqueTopLevelKeys(diffs: readonly FieldDiffEntry[]): readonly string[] {\n const keys = new Set<string>()\n for (const d of diffs) {\n // path is dot-separated; the top-level key is everything before the\n // first `.` or `[`. (`a.b.c` → `a`, `tags[0]` → `tags`, `(root)` → `(root)`).\n const m = /^[^.[]+/.exec(d.path)\n if (m) keys.add(m[0])\n }\n return [...keys]\n}\n\n/**\n * Pull the id field off a record without going through `String(obj)`,\n * which would emit `[object Object]` for nested objects and silently\n * collapse rows that share the same parent. Only string and number ids\n * are accepted; anything else returns the empty string and the record\n * is skipped at the call site.\n */\nfunction readIdField(record: unknown, idKey: string): string {\n if (record === null || typeof record !== 'object') return ''\n const v = (record as Record<string, unknown>)[idKey]\n if (typeof v === 'string') return v\n if (typeof v === 'number' && Number.isFinite(v)) return String(v)\n return ''\n}\n\ninterface FormatBuckets<T> {\n readonly added: readonly VaultDiffEntry<T>[]\n readonly modified: readonly VaultDiffModifiedEntry<T>[]\n readonly deleted: readonly VaultDiffEntry<T>[]\n readonly summary: VaultDiff<T>['summary']\n}\n\nfunction formatDiff<T>(\n detail: 'count' | 'one-line' | 'full',\n b: FormatBuckets<T>,\n): string {\n const head = `${b.summary.add} added · ${b.summary.modify} modified · ${b.summary.delete} deleted`\n if (detail === 'count') return head\n if (b.summary.total === 0) return head + '\\n(no changes)'\n if (detail === 'one-line') return head\n\n const rows: string[] = [head, '']\n for (const e of b.added) rows.push(`${e.collection}/${e.id}\\tadded`)\n for (const e of b.modified) {\n const fields = e.fieldDiffs\n .map((f) => `${f.path}: ${shortJSON(f.from)} → ${shortJSON(f.to)}`)\n .join(', ')\n rows.push(`${e.collection}/${e.id}\\tmodified\\t${fields}`)\n }\n for (const e of b.deleted) rows.push(`${e.collection}/${e.id}\\tdeleted`)\n return rows.join('\\n')\n}\n\nfunction shortJSON(value: unknown): string {\n if (value === undefined) return 'undefined'\n const s = JSON.stringify(value)\n // JSON.stringify returns string for any JSON value except `undefined`\n // (handled above), `function`, and `symbol`. Fall back to a static\n // tag for those — never let an arbitrary object hit the default\n // stringifier (which the lint rule explicitly bans).\n if (typeof s !== 'string') return '<unrepresentable>'\n return s.length > 60 ? s.slice(0, 57) + '...' : s\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/overlay-views/virtual-collection.ts"],"sourcesContent":["import { OverlayIdMismatchError } from '../../kernel/errors.js'\nimport type { Collection } from '../../kernel/collection.js'\nimport type { OverlayedViewStrategy } from './types.js'\n\n/**\n * Virtual-collection proxy returned by `vault.collection(overlayName)`\n * when `overlayName` is a registered `withOverlayedView`.\n *\n * Implements the core `Collection<T>`-shaped read/write surface with\n * merge-on-read semantics:\n * - `get(id)`: overlay row wins iff `overlay[shadowField] === shadowValue`;\n * when `spec.mergeMode` is set, an intermediate status may instead pull\n * a declared subset of fields from the overlay over the base\n * - `list()` / `.query()`: union of ids, per-id merge applied\n * - `put(record)` / `put(id, record)`: routes to overlay; id derived\n * via the base MV's `rowKey` (validated on the two-arg form)\n * - `delete(id)`: removes the overlay row only; base stays\n *\n * Reactive APIs (`live`, `subscribe`, `query().live()`) are out of\n * scope for this release and surface as \"not yet implemented\" — wired in a\n * future sub-issue.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class OverlayedCollection<T extends Record<string, unknown> = any> {\n constructor(\n private readonly spec: OverlayedViewStrategy,\n private readonly baseCollection: Collection<T>,\n private readonly overlayCollection: Collection<T>,\n private readonly baseRowKey: ((row: Record<string, unknown>) => string) | undefined,\n ) {}\n\n /**\n * Convenience accessors for advanced callers that need to bypass the\n * virtual layer (bulk imports, direct overlay queries). Mirrors the\n * spec's \"direct writes to the underlying overlay collection skip\n * the validation\" escape hatch.\n */\n readonly overlay = {\n rowKey: (row: Record<string, unknown>): string => {\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\": base \"${this.spec.base}\" is not an MV — ` +\n `cannot auto-derive id from the row. Use \\`put(id, record)\\` instead.`,\n )\n }\n return this.baseRowKey(row)\n },\n }\n\n /** Get the merged row by id. */\n async get(id: string): Promise<T | null> {\n const overlayRow = await this.overlayCollection.get(id)\n const baseRow = await this.baseCollection.get(id)\n return this.mergeRows(overlayRow, baseRow)\n }\n\n /** List union of base + overlay ids, applying the merge per row. */\n async list(): Promise<T[]> {\n const baseRows = await this.baseCollection.list()\n const overlayRows = await this.overlayCollection.list()\n const idOf = (row: T): string => {\n // Best-effort: use baseRowKey if available, else assume the row\n // has a `.id` field (common pattern). The spec requires every\n // base MV to declare `rowKey`, so the first branch is the\n // canonical path.\n if (this.baseRowKey) return this.baseRowKey(row as Record<string, unknown>)\n const idField = (row as Record<string, unknown>).id\n return typeof idField === 'string' ? idField : ''\n }\n // Key base + overlay rows by id, then union the id sets and run the\n // same per-id merge `get()` uses. Nulls (overlay-only rows that\n // don't qualify and have no base) are filtered out.\n const baseById = new Map<string, T>()\n const overlayById = new Map<string, T>()\n for (const row of baseRows) {\n const id = idOf(row)\n if (id) baseById.set(id, row)\n }\n for (const row of overlayRows) {\n const id = idOf(row)\n if (id) overlayById.set(id, row)\n }\n const out: T[] = []\n for (const id of new Set([...baseById.keys(), ...overlayById.keys()])) {\n const merged = this.mergeRows(overlayById.get(id) ?? null, baseById.get(id) ?? null)\n if (merged !== null) out.push(merged)\n }\n return out\n }\n\n /**\n * Write to the overlay. Two forms:\n * - `put(record)`: id is derived via the base MV's `rowKey(record)`.\n * Throws if the base isn't an MV.\n * - `put(id, record)`: validates `id === rowKey(record)`; throws\n * `OverlayIdMismatchError` on mismatch.\n */\n async put(idOrRecord: string | T, maybeRecord?: T): Promise<void> {\n let id: string\n let record: T\n if (maybeRecord === undefined) {\n // Single-arg form: put(record). Derive id via base rowKey.\n record = idOrRecord as T\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\".put(record): base \"${this.spec.base}\" is not an MV. ` +\n `Use put(id, record) explicitly.`,\n )\n }\n id = this.baseRowKey(record as Record<string, unknown>)\n } else {\n // Two-arg form: put(id, record). Validate against rowKey.\n id = idOrRecord as string\n record = maybeRecord\n if (this.baseRowKey) {\n const expected = this.baseRowKey(record as Record<string, unknown>)\n if (id !== expected) {\n throw new OverlayIdMismatchError(id, expected)\n }\n }\n }\n await this.overlayCollection.put(id, record)\n }\n\n /**\n * Remove the overlay row only. Idempotent (no-op on absent).\n * The base row is untouched — if a base row exists for `id`,\n * subsequent reads return it.\n */\n async delete(id: string): Promise<void> {\n await this.overlayCollection.delete(id)\n }\n\n /**\n * Merge a single id's overlay + base rows into the visible row.\n *\n * Priority (first match wins):\n * 1. Binary shadow win — overlay present AND\n * `overlay[shadowField] === shadowValue` → return the overlay row\n * entirely. This stays FIRST so the original binary behaviour is\n * unchanged whether or not `mergeMode` is configured.\n * 2. Field-level merge — overlay present, `mergeMode` configured,\n * and a rule whose `whenStatus` equals `overlay[shadowField]`.\n * The matched rule pulls its `overlayFields` (those present on\n * the overlay row) on top of the base row. With no base row, the\n * overlay row is returned as-is.\n * 3. Fallback — return the base row (possibly `null`). An\n * overlay-only row that qualifies under neither (1) nor (2) and\n * has no base is therefore NOT surfaced.\n */\n private mergeRows(overlayRow: T | null, baseRow: T | null): T | null {\n const shadowField = this.spec.shadowField\n if (\n overlayRow !== null &&\n (overlayRow as Record<string, unknown>)[shadowField] === this.spec.shadowValue\n ) {\n return overlayRow\n }\n if (overlayRow !== null && this.spec.mergeMode) {\n const status = (overlayRow as Record<string, unknown>)[shadowField]\n const rule = this.spec.mergeMode.rules.find((r) => r.whenStatus === status)\n if (rule) {\n if (baseRow === null) return overlayRow\n const overlaySrc = overlayRow as Record<string, unknown>\n const picked: Record<string, unknown> = {}\n for (const field of rule.overlayFields) {\n if (field in overlaySrc) picked[field] = overlaySrc[field]\n }\n return { ...(baseRow as Record<string, unknown>), ...picked } as T\n }\n }\n return baseRow\n }\n\n // ─── Throw-stubs for the unimplemented Collection<T> surface ───────\n //\n // `Vault.collection(name)` widens the return type to `Collection<T>`\n // for the overlay intercept, but `OverlayedCollection` doesn't\n // implement the full surface. These stubs catch the common\n // reactive / chainable APIs with a clear \"not yet implemented\"\n // error pointing at the relevant issue — so consumers don't hit a\n // cryptic `undefined is not a function` runtime crash.\n //\n // Throw-stubs so consumers get actionable errors rather than cryptic crashes.\n\n /** @throws — chainable Query<T> over a virtual collection is deferred. */\n query(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".query() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` + filter for now, or read from the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections directly. ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — change-stream subscription over a virtual collection is deferred. */\n subscribe(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".subscribe() is not yet implemented for overlay views (#154). ` +\n `Subscribe to the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections individually for now. ` +\n `Merged change-stream lands in a future MV sub-issue.`,\n )\n }\n\n /** @throws — live query over a virtual collection is deferred. */\n live(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".live() is not yet implemented for overlay views (#154). ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — async iteration over a virtual collection is deferred. */\n scan(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".scan() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` for now (no row-count ceiling at niwat scale), or scan the underlying collections directly.`,\n )\n }\n\n /** @throws — lazy-mode query is not applicable to virtual collections. */\n lazyQuery(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".lazyQuery() is not supported. ` +\n `Virtual collections always materialize through base + overlay reads — lazy-mode indexed lookups don't apply.`,\n )\n }\n\n /** @throws — bulk-atomic put is deferred to a future MV sub-issue. */\n putManyAtomic(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".putManyAtomic() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.put(record)\\` calls for now, or write to \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — bulk delete is deferred to a future MV sub-issue. */\n deleteMany(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".deleteMany() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.delete(id)\\` calls for now, or operate on \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — `.first()` over a virtual collection is deferred. */\n first(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".first() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list())[0]\\` for now.`,\n )\n }\n\n /** @throws — `.count()` over a virtual collection is deferred. */\n count(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".count() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list()).length\\` for now.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-lookup/aggregate/reducers.ts","../src/with-lookup/aggregate/aggregation.ts","../src/with-lookup/aggregate/canonical-key.ts","../src/with-lookup/aggregate/groupby.ts"],"sourcesContent":["/**\n * Aggregation reducers for the query DSL.\n *\n * the reducer protocol plus five built-in factories\n * (`count`, `sum`, `avg`, `min`, `max`) consumed by `Query.aggregate()`\n * and, in the future, `Scan.aggregate()`. Every factory accepts\n * an optional `{ seed }` parameter that is plumbed through the\n * protocol but unused by the executor — that's the load-bearing\n * half of constraint #2. When partition-aware aggregation\n * lands, the seed carries the previous partition's running total into\n * the next partition without requiring a protocol change.\n *\n * Reducers are intentionally generic over their internal state type\n * `S` so compound reducers (avg keeps `{sum, count}`, min/max keep a\n * value bag) can model internal bookkeeping without leaking the\n * implementation through the accumulator's public shape. `finalize`\n * collapses `S` back into the user-visible `R`.\n *\n * Reducers are pure data — `init` / `step` / `finalize` / optional\n * `remove` are stateless functions that receive and return `S`. This\n * is the shape that admits O(1) incremental maintenance in a future\n * optimization (delta-aware `LiveAggregation` applies `step` or\n * `remove` per delta), without blocking the simpler \"full re-run on\n * source change\" that ships.\n */\n\nimport { readPath } from '../../kernel/query/predicate.js'\nimport type { MoneyString } from '../../via/money/branded.js'\nimport type { QueryField } from '../../kernel/types.js'\n\n/**\n * A single reducer: factory-produced, ready to plug into an\n * `.aggregate()` spec.\n *\n * Type parameters:\n * - `R` — user-visible result type (what the aggregation returns\n * for this slot, e.g. `number` for `sum()`)\n * - `S` — internal state type, defaults to `R` for simple reducers\n * that don't need compound bookkeeping\n *\n * A reducer is stateless: every method is pure over `S`. `init()` is\n * called once per aggregation run to build the initial state; `step()`\n * folds a record into the state; `remove()` (optional) un-folds a\n * record, enabling incremental live maintenance; `finalize()` reads\n * the final answer out of the state at the end of the run.\n */\nexport interface Reducer<R, S = R> {\n /** Build the initial state for a fresh aggregation run. */\n init(): S\n /** Fold a record into the state. Returns the new state. */\n step(state: S, record: unknown): S\n /**\n * Un-fold a record from the state. Returns the new state.\n *\n * Optional — reducers without `remove` cannot be maintained\n * incrementally and must be re-run from scratch when the underlying\n * record set changes. `sum`, `count`, `avg` implement `remove` in\n * O(1); `min` and `max` implement it in O(N) worst case (when the\n * extremum itself is removed and the next extremum must be\n * recomputed from the remaining contributing values).\n */\n remove?(state: S, record: unknown): S\n /** Collapse the internal state into the user-visible result. */\n finalize(state: S): R\n /**\n * Combine two independent partial states into one (then `finalize` once).\n * Optional. MUST be associative + commutative with `init()` as identity.\n * Never merge finalized results — only states. Enables parallel /\n * hierarchical aggregation (e.g. cross-shard or advisor→firm rollup).\n */\n merge?(a: S, b: S): S\n /**\n * Identifying operation tag stamped by each built-in factory.\n * Used by `summariseAggregateOp` in the introspection walker to\n * render human-readable aggregate descriptors in `dumpSchema()`.\n * Optional so third-party custom reducers are unaffected.\n */\n readonly op?: 'count' | 'sum' | 'avg' | 'min' | 'max'\n /**\n * Field name for field-based reducers (`sum`, `avg`, `min`, `max`).\n * Absent on `count` which aggregates over record count, not a field.\n */\n readonly field?: string\n /**\n * Money-only: target currency for `sum` over a multi-currency money\n * field. Consumed by `wrapMoneyReducers` to convert per-currency\n * subtotals to one figure. Ignored for non-money fields.\n */\n readonly convertTo?: string\n /**\n * Money-only: FX rate map (`'USD->EUR' → rate`) used with `convertTo`.\n */\n readonly fx?: Record<string, number | string>\n}\n\n/**\n * Common options accepted by every reducer factory.\n *\n * `seed` — optional initial value for the internal state. **Unused by\n * the executor**, plumbed through the protocol for constraint\n * #2 (partition-aware aggregation seam). In, partitioned\n * aggregations will pass the previous partition's carry as `seed` so\n * a long time series can be rolled forward one partition at a time\n * without re-aggregating closed partitions.\n *\n * always uses `init()` with the factory's zero value, regardless\n * of whether `seed` was passed. Do not remove the parameter — that's\n * the whole point of having it exist now.\n */\nexport interface ReducerOptions<TSeed = unknown> {\n /** constraint #2 — seed is plumbed through but unused in. */\n readonly seed?: TSeed\n /**\n * Money-only (honored by `sum` over a multi-currency money field):\n * convert per-currency subtotals to this currency for a single figure.\n */\n readonly convertTo?: string\n /** Money-only: FX rate map (`'USD->EUR' → rate`) used with `convertTo`. */\n readonly fx?: Record<string, number | string>\n}\n\n// ---------------------------------------------------------------------------\n// Factories\n// ---------------------------------------------------------------------------\n\n/**\n * Count the number of records that match the query. Ignores field\n * values entirely — the count is over the number of records, not over\n * the number of non-null field values in any column.\n */\nexport function count(opts?: ReducerOptions<number>): Reducer<number> {\n // Seed captured on the closure but unused at execution time in\n //. The reference in _seed keeps lint happy.\n const _seed = opts?.seed\n void _seed\n return {\n op: 'count',\n init: () => 0,\n step: (state) => state + 1,\n remove: (state) => state - 1,\n finalize: (state) => state,\n merge: (a, b) => a + b,\n }\n}\n\n/**\n * Sum a numeric field across all matching records. Non-number values\n * at the field path are coerced to 0 — consumers who want a different\n * behavior (throw, skip, treat as NaN) should filter upstream via\n * `.where()` or write a custom reducer.\n *\n * KNOWN LIMITATION (type imprecision): the declared result type is `number`,\n * but when `field` is a **money** field the runtime returns a decimal *string*\n * (e.g. `'0.30'`, or `{ EUR: '0.30' }` in multi-currency mode) — money sums are\n * BigInt-exact in scaled space and never collapse to a float. The `number` type\n * is therefore a lie for money fields; narrow the result yourself at the call\n * site. A precise fix requires threading money-field declarations into the type\n * system (tracked separately).\n */\nexport function sum(\n field: string,\n opts?: ReducerOptions<number>,\n): Reducer<number> {\n const _seed = opts?.seed\n void _seed\n return {\n op: 'sum',\n field,\n // Money-only metadata, read by `wrapMoneyReducers`. No effect on a\n // generic numeric sum.\n ...(opts?.convertTo !== undefined ? { convertTo: opts.convertTo } : {}),\n ...(opts?.fx !== undefined ? { fx: opts.fx } : {}),\n init: () => 0,\n step: (state, record) => state + readNumber(record, field),\n remove: (state, record) => state - readNumber(record, field),\n finalize: (state) => state,\n merge: (a, b) => a + b,\n }\n}\n\n/**\n * Arithmetic mean of a numeric field across all matching records.\n *\n * Returns `null` for an empty result set (zero records is not a\n * well-defined denominator — returning NaN would poison downstream\n * arithmetic, and throwing would force every consumer to wrap in\n * try/catch just to handle \"no matches\"). Consumers who want an\n * explicit zero should coalesce with `?? 0`.\n *\n * Internal state is `{sum, count}` so the running average can be\n * maintained incrementally — on each delta, both fields update in\n * O(1) and `finalize` divides. Directly storing `avg` as state would\n * not admit incremental removal without also tracking count.\n */\nexport function avg(\n field: string,\n opts?: ReducerOptions<{ sum: number; count: number }>,\n): Reducer<number | null, { sum: number; count: number }> {\n const _seed = opts?.seed\n void _seed\n return {\n op: 'avg',\n field,\n init: () => ({ sum: 0, count: 0 }),\n step: (state, record) => ({\n sum: state.sum + readNumber(record, field),\n count: state.count + 1,\n }),\n remove: (state, record) => ({\n sum: state.sum - readNumber(record, field),\n count: state.count - 1,\n }),\n finalize: (state) => (state.count === 0 ? null : state.sum / state.count),\n merge: (a, b) => ({ sum: a.sum + b.sum, count: a.count + b.count }),\n }\n}\n\nexport interface MinMaxState {\n /**\n * Multiset of contributing field values. Stored as a plain array\n * because we need to support `remove` and a plain array gives us\n * O(1) push + O(N) worst-case removal — which matches the\n * documented min/max removal complexity. A sorted structure would\n * let us drop the O(N) rescan but adds complexity that doesn't\n * need; consumers hitting the O(N) ceiling should file an issue.\n */\n readonly values: number[]\n}\n\nfunction pushValue(state: MinMaxState, value: number): MinMaxState {\n return { values: [...state.values, value] }\n}\n\nfunction removeValue(state: MinMaxState, value: number): MinMaxState {\n // Remove the first matching value — duplicates are fine, we only\n // need to drop one instance per `remove()` call so the multiset\n // count stays consistent with the record count.\n const idx = state.values.indexOf(value)\n if (idx < 0) return state\n const next = state.values.slice()\n next.splice(idx, 1)\n return { values: next }\n}\n\n/**\n * Smallest numeric value of a field across all matching records.\n * Returns `null` for an empty result set. See `avg()` for the\n * reasoning on `null` vs NaN vs throwing.\n *\n * Incremental complexity: O(1) for `step`, O(N) worst case for\n * `remove` when the current minimum is removed (the state holds the\n * full multiset of contributing values and `finalize` scans for the\n * new minimum). Consumers with very large result sets and frequent\n * removals of the current extremum should either accept the cost or\n * wait for a future optimization.\n */\nexport function min(\n field: string,\n opts?: ReducerOptions<number>,\n): Reducer<number | null, MinMaxState> {\n const _seed = opts?.seed\n void _seed\n return {\n op: 'min',\n field,\n init: () => ({ values: [] }),\n step: (state, record) => pushValue(state, readNumber(record, field)),\n remove: (state, record) => removeValue(state, readNumber(record, field)),\n finalize: (state) => {\n if (state.values.length === 0) return null\n let out = state.values[0]!\n for (let i = 1; i < state.values.length; i++) {\n const v = state.values[i]!\n if (v < out) out = v\n }\n return out\n },\n merge: (a, b) => ({ values: [...a.values, ...b.values] }),\n }\n}\n\n/**\n * Largest numeric value of a field across all matching records.\n * Mirror of `min()` — see that doc for semantics, null-on-empty\n * behavior, and the O(N) removal caveat.\n */\nexport function max(\n field: string,\n opts?: ReducerOptions<number>,\n): Reducer<number | null, MinMaxState> {\n const _seed = opts?.seed\n void _seed\n return {\n op: 'max',\n field,\n init: () => ({ values: [] }),\n step: (state, record) => pushValue(state, readNumber(record, field)),\n remove: (state, record) => removeValue(state, readNumber(record, field)),\n finalize: (state) => {\n if (state.values.length === 0) return null\n let out = state.values[0]!\n for (let i = 1; i < state.values.length; i++) {\n const v = state.values[i]!\n if (v > out) out = v\n }\n return out\n },\n merge: (a, b) => ({ values: [...a.values, ...b.values] }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Money-typed reducer constructors\n// ---------------------------------------------------------------------------\n\n/**\n * `sum()` for a **declared money field**, typed to match the runtime.\n *\n * `sum()` returns `Reducer<number>`, but `wrapMoneyReducers` (applied at\n * `query.aggregate()` time, once `moneyFields` is known) rewrites any\n * `sum`/`min`/`max` over a money field to a money reducer that finalizes to a\n * `MoneyString` decimal — so the `number` type is a lie for money fields and\n * consumers need a cast at every read site. `moneySum` is the same reducer with\n * the correct `Reducer<MoneyString>` return type, so no read-site cast is\n * needed. It is the caller's assertion that `field` is a money field; use plain\n * `sum()` for non-money fields. (For a multi-currency money field WITHOUT\n * `convertTo`, the runtime returns a per-currency `Record<string, MoneyString>`\n * rather than a single `MoneyString` — pass `convertTo` to collapse to one\n * currency, or read the map at the boundary.)\n */\nexport function moneySum(field: string, opts?: ReducerOptions<number>): Reducer<MoneyString> {\n // The constructed reducer is the plain numeric `sum`; `wrapMoneyReducers`\n // swaps in the MoneyString-producing money reducer at aggregate() time.\n return sum(field, opts) as unknown as Reducer<MoneyString>\n}\n\n/**\n * `min()` for a declared money field, typed `Reducer<MoneyString | null>`\n * (null on an empty result set in fixed-currency mode, mirroring `min()`). See\n * {@link moneySum} for the late-binding rewrite. Note: `convertTo`/`fx` on\n * `opts` have no effect on min/max (cross-currency min/max is unsupported — use\n * {@link moneySum} for currency conversion). In multi-currency mode the runtime\n * returns a per-currency map and an empty result is `{}` rather than `null`.\n */\nexport function moneyMin(field: string, opts?: ReducerOptions<number>): Reducer<MoneyString | null> {\n return min(field, opts) as unknown as Reducer<MoneyString | null>\n}\n\n/**\n * `max()` for a declared money field, typed `Reducer<MoneyString | null>`\n * (null on an empty result set in fixed-currency mode, mirroring `max()`). See\n * {@link moneyMin} for the `convertTo`/`fx` and multi-currency caveats.\n */\nexport function moneyMax(field: string, opts?: ReducerOptions<number>): Reducer<MoneyString | null> {\n return max(field, opts) as unknown as Reducer<MoneyString | null>\n}\n\n// ---------------------------------------------------------------------------\n// Builder (typed spec-builder for aggregate())\n// ---------------------------------------------------------------------------\n\n/**\n * Typed builder passed to the `aggregate(b => spec)` overload.\n *\n * Each field-taking method narrows `field` to `QueryField<T, S>`, which\n * excludes any field listed in the collection's `sensitive` option at\n * compile time. `count()` carries no field argument and is always allowed.\n *\n * The type parameters match the `Query<T, S>` they come from:\n * - `T` — the record type of the collection\n * - `S` — the union of sensitive field keys (defaults to `never`)\n *\n * ONE shared runtime instance (`reducerBuilder`) serves all `T`/`S`\n * combinations — the field narrowing is type-only; the methods delegate\n * directly to the standalone factories.\n */\nexport interface ReducerBuilder<T, S extends keyof T = never, M extends keyof T & string = never> {\n count(opts?: ReducerOptions<number>): Reducer<number>\n sum<F extends QueryField<T, S>>(field: F, opts?: ReducerOptions<number>): [F] extends [M] ? Reducer<MoneyString> : Reducer<number>\n avg(field: QueryField<T, S>, opts?: ReducerOptions<{ sum: number; count: number }>): ReturnType<typeof avg>\n min<F extends QueryField<T, S>>(field: F, opts?: ReducerOptions<number>): [F] extends [M] ? Reducer<MoneyString | null> : ReturnType<typeof min>\n max<F extends QueryField<T, S>>(field: F, opts?: ReducerOptions<number>): [F] extends [M] ? Reducer<MoneyString | null> : ReturnType<typeof max>\n moneySum(field: QueryField<T, S>, opts?: ReducerOptions<number>): Reducer<MoneyString>\n moneyMin(field: QueryField<T, S>, opts?: ReducerOptions<number>): Reducer<MoneyString | null>\n moneyMax(field: QueryField<T, S>, opts?: ReducerOptions<number>): Reducer<MoneyString | null>\n}\n\n/**\n * Shared runtime instance for the `aggregate(b => spec)` builder form.\n *\n * The field-narrowing to `QueryField<T, S>` is type-only — each method\n * delegates directly to its standalone factory. A `(field: string) => R`\n * factory is assignable to a `(field: QueryField<T,S>) => R` method by\n * parameter-contravariance, so this single instance works for all `T`/`S`.\n */\nexport const reducerBuilder: ReducerBuilder<Record<string, unknown>> = {\n count,\n // The generic F parameter on sum/min/max in the interface is type-only; the\n // standalone factories are plain `(field: string)` functions. With M=never the\n // conditional return collapses to the numeric branch, matching the factories'\n // return type, but TypeScript cannot verify that collapse for a generic method\n // signature — so each factory is cast directly to the method's type.\n sum: sum as ReducerBuilder<Record<string, unknown>>['sum'],\n avg,\n min: min as ReducerBuilder<Record<string, unknown>>['min'],\n max: max as ReducerBuilder<Record<string, unknown>>['max'],\n moneySum, moneyMin, moneyMax,\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read a numeric field from a record. Non-number values (null,\n * undefined, strings, objects) coerce to 0 so sum/avg/min/max don't\n * produce NaN on one bad row. Consumers who want strict typing should\n * validate upstream with Standard Schema, which NOYDB already runs on\n * every `put()`.\n */\nfunction readNumber(record: unknown, field: string): number {\n const value = readPath(record, field)\n // Defensive: a `{ amount, currency }` value means a multi-currency\n // money field reached a generic numeric reducer instead of being\n // rewritten by `wrapMoneyReducers` — that would silently produce a\n // wrong total. Surface it loudly rather than coercing to 0.\n if (\n typeof value === 'object' &&\n value !== null &&\n 'amount' in value &&\n 'currency' in value\n ) {\n throw new Error(\n `aggregate: field \"${field}\" holds a money value but was not money-aware — ` +\n `declare it in the collection's moneyFields so sum/min/max stay exact`,\n )\n }\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n","/**\n * Aggregate execution — the runtime behind `Query.aggregate()`.\n *\n * takes an `AggregateSpec` (a record of named reducers\n * built from `reducers.ts`) and runs every reducer over the records\n * produced by the underlying query. Two terminal surfaces:\n *\n * - `.run(): R` — synchronous one-shot reduction. Matches the\n * existing `Query.toArray()` / `.first()` / `.count()` style.\n * - `.live(): LiveAggregation<R>` — reactive primitive that\n * re-runs the reduction whenever the query's source notifies of\n * a change. uses naive full re-run; incremental delta\n * maintenance is admitted by the reducer protocol (`remove()`)\n * but not wired to the executor yet — a follow-up optimization\n * can switch from full re-run to delta-based without breaking\n * the public API. Consumers get correct, reactive values today.\n *\n * The `Aggregation<R>` wrapper is deliberately tiny — it exists so\n * `.aggregate(spec)` can be chained with either `.run()` or `.live()`\n * without the builder needing two separate terminal methods. It\n * holds the closure over the query execution (produces the current\n * matching record set) and the spec, and stitches them together in\n * either mode.\n *\n * This file depends ONLY on `reducers.ts` — it has no knowledge of\n * the `Query` class. Tests can therefore exercise the reduction\n * surface with plain record arrays, without spinning up a Collection.\n */\n\nimport type { Reducer } from './reducers.js'\n\n/**\n * A named set of reducers, keyed by output field name. Each key\n * becomes a field on the aggregated result.\n *\n * ```ts\n * const spec = {\n * total: sum('amount'),\n * n: count(),\n * avgAmount: avg('amount'),\n * }\n * ```\n */\nexport type AggregateSpec = Readonly<Record<string, Reducer<unknown, unknown>>>\n\n/**\n * Map an `AggregateSpec` to its reduced result shape — each key\n * carries the finalized result type from its reducer. A spec built\n * from `{ total: sum('amount'), n: count() }` yields a result of\n * `{ total: number, n: number }`.\n *\n * This uses a mapped type with a conditional to extract `R` from\n * each `Reducer<R, _>`. The `infer` captures the user-visible result\n * type, discarding the internal state type `S`.\n */\nexport type AggregateResult<Spec extends AggregateSpec> = {\n [K in keyof Spec]: Spec[K] extends Reducer<infer R, unknown> ? R : never\n}\n\n/**\n * Pure reduction over a record array. Runs every reducer's\n * `init → step* → finalize` pipeline exactly once over the records.\n *\n * Called by `Aggregation.run()` and by the live-mode refresh path.\n * Exported for tests and for future `scan().aggregate()` reuse\n * — the streaming path will call the same reducer protocol with a\n * per-page loop instead of a single array.\n */\nexport function reduceRecords<Spec extends AggregateSpec>(\n records: readonly unknown[],\n spec: Spec,\n): AggregateResult<Spec> {\n // Per-slot state, keyed by the spec's output field name.\n const state: Record<string, unknown> = {}\n for (const key of Object.keys(spec)) {\n state[key] = spec[key]!.init()\n }\n for (const record of records) {\n for (const key of Object.keys(spec)) {\n state[key] = spec[key]!.step(state[key], record)\n }\n }\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(spec)) {\n result[key] = spec[key]!.finalize(state[key])\n }\n return result as AggregateResult<Spec>\n}\n\n/**\n * A minimal reactive primitive for aggregation results.\n *\n * Same spirit as the `LiveQuery` in : frame-agnostic, a plain\n * object with `value` / `error` fields and a `subscribe(cb)`\n * notification channel that Vue / React / Solid adapters wrap in\n * their own primitive. Intentionally NOT a Promise — aggregations\n * have a well-defined \"current value\" at every instant, and the\n * reactive consumer wants to read that value synchronously.\n *\n * Error semantics mirror `LiveQuery`: if a re-run throws, the\n * previous successful `value` is preserved and the error is stored\n * in `error` so consumers can render an error state without losing\n * the last-known-good result. The throw does NOT propagate out of\n * the source's change handler (which would tear down the upstream\n * emitter).\n *\n * `stop()` tears down the upstream subscription. It is idempotent —\n * calling it multiple times is safe — and subscribe calls after\n * stop are no-ops (they immediately return a no-op unsubscribe).\n * Always call `stop()` when done; Vue's `onUnmounted` is the\n * canonical place. Raw consumers must do it themselves.\n */\nexport interface LiveAggregation<R> {\n /** Current reduced value. Undefined only if the first compute threw. */\n readonly value: R | undefined\n /** Last execution error, if any. Cleared on the next successful run. */\n readonly error: unknown\n /** Notify on every recomputation (success or error). Returns unsubscribe. */\n subscribe(cb: () => void): () => void\n /** Tear down the upstream subscription. Idempotent. */\n stop(): void\n}\n\n/**\n * Upstream change-notification hook for live aggregation.\n *\n * Matches the shape that `QuerySource.subscribe` already uses — a\n * single method that accepts a callback and returns an unsubscribe\n * function. The `Aggregation` wrapper collects upstreams from the\n * query's source and wires them into a single re-run trigger.\n */\nexport interface AggregationUpstream {\n subscribe(cb: () => void): () => void\n}\n\n/**\n * Internal implementation of `LiveAggregation`. Not exported —\n * consumers get the interface only. The class wraps a `recompute`\n * closure (which runs the full reduction and returns the new value)\n * and a list of upstreams (sources whose changes should trigger a\n * re-run).\n *\n * Error isolation: if an individual listener callback throws, the\n * other listeners still fire and the error is logged to the warn\n * channel. This matches `LiveQuery` from and keeps one misbehaving\n * consumer from tearing down the whole live aggregation.\n */\nclass LiveAggregationImpl<R> implements LiveAggregation<R> {\n public value: R | undefined\n public error: unknown\n private readonly listeners = new Set<() => void>()\n private readonly unsubscribes: Array<() => void> = []\n private stopped = false\n\n constructor(\n private readonly recompute: () => R,\n upstreams: readonly AggregationUpstream[],\n ) {\n // Initial computation — surface any error through the `error`\n // field rather than letting the constructor throw, so consumers\n // can always construct a LiveAggregation and check its state\n // afterwards. Throwing from a constructor would force every\n // caller to wrap in try/catch, which is the opposite of the\n // \"reactive value with error state\" ergonomics we want.\n try {\n this.value = recompute()\n this.error = undefined\n } catch (err) {\n this.value = undefined\n this.error = err\n }\n\n // Wire up upstream subscriptions. Each one triggers a full\n // recomputation; we don't attempt incremental updates in.\n for (const upstream of upstreams) {\n const unsub = upstream.subscribe(() => this.refresh())\n this.unsubscribes.push(unsub)\n }\n }\n\n private refresh(): void {\n if (this.stopped) return\n try {\n this.value = this.recompute()\n this.error = undefined\n } catch (err) {\n // Preserve the previous successful value — consumers render an\n // error state using `error` without losing the last-known-good\n // number. This matches LiveQuery's error-preservation contract.\n this.error = err\n }\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (err) {\n // Isolate listener errors so one bad consumer can't tear\n // down every other subscriber on the same aggregation.\n console.warn('[noy-db] LiveAggregation listener threw:', err)\n }\n }\n }\n\n subscribe(cb: () => void): () => void {\n if (this.stopped) {\n // No-op after stop. Returning a harmless unsubscribe lets\n // consumers use the same teardown pattern unconditionally.\n return () => {}\n }\n this.listeners.add(cb)\n return () => {\n this.listeners.delete(cb)\n }\n }\n\n stop(): void {\n if (this.stopped) return\n this.stopped = true\n for (const unsub of this.unsubscribes) {\n try {\n unsub()\n } catch (err) {\n console.warn('[noy-db] LiveAggregation upstream unsubscribe threw:', err)\n }\n }\n this.unsubscribes.length = 0\n this.listeners.clear()\n }\n}\n\n/**\n * Chainable wrapper returned by `Query.aggregate(spec)`. Holds the\n * execute-records closure and the spec; terminal methods (`run`,\n * `live`) stitch them together in either mode.\n *\n * Why a wrapper instead of two terminal methods on `Query` directly?\n *\n * The `.aggregate(spec)` call is where the spec is bound — both\n * `.run()` and `.live()` need the same spec, and the consumer's\n * fluent style is `query.where(...).aggregate(spec).run()` or\n * `.aggregate(spec).live()`. Wrapping lets the spec be named once\n * and reused for either terminal, and keeps the `Query` class\n * from growing a pair of near-duplicate method overloads\n * (`aggregateRun` / `aggregateLive`) that would be harder to\n * discover.\n */\nexport class Aggregation<R> {\n constructor(\n private readonly executeRecords: () => readonly unknown[],\n private readonly spec: AggregateSpec,\n private readonly upstreams: readonly AggregationUpstream[],\n ) {}\n\n /**\n * Execute the query and reduce the results synchronously.\n * Returns the reduced shape matching the spec — e.g. a spec of\n * `{ total: sum('amount'), n: count() }` returns\n * `{ total: number, n: number }`.\n */\n run(): R {\n return reduceRecords(this.executeRecords(), this.spec) as unknown as R\n }\n\n /**\n * Build a reactive `LiveAggregation<R>` that re-runs the reduction\n * whenever any upstream source notifies of a change. The initial\n * value is computed eagerly in the constructor, so consumers can\n * read `live.value` immediately after calling `.live()`.\n *\n * Always call `live.stop()` when finished — it tears down the\n * upstream subscriptions. Vue's `onUnmounted` is the canonical\n * place.\n *\n * **Implementation note:** every upstream change triggers a full\n * re-reduction. Incremental maintenance (O(1) per delta for\n * sum/count/avg via the reducer protocol's `remove()` method) is a\n * planned follow-up optimization — the protocol already supports\n * it, but the executor doesn't drive it yet. Consumers get\n * correct, reactive values today; future PRs can switch to\n * delta-based maintenance without changing this API.\n */\n live(): LiveAggregation<R> {\n const recompute = (): R =>\n reduceRecords(this.executeRecords(), this.spec) as unknown as R\n return new LiveAggregationImpl<R>(recompute, this.upstreams)\n }\n}\n\n/**\n * Build a `LiveAggregation<V>` from a recompute closure and a list\n * of upstreams. Exposed so sibling files in the query DSL\n * (currently `groupby.ts`) can reuse the reactive primitive\n * without reaching into `LiveAggregationImpl` directly. This keeps\n * the implementation class private while still allowing planned\n * composition with `.groupBy().aggregate().live()`.\n */\nexport function buildLiveAggregation<V>(\n recompute: () => V,\n upstreams: readonly AggregationUpstream[],\n): LiveAggregation<V> {\n return new LiveAggregationImpl<V>(recompute, upstreams)\n}\n","/**\n * Canonicalise a group-key tuple to a stable string for dedup hashing.\n *\n * Sorts field names lexicographically before serialising, so that\n * `.groupBy('a', 'b')` and `.groupBy('b', 'a')` produce identical\n * keys for the same logical group. Values are JSON-stringified;\n * `undefined` and `null` are distinguished (matching the Map-key\n * semantics in `groupAndReduce`).\n *\n * NOT part of the public API. Used by:\n * - `groupAndReduce` for the dedup Map's key\n * - `materialized-views/query-hash` for UNION MV cross-arm row-key dedup (PR 2)\n *\n * Pure: same input → same output, no side effects.\n */\nexport function canonicalGroupKey(\n fields: readonly string[],\n row: Record<string, unknown>,\n): string {\n const sorted = [...fields].sort()\n const parts: string[] = []\n for (const name of sorted) {\n const v = row[name]\n const serialised =\n v === undefined ? 'undefined' : JSON.stringify(v)\n parts.push(`${name}=${serialised}`)\n }\n return parts.join('|')\n}\n","/**\n * Query DSL `.groupBy()` —.\n *\n * Chains after `.where()` / `.filter()` / `.or()` / `.and()` on a\n * Query and before a reducer spec, so consumers can compute\n * per-bucket aggregates without folding in userland:\n *\n * ```ts\n * const byClient = invoices.query()\n * .where('status', '==', 'open')\n * .groupBy('clientId')\n * .aggregate({ total: sum('amount'), n: count() })\n * .run()\n * // → [ { clientId: 'c1', total: 5250, n: 3 }, … ]\n * ```\n *\n * Execution pipeline:\n *\n * 1. Run the query's where/filter clauses (same candidate /\n * filter pipeline as `.aggregate()` directly on Query).\n * 2. Partition the matching records into buckets keyed by\n * `readPath(record, field)`. JS `Map` preserves insertion\n * order, so the first-seen key for a bucket determines its\n * position in the result array — consumers who want a\n * specific ordering should `.sort()` downstream.\n * 3. Enforce cardinality: warn once per field at 10% of the cap\n * (10_000 buckets), throw `GroupCardinalityError` at 100% of\n * the cap (100_000 buckets).\n * 4. For each bucket, build a per-group reducer state and\n * step every record in the bucket through it.\n * 5. Emit one result row per bucket, shaped as\n * `{ [field]: key, ...reduced }`.\n *\n * **Null / undefined keys:** `Map` distinguishes `null` from\n * `undefined`, so records with a missing group field get their own\n * bucket, and records with an explicit `null` value get a separate\n * bucket from that. Consumers who want them merged can coalesce\n * upstream with `.filter()`.\n *\n * **Live mode:** `.groupBy().aggregate().live()` re-runs the full\n * grouping pipeline on every source change. Per-bucket incremental\n * delta maintenance is a future optimization — the reducer\n * protocol's `remove()` hook admits it, but ships naive\n * re-grouping for simplicity.\n *\n * **Type-level stable-key narrowing:** when\n * `dictKey` lands, `groupBy<DictField>()` will narrow the group key\n * type to the stable dictionary key rather than the resolved locale\n * label. That prevents grouping by the locale-resolved label,\n * which would produce different buckets per reader. types the\n * key as `unknown` at the result shape; the dictKey narrowing\n * layers on top without an API break.\n *\n * Partition-awareness seam: when partitioned collections land,\n * per-partition grouping will need to merge sub-results across\n * partitions. The reducer protocol's `{ seed }` parameter\n * (already plumbed through in `reducers.ts`) is the mechanism —\n * groupBy doesn't need its own seam for the moment, because it\n * delegates to the reducer protocol for all per-bucket state.\n */\n\nimport { readPath } from '../../kernel/query/predicate.js'\nimport type {\n AggregateSpec,\n AggregateResult,\n AggregationUpstream,\n LiveAggregation,\n} from './aggregation.js'\nimport { buildLiveAggregation } from './aggregation.js'\nimport type { ReducerBuilder } from './reducers.js'\nimport { reducerBuilder } from './reducers.js'\nimport { canonicalGroupKey } from './canonical-key.js'\nimport { GroupCardinalityError } from '../../kernel/errors.js'\nimport type { MoneyDescriptor } from '../../via/money/descriptor.js'\nimport type { ViaPipeline } from '../../kernel/via/pipeline.js'\nimport { viaBinder } from '../../kernel/via/index.js'\nimport { applyI18nLocale, type I18nTextDescriptor } from '../../via/i18n/core.js'\n\n/**\n * Cardinality thresholds for `.groupBy()`. The warn threshold gives\n * consumers a heads-up before the hard error; the cap is a fixed\n * constant in (not overridable). A `{ maxGroups }` override\n * can be added later without a break if a real consumer asks.\n */\nexport const GROUPBY_WARN_CARDINALITY = 10_000\nexport const GROUPBY_MAX_CARDINALITY = 100_000\n\n/**\n * One-shot warning dedup per-field-set — reactive dashboards\n * re-executing the same grouped query should produce the warning\n * once, not once per re-fire. Keyed on the sorted JSON of grouping\n * field names so `.groupBy('a', 'b')` and `.groupBy('b', 'a')`\n * share the same dedup slot (their result tuples are isomorphic).\n */\nconst warnedCardinalityFields = new Set<string>()\nfunction warnCardinalityApproaching(\n fields: readonly string[],\n observed: number,\n): void {\n const key = JSON.stringify([...fields].sort())\n if (warnedCardinalityFields.has(key)) return\n warnedCardinalityFields.add(key)\n const label = `[${fields.join(', ')}]`\n console.warn(\n `[noy-db] .groupBy(${label}) produced ${observed} distinct groups, ` +\n `${Math.round((observed / GROUPBY_MAX_CARDINALITY) * 100)}% of the ` +\n `${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with ` +\n `.where() before grouping, or switch to a lower-cardinality field.`,\n )\n}\n\n/**\n * Test-only: clear the per-field cardinality warning dedup between\n * tests. Production code never calls this — matching the\n * `resetJoinWarnings` pattern in `join.ts`.\n */\nexport function resetGroupByWarnings(): void {\n warnedCardinalityFields.clear()\n}\n\n/**\n * Result row shape for a grouped aggregation. Each row carries the\n * group key value under the grouping field name plus every reducer\n * output from the spec.\n *\n * types the group key as `unknown` at the result shape — the\n * runtime read via `readPath` can return any value, and narrowing\n * to a specific type would require the caller to assert at the\n * call site. `dictKey` narrowing layers on top of this by\n * adding an overload that constrains `F` when the grouping field\n * is a `dictKey`.\n */\nexport type GroupedRow<F extends string, R> = { [K in F]: unknown } & R\n\n/**\n * Multi-key variant — result-row shape for variadic\n * `.groupBy(...fields)`. Every grouped field name appears on the row\n * (typed as `unknown` for the same reason as `GroupedRow`), plus the\n * reducer outputs from the spec.\n */\nexport type GroupedRowN<F extends readonly string[], R> =\n { [K in F[number]]: unknown } & R\n\n/**\n * Shared base class for the chainable grouped-query wrappers. Holds\n * the constructor + protected fields that both single-key\n * `GroupedQuery<T, F>` and variadic `GroupedQueryN<T, F>` need; each\n * subclass only overrides `aggregate()` with its own result-row\n * generic.\n *\n * Not exported — implementation detail. Adding `.having()` /\n * `.live()` / `.orderByGroup()` etc. in the future lands here once\n * and both subclasses pick it up automatically.\n *\n * @internal\n */\nabstract class GroupedQueryBase {\n /**\n * Field set this grouped query buckets on. Stored in declaration\n * order — the same order is preserved on every result row by\n * `groupAndReduce`. For the single-field constructor, this is\n * `[field]`.\n */\n protected readonly fields: readonly string[]\n\n constructor(\n protected readonly executeRecords: () => readonly unknown[],\n fieldOrFields: string | readonly string[],\n protected readonly upstreams: readonly AggregationUpstream[],\n /**\n * Optional dict label resolver attached by the query builder when\n * the grouping field is a dictKey. Variadic groupings always pass\n * `undefined` — `<field>Label` projection has no meaningful shape\n * for composite keys.\n */\n protected readonly dictLabelResolver?: (\n key: string,\n locale: string,\n fallback?: string | readonly string[],\n ) => Promise<string | undefined>,\n /**\n * The backing collection's compiled Via pipeline — used to rewrite\n * `sum`/`min`/`max` over Via-covered fields (e.g. money) into exact\n * BigInt reducers when `.aggregate(spec)` is terminated.\n */\n protected readonly via?: ViaPipeline,\n ) {\n this.fields =\n typeof fieldOrFields === 'string' ? [fieldOrFields] : [...fieldOrFields]\n }\n\n /** Apply Via-aware reducer rewriting (e.g. money) when the source declares one. */\n protected wrapSpec<Spec extends AggregateSpec>(spec: Spec): Spec {\n return this.via ? this.via.wrapReducers(spec) : spec\n }\n}\n\n/**\n * Chainable wrapper returned by `Query.groupBy(field)`. Terminates\n * with `.aggregate(spec)` which returns a `GroupedAggregation`.\n *\n * Kept minimal — the only operation on a grouped query is\n * aggregation. Ordering, limiting, and further filtering belong on\n * the underlying `Query` before `.groupBy()` is called; applying\n * them post-group would be a different operation (`having` /\n * `groupOrderBy`), out of scope for.\n */\nexport class GroupedQuery<T, F extends string, S extends keyof T = never, M extends keyof T & string = never> extends GroupedQueryBase {\n /**\n * Build a grouped aggregation. Returns a `GroupedAggregation`\n * with `.run()`, `.runAsync()`, and `.live()` terminals — same shape\n * as the non-grouped `.aggregate()` wrapper, just with an array\n * result (one row per bucket) instead of a single reduced object.\n *\n * The builder overload `aggregate(b => spec)` types `b` as\n * `ReducerBuilder<T, S, M>`, so field-taking reducers (`sum`, `avg`,\n * `min`, `max`) refuse any field listed in the collection's\n * `sensitive` option at compile time, and `sum`/`min`/`max` over a\n * declared `moneyFields` (`M`) member return a `MoneyString`. The\n * bare-spec overload is preserved for backward compatibility.\n */\n aggregate<Spec extends AggregateSpec>(spec: Spec): GroupedAggregation<GroupedRow<F, AggregateResult<Spec>>>\n aggregate<Spec extends AggregateSpec>(build: (b: ReducerBuilder<T, S, M>) => Spec): GroupedAggregation<GroupedRow<F, AggregateResult<Spec>>>\n aggregate<Spec extends AggregateSpec>(\n specOrBuild: Spec | ((b: ReducerBuilder<T, S, M>) => Spec),\n ): GroupedAggregation<GroupedRow<F, AggregateResult<Spec>>> {\n const spec: Spec = typeof specOrBuild === 'function'\n ? (specOrBuild as (b: ReducerBuilder<T, S, M>) => Spec)(reducerBuilder as unknown as ReducerBuilder<T, S, M>)\n : specOrBuild\n // T is phantom on the wrapper so consumers can still see the\n // source row type on hover. Reference it to keep lint quiet.\n void undefined as T | undefined\n return new GroupedAggregation<GroupedRow<F, AggregateResult<Spec>>>(\n this.executeRecords,\n this.fields,\n this.wrapSpec(spec),\n this.upstreams,\n this.dictLabelResolver,\n )\n }\n}\n\n/**\n * Variadic-keyed sibling of `GroupedQuery<T, F>`. Constructed by the\n * multi-arg `Query.groupBy(...fields)` overload. The runtime shape is\n * identical — only the type-level result-row narrowing differs.\n */\nexport class GroupedQueryN<T, F extends readonly string[], S extends keyof T = never, M extends keyof T & string = never> extends GroupedQueryBase {\n aggregate<Spec extends AggregateSpec>(spec: Spec): GroupedAggregation<GroupedRowN<F, AggregateResult<Spec>>>\n aggregate<Spec extends AggregateSpec>(build: (b: ReducerBuilder<T, S, M>) => Spec): GroupedAggregation<GroupedRowN<F, AggregateResult<Spec>>>\n aggregate<Spec extends AggregateSpec>(\n specOrBuild: Spec | ((b: ReducerBuilder<T, S, M>) => Spec),\n ): GroupedAggregation<GroupedRowN<F, AggregateResult<Spec>>> {\n const spec: Spec = typeof specOrBuild === 'function'\n ? (specOrBuild as (b: ReducerBuilder<T, S, M>) => Spec)(reducerBuilder as unknown as ReducerBuilder<T, S, M>)\n : specOrBuild\n void undefined as T | undefined\n return new GroupedAggregation<GroupedRowN<F, AggregateResult<Spec>>>(\n this.executeRecords,\n this.fields,\n this.wrapSpec(spec),\n this.upstreams,\n this.dictLabelResolver,\n )\n }\n}\n\n/**\n * Execute the group-and-reduce pipeline. Pure function over a\n * record array and a spec — shared by `GroupedAggregation.run()`\n * and the live-mode refresh path. Exported for tests and for any\n * future `scan().groupBy().aggregate()` reuse.\n *\n * Enforces the cardinality cap incrementally during the partition\n * loop, so a runaway grouping throws at the moment the 100_001st\n * bucket would be created — the consumer doesn't have to wait for\n * the full partition to materialize before the error fires.\n */\nexport function groupAndReduce<R>(\n records: readonly unknown[],\n fieldOrFields: string | readonly string[],\n spec: AggregateSpec,\n moneyFields?: Record<string, MoneyDescriptor>,\n): R[] {\n const fields: readonly string[] =\n typeof fieldOrFields === 'string' ? [fieldOrFields] : fieldOrFields\n if (fields.length === 0) {\n throw new Error('.groupBy() requires at least one field')\n }\n\n // Money-aware aggregation: when the caller declares money descriptors\n // for output/intermediate fields, rewrite any `sum`/`min`/`max` over\n // them into exact BigInt reducers before bucketing. Omitted → spec\n // passes through unchanged (backward compatible). The chainable\n // `GroupedQuery` path already wraps upstream via `wrapSpec`; this\n // covers direct `groupAndReduce` callers (UNION-form MVs) that have\n // no Query wrapper to do it. Builds a transient money binding via the\n // kernel's Via port rather than a Query-attached pipeline — `moneyFields`\n // here is the MV spec's OWN descriptor map, not a collection's.\n if (moneyFields) {\n spec = viaBinder('money')({ moneyFields }).wrapReducers!(spec) as AggregateSpec\n }\n\n // Bucket value is { keyValues, records } so the output row can stamp\n // every grouped field in DECLARATION ORDER. Map preserves insertion\n // order natively (ES2015), so first-seen keys determine ordering.\n interface Bucket {\n keyValues: Record<string, unknown>\n records: unknown[]\n }\n const buckets = new Map<string, Bucket>()\n // Field-label string for error messages — matches the variadic\n // surface (`[a, b]` for multi-key, `\"k\"` for single-key back-compat).\n const fieldLabel = fields.length === 1 ? fields[0]! : `[${fields.join(', ')}]`\n\n for (const record of records) {\n // Read each field's value into a row object, then canonicalise.\n const keyValues: Record<string, unknown> = {}\n for (const f of fields) {\n keyValues[f] = readPath(record, f)\n }\n const dedupKey = canonicalGroupKey(fields, keyValues)\n let bucket = buckets.get(dedupKey)\n if (bucket === undefined) {\n if (buckets.size >= GROUPBY_MAX_CARDINALITY) {\n throw new GroupCardinalityError(\n fieldLabel,\n buckets.size + 1,\n GROUPBY_MAX_CARDINALITY,\n )\n }\n bucket = { keyValues, records: [] }\n buckets.set(dedupKey, bucket)\n }\n bucket.records.push(record)\n }\n\n if (buckets.size >= GROUPBY_WARN_CARDINALITY) {\n warnCardinalityApproaching(fields, buckets.size)\n }\n\n // Reduce each bucket through the spec. Same init/step/finalize\n // pipeline as `reduceRecords` in aggregate.ts, but one state per\n // bucket. Inlining the loop here keeps the per-bucket path tight\n // — calling `reduceRecords` per bucket would recompute\n // `Object.keys(spec)` once per bucket unnecessarily.\n const reducerKeys = Object.keys(spec)\n const out: R[] = []\n for (const bucket of buckets.values()) {\n const state: Record<string, unknown> = {}\n for (const rk of reducerKeys) {\n state[rk] = spec[rk]!.init()\n }\n for (const record of bucket.records) {\n for (const rk of reducerKeys) {\n state[rk] = spec[rk]!.step(state[rk], record)\n }\n }\n // Stamp grouped fields FIRST, in declaration order — this is\n // tested via `Object.keys(row).slice(0, fields.length)`.\n const row: Record<string, unknown> = {}\n for (const f of fields) {\n row[f] = bucket.keyValues[f]\n }\n for (const rk of reducerKeys) {\n row[rk] = spec[rk]!.finalize(state[rk])\n }\n out.push(row as unknown as R)\n }\n return out\n}\n\n/**\n * Grouped aggregation wrapper — the `.groupBy(field).aggregate(spec)`\n * terminal. Shape mirrors `Aggregation<R>` from aggregate.ts: two\n * terminals (`.run()` and `.live()`), spec bound at construction\n * time, upstreams collected for live mode.\n *\n * The generic `R` is the per-row result shape (i.e. a single\n * grouped row), and the terminals return `R[]` — one row per\n * bucket.\n */\nexport class GroupedAggregation<R> {\n private readonly fields: readonly string[]\n\n constructor(\n private readonly executeRecords: () => readonly unknown[],\n fields: string | readonly string[],\n private readonly spec: AggregateSpec,\n private readonly upstreams: readonly AggregationUpstream[],\n /**\n * Optional dict label resolver for `<field>Label` projection\n *. Present when the grouping field is a dictKey.\n */\n private readonly dictLabelResolver?: (\n key: string,\n locale: string,\n fallback?: string | readonly string[],\n ) => Promise<string | undefined>,\n ) {\n this.fields = typeof fields === 'string' ? [fields] : [...fields]\n }\n\n /**\n * Execute the query, group, reduce, and return an array of rows.\n *\n * `opts` (query-form MV grouping): when a `locale` + `i18nFields` are\n * given, the declared group-key `i18nText` fields are resolved to that locale\n * at the `mv` layer BEFORE bucketing — so an i18n group key is a stable string\n * instead of a raw `{locale}` map. The MV executor passes the MV's\n * `i18nLocale`/`i18nFields`; ordinary `.run()` callers pass nothing and are\n * unaffected.\n */\n run(opts?: { locale?: string; i18nFields?: Record<string, I18nTextDescriptor> }): R[] {\n let records = this.executeRecords()\n if (opts?.locale !== undefined && opts.i18nFields !== undefined) {\n const groupI18n: Record<string, I18nTextDescriptor> = {}\n for (const f of this.fields) {\n const d = opts.i18nFields[f]\n if (d !== undefined) groupI18n[f] = d\n }\n if (Object.keys(groupI18n).length > 0) {\n records = records.map((r) => applyI18nLocale(r as Record<string, unknown>, groupI18n, opts.locale!, undefined, 'mv'))\n }\n }\n return groupAndReduce<R>(records, this.fields, this.spec)\n }\n\n /**\n * Execute the query, group, reduce, and resolve `<field>Label` for\n * each result row when the grouping field is a `dictKey` and a\n * `locale` is provided. Returns `R[]` synchronously when\n * no locale is specified (identical to `.run()`).\n *\n * The `<field>Label` field is appended to each row. Rows whose group\n * key has no dictionary entry get `<field>Label: undefined`.\n *\n * Dict-label resolution is single-field only — multi-key groupings\n * do not produce a `<field>Label`. The resolver is only attached\n * by the builder when `fields.length === 1`.\n */\n async runAsync(opts?: {\n locale?: string\n fallback?: string | readonly string[]\n }): Promise<R[]> {\n const rows = groupAndReduce<R>(this.executeRecords(), this.fields, this.spec)\n if (!opts?.locale || !this.dictLabelResolver || this.fields.length !== 1) return rows\n\n const resolve = this.dictLabelResolver\n const locale = opts.locale\n const fallback = opts.fallback\n const field = this.fields[0]!\n const labelKey = `${field}Label`\n\n return Promise.all(\n rows.map(async (row) => {\n const key = (row as Record<string, unknown>)[field]\n if (typeof key !== 'string') return row\n const label = await resolve(key, locale, fallback)\n return { ...(row as Record<string, unknown>), [labelKey]: label } as unknown as R\n }),\n )\n }\n\n /**\n * Build a reactive `LiveAggregation<R[]>` that re-runs the full\n * group-and-reduce pipeline whenever any upstream source notifies\n * of a change. Same error-isolation and idempotent-stop contract\n * as `Aggregation.live()` — the implementation delegates to the\n * same `LiveAggregationImpl` class by threading a fresh\n * recompute closure through the existing constructor.\n *\n * uses naive full re-run on every change. Incremental\n * per-bucket maintenance (apply `step` on inserted records,\n * `remove` on deleted records, route by bucket key) is a future\n * optimization — the reducer protocol admits it, but wiring\n * delta-aware source subscriptions is a separate PR.\n *\n * Always call `live.stop()` when finished.\n */\n live(): LiveAggregation<R[]> {\n const recompute = (): R[] =>\n groupAndReduce<R>(this.executeRecords(), this.fields, this.spec)\n return buildLiveAggregation<R[]>(recompute, this.upstreams)\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-shape/persisted-schemas/canonicalize.ts","../src/with-shape/persisted-schemas/derive.ts"],"sourcesContent":["/**\n * Deterministic JSON serializer: sorts object keys lexicographically at every\n * depth so structurally-equivalent objects produce identical strings. Array\n * order is preserved (arrays are semantically ordered).\n *\n * Used by {@link sha256Hex} to fingerprint a derived JSON Schema for\n * hash-based skip on persisted-schema writes.\n *\n * @module\n */\n\nexport function canonicalize(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value)\n }\n if (Array.isArray(value)) {\n return '[' + value.map(canonicalize).join(',') + ']'\n }\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n const parts = keys.map((k) => JSON.stringify(k) + ':' + canonicalize(obj[k]))\n return '{' + parts.join(',') + '}'\n}\n","/**\n * Derive a {@link PersistedSchemaEnvelope} from a Standard Schema v1\n * validator. Supports zod 3 (via the optional `zod-to-json-schema` peer-dep)\n * and zod 4 (via its native `z.toJSONSchema()`); both are loaded lazily so\n * hub never statically imports zod. Other validator families write a stub\n * envelope flagging the kind.\n *\n * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md\n *\n * @module\n */\n\nimport { canonicalize } from './canonicalize.js'\nimport { sha256Hex } from '../../kernel/enclave/index.js'\nimport type { PersistedSchemaEnvelope, PersistedSchemaKind } from './types.js'\n\n/**\n * Heuristic Zod detection — uses the Standard Schema v1 `~standard.vendor`\n * property (present in Zod v3.23+ and Zod v4+). Falls back to the\n * `_def.typeName` heuristic for older Zod v3 builds that predate Standard\n * Schema support.\n */\nexport function isZodSchema(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false\n // Standard Schema v1 vendor tag — most reliable across Zod majors\n const std = (value as { '~standard'?: { vendor?: unknown } })['~standard']\n if (std && typeof std === 'object' && (std as { vendor?: unknown }).vendor === 'zod') return true\n // Zod v3 fallback: _def.typeName starts with 'Zod' (e.g. 'ZodObject')\n const def = (value as { _def?: { typeName?: unknown } })._def\n if (!def || typeof def !== 'object') return false\n return typeof (def as { typeName?: unknown }).typeName === 'string'\n && ((def as { typeName: string }).typeName).startsWith('Zod')\n}\n\nfunction detectKind(validator: unknown): PersistedSchemaKind {\n if (isZodSchema(validator)) return 'Zod'\n return 'Unknown'\n}\n\n/**\n * Returns `true` when the validator was created with the Zod v4 native API\n * (i.e. `import { z } from 'zod'` where zod resolves to v4). Zod v4 native\n * schemas carry a `_zod` property whereas Zod v3 schemas (and the v3-compat\n * shim shipped inside zod@4 at `zod/v3`) use `_def.typeName`.\n *\n * Kept duck-typed so hub never statically imports zod.\n */\nexport function isZod4Schema(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false\n return '_zod' in value\n}\n\n/**\n * Lazy-require `zod-to-json-schema`. Returns the converter, or throws a\n * clear error if the peer-dep isn't installed.\n */\nasync function loadZodToJsonSchemaConverter(): Promise<(s: unknown) => object> {\n try {\n const mod = (await import('zod-to-json-schema')) as { zodToJsonSchema?: (s: unknown) => object }\n if (!mod.zodToJsonSchema) {\n throw new Error('zod-to-json-schema export missing')\n }\n return mod.zodToJsonSchema\n } catch (err) {\n throw new Error(\n 'persistJsonSchema requires the optional peer-dep `zod-to-json-schema`. '\n + 'Install it: `pnpm add zod-to-json-schema` (or npm/yarn equivalent). '\n + `Original error: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n}\n\n/**\n * Lazy-require Zod v4's built-in `toJSONSchema`. Used when the validator is a\n * Zod v4 native schema and `zod-to-json-schema` is absent or cannot handle it.\n * This is a dynamic import so it does not add a static zod dependency to hub.\n */\nasync function loadZodV4Converter(): Promise<(s: unknown) => object> {\n try {\n const mod = (await import('zod')) as { toJSONSchema?: (s: unknown) => object }\n if (typeof mod.toJSONSchema !== 'function') {\n throw new Error('zod.toJSONSchema not available (zod v4 required)')\n }\n return mod.toJSONSchema\n } catch (err) {\n throw new Error(\n 'persistJsonSchema with a Zod v4 schema requires zod@4 with toJSONSchema. '\n + `Original error: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n}\n\nexport async function derivePersistedSchema(\n validator: unknown,\n): Promise<PersistedSchemaEnvelope> {\n const kind = detectKind(validator)\n const derivedAt = new Date().toISOString()\n\n if (kind === 'Zod') {\n let jsonSchema: object\n\n if (isZod4Schema(validator)) {\n // Zod v4 native schemas: use zod's built-in toJSONSchema (available in\n // zod@4) because zod-to-json-schema@3.x parses _def.typeName which is\n // absent in native v4 schemas.\n const convert = await loadZodV4Converter()\n jsonSchema = convert(validator)\n } else {\n // Zod v3 schemas (or v3-compat shim): use the optional peer-dep\n // zod-to-json-schema for backward compatibility.\n const convert = await loadZodToJsonSchemaConverter()\n jsonSchema = convert(validator)\n }\n\n const canonical = canonicalize(jsonSchema)\n const hash = await sha256Hex(new TextEncoder().encode(canonical))\n return { _noydb_schema: 1, kind, jsonSchema, hash, derivedAt }\n }\n\n return {\n _noydb_schema: 1,\n kind,\n jsonSchema: null,\n hash: null,\n reason: `derivation not yet supported for kind=${kind} (v0 supports Zod only)`,\n derivedAt,\n }\n}\n"],"mappings":";;;;;AAWO,SAAS,aAAa,OAAwB;AACnD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,MAAM,IAAI,YAAY,EAAE,KAAK,GAAG,IAAI;AAAA,EACnD;AACA,QAAM,MAAM;AACZ,QAAM,OAAO,OAAO,KAAK,GAAG,EAAE,KAAK;AACnC,QAAM,QAAQ,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,MAAM,aAAa,IAAI,CAAC,CAAC,CAAC;AAC5E,SAAO,MAAM,MAAM,KAAK,GAAG,IAAI;AACjC;;;ACAO,SAAS,YAAY,OAAyB;AACnD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAExD,QAAM,MAAO,MAAiD,WAAW;AACzE,MAAI,OAAO,OAAO,QAAQ,YAAa,IAA6B,WAAW,MAAO,QAAO;AAE7F,QAAM,MAAO,MAA4C;AACzD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,SAAO,OAAQ,IAA+B,aAAa,YACpD,IAA6B,SAAU,WAAW,KAAK;AAChE;AAEA,SAAS,WAAW,WAAyC;AAC3D,MAAI,YAAY,SAAS,EAAG,QAAO;AACnC,SAAO;AACT;AAUO,SAAS,aAAa,OAAyB;AACpD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,SAAO,UAAU;AACnB;AAMA,eAAe,+BAAgE;AAC7E,MAAI;AACF,UAAM,MAAO,MAAM,OAAO,oBAAoB;AAC9C,QAAI,CAAC,IAAI,iBAAiB;AACxB,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,WAAO,IAAI;AAAA,EACb,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,kKAEqB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACvE;AAAA,EACF;AACF;AAOA,eAAe,qBAAsD;AACnE,MAAI;AACF,UAAM,MAAO,MAAM,OAAO,mBAAK;AAC/B,QAAI,OAAO,IAAI,iBAAiB,YAAY;AAC1C,YAAM,IAAI,MAAM,kDAAkD;AAAA,IACpE;AACA,WAAO,IAAI;AAAA,EACb,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,4FACqB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACvE;AAAA,EACF;AACF;AAEA,eAAsB,sBACpB,WACkC;AAClC,QAAM,OAAO,WAAW,SAAS;AACjC,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAEzC,MAAI,SAAS,OAAO;AAClB,QAAI;AAEJ,QAAI,aAAa,SAAS,GAAG;AAI3B,YAAM,UAAU,MAAM,mBAAmB;AACzC,mBAAa,QAAQ,SAAS;AAAA,IAChC,OAAO;AAGL,YAAM,UAAU,MAAM,6BAA6B;AACnD,mBAAa,QAAQ,SAAS;AAAA,IAChC;AAEA,UAAM,YAAY,aAAa,UAAU;AACzC,UAAM,OAAO,MAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AAChE,WAAO,EAAE,eAAe,GAAG,MAAM,YAAY,MAAM,UAAU;AAAA,EAC/D;AAEA,SAAO;AAAA,IACL,eAAe;AAAA,IACf;AAAA,IACA,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,QAAQ,yCAAyC,IAAI;AAAA,IACrD;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/materialized-views/dependency-analyzer.ts","../src/with-formula/materialized-views/query-hash.ts","../src/with-formula/materialized-views/registry.ts"],"sourcesContent":["import type { Query, QueryPlan } from '../../kernel/query/builder.js'\nimport type { JoinContext } from '../../kernel/query/join.js'\nimport type { MaterializedViewStrategy } from './types.js'\n\n/**\n * Walks a `Query<T>` plan and returns the set of source collection\n * names that any source-write should trigger a refresh on.\n *\n * Handles:\n * - root collection (the one the query was built from)\n * - FK join targets (`.join(field, { as })`)\n *\n * Also handles:\n * - cross-join targets (`.crossJoin(target, { as })`) — v3\n *\n * Deferred:\n * - `.wherePredicate(name)` — v2 predicate primitive\n * - Overlay-name expansion to {base, overlay}\n *\n * The set is materialized at MV registration time. The MV registry\n * uses it to (a) dispatch `onSourceWrite` only to MVs that actually\n * care, and (b) contribute edges to the shared cycle-detection graph.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function analyzeDependencies(query: Query<any>): Set<string> {\n const deps = new Set<string>()\n const plan = query._plan()\n const ctx = query._joinContext()\n\n // The root collection is always a dependency.\n if (ctx?.leftCollection) {\n deps.add(ctx.leftCollection)\n }\n\n // FK join targets contribute additional sources.\n for (const leg of plan.joins) {\n deps.add(leg.target)\n }\n\n // Cross-join targets are also dependency sources — writes to either side\n // must trigger MV refresh. Symmetric with FK-join target handling above.\n for (const clause of plan.clauses) {\n if (clause.type === 'crossJoin') {\n deps.add(clause.target)\n }\n }\n\n // Sub-plans inside OR clauses can carry nested joins. Walk them.\n // (Today only top-level `.join()` populates `plan.joins`, but the\n // OR-group machinery permits sub-plans, so we recurse defensively.)\n walkClausesForJoins(plan, deps, ctx)\n\n return deps\n}\n\nfunction walkClausesForJoins(\n plan: QueryPlan,\n deps: Set<string>,\n ctx: JoinContext | undefined,\n): void {\n void ctx\n // Today `plan.joins` carries all join legs at top level. Sub-plans\n // inside OR groups don't currently support nested joins, so the loop\n // below is a no-op safety net for future builder extensions.\n for (const clause of plan.clauses) {\n if (clause.type === 'group') {\n // Group clauses don't (yet) carry their own joins; this is a\n // forward-compat anchor for when OR-groups support nested\n // sources.\n }\n }\n}\n\n/**\n * Convenience: produce a stable string summary of the query plan\n * suitable for `queryHash` derivation. Captures everything the\n * dependency analyzer reads + the where/orderBy/limit/offset\n * structure that affects materialized rows.\n *\n * `joinContext` is intentionally NOT included — the join-resolution\n * function references would defeat hash determinism. The set of join\n * TARGETS (collection names) IS included via the plan.joins legs.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function summarizeQueryPlan(query: Query<any>): string {\n const plan = query._plan()\n const ctx = query._joinContext()\n return JSON.stringify({\n root: ctx?.leftCollection ?? null,\n clauses: plan.clauses.map(c => {\n if (c.type === 'crossJoin') {\n return {\n type: 'crossJoin',\n target: c.target,\n as: c.as,\n // Inline on: callback: use sentinel — drift detection disabled for this MV\n onPredicateName: c.onPredicateName ?? (c.on ? '[inline]' : null),\n maxRows: c.maxRows ?? null,\n }\n }\n return c\n }),\n orderBy: plan.orderBy,\n limit: plan.limit ?? null,\n offset: plan.offset,\n joins: plan.joins.map(j => ({ field: j.field, as: j.as, target: j.target, mode: j.mode })),\n })\n}\n\n/**\n * Canonical string description of a UNION MV's plan, used as input to\n * `computeQueryHash`.\n *\n * Asymmetry note:\n * - Arm collection names are NOT sorted. Declaration order is\n * semantically meaningful for the dedup-only UNION path —\n * `materializeUnionResult` iterates `spec.unionSources` in\n * declaration order and keeps the first-seen row per composite key\n * (tie-break precedence). If we sorted arms here, a consumer who\n * reordered `unionSources` to change precedence would compute the\n * same `queryHash`, refresh would be a no-op, and stale MV rows\n * would persist. Hashing in declaration order makes any reorder\n * trigger a refresh.\n * - `groupBy` fields ARE sorted. Multi-key groupBy buckets are\n * commutative (`canonicalGroupKey` produces the same composite key\n * regardless of field order in the input spec).\n * - `aggregate` keys ARE sorted. Reducer-spec keys are independent\n * of each other — order of declaration doesn't change output.\n *\n * Per-arm `map` functions are NOT fingerprinted; consumers must bump\n * the MV's `name` (or rely on application-level cache busting) when\n * `map` semantics change non-equivalently.\n */\nexport function summarizeUnionPlan<T extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<T>,\n): string {\n return `union(${summarizeUnionArms(spec)})${summarizeGroupingTail(spec)}`\n}\n\n/**\n * Canonical string description of a projection MV's plan (#810), used\n * as input to `computeQueryHash`.\n *\n * Leg descriptors ARE sorted (contrast with UNION arm order): every\n * leg attaches under a distinct `as` alias, forward legs are\n * one-to-one attachments and collect legs are independent\n * hash-grouped passes, so declaration order never changes the\n * materialized rows — a pure reorder must NOT force a refresh.\n *\n * The projection `map` is NOT fingerprinted (identical limitation as\n * the UNION arm `map`); consumers must bump the MV's `name` when\n * `map` semantics change non-equivalently.\n */\nexport function summarizeProjectionPlan<T extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<T>,\n): string {\n const projection = spec.projection!\n const legs = projection.joins\n .map(leg =>\n 'collect' in leg\n ? `collect:${leg.collect}.${leg.on}→${leg.as}`\n : `field:${leg.field}→${leg.as}`,\n )\n .sort()\n const body = JSON.stringify({ source: projection.source, legs })\n return `projection(${body})${summarizeGroupingTail(spec)}`\n}\n\n/** Arm descriptors for `summarizeUnionPlan` — declaration order preserved (see its doc). */\nfunction summarizeUnionArms<T extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<T>,\n): string {\n return (spec.unionSources ?? [])\n .map(s => {\n // Fold each arm's join legs into the summary so adding or\n // reordering joins (which changes the materialized rows) bumps\n // queryHash. Leg order is declaration-significant (legs chain), so\n // it is NOT sorted — same rationale as arm declaration order.\n const joins = s.join?.length\n ? `[${s.join.map(j => `${j.field}→${j.as}`).join(';')}]`\n : ''\n return `${s.collection}${joins}`\n })\n .join(',')\n}\n\n/**\n * Shared `|groupBy(…)|aggregate(…)|money(…)` tail for the UNION and\n * projection summaries — both feed the same post-map grouping pipeline,\n * so the same spec fields fold into both hashes with the same rules.\n */\nfunction summarizeGroupingTail<T extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<T>,\n): string {\n const groupBy: string = Array.isArray(spec.groupBy)\n ? [...spec.groupBy].sort().join(',')\n : typeof spec.groupBy === 'string'\n ? spec.groupBy\n : ''\n const aggKeys = spec.aggregate ? Object.keys(spec.aggregate).sort().join(',') : ''\n // `moneyFields` changes reducer semantics (float → exact BigInt), so\n // declaring / removing / re-keying it must bump queryHash. Keys are\n // sorted — they're independent of each other (one descriptor per\n // output field), same rationale as aggregate keys.\n const moneyKeys = spec.moneyFields ? Object.keys(spec.moneyFields).sort().join(',') : ''\n return `|groupBy(${groupBy})|aggregate(${aggKeys})|money(${moneyKeys})`\n}\n","/**\n * Deterministic hash of a materialized view strategy's \"shape\": MV\n * name + canonical query-plan summary + sorted dependency-set.\n *\n * Used to detect strategy drift: a row whose `_materializedFrom.queryHash`\n * doesn't match the current strategy is considered stale.\n *\n * Web Crypto SHA-256 — no extra deps. Mirrors the v1\n * `computeStrategyHash` pattern.\n */\nimport { sha256Hex } from '../../kernel/enclave/index.js'\n\nexport async function computeQueryHash(\n mvName: string,\n /**\n * Source-collection set the query depends on. Sorted before\n * canonicalization so set iteration order doesn't affect the hash.\n */\n dependencies: ReadonlySet<string>,\n /**\n * Stringified query-plan summary. The caller produces this from the\n * `Query<T>` builder — concretely: a JSON serialization of clauses +\n * orderBy + limit + offset + joins. Function bodies inside\n * `wherePredicate` are NOT included here (those carry their own\n * `predicateHash` to be folded in by a later sub-issue).\n */\n queryPlanSummary: string,\n): Promise<string> {\n const canonical = JSON.stringify({\n mvName,\n dependencies: [...dependencies].sort(),\n queryPlanSummary,\n })\n const bytes = new TextEncoder().encode(canonical)\n return sha256Hex(bytes)\n}\n\n/**\n * Canonicalize a query plan for hashing. Walks the plan structure\n * with sorted keys so insertion order doesn't perturb the result.\n * Lives here rather than in `query/builder.ts` to keep that module\n * stable across MV-specific evolutions.\n *\n * @internal exported for testing\n */\nexport function canonicalizeQueryPlan(plan: unknown): string {\n return JSON.stringify(plan, (_key, value) => {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n const sorted: Record<string, unknown> = {}\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[k] = (value as Record<string, unknown>)[k]\n }\n return sorted\n }\n return value\n })\n}\n","import { MaterializedViewSourceUnknownError } from '../../kernel/errors.js'\nimport type { ViaGraph, FieldRef, EdgeKind, Grain } from '../../kernel/via/graph.js'\nimport type { Clause, FieldClause } from '../../kernel/query/predicate.js'\nimport type { DeclaredPredicate } from '../../kernel/query/builder.js'\nimport { analyzeDependencies, summarizeQueryPlan, summarizeUnionPlan, summarizeProjectionPlan } from './dependency-analyzer.js'\nimport { computeQueryHash } from './query-hash.js'\nimport type { MaterializedViewStrategy, MVQueryContext } from './types.js'\n\n/**\n * Whole-record artifact-grain field marker (#638 Task 2) — MUST match\n * `derivations/registry.ts`'s and `vault.ts`'s overlay-edge marker so\n * cross-registry edges (a derivation feeding an MV, or vice versa) resolve\n * to the SAME graph node. See that file's `WHOLE_RECORD` doc comment.\n */\nconst WHOLE_RECORD = '*'\n\n/**\n * One registered MV strategy alongside its derived metadata. Stored\n * type-erased on `TRow` so the registry can hold heterogeneous MVs.\n */\nexport interface RegisteredMV {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly spec: MaterializedViewStrategy<any>\n /** Output collection name (`spec.output?.collection ?? spec.name`). */\n readonly outputCollection: string\n /** Set of source collections; populated at registration via the analyzer. */\n readonly dependencies: ReadonlySet<string>\n /** Canonical `queryHash` — `_materializedFrom.queryHash` for every emitted row. */\n readonly queryHash: string\n /**\n * Top-level FieldClauses on the partition field, captured at\n * registration time. Used by the cycle detector to resolve\n * same-collection-as-source edges via the partition-discriminator\n * check. Empty when `spec.output?.partition` is undefined.\n */\n readonly partitionClauses: readonly FieldClause[]\n /**\n * #638 Task 2 — `'record'` for a row-per-source-row Query<T> or\n * `unionSources` MV, `'aggregate'` for a `.groupBy().aggregate()` MV\n * (the shape that requires explicit `sources`, since the dependency\n * analyzer can't walk an aggregate plan). Feeds `edges()`'s grain.\n */\n readonly grain: Grain\n}\n\n/**\n * Vault-internal registry of MV strategies. Owned by `Vault`; not\n * exported. Parallel to v1's `DerivationRegistry`; the two graphs share\n * a single cycle-detection pass at vault open (see `validate`).\n *\n * @internal\n */\nexport class MaterializedViewRegistry {\n /** Keyed by `spec.name`. */\n private readonly _byName = new Map<string, RegisteredMV>()\n /** Keyed by dependency source-collection → MVs that depend on it. */\n private readonly _bySource = new Map<string, RegisteredMV[]>()\n /**\n * Projection MVs (#810) whose forward-leg ref() targets could not be\n * resolved at registration time. A forward leg names only the FK\n * FIELD; its dependency is the field's ref() TARGET, declared on the\n * source collection — but registration runs at vault open, BEFORE\n * user code declares collections (and their refs). Constructing the\n * source collection here to look them up would be worse: a later\n * `vault.collection(source, { refs })` call only registers refs on a\n * cache miss, so an early construction would silently drop them.\n * Instead each entry carries a non-constructive ref-registry probe;\n * `mvsForSource` retries on every dispatch until every forward\n * target has resolved (see `_resolvePendingForwardDeps`).\n */\n private readonly _pendingForwardDeps: Array<{\n reg: RegisteredMV\n fields: Set<string>\n resolveTarget: (field: string) => string | null\n }> = []\n\n /**\n * Register an MV. Invokes `spec.query()` once at registration time to\n * read the plan + join context; the resulting `Query<T>` is discarded\n * after dependency extraction. `vault.collection(...)` must therefore\n * be functional by the time this runs — typically wired from\n * `Vault._initMaterializedViews` after collection bootstrap.\n *\n * Throws `MaterializedViewSourceUnknownError` if the analyzer\n * surfaces a dependency the vault doesn't know about (when a\n * `knownCollections` checker is supplied).\n */\n async register(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: MaterializedViewStrategy<any>,\n db: MVQueryContext,\n options?: { knownCollections?: (name: string) => boolean },\n ): Promise<void> {\n // Build a predicate-aware db wrapper. If `spec.predicates` is\n // declared, the wrapper intercepts `.collection().query()` and\n // attaches the predicates map to the resulting Query<T>. With no\n // predicates declared, the wrapper is the original db unchanged.\n const dbForQuery = spec.predicates ? wrapDbWithPredicates(db, spec.predicates) : db\n\n // Invoke the query callback once to inspect its plan / dependencies.\n // For Query<T> shapes the analyzer extracts deps + plan summary\n // automatically. Aggregation / GroupedAggregation shapes don't\n // expose the underlying Query, so the spec must declare `sources`\n // explicitly. `partitionClauses` are only populated for Query<T>\n // since same-collection-partition is a non-aggregate concern.\n // UNION-form strategies: dependencies and plan summary come\n // straight off the strategy — no `query` callback to introspect.\n // The dependency-analyzer + summarizer are bypassed entirely; the\n // executor handles materialization via `materializeUnionResult`.\n let dependencies: Set<string>\n let queryPlanSummary: string\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let qAny: any = null\n let isQuery = false\n // Projection-form (#810): forward legs whose ref() target wasn't\n // resolvable at registration time — folded in lazily, see\n // `_pendingForwardDeps`.\n const pendingForwardFields: string[] = []\n if (spec.unionSources) {\n dependencies = new Set(spec.unionSources.map(s => s.collection))\n // Per-arm joins resolve right-side collections that aren't among\n // the arm `collection`s. The consumer lists those in `sources`;\n // fold them into the dependency set so a write to a join-target\n // collection triggers MV refresh (and contributes a cycle edge).\n if (spec.sources) for (const s of spec.sources) dependencies.add(s)\n queryPlanSummary = summarizeUnionPlan(spec)\n } else if (spec.projection) {\n // Projection-form (#810): dependencies are all AUTO — the primary\n // source, every collect leg's collection (both literal names), and\n // every forward leg's ref() target. Forward targets usually can't\n // resolve yet (refs are declared after vault open) — those go\n // through the pending-resolution path below. Explicit `sources`\n // remains additive, same as the other two forms.\n const projection = spec.projection\n dependencies = new Set([projection.source])\n for (const leg of projection.joins) {\n if ('collect' in leg) dependencies.add(leg.collect)\n else pendingForwardFields.push(leg.field)\n }\n if (spec.sources) for (const s of spec.sources) dependencies.add(s)\n queryPlanSummary = summarizeProjectionPlan(spec)\n } else {\n const q = spec.query!(dbForQuery)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n qAny = q as any\n isQuery = typeof qAny._plan === 'function'\n if (isQuery) {\n // `q` is the `Query` arm of the union here (runtime-confirmed via\n // `qAny._plan` above); reuse the already-`any` `qAny` the block uses\n // for `_plan()` rather than re-narrowing.\n dependencies = analyzeDependencies(qAny)\n queryPlanSummary = summarizeQueryPlan(qAny)\n // Fold `.wherePredicate(name, ctx)` references into the plan\n // summary so predicate function or ctx changes (signalled by\n // bumping `hash` or supplying a different ctx) propagate into\n // `queryHash` and force refresh on next visit.\n const predicateRefs = extractPredicateRefs(qAny._plan())\n if (predicateRefs.length > 0) {\n queryPlanSummary = JSON.stringify({ plan: queryPlanSummary, predicates: predicateRefs })\n }\n // If `sources` is ALSO declared, take the union (consumer's\n // explicit list extends the auto-analyzed set).\n if (spec.sources) for (const s of spec.sources) dependencies.add(s)\n } else {\n // Aggregate shape: require explicit `sources`.\n if (!spec.sources || spec.sources.length === 0) {\n throw new Error(\n `withMaterializedView \"${spec.name}\": query() returned an aggregate ` +\n `(Aggregation or GroupedAggregation) but no \\`sources\\` field is declared. ` +\n `The dependency analyzer cannot walk through groupBy().aggregate() ` +\n `back to the source — declare sources: [...] explicitly.`,\n )\n }\n dependencies = new Set(spec.sources)\n // Aggregate plans don't carry a chainable query plan for summary\n // purposes; the dep-set + spec.name serve as the queryHash inputs.\n queryPlanSummary = JSON.stringify({ aggregate: true, sources: [...spec.sources].sort() })\n }\n }\n\n // Sanity-check declared dependencies against the vault's known\n // collections. Optional — when the checker isn't supplied (test\n // wiring, in-process composition) the registration succeeds and\n // any typo surfaces at first onSourceWrite as a no-op.\n if (options?.knownCollections) {\n for (const dep of dependencies) {\n if (!options.knownCollections(dep)) {\n throw new MaterializedViewSourceUnknownError(spec.name, dep)\n }\n }\n }\n\n const outputCollection = spec.output?.collection ?? spec.name\n const queryHash = await computeQueryHash(spec.name, dependencies, queryPlanSummary)\n // For same-collection-as-source MVs, capture the where-clauses on\n // the partition field so cycle detection can prove disjointness.\n // Only applicable to Query<T> shapes — aggregate MVs don't carry\n // a chainable plan to inspect (and same-collection aggregation\n // doesn't make sense for same-collection aggregation).\n const partitionClauses: FieldClause[] = []\n const partitionField = spec.output?.partition?.field\n if (partitionField !== undefined && isQuery) {\n const plan = qAny._plan()\n for (const clause of plan.clauses) {\n if (isFieldClauseOnField(clause, partitionField)) partitionClauses.push(clause)\n }\n }\n // #638 Task 2 — 'aggregate' only for the explicit-sources aggregate shape\n // (groupBy().aggregate() with no chainable plan); a row-per-source-row\n // Query<T>, unionSources, or projection MV is 'record'.\n const grain: Grain = spec.unionSources || spec.projection || isQuery ? 'record' : 'aggregate'\n const reg: RegisteredMV = { spec, outputCollection, dependencies, queryHash, partitionClauses, grain }\n\n this._byName.set(spec.name, reg)\n for (const dep of dependencies) {\n const arr = this._bySource.get(dep)\n if (arr) arr.push(reg)\n else this._bySource.set(dep, [reg])\n }\n if (pendingForwardFields.length > 0) {\n // Non-constructive ref-registry probe: the Vault (the real\n // MVQueryContext) carries its RefRegistry as a private field,\n // reachable structurally at runtime. Test stubs without one simply\n // never resolve — forward deps then come only from explicit `sources`.\n const refRegistry = (db as unknown as {\n refRegistry?: { getOutbound(collection: string): Record<string, { target: string }> }\n }).refRegistry\n const source = spec.projection!.source\n this._pendingForwardDeps.push({\n reg,\n fields: new Set(pendingForwardFields),\n resolveTarget: (field) => refRegistry?.getOutbound(source)[field]?.target ?? null,\n })\n // Immediate attempt — covers refs already declared by the time\n // this MV registers (re-registration flows, test wiring).\n this._resolvePendingForwardDeps()\n }\n }\n\n /** All MVs that depend on `source`, in registration order. */\n mvsForSource(source: string): ReadonlyArray<RegisteredMV> {\n if (this._pendingForwardDeps.length > 0) this._resolvePendingForwardDeps()\n return this._bySource.get(source) ?? []\n }\n\n /**\n * Retry ref() resolution for every pending projection forward leg\n * (#810). A field resolves once its source collection's refs have\n * been declared; the target then folds into the owning MV's\n * dependency set and `_bySource`, so subsequent writes to it\n * dispatch a refresh. Idempotent and cheap — the pending list\n * empties as fields resolve, and `mvsForSource` skips the call\n * entirely once it's empty.\n *\n * Late-resolved targets do NOT retro-feed `edges()` — the cycle\n * pass runs once at vault open, before refs exist. A cycle routed\n * exclusively through a forward leg is therefore not detected at\n * open (collect + source edges, the literal names, are).\n */\n private _resolvePendingForwardDeps(): void {\n for (let i = this._pendingForwardDeps.length - 1; i >= 0; i--) {\n const entry = this._pendingForwardDeps[i]!\n for (const field of [...entry.fields]) {\n const target = entry.resolveTarget(field)\n if (target === null) continue\n entry.fields.delete(field)\n if (entry.reg.dependencies.has(target)) continue\n // `dependencies` is declared ReadonlySet for consumers; the\n // registry owns the underlying Set and is the one writer.\n ;(entry.reg.dependencies as Set<string>).add(target)\n const arr = this._bySource.get(target)\n if (arr) arr.push(entry.reg)\n else this._bySource.set(target, [entry.reg])\n }\n if (entry.fields.size === 0) this._pendingForwardDeps.splice(i, 1)\n }\n }\n\n /** Single MV by name, or `undefined`. */\n byName(name: string): RegisteredMV | undefined {\n return this._byName.get(name)\n }\n\n /** Iterate over every registered MV. */\n all(): ReadonlyArray<RegisteredMV> {\n return [...this._byName.values()]\n }\n\n /**\n * Graph edges for #638 Task 2: one `'mv'` edge per registered MV — target\n * = the output collection (a `WHOLE_RECORD` artifact node), sources =\n * every dependency (also `WHOLE_RECORD` nodes). Same-collection edges\n * (`dep === outputCollection`) are skipped IFF the MV declares an\n * `output.partition` discriminator AND the query has a where-clause that\n * provably excludes the partition value (`partitionDisjoint`) — the SAME\n * condition the old local DFS used. Grain comes from the registration-time\n * `RegisteredMV.grain`.\n */\n edges(): ReadonlyArray<{ readonly target: FieldRef; readonly sources: readonly FieldRef[]; readonly kind: EdgeKind; readonly grain: Grain }> {\n const out: Array<{ target: FieldRef; sources: FieldRef[]; kind: EdgeKind; grain: Grain }> = []\n for (const reg of this._byName.values()) {\n const sources: FieldRef[] = []\n for (const dep of reg.dependencies) {\n if (dep === reg.outputCollection && partitionDisjoint(reg)) continue\n sources.push({ collection: dep, field: WHOLE_RECORD })\n }\n if (sources.length === 0) continue\n out.push({ target: { collection: reg.outputCollection, field: WHOLE_RECORD }, sources, kind: 'mv', grain: reg.grain })\n }\n return out\n }\n\n /**\n * Cycle detection, delegated to `ViaGraph.assertAcyclic()` (#638 Task 2 —\n * retires the local DFS). `graph` is the caller's shared per-vault graph,\n * ALREADY carrying this registry's `edges()` AND `DerivationRegistry`'s\n * (registered by the caller — see `Vault._initMaterializedViews`, which\n * runs after `_initDerivations`, so a pure-derivation cycle already threw\n * `DerivationCycleError` there; any cycle surfacing here necessarily\n * touches an MV edge). Throws `MaterializedViewCycleError` — SAME class as\n * before.\n */\n validate(graph: ViaGraph): void {\n graph.assertAcyclic()\n }\n}\n\n/**\n * Type guard: is the clause a top-level `FieldClause` on the given\n * field? Used by the partition-disjoint check.\n *\n * @internal\n */\nfunction isFieldClauseOnField(clause: Clause, field: string): clause is FieldClause {\n return clause.type === 'field' && clause.field === field\n}\n\n/**\n * Wrap an `MVQueryContext` so its `.collection().query()` returns a\n * Query<T> with the MV's declared predicates attached. Bare Queries\n * (outside of any MV) don't gain `.wherePredicate()` — only Queries\n * obtained through this wrapped db do.\n *\n * @internal\n */\nexport function wrapDbWithPredicates(\n db: MVQueryContext,\n predicates: NonNullable<MaterializedViewStrategy<Record<string, unknown>>['predicates']>,\n): MVQueryContext {\n // Build the predicate map once — the fn signature in the MV spec\n // is row-typed but the QueryBuilder casts to unknown, so we widen\n // here for the Map.\n const map = new Map<string, DeclaredPredicate>()\n for (const [name, decl] of Object.entries(predicates)) {\n map.set(name, {\n hash: decl.hash,\n fn: decl.fn as (record: unknown, ctx?: unknown) => boolean,\n })\n }\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n collection<T extends Record<string, unknown>>(name: string): any {\n const c = db.collection<T>(name)\n // Return an object that delegates everything to `c` but\n // overrides `.query()` to attach predicates via the new\n // `Query._withPredicates()` accessor.\n return new Proxy(c, {\n get(target, prop, receiver) {\n if (prop === 'query') {\n return (...args: unknown[]) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const q = (target.query as any)(...args)\n // For non-aggregate Query<T>, attach predicates. For\n // legacy predicate-arg overload that returns T[] (sync\n // filter), pass through unchanged.\n \n if (q && typeof q._withPredicates === 'function') {\n return q._withPredicates(map)\n }\n return q\n }\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n },\n }\n}\n\n/**\n * Walk a QueryPlan's clauses and collect predicate-reference markers\n * for `queryHash` derivation. Returns a sorted array (deterministic\n * order) of `{ name, predicateHash, ctxHash }` tuples — these are the\n * hashable identity of each `.wherePredicate()` call site.\n *\n * @internal\n */\nfunction extractPredicateRefs(\n plan: { clauses: readonly Clause[] },\n): Array<{ name: string; predicateHash: string; ctxHash: string }> {\n const refs: Array<{ name: string; predicateHash: string; ctxHash: string }> = []\n const walk = (clauses: readonly Clause[]): void => {\n for (const c of clauses) {\n if (c.type === 'wherePredicate') {\n refs.push({ name: c.name, predicateHash: c.predicateHash, ctxHash: c.ctxHash })\n } else if (c.type === 'group') {\n walk(c.clauses)\n }\n }\n }\n walk(plan.clauses)\n // Stable-sort by (name, predicateHash, ctxHash) — same predicate\n // appearing twice with different ctx hashes both flow through.\n refs.sort((a, b) => {\n if (a.name !== b.name) return a.name < b.name ? -1 : 1\n if (a.predicateHash !== b.predicateHash) return a.predicateHash < b.predicateHash ? -1 : 1\n return a.ctxHash < b.ctxHash ? -1 : a.ctxHash > b.ctxHash ? 1 : 0\n })\n return refs\n}\n\n/**\n * Provability check for the same-collection partition-discriminator.\n * Returns `true` when\n * the captured partition clauses on the MV's query provably exclude\n * the partition's value — meaning the input filter and the output\n * partition are disjoint and the same-collection edge isn't really a\n * cycle.\n *\n * Supported provability shapes (narrow on purpose — DERIV-PP30-001\n * is the load-bearing case):\n *\n * - `.where(field, '==', X)` where X !== partition.value → disjoint\n * - `.where(field, '!=', partition.value)` → disjoint\n * - `.where(field, 'in', [...])` where partition.value NOT in list → disjoint\n *\n * Anything else (no clause on the partition field, an 'in' list that\n * contains partition.value, unsupported operators) → not disjoint,\n * the cycle detector surfaces `MaterializedViewCycleError`.\n *\n * @internal\n */\nfunction partitionDisjoint(reg: RegisteredMV): boolean {\n const partition = reg.spec.output?.partition\n if (partition === undefined) return false\n const value = partition.value\n // The OR-semantics of multiple where-clauses on the same field\n // would muddy this check. v2 only treats AND-chained clauses;\n // any clause that proves disjoint is sufficient.\n for (const c of reg.partitionClauses) {\n if (c.op === '==' && c.value !== value) return true\n if (c.op === '!=' && c.value === value) return true\n if (c.op === 'in' && Array.isArray(c.value)) {\n const list = c.value as readonly unknown[]\n if (!list.includes(value)) return true\n }\n }\n return false\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/guards/with-guard.ts","../src/with-audit/guards/immutable-guard.ts","../src/with-audit/guards/transition-guard.ts"],"sourcesContent":["import { ValidationError } from '../../kernel/errors.js'\nimport type { GuardStrategy, GuardStrategyHandle } from './types.js'\n\n/**\n * Register a guard for a collection. Guards run on every `put()` /\n * `delete()` for the named collection (after permissions, before\n * encryption) and may:\n *\n * - `check` — block writes by throwing (typically `RecordLockedError`)\n * - `frozenFields` — freeze specific fields once a condition is true\n * - `amendment` — declare an authorized-override path with invariant\n *\n * Pass the returned handle to `createNoydb({ strategies: [...] })`.\n *\n * @see docs/superpowers/specs/2026-05-18-guards-design.md\n */\nexport function withGuard<T extends Record<string, unknown>>(\n strategy: GuardStrategy<T>,\n): GuardStrategyHandle<T> {\n if (!strategy.collection || strategy.collection.length === 0) {\n throw new ValidationError('withGuard: collection name is required')\n }\n return {\n __noydb_strategy: 'guard',\n spec: strategy,\n }\n}\n","/**\n * `immutableGuard` — declarative WORM / append-only sugar over the guard\n * service.\n *\n * Issued fiscal documents (invoices, DDTs) must be immutable after issue.\n * That is expressible today with a hand-rolled `withGuard` (block on\n * `check`/`onDelete`, allow an admin `amendment`), but the boilerplate is\n * repetitive and easy to get subtly wrong. `immutableGuard` generates\n * exactly that guard from a declarative config, reusing the guard\n * machinery wholesale — `check`/`onDelete` rejection, the ledgered\n * `amendment` override, and composition with `periods`/`history`.\n *\n * ```ts\n * createNoydb({ guardStrategies: [\n * immutableGuard({\n * collection: 'invoices',\n * after: (r) => r.status === 'issued', // immutable once issued\n * }),\n * ] })\n * ```\n *\n * A record is mutable until `after(record)` holds; from then on, updates\n * and deletes throw `RecordLockedError` unless performed inside an\n * `amendment` transaction by an authorized role (the override is\n * ledgered by the guard amendment mechanism). `appendOnly: true` is\n * shorthand for `after: () => true` — immutable from creation.\n */\n\nimport { withGuard } from './with-guard.js'\nimport type { GuardStrategy, GuardStrategyHandle, GuardContext, GuardChange } from './types.js'\nimport { RecordLockedError, ValidationError } from '../../kernel/errors.js'\n\nexport interface ImmutableGuardConfig<T extends Record<string, unknown>> {\n /** The collection to make WORM. */\n collection: string\n /**\n * A record becomes immutable once this predicate holds. Evaluated on\n * the *existing* (already-persisted) record, so the write that first\n * makes it true is still allowed; subsequent writes are blocked.\n * Mutually exclusive with `appendOnly`.\n */\n after?: (record: T) => boolean\n /** Shorthand for `after: () => true` — immutable from creation. */\n appendOnly?: boolean\n /** Roles permitted to override via an amendment transaction. Default `['admin', 'owner']`. */\n amendmentRoles?: ReadonlyArray<'admin' | 'owner'>\n /**\n * Optional set-level invariant run over the amendment change-set after\n * the writes execute. Signature matches `GuardStrategy.amendment.invariant`\n * exactly: it receives every {before, after} pair touching this\n * collection in the amendment plus the guard context; throwing reverts\n * the whole amendment and surfaces as `InvariantError`.\n *\n * Use this to keep a constraint inviolable EVEN under amendment — e.g.\n * forbid deleting an issued document by re-throwing on any\n * `before !== null && after === null` change, or assert a cross-record\n * sum is preserved. When omitted the amendment is unconditionally\n * allowed (the amendment itself is the sanctioned, ledgered override) —\n * this is the backward-compatible default.\n */\n amendmentInvariant?: (\n changes: ReadonlyArray<GuardChange<T>>,\n ctx: GuardContext<T>,\n ) => Promise<void> | void\n}\n\nfunction recordId(record: Record<string, unknown> | null): string {\n const id = record?.id\n return typeof id === 'string' ? id : ''\n}\n\n/**\n * Build an immutability guard. Pass the returned handle to\n * `createNoydb({ guardStrategies: [...] })`.\n */\nexport function immutableGuard<T extends Record<string, unknown>>(\n config: ImmutableGuardConfig<T>,\n): GuardStrategyHandle<T> {\n const { collection, after, appendOnly, amendmentRoles, amendmentInvariant } = config\n if (appendOnly && after !== undefined) {\n throw new ValidationError('immutableGuard: `after` and `appendOnly` are mutually exclusive')\n }\n if (!appendOnly && after === undefined) {\n throw new ValidationError('immutableGuard: provide `after` or `appendOnly: true`')\n }\n\n const isImmutable: (record: T) => boolean = appendOnly ? () => true : after!\n const reason = appendOnly ? 'append-only collection' : 'record is immutable after issue'\n\n const spec: GuardStrategy<T> = {\n collection,\n // Block updates to an already-immutable record. Inserts (existing\n // null) and the transition write that first makes the record\n // immutable are allowed — `after` reads the prior state.\n check: (incoming: T, ctx: GuardContext<T>) => {\n if (ctx.existing !== null && isImmutable(ctx.existing)) {\n throw new RecordLockedError(collection, recordId(incoming as Record<string, unknown>), reason)\n }\n },\n // Block deletes of an immutable record.\n onDelete: (existing: T) => {\n if (isImmutable(existing)) {\n throw new RecordLockedError(collection, recordId(existing as Record<string, unknown>), reason)\n }\n },\n // The authorized override: inside an amendment transaction the\n // check/onDelete are skipped and the change is ledgered. By default\n // there is no extra invariant — the amendment itself is the\n // sanctioned exception. Callers may supply `amendmentInvariant` to\n // keep a constraint inviolable even under amendment (e.g. forbid\n // deletes, or preserve a cross-record sum); a throw reverts the\n // amendment and surfaces as `InvariantError`.\n amendment: {\n roles: amendmentRoles ?? ['admin', 'owner'],\n invariant: amendmentInvariant ?? (() => {\n /* allow — the amendment is the override, and is ledgered */\n }),\n },\n }\n\n return withGuard<T>(spec)\n}\n","/**\n * `transitionGuard` — declarative state-machine sugar over the guard\n * service.\n *\n * Any record with a lifecycle field (invoice `status`, order state,\n * ticket workflow, subscription phase) needs transition validation: a\n * write may only move the field along a declared arc. That is expressible\n * with a hand-rolled `withGuard({ check })`, but every consumer\n * re-implements the same graph lookup + error. `transitionGuard`\n * generates exactly that guard from a state graph, reusing the guard\n * machinery wholesale — `check` rejection, the ledgered `amendment`\n * override, and composition with `periods`/`history`.\n *\n * It generalizes {@link immutableGuard}: WORM is the special case \"every\n * state has no outgoing arcs\", i.e. `transitions` mapping each state to `[]`.\n *\n * ```ts\n * createNoydb({ guardStrategies: [\n * transitionGuard<Sale>({\n * collection: 'sales', field: 'status',\n * transitions: { // absence of an arc = forbidden\n * draft: ['to_verify', 'cancelled'],\n * to_verify: ['proforma', 'draft', 'cancelled'],\n * proforma: ['invoiced', 'cancelled'],\n * invoiced: ['paid'], paid: [], cancelled: [],\n * },\n * initial: ['draft', 'to_verify'], // allowed status on insert\n * }),\n * ] })\n * ```\n *\n * Semantics:\n * - **Insert** (`ctx.existing === null`): `incoming[field]` must be in\n * `initial`. When `initial` is omitted, any value is allowed on insert.\n * - **Update**: the arc `(existing[field] → incoming[field])` must be\n * listed in `transitions[from]`, else `IllegalTransitionError`. A\n * same-value write (`from === to`) is allowed when `allowIdempotent`\n * (default `true`) — so writes that touch other fields without moving\n * state pass.\n * - **Override**: inside an `amendment` transaction by an authorized role\n * the check is skipped and the change is ledgered (mirrors every guard).\n *\n * The status graph is caller-supplied data — no UI, no domain logic.\n */\n\nimport { withGuard } from './with-guard.js'\nimport type { GuardStrategy, GuardStrategyHandle, GuardContext, GuardChange } from './types.js'\nimport { IllegalTransitionError, ValidationError } from '../../kernel/errors.js'\n\nexport interface TransitionGuardConfig<T extends Record<string, unknown>> {\n /** The collection whose state field is governed. */\n collection: string\n /** The state field on the record (e.g. `'status'`). */\n field: keyof T & string\n /**\n * The transition graph: each state maps to the states it may move to.\n * A state absent from the map (or mapped to `[]`) is terminal — no\n * outgoing arc, so any non-idempotent write from it is rejected.\n */\n transitions: Readonly<Record<string, readonly string[]>>\n /**\n * States allowed as the initial value on insert (`existing === null`).\n * Omit to allow any value on insert.\n */\n initial?: readonly string[]\n /**\n * Allow a same-value write (`from === to`) on update. Default `true` —\n * lets a put that changes other fields, but not the state, through.\n */\n allowIdempotent?: boolean\n /** Roles permitted to override via an amendment transaction. Default `['admin', 'owner']`. */\n amendmentRoles?: ReadonlyArray<'admin' | 'owner'>\n /**\n * Optional set-level invariant run over the amendment change-set after\n * the writes execute. Signature matches `GuardStrategy.amendment.invariant`\n * exactly. When omitted the amendment is unconditionally allowed (the\n * amendment itself is the sanctioned, ledgered override) — the\n * backward-compatible default. Mirrors {@link immutableGuard}.\n */\n amendmentInvariant?: (\n changes: ReadonlyArray<GuardChange<T>>,\n ctx: GuardContext<T>,\n ) => Promise<void> | void\n}\n\nfunction recordId(record: Record<string, unknown> | null): string {\n const id = record?.id\n return typeof id === 'string' ? id : ''\n}\n\nfunction stateOf(record: Record<string, unknown>, field: string): string {\n const v = record[field]\n return typeof v === 'string' ? v : String(v)\n}\n\n/**\n * Build a state-machine transition guard. Pass the returned handle to\n * `createNoydb({ guardStrategies: [...] })`.\n */\nexport function transitionGuard<T extends Record<string, unknown>>(\n config: TransitionGuardConfig<T>,\n): GuardStrategyHandle<T> {\n const { collection, field, transitions, initial, amendmentRoles, amendmentInvariant } = config\n const allowIdempotent = config.allowIdempotent ?? true\n\n if (!field) {\n throw new ValidationError('transitionGuard: `field` is required')\n }\n if (transitions === undefined || typeof transitions !== 'object') {\n throw new ValidationError('transitionGuard: `transitions` must be a state→states map')\n }\n\n const spec: GuardStrategy<T> = {\n collection,\n check: (incoming: T, ctx: GuardContext<T>) => {\n const rec = incoming as Record<string, unknown>\n const to = stateOf(rec, field)\n\n // Insert — gate on the allowed initial set (any value if unset).\n if (ctx.existing === null) {\n if (initial !== undefined && !initial.includes(to)) {\n throw new IllegalTransitionError(collection, recordId(rec), '(none)', to)\n }\n return\n }\n\n // Update — the arc (from → to) must be a declared edge.\n const from = stateOf(ctx.existing as Record<string, unknown>, field)\n if (from === to) {\n if (allowIdempotent) return\n throw new IllegalTransitionError(collection, recordId(rec), from, to)\n }\n const allowed = transitions[from] ?? []\n if (!allowed.includes(to)) {\n throw new IllegalTransitionError(collection, recordId(rec), from, to)\n }\n },\n // The authorized override: inside an amendment transaction the check\n // is skipped and the change is ledgered. By default no extra invariant\n // — the amendment itself is the sanctioned exception. Callers may\n // supply `amendmentInvariant` to keep a constraint inviolable even\n // under amendment; a throw reverts the amendment as `InvariantError`.\n amendment: {\n roles: amendmentRoles ?? ['admin', 'owner'],\n invariant: amendmentInvariant ?? (() => {\n /* allow — the amendment is the override, and is ledgered */\n }),\n },\n }\n\n return withGuard<T>(spec)\n}\n"],"mappings":"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","names":["recordId"]}
|