@noy-db/hub 0.2.0-pre.14 → 0.2.0-pre.16

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 (219) hide show
  1. package/dist/aggregate/index.cjs +160 -64
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +2 -2
  4. package/dist/aggregate/index.d.ts +2 -2
  5. package/dist/aggregate/index.js +3 -3
  6. package/dist/attestation/index.cjs.map +1 -1
  7. package/dist/attestation/index.d.cts +5 -5
  8. package/dist/attestation/index.d.ts +5 -5
  9. package/dist/attestation/index.js +4 -4
  10. package/dist/blobs/index.cjs.map +1 -1
  11. package/dist/blobs/index.d.cts +6 -6
  12. package/dist/blobs/index.d.ts +6 -6
  13. package/dist/blobs/index.js +3 -3
  14. package/dist/bundle/index.cjs +607 -114
  15. package/dist/bundle/index.cjs.map +1 -1
  16. package/dist/bundle/index.d.cts +7 -7
  17. package/dist/bundle/index.d.ts +7 -7
  18. package/dist/bundle/index.js +7 -7
  19. package/dist/{chunk-BIYRQQV6.js → chunk-3YWP3WBP.js} +3 -3
  20. package/dist/{chunk-VU7SWWT5.js → chunk-42FEUPZQ.js} +10 -6
  21. package/dist/chunk-42FEUPZQ.js.map +1 -0
  22. package/dist/{chunk-ACKFRSAH.js → chunk-667MB6AH.js} +132 -54
  23. package/dist/chunk-667MB6AH.js.map +1 -0
  24. package/dist/{chunk-A5ZOOZFB.js → chunk-6H2ZUNR7.js} +2 -2
  25. package/dist/{chunk-7HT2MEZ5.js → chunk-7BQ4QWYX.js} +3 -3
  26. package/dist/{chunk-DQU36Q7I.js → chunk-7Z7KSVA5.js} +13 -4
  27. package/dist/chunk-7Z7KSVA5.js.map +1 -0
  28. package/dist/{chunk-WBAYSNUQ.js → chunk-BI6ETQPF.js} +2 -2
  29. package/dist/{chunk-56DJ7JVK.js → chunk-BR3AMFGS.js} +2 -2
  30. package/dist/chunk-CJORTUJ2.js +524 -0
  31. package/dist/chunk-CJORTUJ2.js.map +1 -0
  32. package/dist/{chunk-YNTBADIY.js → chunk-CZI2A4MQ.js} +2 -2
  33. package/dist/{chunk-COFPAMX6.js → chunk-DLZ2ONOD.js} +3 -3
  34. package/dist/{chunk-KGCORI4L.js → chunk-DUREQF5W.js} +266 -66
  35. package/dist/chunk-DUREQF5W.js.map +1 -0
  36. package/dist/{chunk-PE4AQGFH.js → chunk-E2CDVKMH.js} +3 -3
  37. package/dist/{chunk-GC4V7RU7.js → chunk-F3BPIPLS.js} +1 -1
  38. package/dist/{chunk-GC4V7RU7.js.map → chunk-F3BPIPLS.js.map} +1 -1
  39. package/dist/{chunk-L2FE64BU.js → chunk-FFXM3ZIF.js} +2 -2
  40. package/dist/{chunk-5LQG6ZO2.js → chunk-G4SCICH5.js} +8 -3
  41. package/dist/chunk-G4SCICH5.js.map +1 -0
  42. package/dist/{chunk-WGHU7BLI.js → chunk-GNI5STXQ.js} +2 -2
  43. package/dist/{chunk-UWNYBOOO.js → chunk-HBXJ37ZY.js} +11 -5
  44. package/dist/chunk-HBXJ37ZY.js.map +1 -0
  45. package/dist/{chunk-NP6EZT44.js → chunk-IQLVUT37.js} +2 -2
  46. package/dist/{chunk-7PS7EOCF.js → chunk-IXBIFDEW.js} +2 -2
  47. package/dist/{chunk-LX3CB26H.js → chunk-KABJXG2F.js} +2 -2
  48. package/dist/{chunk-3EWA37FV.js → chunk-L2BNJ6HM.js} +32 -276
  49. package/dist/chunk-L2BNJ6HM.js.map +1 -0
  50. package/dist/{chunk-DKO2QFSA.js → chunk-OB2ZJQ2D.js} +2 -2
  51. package/dist/{chunk-4PEFEETV.js → chunk-OMAMZKKD.js} +2 -2
  52. package/dist/{chunk-EGD5DXFT.js → chunk-OQSRJG6A.js} +13 -1
  53. package/dist/chunk-OQSRJG6A.js.map +1 -0
  54. package/dist/{chunk-KI6HAJWL.js → chunk-QSUK7YWK.js} +2 -2
  55. package/dist/{chunk-YHPM5D7Y.js → chunk-QVIEAYTP.js} +61 -2
  56. package/dist/chunk-QVIEAYTP.js.map +1 -0
  57. package/dist/{chunk-NSCVNK5K.js → chunk-SCJPI4Z5.js} +3 -3
  58. package/dist/{chunk-ZWTNWAO4.js → chunk-TKIY625R.js} +13 -3
  59. package/dist/chunk-TKIY625R.js.map +1 -0
  60. package/dist/{chunk-OHVFWCJP.js → chunk-VLMPU56Q.js} +48 -18
  61. package/dist/chunk-VLMPU56Q.js.map +1 -0
  62. package/dist/{chunk-6AJBSQU4.js → chunk-XL35NSEN.js} +2 -2
  63. package/dist/{chunk-WIBHRONM.js → chunk-XWH4MXIU.js} +2 -2
  64. package/dist/consent/index.d.cts +6 -6
  65. package/dist/consent/index.d.ts +6 -6
  66. package/dist/derivations/index.cjs +24 -3
  67. package/dist/derivations/index.cjs.map +1 -1
  68. package/dist/derivations/index.d.cts +7 -7
  69. package/dist/derivations/index.d.ts +7 -7
  70. package/dist/derivations/index.js +2 -2
  71. package/dist/{dev-unlock-BF4OSxRv.d.cts → dev-unlock-8XzcD2Z4.d.cts} +1 -1
  72. package/dist/{dev-unlock-DV7ujTCI.d.ts → dev-unlock-DR3upLd1.d.ts} +1 -1
  73. package/dist/executor-AZLS3KBK.js +11 -0
  74. package/dist/{fanout-sidecar-N6OJX6QR.js → fanout-sidecar-67CMI3UT.js} +2 -2
  75. package/dist/guards/index.cjs +9 -5
  76. package/dist/guards/index.cjs.map +1 -1
  77. package/dist/guards/index.d.cts +7 -7
  78. package/dist/guards/index.d.ts +7 -7
  79. package/dist/guards/index.js +1 -1
  80. package/dist/{hash-DswxkLtW.d.ts → hash-CDjye9KV.d.ts} +1 -1
  81. package/dist/{hash-BcF5WQXl.d.cts → hash-DuQ88_5W.d.cts} +1 -1
  82. package/dist/history/index.cjs.map +1 -1
  83. package/dist/history/index.d.cts +7 -7
  84. package/dist/history/index.d.ts +7 -7
  85. package/dist/history/index.js +2 -2
  86. package/dist/i18n/index.cjs.map +1 -1
  87. package/dist/i18n/index.d.cts +6 -6
  88. package/dist/i18n/index.d.ts +6 -6
  89. package/dist/i18n/index.js +3 -3
  90. package/dist/{immutable-guard-7KqslW2K.d.cts → immutable-guard-CRPvu24K.d.cts} +16 -1
  91. package/dist/{immutable-guard-C8IYdzfu.d.ts → immutable-guard-Dov3WvwF.d.ts} +16 -1
  92. package/dist/{index-Cqzp4tt9.d.ts → index-C8Bk3-VF.d.cts} +11 -3
  93. package/dist/{index-CUVOMtgg.d.cts → index-nP99bXLg.d.ts} +11 -3
  94. package/dist/index.cjs +840 -122
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.d.cts +146 -15
  97. package/dist/index.d.ts +146 -15
  98. package/dist/index.js +153 -35
  99. package/dist/index.js.map +1 -1
  100. package/dist/indexing/index.cjs +92 -31
  101. package/dist/indexing/index.cjs.map +1 -1
  102. package/dist/indexing/index.d.cts +3 -3
  103. package/dist/indexing/index.d.ts +3 -3
  104. package/dist/indexing/index.js +3 -3
  105. package/dist/{issue-ADVS4OVP.js → issue-RZP3VI6O.js} +4 -4
  106. package/dist/{lazy-builder-D5GU14TS.d.ts → lazy-builder-ChSqcF5t.d.ts} +1 -1
  107. package/dist/{lazy-builder-Ci5_YG73.d.cts → lazy-builder-eYZzLEL1.d.cts} +1 -1
  108. package/dist/{ledger-CWSE3BLF.js → ledger-A3LL253R.js} +3 -3
  109. package/dist/materialized-views/index.cjs +409 -7
  110. package/dist/materialized-views/index.cjs.map +1 -1
  111. package/dist/materialized-views/index.d.cts +7 -7
  112. package/dist/materialized-views/index.d.ts +7 -7
  113. package/dist/materialized-views/index.js +6 -6
  114. package/dist/noydb-WCMY2ZOW.js +35 -0
  115. package/dist/overlay-views/index.cjs +47 -17
  116. package/dist/overlay-views/index.cjs.map +1 -1
  117. package/dist/overlay-views/index.d.cts +28 -10
  118. package/dist/overlay-views/index.d.ts +28 -10
  119. package/dist/overlay-views/index.js +1 -1
  120. package/dist/periods/index.cjs.map +1 -1
  121. package/dist/periods/index.d.cts +6 -6
  122. package/dist/periods/index.d.ts +6 -6
  123. package/dist/periods/index.js +3 -3
  124. package/dist/{predicate-Bt5ft-9c.d.cts → predicate-BmhBSPCH.d.cts} +59 -2
  125. package/dist/{predicate-Bt5ft-9c.d.ts → predicate-BmhBSPCH.d.ts} +59 -2
  126. package/dist/{public-envelope-SYHEYQ3X.js → public-envelope-YP2UWMLG.js} +3 -3
  127. package/dist/query/index.cjs +604 -205
  128. package/dist/query/index.cjs.map +1 -1
  129. package/dist/query/index.d.cts +3 -3
  130. package/dist/query/index.d.ts +3 -3
  131. package/dist/query/index.js +5 -5
  132. package/dist/{registry-XGLNADIE.js → registry-EB6SISTA.js} +2 -2
  133. package/dist/{registry-DK5YWAAA.js → registry-UTA4CLQS.js} +2 -2
  134. package/dist/{revoke-ZDFKMR5E.js → revoke-HNMQZSCL.js} +4 -4
  135. package/dist/session/index.d.cts +7 -7
  136. package/dist/session/index.d.ts +7 -7
  137. package/dist/shadow/index.d.cts +6 -6
  138. package/dist/shadow/index.d.ts +6 -6
  139. package/dist/{signer-P5D7Y72U.js → signer-DCMNKXSF.js} +3 -3
  140. package/dist/snapshots/index.d.cts +6 -6
  141. package/dist/snapshots/index.d.ts +6 -6
  142. package/dist/snapshots/index.js +3 -3
  143. package/dist/{stale-7FRJVHN6.js → stale-W5PQTRYH.js} +2 -2
  144. package/dist/store/index.d.cts +6 -6
  145. package/dist/store/index.d.ts +6 -6
  146. package/dist/{strategy-CrS7PnbE.d.cts → strategy-BtW8fAjz.d.cts} +2 -2
  147. package/dist/{strategy-CrS7PnbE.d.ts → strategy-BtW8fAjz.d.ts} +2 -2
  148. package/dist/sync/index.cjs.map +1 -1
  149. package/dist/sync/index.d.cts +5 -5
  150. package/dist/sync/index.d.ts +5 -5
  151. package/dist/sync/index.js +2 -2
  152. package/dist/team/index.cjs.map +1 -1
  153. package/dist/team/index.d.cts +6 -6
  154. package/dist/team/index.d.ts +6 -6
  155. package/dist/team/index.js +5 -5
  156. package/dist/tx/index.cjs +66 -3
  157. package/dist/tx/index.cjs.map +1 -1
  158. package/dist/tx/index.d.cts +24 -8
  159. package/dist/tx/index.d.ts +24 -8
  160. package/dist/tx/index.js +7 -3
  161. package/dist/tx/index.js.map +1 -1
  162. package/dist/{types-V5R2-pd4.d.cts → types-Bze6vkwm.d.cts} +391 -144
  163. package/dist/{types-BFHQUjdy.d.ts → types-DrmBTscX.d.ts} +391 -144
  164. package/dist/{ulid-p2nKiiKg.d.ts → ulid-DbBVrNSt.d.ts} +1 -1
  165. package/dist/{ulid-CwNf9e6-.d.cts → ulid-DfZlAh0u.d.cts} +1 -1
  166. package/dist/{vault-group-W7QC4UYW.js → vault-group-DX2HFQMX.js} +3 -3
  167. package/dist/{with-derivation-C9K43BOB.d.cts → with-derivation-CCqAchD5.d.cts} +1 -1
  168. package/dist/{with-derivation-Ds9yZgCj.d.ts → with-derivation-_lySGdlm.d.ts} +1 -1
  169. package/dist/{with-materialized-view-DgQcAjYv.d.cts → with-materialized-view--4PsvMDu.d.cts} +1 -1
  170. package/dist/{with-materialized-view-DwR4jkV5.d.ts → with-materialized-view-QT1Tp7NO.d.ts} +1 -1
  171. package/dist/{with-overlayed-view-ByyhHdVr.d.ts → with-overlayed-view-BEXfpzSb.d.ts} +1 -1
  172. package/dist/{with-overlayed-view-7-rUB3vD.d.cts → with-overlayed-view-DlH5qmeB.d.cts} +1 -1
  173. package/package.json +3 -3
  174. package/dist/chunk-3EWA37FV.js.map +0 -1
  175. package/dist/chunk-5LQG6ZO2.js.map +0 -1
  176. package/dist/chunk-ACKFRSAH.js.map +0 -1
  177. package/dist/chunk-DQU36Q7I.js.map +0 -1
  178. package/dist/chunk-EGD5DXFT.js.map +0 -1
  179. package/dist/chunk-KGCORI4L.js.map +0 -1
  180. package/dist/chunk-OHVFWCJP.js.map +0 -1
  181. package/dist/chunk-TV3YZ35S.js +0 -90
  182. package/dist/chunk-TV3YZ35S.js.map +0 -1
  183. package/dist/chunk-UWNYBOOO.js.map +0 -1
  184. package/dist/chunk-VU7SWWT5.js.map +0 -1
  185. package/dist/chunk-YHPM5D7Y.js.map +0 -1
  186. package/dist/chunk-ZWTNWAO4.js.map +0 -1
  187. package/dist/executor-723ZP6TH.js +0 -11
  188. package/dist/noydb-VZ4JVW55.js +0 -35
  189. /package/dist/{chunk-BIYRQQV6.js.map → chunk-3YWP3WBP.js.map} +0 -0
  190. /package/dist/{chunk-A5ZOOZFB.js.map → chunk-6H2ZUNR7.js.map} +0 -0
  191. /package/dist/{chunk-7HT2MEZ5.js.map → chunk-7BQ4QWYX.js.map} +0 -0
  192. /package/dist/{chunk-WBAYSNUQ.js.map → chunk-BI6ETQPF.js.map} +0 -0
  193. /package/dist/{chunk-56DJ7JVK.js.map → chunk-BR3AMFGS.js.map} +0 -0
  194. /package/dist/{chunk-YNTBADIY.js.map → chunk-CZI2A4MQ.js.map} +0 -0
  195. /package/dist/{chunk-COFPAMX6.js.map → chunk-DLZ2ONOD.js.map} +0 -0
  196. /package/dist/{chunk-PE4AQGFH.js.map → chunk-E2CDVKMH.js.map} +0 -0
  197. /package/dist/{chunk-L2FE64BU.js.map → chunk-FFXM3ZIF.js.map} +0 -0
  198. /package/dist/{chunk-WGHU7BLI.js.map → chunk-GNI5STXQ.js.map} +0 -0
  199. /package/dist/{chunk-NP6EZT44.js.map → chunk-IQLVUT37.js.map} +0 -0
  200. /package/dist/{chunk-7PS7EOCF.js.map → chunk-IXBIFDEW.js.map} +0 -0
  201. /package/dist/{chunk-LX3CB26H.js.map → chunk-KABJXG2F.js.map} +0 -0
  202. /package/dist/{chunk-DKO2QFSA.js.map → chunk-OB2ZJQ2D.js.map} +0 -0
  203. /package/dist/{chunk-4PEFEETV.js.map → chunk-OMAMZKKD.js.map} +0 -0
  204. /package/dist/{chunk-KI6HAJWL.js.map → chunk-QSUK7YWK.js.map} +0 -0
  205. /package/dist/{chunk-NSCVNK5K.js.map → chunk-SCJPI4Z5.js.map} +0 -0
  206. /package/dist/{chunk-6AJBSQU4.js.map → chunk-XL35NSEN.js.map} +0 -0
  207. /package/dist/{chunk-WIBHRONM.js.map → chunk-XWH4MXIU.js.map} +0 -0
  208. /package/dist/{executor-723ZP6TH.js.map → executor-AZLS3KBK.js.map} +0 -0
  209. /package/dist/{fanout-sidecar-N6OJX6QR.js.map → fanout-sidecar-67CMI3UT.js.map} +0 -0
  210. /package/dist/{issue-ADVS4OVP.js.map → issue-RZP3VI6O.js.map} +0 -0
  211. /package/dist/{ledger-CWSE3BLF.js.map → ledger-A3LL253R.js.map} +0 -0
  212. /package/dist/{noydb-VZ4JVW55.js.map → noydb-WCMY2ZOW.js.map} +0 -0
  213. /package/dist/{public-envelope-SYHEYQ3X.js.map → public-envelope-YP2UWMLG.js.map} +0 -0
  214. /package/dist/{registry-DK5YWAAA.js.map → registry-EB6SISTA.js.map} +0 -0
  215. /package/dist/{registry-XGLNADIE.js.map → registry-UTA4CLQS.js.map} +0 -0
  216. /package/dist/{revoke-ZDFKMR5E.js.map → revoke-HNMQZSCL.js.map} +0 -0
  217. /package/dist/{signer-P5D7Y72U.js.map → signer-DCMNKXSF.js.map} +0 -0
  218. /package/dist/{stale-7FRJVHN6.js.map → stale-W5PQTRYH.js.map} +0 -0
  219. /package/dist/{vault-group-W7QC4UYW.js.map → vault-group-DX2HFQMX.js.map} +0 -0
