@noy-db/hub 0.2.0-pre.10 → 0.2.0-pre.12

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