@@ -3894,6 +3894,115 @@ var init_descriptor = __esm({
3894
3894
  }
3895
3895
  });
3896
3896
 
3897
+ // src/money/paths.ts
3898
+ function parseMoneyPath(path) {
3899
+ const cached = parseCache.get(path);
3900
+ if (cached) return cached;
3901
+ if (typeof path !== "string" || path.length === 0) {
3902
+ throw new ValidationError("moneyFields: path must be a non-empty string");
3903
+ }
3904
+ const segments = [];
3905
+ for (const part of path.split(".")) {
3906
+ const m = SEGMENT_RE.exec(part);
3907
+ if (!m) {
3908
+ throw new ValidationError(
3909
+ `moneyFields: invalid path "${path}" \u2014 segment "${part}" must be a key, "key[]", "*", or "*[]"`
3910
+ );
3911
+ }
3912
+ const array = m[2] === "[]";
3913
+ segments.push(
3914
+ m[1] === "*" ? { kind: "wildcard", array } : { kind: "key", key: m[1], array }
3915
+ );
3916
+ }
3917
+ parseCache.set(path, segments);
3918
+ return segments;
3919
+ }
3920
+ function isSimpleMoneyPath(path) {
3921
+ return !path.includes(".") && !path.includes("[") && !path.includes("*");
3922
+ }
3923
+ function validateMoneyFieldPaths(moneyFields) {
3924
+ for (const path of Object.keys(moneyFields)) parseMoneyPath(path);
3925
+ }
3926
+ function transformAtMoneyPath(node, path, segments, index, visit, lenient) {
3927
+ if (node === null || node === void 0) return node;
3928
+ const seg = segments[index];
3929
+ const last = index === segments.length - 1;
3930
+ if (seg.kind === "key") {
3931
+ if (typeof node !== "object" || Array.isArray(node)) {
3932
+ if (lenient) return node;
3933
+ throw new ValidationError(
3934
+ `moneyFields: path "${path}" expected an object at segment "${seg.key}", got ${Array.isArray(node) ? "an array" : typeof node}`
3935
+ );
3936
+ }
3937
+ const obj2 = node;
3938
+ if (!(seg.key in obj2) || obj2[seg.key] === null || obj2[seg.key] === void 0) return node;
3939
+ if (seg.array) {
3940
+ const arr = obj2[seg.key];
3941
+ if (!Array.isArray(arr)) {
3942
+ if (lenient) return node;
3943
+ throw new ValidationError(
3944
+ `moneyFields: path "${path}" declares "${seg.key}[]" but the value is not an array`
3945
+ );
3946
+ }
3947
+ const cloned = [...arr];
3948
+ if (last) {
3949
+ for (let i = 0; i < cloned.length; i++) visit(cloned, i);
3950
+ } else {
3951
+ for (let i = 0; i < cloned.length; i++) {
3952
+ cloned[i] = transformAtMoneyPath(cloned[i], path, segments, index + 1, visit, lenient);
3953
+ }
3954
+ }
3955
+ return { ...obj2, [seg.key]: cloned };
3956
+ }
3957
+ const clone3 = { ...obj2 };
3958
+ if (last) {
3959
+ visit(clone3, seg.key);
3960
+ } else {
3961
+ clone3[seg.key] = transformAtMoneyPath(clone3[seg.key], path, segments, index + 1, visit, lenient);
3962
+ }
3963
+ return clone3;
3964
+ }
3965
+ if (seg.array) {
3966
+ if (!Array.isArray(node)) {
3967
+ if (lenient) return node;
3968
+ throw new ValidationError(`moneyFields: path "${path}" declares "*[]" but the value is not an array`);
3969
+ }
3970
+ const cloned = [...node];
3971
+ if (last) {
3972
+ for (let i = 0; i < cloned.length; i++) visit(cloned, i);
3973
+ } else {
3974
+ for (let i = 0; i < cloned.length; i++) {
3975
+ cloned[i] = transformAtMoneyPath(cloned[i], path, segments, index + 1, visit, lenient);
3976
+ }
3977
+ }
3978
+ return cloned;
3979
+ }
3980
+ if (typeof node !== "object" || Array.isArray(node)) {
3981
+ if (lenient) return node;
3982
+ throw new ValidationError(
3983
+ `moneyFields: path "${path}" applies "*" to a non-object (${Array.isArray(node) ? 'array \u2014 use "*[]"' : typeof node})`
3984
+ );
3985
+ }
3986
+ const obj = node;
3987
+ const clone2 = { ...obj };
3988
+ for (const key of Object.keys(obj)) {
3989
+ const v = clone2[key];
3990
+ if (v === null || v === void 0) continue;
3991
+ if (last) visit(clone2, key);
3992
+ else clone2[key] = transformAtMoneyPath(v, path, segments, index + 1, visit, lenient);
3993
+ }
3994
+ return clone2;
3995
+ }
3996
+ var SEGMENT_RE, parseCache;
3997
+ var init_paths = __esm({
3998
+ "src/money/paths.ts"() {
3999
+ "use strict";
4000
+ init_errors();
4001
+ SEGMENT_RE = /^(\*|[^.[\]*]+)(\[\])?$/;
4002
+ parseCache = /* @__PURE__ */ new Map();
4003
+ }
4004
+ });
4005
+
3897
4006
  // src/money/normalize.ts
3898
4007
  function isMoneyValueObject(v) {
3899
4008
  return typeof v === "object" && v !== null && "currency" in v;
@@ -3906,33 +4015,68 @@ function quantizeAmount(field, input, scale, rounding) {
3906
4015
  }
3907
4016
  return r.value.toString();
3908
4017
  }
4018
+ function canonicalizeStoredMoney(record, moneyFields) {
4019
+ if (record === null || record === void 0) return record;
4020
+ if (!moneyFields || Object.keys(moneyFields).length === 0) return record;
4021
+ return decodeMoneyFields(record, moneyFields, "raw");
4022
+ }
4023
+ function canonicalizeIncomingMoney(record, moneyFields) {
4024
+ if (!moneyFields || Object.keys(moneyFields).length === 0) return record;
4025
+ try {
4026
+ return decodeMoneyFields(
4027
+ quantizeMoneyFields(record, moneyFields),
4028
+ moneyFields,
4029
+ "raw"
4030
+ );
4031
+ } catch {
4032
+ return record;
4033
+ }
4034
+ }
4035
+ function quantizeValue(field, raw, desc) {
4036
+ if (desc.mode === "fixed") {
4037
+ const currency2 = desc.fixedCurrency;
4038
+ return quantizeAmount(field, raw, desc.scaleFor(currency2), desc.rounding);
4039
+ }
4040
+ let amount;
4041
+ let currency;
4042
+ if (isMoneyValueObject(raw)) {
4043
+ currency = String(raw.currency);
4044
+ amount = raw.amount;
4045
+ } else {
4046
+ const sole = desc.soleCurrency();
4047
+ if (sole === void 0) {
4048
+ throw new TypeError(
4049
+ `money: field "${field}" is multi-currency \u2014 write { amount, currency }, not a bare amount`
4050
+ );
4051
+ }
4052
+ currency = sole;
4053
+ amount = raw;
4054
+ }
4055
+ const scale = desc.scaleFor(currency);
4056
+ return { amount: quantizeAmount(field, amount, scale, desc.rounding), currency };
4057
+ }
3909
4058
  function quantizeMoneyFields(record, moneyFields) {
3910
- const out = { ...record };
3911
- for (const [field, desc] of Object.entries(moneyFields)) {
3912
- const raw = out[field];
3913
- if (raw === null || raw === void 0) continue;
3914
- if (desc.mode === "fixed") {
3915
- const currency2 = desc.fixedCurrency;
3916
- out[field] = quantizeAmount(field, raw, desc.scaleFor(currency2), desc.rounding);
4059
+ let out = { ...record };
4060
+ for (const [path, desc] of Object.entries(moneyFields)) {
4061
+ if (isSimpleMoneyPath(path)) {
4062
+ const raw = out[path];
4063
+ if (raw === null || raw === void 0) continue;
4064
+ out[path] = quantizeValue(path, raw, desc);
3917
4065
  continue;
3918
4066
  }
3919
- let amount;
3920
- let currency;
3921
- if (isMoneyValueObject(raw)) {
3922
- currency = String(raw.currency);
3923
- amount = raw.amount;
3924
- } else {
3925
- const sole = desc.soleCurrency();
3926
- if (sole === void 0) {
3927
- throw new TypeError(
3928
- `money: field "${field}" is multi-currency \u2014 write { amount, currency }, not a bare amount`
3929
- );
3930
- }
3931
- currency = sole;
3932
- amount = raw;
3933
- }
3934
- const scale = desc.scaleFor(currency);
3935
- out[field] = { amount: quantizeAmount(field, amount, scale, desc.rounding), currency };
4067
+ out = transformAtMoneyPath(
4068
+ out,
4069
+ path,
4070
+ parseMoneyPath(path),
4071
+ 0,
4072
+ (container, key) => {
4073
+ const raw = container[key];
4074
+ if (raw === null || raw === void 0) return;
4075
+ container[key] = quantizeValue(path, raw, desc);
4076
+ },
4077
+ /* lenient */
4078
+ false
4079
+ );
3936
4080
  }
3937
4081
  return out;
3938
4082
  }
@@ -3945,33 +4089,70 @@ function formatCurrency(decimal, currency, scale, locale) {
3945
4089
  });
3946
4090
  return fmt.format(decimal);
3947
4091
  }
4092
+ function decodeValue(stored, desc) {
4093
+ let currency;
4094
+ let scaledIntString;
4095
+ if (desc.mode === "fixed") {
4096
+ if (typeof stored !== "string" && typeof stored !== "number") return null;
4097
+ currency = desc.fixedCurrency;
4098
+ scaledIntString = String(stored);
4099
+ } else {
4100
+ if (!isMoneyValueObject(stored)) return null;
4101
+ const amount = stored.amount;
4102
+ if (typeof stored.currency !== "string" || typeof amount !== "string" && typeof amount !== "number") return null;
4103
+ currency = stored.currency;
4104
+ scaledIntString = String(amount);
4105
+ }
4106
+ const scale = desc.scaleFor(currency);
4107
+ let decimal;
4108
+ try {
4109
+ decimal = formatScaledInt(BigInt(scaledIntString), scale);
4110
+ } catch {
4111
+ return null;
4112
+ }
4113
+ return {
4114
+ decoded: desc.mode === "fixed" ? decimal : { amount: decimal, currency },
4115
+ decimal,
4116
+ currency,
4117
+ scale
4118
+ };
4119
+ }
3948
4120
  function decodeMoneyFields(record, moneyFields, locale) {
3949
- const out = { ...record };
4121
+ let out = { ...record };
3950
4122
  const format = locale !== "raw";
3951
4123
  const fmtLocale = typeof locale === "string" && locale !== "raw" ? locale : "en-US";
3952
- for (const [field, desc] of Object.entries(moneyFields)) {
3953
- const stored = out[field];
3954
- if (stored === null || stored === void 0) continue;
3955
- let currency;
3956
- let scaledIntString;
3957
- if (desc.mode === "fixed") {
3958
- if (typeof stored !== "string" && typeof stored !== "number") continue;
3959
- currency = desc.fixedCurrency;
3960
- scaledIntString = String(stored);
3961
- } else {
3962
- if (!isMoneyValueObject(stored)) continue;
3963
- const amount = stored.amount;
3964
- if (typeof stored.currency !== "string" || typeof amount !== "string" && typeof amount !== "number") continue;
3965
- currency = stored.currency;
3966
- scaledIntString = String(amount);
3967
- }
3968
- const scale = desc.scaleFor(currency);
3969
- const decimal = formatScaledInt(BigInt(scaledIntString), scale);
3970
- out[field] = desc.mode === "fixed" ? decimal : { amount: decimal, currency };
3971
- if (format) {
3972
- out[`${field}Formatted`] = formatCurrency(decimal, currency, scale, fmtLocale);
3973
- out[`${field}Number`] = Number(decimal);
4124
+ for (const [path, desc] of Object.entries(moneyFields)) {
4125
+ if (isSimpleMoneyPath(path)) {
4126
+ const stored = out[path];
4127
+ if (stored === null || stored === void 0) continue;
4128
+ const r = decodeValue(stored, desc);
4129
+ if (r === null) continue;
4130
+ out[path] = r.decoded;
4131
+ if (format) {
4132
+ out[`${path}Formatted`] = formatCurrency(r.decimal, r.currency, r.scale, fmtLocale);
4133
+ out[`${path}Number`] = Number(r.decimal);
4134
+ }
4135
+ continue;
3974
4136
  }
4137
+ out = transformAtMoneyPath(
4138
+ out,
4139
+ path,
4140
+ parseMoneyPath(path),
4141
+ 0,
4142
+ (container, key) => {
4143
+ const stored = container[key];
4144
+ if (stored === null || stored === void 0) return;
4145
+ const r = decodeValue(stored, desc);
4146
+ if (r === null) return;
4147
+ container[key] = r.decoded;
4148
+ if (format && typeof key === "string" && !Array.isArray(container)) {
4149
+ container[`${key}Formatted`] = formatCurrency(r.decimal, r.currency, r.scale, fmtLocale);
4150
+ container[`${key}Number`] = Number(r.decimal);
4151
+ }
4152
+ },
4153
+ /* lenient */
4154
+ true
4155
+ );
3975
4156
  }
3976
4157
  return out;
3977
4158
  }
@@ -3980,6 +4161,7 @@ var init_normalize = __esm({
3980
4161
  "use strict";
3981
4162
  init_fixed_point();
3982
4163
  init_descriptor();
4164
+ init_paths();
3983
4165
  }
3984
4166
  });
3985
4167
 
@@ -4152,6 +4334,146 @@ var init_strategy3 = __esm({
4152
4334
  }
4153
4335
  });
4154
4336
 
4337
+ // src/money/where.ts
4338
+ function isMoneyValueObject2(v) {
4339
+ return typeof v === "object" && v !== null && "currency" in v;
4340
+ }
4341
+ function parseOperand(field, raw, desc) {
4342
+ let amount;
4343
+ let currency;
4344
+ if (desc.mode === "fixed") {
4345
+ currency = desc.fixedCurrency;
4346
+ amount = raw;
4347
+ } else if (isMoneyValueObject2(raw)) {
4348
+ currency = String(raw.currency);
4349
+ amount = raw.amount;
4350
+ } else {
4351
+ const sole = desc.soleCurrency();
4352
+ if (sole === void 0) {
4353
+ throw new MoneyUnsupportedError(
4354
+ `where("${field}"): field is multi-currency \u2014 compare against { amount, currency }, not a bare amount`
4355
+ );
4356
+ }
4357
+ currency = sole;
4358
+ amount = raw;
4359
+ }
4360
+ if (typeof amount !== "number" && typeof amount !== "string") {
4361
+ throw new MoneyUnsupportedError(
4362
+ `where("${field}"): operand ${JSON.stringify(raw)} is not a money amount`
4363
+ );
4364
+ }
4365
+ const r = parseToScaledInt(amount, desc.scaleFor(currency), desc.rounding);
4366
+ if (!r.ok) {
4367
+ throw new MoneyUnsupportedError(
4368
+ `where("${field}"): operand ${JSON.stringify(amount)} is not a finite decimal`
4369
+ );
4370
+ }
4371
+ return { scaled: r.value.toString(), currency };
4372
+ }
4373
+ function moneyFieldClause(field, op, value, desc) {
4374
+ switch (op) {
4375
+ case "==":
4376
+ case "!=":
4377
+ case "<":
4378
+ case "<=":
4379
+ case ">":
4380
+ case ">=": {
4381
+ const e = parseOperand(field, value, desc);
4382
+ return withMoney(field, op, value, desc, [e]);
4383
+ }
4384
+ case "between": {
4385
+ if (!Array.isArray(value) || value.length !== 2) {
4386
+ throw new MoneyUnsupportedError(`where("${field}"): 'between' needs a [lo, hi] tuple`);
4387
+ }
4388
+ const lo = parseOperand(field, value[0], desc);
4389
+ const hi = parseOperand(field, value[1], desc);
4390
+ if (lo.currency !== hi.currency) {
4391
+ throw new MoneyUnsupportedError(
4392
+ `where("${field}"): 'between' bounds mix currencies (${lo.currency} vs ${hi.currency})`
4393
+ );
4394
+ }
4395
+ return withMoney(field, op, value, desc, [lo, hi]);
4396
+ }
4397
+ case "in": {
4398
+ if (!Array.isArray(value)) {
4399
+ throw new MoneyUnsupportedError(`where("${field}"): 'in' needs an array of amounts`);
4400
+ }
4401
+ return withMoney(field, op, value, desc, value.map((v) => parseOperand(field, v, desc)));
4402
+ }
4403
+ default:
4404
+ throw new MoneyUnsupportedError(
4405
+ `where("${field}"): operator '${op}' is not supported on a money field`
4406
+ );
4407
+ }
4408
+ }
4409
+ function withMoney(field, op, originalValue, desc, entries) {
4410
+ const money = { mode: desc.mode, entries };
4411
+ const value = desc.mode !== "fixed" ? originalValue : entries.length === 1 && op !== "in" && op !== "between" ? entries[0].scaled : entries.map((e) => e.scaled);
4412
+ return { type: "field", field, op, value, money };
4413
+ }
4414
+ function readStored(actual, operand) {
4415
+ let amount;
4416
+ let currency;
4417
+ if (operand.mode === "fixed") {
4418
+ if (typeof actual !== "string" && typeof actual !== "number") return null;
4419
+ amount = actual;
4420
+ currency = operand.entries[0]?.currency ?? "";
4421
+ } else {
4422
+ if (!isMoneyValueObject2(actual)) return null;
4423
+ if (typeof actual.currency !== "string") return null;
4424
+ amount = actual.amount;
4425
+ currency = actual.currency;
4426
+ }
4427
+ if (typeof amount !== "string" && typeof amount !== "number") return null;
4428
+ try {
4429
+ return { scaled: BigInt(amount).toString(), currency };
4430
+ } catch {
4431
+ return null;
4432
+ }
4433
+ }
4434
+ function evaluateMoneyClause(actual, op, operand) {
4435
+ const stored = readStored(actual, operand);
4436
+ if (stored === null) return op === "!=";
4437
+ const a = BigInt(stored.scaled);
4438
+ if (op === "in") {
4439
+ return operand.entries.some(
4440
+ (e2) => e2.currency === stored.currency && BigInt(e2.scaled) === a
4441
+ );
4442
+ }
4443
+ if (op === "between") {
4444
+ const [lo, hi] = operand.entries;
4445
+ if (!lo || !hi || lo.currency !== stored.currency) return false;
4446
+ return a >= BigInt(lo.scaled) && a <= BigInt(hi.scaled);
4447
+ }
4448
+ const e = operand.entries[0];
4449
+ if (!e) return false;
4450
+ if (e.currency !== stored.currency) return op === "!=";
4451
+ const b = BigInt(e.scaled);
4452
+ switch (op) {
4453
+ case "==":
4454
+ return a === b;
4455
+ case "!=":
4456
+ return a !== b;
4457
+ case "<":
4458
+ return a < b;
4459
+ case "<=":
4460
+ return a <= b;
4461
+ case ">":
4462
+ return a > b;
4463
+ case ">=":
4464
+ return a >= b;
4465
+ default:
4466
+ return false;
4467
+ }
4468
+ }
4469
+ var init_where = __esm({
4470
+ "src/money/where.ts"() {
4471
+ "use strict";
4472
+ init_fixed_point();
4473
+ init_descriptor();
4474
+ }
4475
+ });
4476
+
4155
4477
  // src/query/predicate.ts
4156
4478
  function readPath(record, path) {
4157
4479
  if (record === null || record === void 0) return void 0;
@@ -4169,6 +4491,7 @@ function readPath(record, path) {
4169
4491
  function evaluateFieldClause(record, clause) {
4170
4492
  const actual = readPath(record, clause.field);
4171
4493
  const { op, value } = clause;
4494
+ if (clause.money) return evaluateMoneyClause(actual, op, clause.money);
4172
4495
  switch (op) {
4173
4496
  case "==":
4174
4497
  return actual === value;
@@ -4209,14 +4532,14 @@ function isComparable(a, b) {
4209
4532
  if (a instanceof Date && b instanceof Date) return true;
4210
4533
  return false;
4211
4534
  }
4212
- function evaluateClause(record, clause) {
4535
+ function evaluateClause(record, clause, fnRecord) {
4213
4536
  switch (clause.type) {
4214
4537
  case "field":
4215
4538
  return evaluateFieldClause(record, clause);
4216
4539
  case "filter":
4217
- return clause.fn(record);
4540
+ return clause.fn(fnRecord !== void 0 ? fnRecord : record);
4218
4541
  case "wherePredicate":
4219
- return clause.fn(record, clause.ctx);
4542
+ return clause.fn(fnRecord !== void 0 ? fnRecord : record, clause.ctx);
4220
4543
  case "crossJoin":
4221
4544
  throw new Error(
4222
4545
  `evaluateClause: 'crossJoin' clauses are expansion primitives and are not evaluated per-record. This is a query planner routing error \u2014 crossJoin clauses must be extracted from the clause list before calling evaluateClause or filterRecords.`
@@ -4224,20 +4547,28 @@ function evaluateClause(record, clause) {
4224
4547
  case "group":
4225
4548
  if (clause.op === "and") {
4226
4549
  for (const child of clause.clauses) {
4227
- if (!evaluateClause(record, child)) return false;
4550
+ if (!evaluateClause(record, child, fnRecord)) return false;
4228
4551
  }
4229
4552
  return true;
4230
4553
  } else {
4231
4554
  for (const child of clause.clauses) {
4232
- if (evaluateClause(record, child)) return true;
4555
+ if (evaluateClause(record, child, fnRecord)) return true;
4233
4556
  }
4234
4557
  return false;
4235
4558
  }
4236
4559
  }
4237
4560
  }
4561
+ function hasFnClause(clauses) {
4562
+ for (const c of clauses) {
4563
+ if (c.type === "filter" || c.type === "wherePredicate") return true;
4564
+ if (c.type === "group" && hasFnClause(c.clauses)) return true;
4565
+ }
4566
+ return false;
4567
+ }
4238
4568
  var init_predicate = __esm({
4239
4569
  "src/query/predicate.ts"() {
4240
4570
  "use strict";
4571
+ init_where();
4241
4572
  }
4242
4573
  });
4243
4574
 
@@ -4503,13 +4834,22 @@ var init_strategy4 = __esm({
4503
4834
  });
4504
4835
 
4505
4836
  // src/money/money-reducer.ts
4506
- function toScaledInt(v) {
4507
- if (typeof v === "string" || typeof v === "number" || typeof v === "bigint") {
4508
- try {
4509
- return BigInt(v);
4510
- } catch {
4511
- return null;
4837
+ function toScaledIntFromAny(v, scale) {
4838
+ if (typeof v === "bigint") return v;
4839
+ if (typeof v === "number") {
4840
+ const r = parseToScaledInt(v, scale);
4841
+ return r.ok ? r.value : null;
4842
+ }
4843
+ if (typeof v === "string") {
4844
+ if (!v.includes(".")) {
4845
+ try {
4846
+ return BigInt(v);
4847
+ } catch {
4848
+ return null;
4849
+ }
4512
4850
  }
4851
+ const r = parseToScaledInt(v, scale);
4852
+ return r.ok ? r.value : null;
4513
4853
  }
4514
4854
  return null;
4515
4855
  }
@@ -4517,13 +4857,15 @@ function readMoney(record, field, desc) {
4517
4857
  const raw = readPath(record, field);
4518
4858
  if (raw === null || raw === void 0) return null;
4519
4859
  if (desc.mode === "fixed") {
4520
- const value2 = toScaledInt(raw);
4521
- return value2 === null ? null : { currency: desc.fixedCurrency, value: value2 };
4860
+ const cur = desc.fixedCurrency;
4861
+ const value2 = toScaledIntFromAny(raw, desc.scaleFor(cur));
4862
+ return value2 === null ? null : { currency: cur, value: value2 };
4522
4863
  }
4523
4864
  if (typeof raw !== "object") return null;
4524
4865
  const o = raw;
4525
4866
  if (typeof o.currency !== "string") return null;
4526
- const value = toScaledInt(o.amount);
4867
+ const scale = desc.allows(o.currency) ? desc.scaleFor(o.currency) : 0;
4868
+ const value = toScaledIntFromAny(o.amount, scale);
4527
4869
  return value === null ? null : { currency: o.currency, value };
4528
4870
  }
4529
4871
  function targetScaleFor(desc, currency) {
@@ -4698,7 +5040,7 @@ function executePlanWithSource(source, plan, joinContext) {
4698
5040
  result = executeClausePipeline(source, plan.clauses, joinContext);
4699
5041
  } else {
4700
5042
  const { candidates, remainingClauses } = candidateRecords(source, plan.clauses);
4701
- result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses);
5043
+ result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
4702
5044
  }
4703
5045
  if (plan.orderBy.length > 0) {
4704
5046
  result = sortRecords(result, plan.orderBy);
@@ -4721,6 +5063,7 @@ function candidateRecords(source, clauses) {
4721
5063
  const clause = clauses[i];
4722
5064
  if (clause.type !== "field") continue;
4723
5065
  if (!indexes.has(clause.field)) continue;
5066
+ if (clause.money?.mode === "multi") continue;
4724
5067
  let ids = null;
4725
5068
  if (clause.op === "==") {
4726
5069
  ids = indexes.lookupEqual(clause.field, clause.value);
@@ -4748,13 +5091,20 @@ function materializeIds(ids, lookupById) {
4748
5091
  }
4749
5092
  return out;
4750
5093
  }
4751
- function filterRecords(records, clauses) {
5094
+ function fnViewDecoder(source) {
5095
+ const mf = source.moneyFields;
5096
+ if (!mf || Object.keys(mf).length === 0) return void 0;
5097
+ return (r) => decodeMoneyFields(r, mf, "raw");
5098
+ }
5099
+ function filterRecords(records, clauses, decodeForFns) {
4752
5100
  if (clauses.length === 0) return [...records];
5101
+ const needsFnView = decodeForFns !== void 0 && hasFnClause(clauses);
4753
5102
  const out = [];
4754
5103
  for (const r of records) {
5104
+ const fnView = needsFnView ? decodeForFns(r) : void 0;
4755
5105
  let matches = true;
4756
5106
  for (const clause of clauses) {
4757
- if (!evaluateClause(r, clause)) {
5107
+ if (!evaluateClause(r, clause, fnView)) {
4758
5108
  matches = false;
4759
5109
  break;
4760
5110
  }
@@ -4766,10 +5116,11 @@ function filterRecords(records, clauses) {
4766
5116
  function executeClausePipeline(source, clauses, joinContext) {
4767
5117
  let rel = [...source.snapshot()];
4768
5118
  let filterBatch = [];
5119
+ const decodeForFns = fnViewDecoder(source);
4769
5120
  for (const clause of clauses) {
4770
5121
  if (clause.type === "crossJoin") {
4771
5122
  if (filterBatch.length > 0) {
4772
- rel = filterRecords(rel, filterBatch);
5123
+ rel = filterRecords(rel, filterBatch, decodeForFns);
4773
5124
  filterBatch = [];
4774
5125
  }
4775
5126
  const rightSource = joinContext.resolveSource(clause.target);
@@ -4782,7 +5133,7 @@ function executeClausePipeline(source, clauses, joinContext) {
4782
5133
  }
4783
5134
  }
4784
5135
  if (filterBatch.length > 0) {
4785
- rel = filterRecords(rel, filterBatch);
5136
+ rel = filterRecords(rel, filterBatch, decodeForFns);
4786
5137
  }
4787
5138
  return rel;
4788
5139
  }
@@ -4963,6 +5314,7 @@ var init_builder = __esm({
4963
5314
  init_strategy4();
4964
5315
  init_money_reducer();
4965
5316
  init_normalize();
5317
+ init_where();
4966
5318
  EMPTY_PLAN = {
4967
5319
  clauses: [],
4968
5320
  orderBy: [],
@@ -5054,9 +5406,18 @@ var init_builder = __esm({
5054
5406
  this.predicates
5055
5407
  );
5056
5408
  }
5057
- /** Add a field comparison. Multiple where() calls are AND-combined. */
5409
+ /**
5410
+ * Add a field comparison. Multiple where() calls are AND-combined.
5411
+ *
5412
+ * A declared money field compares in MAJOR units (#336): the operand
5413
+ * (`10000`, `'10000.00'`, or `{ amount, currency }` in multi mode) is
5414
+ * quantized into stored scaled-int space at build time and evaluated
5415
+ * BigInt-exact per record. A malformed operand or a string operator
5416
+ * (`contains`/`startsWith`) throws here, at the call site.
5417
+ */
5058
5418
  where(field, op, value) {
5059
- const clause = { type: "field", field, op, value };
5419
+ const desc = this.source.moneyFields?.[field];
5420
+ const clause = desc ? moneyFieldClause(field, op, value, desc) : { type: "field", field, op, value };
5060
5421
  return new _Query(
5061
5422
  this.source,
5062
5423
  { ...this.plan, clauses: [...this.plan.clauses, clause] },
@@ -5382,7 +5743,7 @@ var init_builder = __esm({
5382
5743
  }
5383
5744
  const { candidates, remainingClauses } = candidateRecords(this.source, this.plan.clauses);
5384
5745
  if (remainingClauses.length === 0) return candidates.length;
5385
- return filterRecords(candidates, remainingClauses).length;
5746
+ return filterRecords(candidates, remainingClauses, fnViewDecoder(this.source)).length;
5386
5747
  }
5387
5748
  /**
5388
5749
  * Reduce the matching records through a named set of reducers.
@@ -5439,7 +5800,7 @@ var init_builder = __esm({
5439
5800
  return executeClausePipeline(source, clauses, joinCtx);
5440
5801
  }
5441
5802
  const { candidates, remainingClauses } = candidateRecords(source, clauses);
5442
- return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses);
5803
+ return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
5443
5804
  };
5444
5805
  const upstreams = [];
5445
5806
  if (source.subscribe) {
@@ -5462,7 +5823,7 @@ var init_builder = __esm({
5462
5823
  return executeClausePipeline(source, clauses, joinCtx);
5463
5824
  }
5464
5825
  const { candidates, remainingClauses } = candidateRecords(source, clauses);
5465
- return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses);
5826
+ return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
5466
5827
  };
5467
5828
  const upstreams = [];
5468
5829
  if (source.subscribe) {
@@ -5655,11 +6016,14 @@ function warnCardinalityApproaching(fields, observed) {
5655
6016
  `[noy-db] .groupBy(${label}) produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
5656
6017
  );
5657
6018
  }
5658
- function groupAndReduce(records, fieldOrFields, spec) {
6019
+ function groupAndReduce(records, fieldOrFields, spec, moneyFields) {
5659
6020
  const fields = typeof fieldOrFields === "string" ? [fieldOrFields] : fieldOrFields;
5660
6021
  if (fields.length === 0) {
5661
6022
  throw new Error(".groupBy() requires at least one field");
5662
6023
  }
6024
+ if (moneyFields) {
6025
+ spec = wrapMoneyReducers(spec, moneyFields);
6026
+ }
5663
6027
  const buckets = /* @__PURE__ */ new Map();
5664
6028
  const fieldLabel = fields.length === 1 ? fields[0] : `[${fields.join(", ")}]`;
5665
6029
  for (const record of records) {
@@ -5715,6 +6079,7 @@ var init_groupby = __esm({
5715
6079
  init_predicate();
5716
6080
  init_canonical_key();
5717
6081
  init_errors();
6082
+ init_money_reducer();
5718
6083
  GROUPBY_WARN_CARDINALITY = 1e4;
5719
6084
  GROUPBY_MAX_CARDINALITY = 1e5;
5720
6085
  warnedCardinalityFields = /* @__PURE__ */ new Set();
@@ -5735,6 +6100,7 @@ var init_scan_builder = __esm({
5735
6100
  init_predicate();
5736
6101
  init_errors();
5737
6102
  init_normalize();
6103
+ init_where();
5738
6104
  DEFAULT_SCAN_PAGE_SIZE = 100;
5739
6105
  ScanBuilder = class _ScanBuilder {
5740
6106
  pageProvider;
@@ -5798,7 +6164,8 @@ var init_scan_builder = __esm({
5798
6164
  * evaluates clauses per record in O(1) per clause.
5799
6165
  */
5800
6166
  where(field, op, value) {
5801
- const clause = { type: "field", field, op, value };
6167
+ const desc = this.moneyFields?.[field];
6168
+ const clause = desc ? moneyFieldClause(field, op, value, desc) : { type: "field", field, op, value };
5802
6169
  return new _ScanBuilder(
5803
6170
  this.pageProvider,
5804
6171
  this.pageSize,
@@ -6144,8 +6511,9 @@ var init_scan_builder = __esm({
6144
6511
  */
6145
6512
  recordMatches(record) {
6146
6513
  if (this.clauses.length === 0) return true;
6514
+ const fnView = this.moneyFields && Object.keys(this.moneyFields).length > 0 && hasFnClause(this.clauses) ? this.decodeMoney(record) : void 0;
6147
6515
  for (const clause of this.clauses) {
6148
- if (!evaluateClause(record, clause)) return false;
6516
+ if (!evaluateClause(record, clause, fnView)) return false;
6149
6517
  }
6150
6518
  return true;
6151
6519
  }
@@ -7113,10 +7481,14 @@ function summarizeQueryPlan(query) {
7113
7481
  });
7114
7482
  }
7115
7483
  function summarizeUnionPlan(spec) {
7116
- const arms = (spec.unionSources ?? []).map((s) => s.collection).join(",");
7484
+ const arms = (spec.unionSources ?? []).map((s) => {
7485
+ const joins = s.join?.length ? `[${s.join.map((j) => `${j.field}\u2192${j.as}`).join(";")}]` : "";
7486
+ return `${s.collection}${joins}`;
7487
+ }).join(",");
7117
7488
  const groupBy = Array.isArray(spec.groupBy) ? [...spec.groupBy].sort().join(",") : typeof spec.groupBy === "string" ? spec.groupBy : "";
7118
7489
  const aggKeys = spec.aggregate ? Object.keys(spec.aggregate).sort().join(",") : "";
7119
- return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})`;
7490
+ const moneyKeys = spec.moneyFields ? Object.keys(spec.moneyFields).sort().join(",") : "";
7491
+ return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})|money(${moneyKeys})`;
7120
7492
  }
7121
7493
  var init_dependency_analyzer = __esm({
7122
7494
  "src/materialized-views/dependency-analyzer.ts"() {
@@ -7243,6 +7615,7 @@ var init_registry = __esm({
7243
7615
  let isQuery = false;
7244
7616
  if (spec.unionSources) {
7245
7617
  dependencies = new Set(spec.unionSources.map((s) => s.collection));
7618
+ if (spec.sources) for (const s of spec.sources) dependencies.add(s);
7246
7619
  queryPlanSummary = summarizeUnionPlan(spec);
7247
7620
  } else {
7248
7621
  const q = spec.query(dbForQuery);
@@ -7397,7 +7770,13 @@ async function materializeUnionResult(spec, db) {
7397
7770
  const unified = [];
7398
7771
  for (const arm of spec.unionSources) {
7399
7772
  const coll = db.collection(arm.collection);
7400
- const sourceRows = coll.query().toArray();
7773
+ let q = coll.query();
7774
+ if (arm.join?.length) {
7775
+ for (const leg of arm.join) {
7776
+ q = q.join(leg.field, { as: leg.as, maxRows: leg.maxRows, strategy: leg.strategy });
7777
+ }
7778
+ }
7779
+ const sourceRows = q.toArray();
7401
7780
  for (const r of sourceRows) {
7402
7781
  const mapped = arm.map(r);
7403
7782
  if (mapped == null) continue;
@@ -7414,7 +7793,7 @@ async function materializeUnionResult(spec, db) {
7414
7793
  }
7415
7794
  return [...seen.values()];
7416
7795
  }
7417
- return groupAndReduce(unified, groupFields, spec.aggregate);
7796
+ return groupAndReduce(unified, groupFields, spec.aggregate, spec.moneyFields);
7418
7797
  }
7419
7798
  async function listOutputIds(outputColl) {
7420
7799
  const cAny = outputColl;
@@ -7686,6 +8065,7 @@ var init_collection = __esm({
7686
8065
  init_strategy();
7687
8066
  init_core();
7688
8067
  init_normalize();
8068
+ init_paths();
7689
8069
  init_computed();
7690
8070
  init_strategy2();
7691
8071
  init_policy();
@@ -8008,6 +8388,7 @@ var init_collection = __esm({
8008
8388
  this.joinResolver = opts.joinResolver;
8009
8389
  this.i18nFields = opts.i18nFields;
8010
8390
  this.dictKeyFields = opts.dictKeyFields;
8391
+ if (opts.moneyFields) validateMoneyFieldPaths(opts.moneyFields);
8011
8392
  this.moneyFields = opts.moneyFields;
8012
8393
  this.computed = opts.computed;
8013
8394
  this.dictLabelResolver = opts.dictLabelResolver;
@@ -8141,7 +8522,9 @@ var init_collection = __esm({
8141
8522
  * declaration; this reconciles that ordering. First-wins. Not public.
8142
8523
  */
8143
8524
  _applyMoneyFields(moneyFields) {
8144
- if (this.moneyFields === void 0) this.moneyFields = moneyFields;
8525
+ if (this.moneyFields !== void 0) return;
8526
+ validateMoneyFieldPaths(moneyFields);
8527
+ this.moneyFields = moneyFields;
8145
8528
  }
8146
8529
  /** @internal — attach computed fields post-construction. See {@link _applyMoneyFields}. */
8147
8530
  _applyComputed(computed) {
@@ -8308,6 +8691,7 @@ var init_collection = __esm({
8308
8691
  if (!hasWritePermission(this.keyring, this.name)) {
8309
8692
  throw new ReadOnlyError();
8310
8693
  }
8694
+ record = canonicalizeIncomingMoney(record, this.moneyFields);
8311
8695
  if (this.subsystemBus?.hasGateHandlers("beforePut")) {
8312
8696
  const existingEnv = await this.adapter.get(this.vault, this.name, id);
8313
8697
  let existingRecord = null;
@@ -8324,7 +8708,7 @@ var init_collection = __esm({
8324
8708
  collection: this.name,
8325
8709
  docId: id,
8326
8710
  incoming: record,
8327
- existing: existingRecord,
8711
+ existing: canonicalizeStoredMoney(existingRecord, this.moneyFields),
8328
8712
  existingVersion: existingEnv?._v ?? 0,
8329
8713
  existingTs: existingEnv?._ts,
8330
8714
  userId: this.keyring.userId,
@@ -8593,7 +8977,7 @@ var init_collection = __esm({
8593
8977
  */
8594
8978
  async dispatchDerivations(id, record, version) {
8595
8979
  if (this.derivationSource === void 0) return;
8596
- const incoming = record;
8980
+ const incoming = canonicalizeStoredMoney(record, this.moneyFields);
8597
8981
  if (incoming && typeof incoming === "object" && "_derivedFrom" in incoming) return;
8598
8982
  const registry = this.derivationSource.registry();
8599
8983
  const strategies = registry.strategiesForSource(this.name);
@@ -8605,9 +8989,18 @@ var init_collection = __esm({
8605
8989
  if (DerivationExecutor2 === null) {
8606
8990
  ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports)));
8607
8991
  }
8608
- const sourceWithId = { ...incoming, id };
8992
+ let sourceWithId;
8993
+ let sourceVersion = version;
8994
+ if (spec.source === this.name) {
8995
+ sourceWithId = { ...incoming, id };
8996
+ } else {
8997
+ const primary = await this.derivationSource.getCollection(spec.source).get(id);
8998
+ if (primary === null || primary === void 0) continue;
8999
+ sourceWithId = { ...primary, id };
9000
+ sourceVersion = 0;
9001
+ }
8609
9002
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
8610
- const result = await DerivationExecutor2.run(spec, sourceWithId, version, strategyHash, ctx);
9003
+ const result = await DerivationExecutor2.run(spec, sourceWithId, sourceVersion, strategyHash, ctx);
8611
9004
  for (const key of Object.keys(spec.outputs)) {
8612
9005
  const out = result.outputs[key];
8613
9006
  if (!out) continue;
@@ -8826,7 +9219,7 @@ var init_collection = __esm({
8826
9219
  vault: this.vault,
8827
9220
  collection: this.name,
8828
9221
  docId: id,
8829
- existing: existingRecord,
9222
+ existing: canonicalizeStoredMoney(existingRecord, this.moneyFields),
8830
9223
  existingVersion: existingEnv._v,
8831
9224
  existingTs: existingEnv._ts,
8832
9225
  internal,
@@ -10559,37 +10952,34 @@ var init_virtual_collection = __esm({
10559
10952
  /** Get the merged row by id. */
10560
10953
  async get(id) {
10561
10954
  const overlayRow = await this.overlayCollection.get(id);
10562
- if (overlayRow !== null && this.shadowPredicateApplies(overlayRow)) {
10563
- return overlayRow;
10564
- }
10565
10955
  const baseRow = await this.baseCollection.get(id);
10566
- if (baseRow !== null) return baseRow;
10567
- return null;
10956
+ return this.mergeRows(overlayRow, baseRow);
10568
10957
  }
10569
10958
  /** List union of base + overlay ids, applying the merge per row. */
10570
10959
  async list() {
10571
10960
  const baseRows = await this.baseCollection.list();
10572
10961
  const overlayRows = await this.overlayCollection.list();
10573
- const merged = /* @__PURE__ */ new Map();
10574
10962
  const idOf = (row) => {
10575
10963
  if (this.baseRowKey) return this.baseRowKey(row);
10576
10964
  const idField = row.id;
10577
10965
  return typeof idField === "string" ? idField : "";
10578
10966
  };
10967
+ const baseById = /* @__PURE__ */ new Map();
10968
+ const overlayById = /* @__PURE__ */ new Map();
10579
10969
  for (const row of baseRows) {
10580
10970
  const id = idOf(row);
10581
- if (id) merged.set(id, row);
10971
+ if (id) baseById.set(id, row);
10582
10972
  }
10583
10973
  for (const row of overlayRows) {
10584
10974
  const id = idOf(row);
10585
- if (!id) continue;
10586
- if (this.shadowPredicateApplies(row)) {
10587
- merged.set(id, row);
10588
- } else if (!merged.has(id)) {
10589
- continue;
10590
- }
10975
+ if (id) overlayById.set(id, row);
10976
+ }
10977
+ const out = [];
10978
+ for (const id of /* @__PURE__ */ new Set([...baseById.keys(), ...overlayById.keys()])) {
10979
+ const merged = this.mergeRows(overlayById.get(id) ?? null, baseById.get(id) ?? null);
10980
+ if (merged !== null) out.push(merged);
10591
10981
  }
10592
- return [...merged.values()];
10982
+ return out;
10593
10983
  }
10594
10984
  /**
10595
10985
  * Write to the overlay. Two forms:
@@ -10629,9 +11019,42 @@ var init_virtual_collection = __esm({
10629
11019
  async delete(id) {
10630
11020
  await this.overlayCollection.delete(id);
10631
11021
  }
10632
- /** True when `overlay[shadowField] === shadowValue`. */
10633
- shadowPredicateApplies(row) {
10634
- return row[this.spec.shadowField] === this.spec.shadowValue;
11022
+ /**
11023
+ * Merge a single id's overlay + base rows into the visible row.
11024
+ *
11025
+ * Priority (first match wins):
11026
+ * 1. Binary shadow win — overlay present AND
11027
+ * `overlay[shadowField] === shadowValue` → return the overlay row
11028
+ * entirely. This stays FIRST so the original binary behaviour is
11029
+ * unchanged whether or not `mergeMode` is configured.
11030
+ * 2. Field-level merge — overlay present, `mergeMode` configured,
11031
+ * and a rule whose `whenStatus` equals `overlay[shadowField]`.
11032
+ * The matched rule pulls its `overlayFields` (those present on
11033
+ * the overlay row) on top of the base row. With no base row, the
11034
+ * overlay row is returned as-is.
11035
+ * 3. Fallback — return the base row (possibly `null`). An
11036
+ * overlay-only row that qualifies under neither (1) nor (2) and
11037
+ * has no base is therefore NOT surfaced.
11038
+ */
11039
+ mergeRows(overlayRow, baseRow) {
11040
+ const shadowField = this.spec.shadowField;
11041
+ if (overlayRow !== null && overlayRow[shadowField] === this.spec.shadowValue) {
11042
+ return overlayRow;
11043
+ }
11044
+ if (overlayRow !== null && this.spec.mergeMode) {
11045
+ const status = overlayRow[shadowField];
11046
+ const rule = this.spec.mergeMode.rules.find((r) => r.whenStatus === status);
11047
+ if (rule) {
11048
+ if (baseRow === null) return overlayRow;
11049
+ const overlaySrc = overlayRow;
11050
+ const picked = {};
11051
+ for (const field of rule.overlayFields) {
11052
+ if (field in overlaySrc) picked[field] = overlaySrc[field];
11053
+ }
11054
+ return { ...baseRow, ...picked };
11055
+ }
11056
+ }
11057
+ return baseRow;
10635
11058
  }
10636
11059
  // ─── Throw-stubs for the unimplemented Collection<T> surface ───────
10637
11060
  //
@@ -10781,6 +11204,21 @@ var init_archive = __esm({
10781
11204
  });
10782
11205
 
10783
11206
  // src/sequence/index.ts
11207
+ function resolveSequenceKey(series, opts) {
11208
+ const partition = opts?.partition;
11209
+ if (!partition || partition.length === 0) return series;
11210
+ const parts = partition.map((p) => {
11211
+ if (typeof p === "number" && !Number.isFinite(p)) {
11212
+ throw new ValidationError(`sequence partition component must be a finite number, got ${p}`);
11213
+ }
11214
+ const s = String(p);
11215
+ if (s === "") {
11216
+ throw new ValidationError("sequence partition component must not be empty");
11217
+ }
11218
+ return encodeURIComponent(s);
11219
+ });
11220
+ return `${series}\0${parts.join("/")}`;
11221
+ }
10784
11222
  async function sleepBackoff2(attempt) {
10785
11223
  const ceil = Math.min(2 ** attempt, 32);
10786
11224
  const ms = Math.floor(Math.random() * ceil);
@@ -10820,7 +11258,8 @@ var init_sequence = __esm({
10820
11258
  handle(name) {
10821
11259
  return {
10822
11260
  next: () => this.next(name),
10823
- peek: () => this.peek(name)
11261
+ peek: () => this.peek(name),
11262
+ seedTo: (n) => this.seedTo(name, n)
10824
11263
  };
10825
11264
  }
10826
11265
  assertOnline() {
@@ -10873,6 +11312,30 @@ var init_sequence = __esm({
10873
11312
  void lastConflict;
10874
11313
  throw new SequenceContentionError(name, MAX_NEXT_ATTEMPTS);
10875
11314
  }
11315
+ async seedTo(name, n) {
11316
+ this.assertOnline();
11317
+ if (n <= 0) return;
11318
+ let lastConflict;
11319
+ for (let attempt = 0; attempt < MAX_NEXT_ATTEMPTS; attempt++) {
11320
+ const { env, value } = await this.read(name);
11321
+ if (value >= n) return;
11322
+ const expectedVersion = env?._v ?? 0;
11323
+ const envelope = await this.encryptState({ value: n }, expectedVersion + 1);
11324
+ try {
11325
+ await this.adapter.put(this.vault, SEQUENCE_COLLECTION, name, envelope, expectedVersion);
11326
+ return;
11327
+ } catch (err) {
11328
+ if (err instanceof ConflictError) {
11329
+ lastConflict = err;
11330
+ if (attempt < MAX_NEXT_ATTEMPTS - 1) await sleepBackoff2(attempt);
11331
+ continue;
11332
+ }
11333
+ throw err;
11334
+ }
11335
+ }
11336
+ void lastConflict;
11337
+ throw new SequenceContentionError(name, MAX_NEXT_ATTEMPTS);
11338
+ }
10876
11339
  };
10877
11340
  }
10878
11341
  });
@@ -12887,11 +13350,15 @@ var init_read_only_facade = __esm({
12887
13350
  });
12888
13351
 
12889
13352
  // src/derivations/strategy-hash.ts
12890
- async function computeStrategyHash(source, outputKeys, derive) {
13353
+ async function computeStrategyHash(source, outputKeys, derive, sources) {
12891
13354
  const canonical2 = JSON.stringify({
12892
13355
  source,
12893
13356
  outputs: [...outputKeys].sort(),
12894
- derive: derive.toString()
13357
+ derive: derive.toString(),
13358
+ // Declared sibling sources (#344) — adding/removing a trigger
13359
+ // collection invalidates cached derived records. Omitted when empty
13360
+ // so strategies without siblings keep their existing hash.
13361
+ ...sources?.length ? { sources: [...sources].sort() } : {}
12895
13362
  });
12896
13363
  const bytes = new TextEncoder().encode(canonical2);
12897
13364
  const digest = await crypto.subtle.digest("SHA-256", bytes);
@@ -12920,11 +13387,16 @@ var init_registry3 = __esm({
12920
13387
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12921
13388
  async register(spec) {
12922
13389
  const outputKeys = Object.keys(spec.outputs);
12923
- const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive);
13390
+ const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive, spec.sources);
12924
13391
  const reg = { spec, strategyHash };
12925
13392
  const fromSource = this._bySource.get(spec.source);
12926
13393
  if (fromSource) fromSource.push(reg);
12927
13394
  else this._bySource.set(spec.source, [reg]);
13395
+ for (const extra of spec.sources ?? []) {
13396
+ const fromExtra = this._bySource.get(extra);
13397
+ if (fromExtra) fromExtra.push(reg);
13398
+ else this._bySource.set(extra, [reg]);
13399
+ }
12928
13400
  for (const key of outputKeys) {
12929
13401
  const output = spec.outputs[key];
12930
13402
  if (!output) continue;
@@ -14100,17 +14572,23 @@ var init_vault = __esm({
14100
14572
  * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
14101
14573
  * ```
14102
14574
  */
14103
- sequence(name) {
14104
- if (this.numberingConfigs.has(name)) {
14575
+ sequence(series, opts) {
14576
+ if (series.includes("\0")) {
14577
+ throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
14578
+ }
14579
+ if (this.numberingConfigs.has(series)) {
14105
14580
  const eng = this.deferred();
14106
14581
  return {
14107
- next: async (opts) => {
14108
- if (!opts?.for) {
14109
- throw new ValidationError(`sequence("${name}") is a deferred-numbering series; call next({ for: recordId }).`);
14582
+ next: async (nextOpts) => {
14583
+ if (!nextOpts?.for) {
14584
+ throw new ValidationError(`sequence("${series}") is a deferred-numbering series; call next({ for: recordId }).`);
14110
14585
  }
14111
- return (await eng.enqueue(name, opts.for)).assigned;
14586
+ return (await eng.enqueue(series, nextOpts.for)).assigned;
14112
14587
  },
14113
- peek: () => eng.peek(name)
14588
+ peek: () => eng.peek(series),
14589
+ seedTo: () => {
14590
+ throw new ValidationError(`sequence("${series}") is a deferred-numbering series; seedTo is CAS-only.`);
14591
+ }
14114
14592
  };
14115
14593
  }
14116
14594
  if (!this.sequenceStore) {
@@ -14122,7 +14600,7 @@ var init_vault = __esm({
14122
14600
  actor: this.keyring.userId
14123
14601
  });
14124
14602
  }
14125
- return this.sequenceStore.handle(name);
14603
+ return this.sequenceStore.handle(resolveSequenceKey(series, opts));
14126
14604
  }
14127
14605
  /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
14128
14606
  deferred() {
@@ -14414,9 +14892,24 @@ var init_vault = __esm({
14414
14892
  });
14415
14893
  }
14416
14894
  if (rule.mode === "cascade") {
14895
+ const txCtx = this.noydb._activeTxContextOrNull;
14417
14896
  for (const match of matches) {
14418
14897
  const matchId = match["id"] ?? null;
14419
14898
  if (matchId === null) continue;
14899
+ if (txCtx !== null) {
14900
+ const prior = await this.adapter.get(this.name, rule.collection, matchId);
14901
+ if (prior !== null) {
14902
+ txCtx._executed.push({
14903
+ op: {
14904
+ type: "delete",
14905
+ vaultName: this.name,
14906
+ collectionName: rule.collection,
14907
+ id: matchId
14908
+ },
14909
+ priorEnvelope: prior
14910
+ });
14911
+ }
14912
+ }
14420
14913
  await fromCollection.delete(matchId);
14421
14914
  }
14422
14915
  }
@@ -16805,7 +17298,7 @@ var init_strategy11 = __esm({
16805
17298
  'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
16806
17299
  );
16807
17300
  NO_TX = {
16808
- async runTransaction() {
17301
+ async runTransaction(_db, _fn, _options, _txInvariants) {
16809
17302
  throw NOT_ENABLED6;
16810
17303
  },
16811
17304
  async runDryRun() {