@ethisyscore/core-utils 1.94.0 → 1.95.0

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 (57) hide show
  1. package/dist/chunk-2F5FENXB.js +84 -0
  2. package/dist/chunk-2F5FENXB.js.map +1 -0
  3. package/dist/chunk-3LIIUXBE.js +14 -0
  4. package/dist/chunk-3LIIUXBE.js.map +1 -0
  5. package/dist/chunk-G2XGZH7P.cjs +16 -0
  6. package/dist/chunk-G2XGZH7P.cjs.map +1 -0
  7. package/dist/chunk-GAZLFZEL.js +105 -0
  8. package/dist/chunk-GAZLFZEL.js.map +1 -0
  9. package/dist/chunk-LT7I4JVK.cjs +121 -0
  10. package/dist/chunk-LT7I4JVK.cjs.map +1 -0
  11. package/dist/chunk-OEDAX6WN.js +121 -0
  12. package/dist/chunk-OEDAX6WN.js.map +1 -0
  13. package/dist/chunk-P5FSVRZO.cjs +127 -0
  14. package/dist/chunk-P5FSVRZO.cjs.map +1 -0
  15. package/dist/chunk-RLPXYKSM.cjs +23 -0
  16. package/dist/chunk-RLPXYKSM.cjs.map +1 -0
  17. package/dist/chunk-TN6DXBVD.js +252 -0
  18. package/dist/chunk-TN6DXBVD.js.map +1 -0
  19. package/dist/chunk-YNBKRMK3.cjs +290 -0
  20. package/dist/chunk-YNBKRMK3.cjs.map +1 -0
  21. package/dist/chunk-ZQCGDT7D.js +19 -0
  22. package/dist/chunk-ZQCGDT7D.js.map +1 -0
  23. package/dist/chunk-ZR7TABGT.cjs +94 -0
  24. package/dist/chunk-ZR7TABGT.cjs.map +1 -0
  25. package/dist/date/index.cjs +210 -397
  26. package/dist/date/index.cjs.map +1 -1
  27. package/dist/date/index.js +2 -349
  28. package/dist/date/index.js.map +1 -1
  29. package/dist/date/org-format.cjs +13 -151
  30. package/dist/date/org-format.cjs.map +1 -1
  31. package/dist/date/org-format.js +3 -141
  32. package/dist/date/org-format.js.map +1 -1
  33. package/dist/index.cjs +286 -600
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +6 -539
  38. package/dist/index.js.map +1 -1
  39. package/dist/list/index.cjs +7 -11
  40. package/dist/list/index.cjs.map +1 -1
  41. package/dist/list/index.js +1 -12
  42. package/dist/list/index.js.map +1 -1
  43. package/dist/money/index.cjs +39 -51
  44. package/dist/money/index.cjs.map +1 -1
  45. package/dist/money/index.d.cts +129 -8
  46. package/dist/money/index.d.ts +129 -8
  47. package/dist/money/index.js +1 -49
  48. package/dist/money/index.js.map +1 -1
  49. package/dist/number/index.cjs +15 -18
  50. package/dist/number/index.cjs.map +1 -1
  51. package/dist/number/index.js +1 -17
  52. package/dist/number/index.js.map +1 -1
  53. package/dist/patch/index.cjs +22 -121
  54. package/dist/patch/index.cjs.map +1 -1
  55. package/dist/patch/index.js +1 -119
  56. package/dist/patch/index.js.map +1 -1
  57. package/package.json +1 -1
@@ -12,6 +12,11 @@
12
12
  * (the amount's own currency, or the org's reporting currency) and CONVERTING
13
13
  * between currencies are host-aware concerns handled by the plugin-ui `useCurrency`
14
14
  * hook, which formats through this module.
15
+ *
16
+ * When no currency is given, `formatMoney` falls back to the organisation's
17
+ * reporting currency via the {@link ./orgCurrency} seam (the same module-ref +
18
+ * fail-open-to-`GBP` mechanism as the date org-format seam), so a plugin can render
19
+ * reporting-currency amounts without threading a code or hardcoding one.
15
20
  */
16
21
  /**
17
22
  * Deterministic fallback locale. Matches the date seam's UK fallback so money and
@@ -21,8 +26,12 @@
21
26
  declare const MONEY_FALLBACK_LOCALE = "en-GB";
22
27
  /** Options for {@link formatMoney}. */
23
28
  interface FormatMoneyOptions {
24
- /** ISO 4217 code (e.g. `"GBP"`, `"EUR"`) - drives `Intl` currency formatting. */
25
- currencyCode: string;
29
+ /**
30
+ * ISO 4217 code (e.g. `"GBP"`, `"EUR"`) - drives `Intl` currency formatting.
31
+ * Optional: when omitted, `formatMoney` resolves the organisation's reporting
32
+ * currency (see {@link ./orgCurrency}), falling back to `GBP` when none is stamped.
33
+ */
34
+ currencyCode?: string;
26
35
  /**
27
36
  * Fraction digits to show. When null/undefined, `Intl`'s per-currency default is
28
37
  * used (GBP -> 2, JPY -> 0). Pass a number to force a currency's decimal-places
@@ -39,12 +48,23 @@ interface FormatMoneyOptions {
39
48
  symbol?: string | null;
40
49
  }
41
50
  /**
42
- * Formats a monetary amount in the given currency. On an unknown/invalid currency
43
- * code (which makes `Intl.NumberFormat` throw) it falls back to a symbol/code prefix
44
- * plus the fixed-decimal amount, so a bad code degrades to a readable string rather
45
- * than throwing on a render path.
51
+ * Formats a monetary amount. The currency is resolved in this order:
52
+ *
53
+ * - `formatMoney(amount, "EUR")` a positional ISO code, with the currency's
54
+ * natural decimals.
55
+ * - `formatMoney(amount, { currencyCode, decimalPlaces, locale, symbol })` — the
56
+ * options form; any of `currencyCode`/`decimalPlaces` may be omitted.
57
+ * - `formatMoney(amount)` (or an options object without `currencyCode`) — the
58
+ * organisation's reporting currency stamped via the {@link ./orgCurrency} seam,
59
+ * including that currency's decimal-places override when the caller gave none.
60
+ * When no org currency is stamped it falls back to {@link MONEY_FALLBACK_CURRENCY}
61
+ * (`GBP`), the same deterministic fail-open as the date org-format seam.
62
+ *
63
+ * On an unknown/invalid currency code (which makes `Intl.NumberFormat` throw) it
64
+ * falls back to a symbol/code prefix plus the fixed-decimal amount, so a bad code
65
+ * degrades to a readable string rather than throwing on a render path.
46
66
  */
47
- declare function formatMoney(amount: number, options: FormatMoneyOptions): string;
67
+ declare function formatMoney(amount: number, currency?: string | FormatMoneyOptions): string;
48
68
  /** Options for {@link formatCompactMoney}. */
49
69
  interface FormatCompactMoneyOptions {
50
70
  /** ISO 4217 code (e.g. `"GBP"`, `"EUR"`) - resolves the leading symbol. */
@@ -80,4 +100,105 @@ declare function formatCompactMoney(amount: number, options: FormatCompactMoneyO
80
100
  */
81
101
  declare function getCurrencySymbol(currencyCode: string, locale?: string, symbol?: string | null): string;
82
102
 
83
- export { type FormatCompactMoneyOptions, type FormatMoneyOptions, MONEY_FALLBACK_LOCALE, formatCompactMoney, formatMoney, getCurrencySymbol };
103
+ /**
104
+ * Organisation-driven reporting-currency resolution.
105
+ *
106
+ * Plugin (and host) surfaces render reporting-currency amounts in whatever the
107
+ * organisation has configured as its reporting currency, without threading a
108
+ * currency code through every call site. This module holds the module-level
109
+ * reporting-currency ref, its setter, and the readers with a deterministic
110
+ * fallback constant. It is pure — no React, no host access — so it lives in
111
+ * core-utils behind the `@ethisyscore/core-utils/money` sub-path, mirroring the
112
+ * date org-format seam (`@ethisyscore/core-utils/date/org-format`).
113
+ *
114
+ * The ref is stamped by the surface sync hook — the SDK
115
+ * `useReportingCurrencySync` reads the host
116
+ * `settings:get-organisation-reporting-currency` tool and calls
117
+ * `setOrgReportingCurrency`. When no reporting currency is stamped (a
118
+ * standalone/mock run, an old host, or a failed fetch) resolution falls back to
119
+ * the deterministic UK default `GBP`, NOT the OS locale, so output is
120
+ * machine-stable regardless of the host. This is the exact fail-open contract of
121
+ * the date seam's UK `dd/MM/yyyy` fallback.
122
+ *
123
+ * Deciding WHICH currency an amount is in (its own currency vs the org's
124
+ * reporting currency) and CONVERTING between currencies stay host-aware concerns
125
+ * of the plugin-ui `useCurrency` hook. This seam only answers "what is the org's
126
+ * reporting currency" for the pure {@link formatMoney} default path.
127
+ */
128
+ /**
129
+ * Deterministic fallback reporting currency when no org currency is stamped.
130
+ * Matches the date seam's UK fallback (`ORG_DATE_FALLBACK_FORMAT`) so money and
131
+ * dates degrade consistently, and so output does not drift with the OS locale of
132
+ * whatever host renders it.
133
+ */
134
+ declare const MONEY_FALLBACK_CURRENCY = "GBP";
135
+ /** The organisation's reporting currency as stamped by the surface sync hook. */
136
+ interface OrgReportingCurrency {
137
+ /** ISO 4217 code (e.g. `"GBP"`, `"EUR"`) drives `Intl` currency formatting. */
138
+ code: string;
139
+ /**
140
+ * Host decimal-places override for the currency, or null to use the currency's
141
+ * ISO default (GBP -> 2, JPY -> 0). Applied by {@link formatMoney} only when the
142
+ * caller supplies no explicit decimalPlaces.
143
+ */
144
+ decimalPlaces: number | null;
145
+ }
146
+ /**
147
+ * Stamps the module-level reporting-currency ref. Called by the surface sync hook
148
+ * each time the organisation's reporting currency resolves. Pass `null` to clear
149
+ * it and fall back to the deterministic {@link MONEY_FALLBACK_CURRENCY}.
150
+ */
151
+ declare function setOrgReportingCurrency(currency: OrgReportingCurrency | null): void;
152
+ /** The org's stamped reporting currency, or null to fall back to the UK default. */
153
+ declare function getOrgReportingCurrency(): OrgReportingCurrency | null;
154
+ /**
155
+ * The org's effective reporting-currency code: the stamped code when present, else
156
+ * the deterministic {@link MONEY_FALLBACK_CURRENCY} (`GBP`). Never null, so callers
157
+ * always have a well-formed code to format with.
158
+ */
159
+ declare function resolveOrgReportingCurrencyCode(): string;
160
+
161
+ /**
162
+ * Framework-agnostic money PARSING — the inverse of {@link formatMoney} for form
163
+ * inputs and editable amount fields.
164
+ *
165
+ * A user (or a round-tripped display value) types an amount in the en-GB /
166
+ * dot-decimal display shape — a currency symbol or ISO code prefix, comma grouping
167
+ * separators, stray whitespace, and a dot decimal point ("£1,234.56", "GBP 1,234.56").
168
+ * This turns that back into a plain number, or null when there is no meaningful value
169
+ * to parse.
170
+ *
171
+ * It is deliberately lenient about the symbol, currency-code prefix and grouping so
172
+ * it tolerates the display forms {@link formatMoney} produces in the en-GB fallback
173
+ * locale, but it is NOT a locale-universal inverse of {@link formatMoney}. Only the
174
+ * dot-decimal shape is understood: a comma is always treated as a grouping separator
175
+ * and dropped, so comma-decimal locales (de-DE `1.234,56`) are OUT OF SCOPE and would
176
+ * mis-parse. The platform stores and edits amounts in the dot-decimal numeric form.
177
+ *
178
+ * Ported from the per-plugin `parseCurrency` helper so consuming plugins drop the
179
+ * local copy.
180
+ */
181
+ /**
182
+ * Parses a formatted currency string back to a number. Returns null for
183
+ * null/undefined, an empty/whitespace-only string, or the literal `"N/A"`
184
+ * placeholder; normalises the Unicode minus, strips every non-numeric character
185
+ * (currency symbols, alpha currency-code prefixes, grouping commas and whitespace),
186
+ * then `parseFloat`s the remainder, returning null when the result is not a number.
187
+ *
188
+ * Parses only the en-GB / dot-decimal display shape (comma thousands, dot decimal);
189
+ * see the module docs — it is NOT a locale-universal inverse of {@link formatMoney}.
190
+ * A leading minus is preserved, so `"-£10.00"` parses to `-10`. Garbage that contains
191
+ * no leading number (`"abc"`) yields null.
192
+ *
193
+ * @example
194
+ * parseMoney("£1,234.56") // 1234.56
195
+ * parseMoney("GBP 1,234.56") // 1234.56
196
+ * parseMoney("−£10.00") // -10 (Unicode minus U+2212)
197
+ * parseMoney("ZZZZ10.00") // 10 (invalid-code fallback prefix)
198
+ * parseMoney("N/A") // null
199
+ * parseMoney("") // null
200
+ * parseMoney(null) // null
201
+ */
202
+ declare function parseMoney(value: string | null | undefined): number | null;
203
+
204
+ export { type FormatCompactMoneyOptions, type FormatMoneyOptions, MONEY_FALLBACK_CURRENCY, MONEY_FALLBACK_LOCALE, type OrgReportingCurrency, formatCompactMoney, formatMoney, getCurrencySymbol, getOrgReportingCurrency, parseMoney, resolveOrgReportingCurrencyCode, setOrgReportingCurrency };
@@ -1,51 +1,3 @@
1
- // src/money/format.ts
2
- var MONEY_FALLBACK_LOCALE = "en-GB";
3
- function formatMoney(amount, options) {
4
- const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;
5
- const normalizedCode = currencyCode.toUpperCase();
6
- try {
7
- const formatter = new Intl.NumberFormat(locale, {
8
- style: "currency",
9
- currency: normalizedCode,
10
- // Narrow symbol so a clear currency context renders "$100", not "US$100".
11
- currencyDisplay: "narrowSymbol",
12
- ...decimalPlaces != null ? { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces } : {}
13
- });
14
- return formatter.format(amount);
15
- } catch {
16
- const fractionDigits = decimalPlaces ?? 2;
17
- const prefix = symbol ?? normalizedCode;
18
- const sign = amount < 0 ? "-" : "";
19
- return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;
20
- }
21
- }
22
- function formatCompactMoney(amount, options) {
23
- const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;
24
- const sym = getCurrencySymbol(currencyCode, locale, symbol);
25
- const sign = amount < 0 ? "-" : "";
26
- const abs = Math.abs(amount);
27
- if (abs >= 1e6) {
28
- return `${sign}${sym}${(abs / 1e6).toFixed(1)}m`;
29
- }
30
- if (abs >= 1e3) {
31
- return `${sign}${sym}${(abs / 1e3).toFixed(0)}k`;
32
- }
33
- return `${sign}${sym}${abs.toFixed(0)}`;
34
- }
35
- function getCurrencySymbol(currencyCode, locale = MONEY_FALLBACK_LOCALE, symbol) {
36
- const normalizedCode = currencyCode.toUpperCase();
37
- try {
38
- const parts = new Intl.NumberFormat(locale, {
39
- style: "currency",
40
- currency: normalizedCode,
41
- currencyDisplay: "narrowSymbol"
42
- }).formatToParts(0);
43
- return parts.find((part) => part.type === "currency")?.value ?? symbol ?? normalizedCode;
44
- } catch {
45
- return symbol ?? normalizedCode;
46
- }
47
- }
48
-
49
- export { MONEY_FALLBACK_LOCALE, formatCompactMoney, formatMoney, getCurrencySymbol };
1
+ export { MONEY_FALLBACK_CURRENCY, MONEY_FALLBACK_LOCALE, formatCompactMoney, formatMoney, getCurrencySymbol, getOrgReportingCurrency, parseMoney, resolveOrgReportingCurrencyCode, setOrgReportingCurrency } from '../chunk-2F5FENXB.js';
50
2
  //# sourceMappingURL=index.js.map
51
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/money/format.ts"],"names":[],"mappings":";AAqBO,IAAM,qBAAA,GAAwB;AA4B9B,SAAS,WAAA,CAAY,QAAgB,OAAA,EAAqC;AAC/E,EAAA,MAAM,EAAE,YAAA,EAAc,aAAA,EAAe,MAAA,GAAS,qBAAA,EAAuB,QAAO,GAAI,OAAA;AAIhF,EAAA,MAAM,cAAA,GAAiB,aAAa,WAAA,EAAY;AAEhD,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC9C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA;AAAA,MAEV,eAAA,EAAiB,cAAA;AAAA,MACjB,GAAI,iBAAiB,IAAA,GACjB,EAAE,uBAAuB,aAAA,EAAe,qBAAA,EAAuB,aAAA,EAAc,GAC7E;AAAC,KACN,CAAA;AACD,IAAA,OAAO,SAAA,CAAU,OAAO,MAAM,CAAA;AAAA,EAChC,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,iBAAiB,aAAA,IAAiB,CAAA;AACxC,IAAA,MAAM,SAAS,MAAA,IAAU,cAAA;AAEzB,IAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,MAAM,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,OAAA,CAAQ,cAAc,CAAC,CAAA,CAAA;AAAA,EACpE;AACF;AA8BO,SAAS,kBAAA,CAAmB,QAAgB,OAAA,EAA4C;AAC7F,EAAA,MAAM,EAAE,YAAA,EAAc,MAAA,EAAQ,MAAA,GAAS,uBAAsB,GAAI,OAAA;AAEjE,EAAA,MAAM,GAAA,GAAM,iBAAA,CAAkB,YAAA,EAAc,MAAA,EAAQ,MAAM,CAAA;AAC1D,EAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAE3B,EAAA,IAAI,OAAO,GAAA,EAAW;AACpB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAW,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACrD;AACA,EAAA,IAAI,OAAO,GAAA,EAAO;AAChB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAO,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACjD;AACA,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,GAAG,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA;AACvC;AAOO,SAAS,iBAAA,CACd,YAAA,EACA,MAAA,GAAiB,qBAAA,EACjB,MAAA,EACQ;AAER,EAAA,MAAM,cAAA,GAAiB,aAAa,WAAA,EAAY;AAEhD,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC1C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA,MACV,eAAA,EAAiB;AAAA,KAClB,CAAA,CAAE,aAAA,CAAc,CAAC,CAAA;AAElB,IAAA,OAAO,KAAA,CAAM,KAAK,CAAC,IAAA,KAAS,KAAK,IAAA,KAAS,UAAU,CAAA,EAAG,KAAA,IAAS,MAAA,IAAU,cAAA;AAAA,EAC5E,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA,IAAU,cAAA;AAAA,EACnB;AACF","file":"index.js","sourcesContent":["/**\n * Framework-agnostic money DISPLAY formatting.\n *\n * The platform stores amounts as a plain number plus a currency (an ISO 4217 code,\n * optionally a symbol and a decimal-places override from the host `Currency` row).\n * This module turns that into a localized string via `Intl.NumberFormat`, with a\n * deterministic fallback so output is machine-stable regardless of the host OS\n * locale - the same principle as the date org-format seam.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/money` sub-path. Resolving WHICH currency to render in\n * (the amount's own currency, or the org's reporting currency) and CONVERTING\n * between currencies are host-aware concerns handled by the plugin-ui `useCurrency`\n * hook, which formats through this module.\n */\n\n/**\n * Deterministic fallback locale. Matches the date seam's UK fallback so money and\n * dates render consistently when no explicit locale is supplied, and so output does\n * not drift with the OS locale of whatever host renders it.\n */\nexport const MONEY_FALLBACK_LOCALE = \"en-GB\";\n\n/** Options for {@link formatMoney}. */\nexport interface FormatMoneyOptions {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - drives `Intl` currency formatting. */\n currencyCode: string;\n /**\n * Fraction digits to show. When null/undefined, `Intl`'s per-currency default is\n * used (GBP -> 2, JPY -> 0). Pass a number to force a currency's decimal-places\n * override from the host `Currency` row.\n */\n decimalPlaces?: number | null;\n /** BCP-47 locale; defaults to {@link MONEY_FALLBACK_LOCALE} for stable output. */\n locale?: string;\n /**\n * Symbol to use only in the fallback path when `Intl` cannot format the currency\n * code (e.g. a non-ISO custom code). Ignored on the happy path, where `Intl`\n * supplies the symbol.\n */\n symbol?: string | null;\n}\n\n/**\n * Formats a monetary amount in the given currency. On an unknown/invalid currency\n * code (which makes `Intl.NumberFormat` throw) it falls back to a symbol/code prefix\n * plus the fixed-decimal amount, so a bad code degrades to a readable string rather\n * than throwing on a render path.\n */\nexport function formatMoney(amount: number, options: FormatMoneyOptions): string {\n const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;\n\n // Intl.NumberFormat requires a well-formed (uppercase) ISO 4217 code; a lowercase\n // code (e.g. \"usd\") throws and forces the fallback, so normalise up front.\n const normalizedCode = currencyCode.toUpperCase();\n\n try {\n const formatter = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n // Narrow symbol so a clear currency context renders \"$100\", not \"US$100\".\n currencyDisplay: \"narrowSymbol\",\n ...(decimalPlaces != null\n ? { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces }\n : {}),\n });\n return formatter.format(amount);\n } catch {\n const fractionDigits = decimalPlaces ?? 2;\n const prefix = symbol ?? normalizedCode;\n // Keep the minus sign before the symbol/code prefix (\"-£10.00\", not \"£-10.00\").\n const sign = amount < 0 ? \"-\" : \"\";\n return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;\n }\n}\n\n/** Options for {@link formatCompactMoney}. */\nexport interface FormatCompactMoneyOptions {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - resolves the leading symbol. */\n currencyCode: string;\n /**\n * Fallback symbol used ONLY when `Intl` cannot resolve `currencyCode` (a non-ISO\n * custom code). For a valid ISO code `Intl`'s own symbol always wins - this option\n * does not override it. Mirrors {@link FormatMoneyOptions.symbol} and the\n * {@link getCurrencySymbol} fallback contract.\n */\n symbol?: string | null;\n /** BCP-47 locale used only to resolve the symbol; defaults to {@link MONEY_FALLBACK_LOCALE}. */\n locale?: string;\n}\n\n/**\n * Formats an amount as an abbreviated currency string for compact display, such\n * as chart axis ticks where space is tight - e.g. `\"£1.3m\"`, `\"£500k\"`, `\"£99\"`.\n *\n * Thresholds on the absolute value: >= 1,000,000 renders in millions with a\n * lowercase `m` and one decimal place; >= 1,000 renders in thousands with a\n * lowercase `k` and no decimals; otherwise the whole amount with no decimals.\n * A leading minus is kept before the symbol (e.g. `\"-£1.3m\"`).\n *\n * Unlike {@link formatMoney} this is NOT org-locale driven beyond resolving the\n * leading symbol via {@link getCurrencySymbol}; the number itself is formatted\n * with fixed abbreviations so axis labels stay short and machine-stable.\n */\nexport function formatCompactMoney(amount: number, options: FormatCompactMoneyOptions): string {\n const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;\n\n const sym = getCurrencySymbol(currencyCode, locale, symbol);\n const sign = amount < 0 ? \"-\" : \"\";\n const abs = Math.abs(amount);\n\n if (abs >= 1_000_000) {\n return `${sign}${sym}${(abs / 1_000_000).toFixed(1)}m`;\n }\n if (abs >= 1_000) {\n return `${sign}${sym}${(abs / 1_000).toFixed(0)}k`;\n }\n return `${sign}${sym}${abs.toFixed(0)}`;\n}\n\n/**\n * Resolves the currency symbol for a code in a locale (e.g. `\"GBP\"` -> `\"£\"`),\n * for use in input adornments and labels. Falls back to the supplied `symbol`, then\n * the code itself, when `Intl` cannot resolve it.\n */\nexport function getCurrencySymbol(\n currencyCode: string,\n locale: string = MONEY_FALLBACK_LOCALE,\n symbol?: string | null,\n): string {\n // Intl needs an uppercase ISO code; a lowercase one throws (see formatMoney).\n const normalizedCode = currencyCode.toUpperCase();\n\n try {\n const parts = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n currencyDisplay: \"narrowSymbol\",\n }).formatToParts(0);\n\n return parts.find((part) => part.type === \"currency\")?.value ?? symbol ?? normalizedCode;\n } catch {\n return symbol ?? normalizedCode;\n }\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,23 +1,20 @@
1
1
  'use strict';
2
2
 
3
- // src/number/format.ts
4
- var NUMBER_FALLBACK_LOCALE = "en-GB";
5
- function formatNumber(value, options) {
6
- if (value == null) {
7
- return "\u2014";
8
- }
9
- const dp = options?.decimalPlaces ?? 2;
10
- return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {
11
- minimumFractionDigits: dp,
12
- maximumFractionDigits: dp
13
- }).format(value);
14
- }
15
- function formatPercent(ratio, options) {
16
- return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;
17
- }
3
+ var chunkRLPXYKSM_cjs = require('../chunk-RLPXYKSM.cjs');
18
4
 
19
- exports.NUMBER_FALLBACK_LOCALE = NUMBER_FALLBACK_LOCALE;
20
- exports.formatNumber = formatNumber;
21
- exports.formatPercent = formatPercent;
5
+
6
+
7
+ Object.defineProperty(exports, "NUMBER_FALLBACK_LOCALE", {
8
+ enumerable: true,
9
+ get: function () { return chunkRLPXYKSM_cjs.NUMBER_FALLBACK_LOCALE; }
10
+ });
11
+ Object.defineProperty(exports, "formatNumber", {
12
+ enumerable: true,
13
+ get: function () { return chunkRLPXYKSM_cjs.formatNumber; }
14
+ });
15
+ Object.defineProperty(exports, "formatPercent", {
16
+ enumerable: true,
17
+ get: function () { return chunkRLPXYKSM_cjs.formatPercent; }
18
+ });
22
19
  //# sourceMappingURL=index.cjs.map
23
20
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/number/format.ts"],"names":[],"mappings":";;;AAgBO,IAAM,sBAAA,GAAyB;AAS/B,SAAS,YAAA,CACd,OACA,OAAA,EACQ;AACR,EAAA,IAAI,SAAS,IAAA,EAAM;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,EAAA,GAAK,SAAS,aAAA,IAAiB,CAAA;AACrC,EAAA,OAAO,IAAI,IAAA,CAAK,YAAA,CAAa,OAAA,EAAS,UAAU,sBAAA,EAAwB;AAAA,IACtE,qBAAA,EAAuB,EAAA;AAAA,IACvB,qBAAA,EAAuB;AAAA,GACxB,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACjB;AAWO,SAAS,aAAA,CAAc,OAAe,OAAA,EAA8C;AACzF,EAAA,OAAO,IAAI,KAAA,GAAQ,GAAA,EAAK,QAAQ,OAAA,EAAS,aAAA,IAAiB,CAAC,CAAC,CAAA,CAAA,CAAA;AAC9D","file":"index.cjs","sourcesContent":["/**\n * Framework-agnostic number DISPLAY formatting.\n *\n * Pure helpers for rendering plain numeric values and ratios as strings, with a\n * deterministic `en-GB` fallback locale so output is machine-stable regardless\n * of the host OS locale - the same principle as the money and date seams.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/number` sub-path.\n */\n\n/**\n * Deterministic fallback locale. Matches the money/date seams so numbers render\n * consistently when no explicit locale is supplied, and so output does not drift\n * with the OS locale of whatever host renders it.\n */\nexport const NUMBER_FALLBACK_LOCALE = \"en-GB\";\n\n/**\n * Formats a numeric value with grouping and a fixed number of decimal places via\n * `Intl.NumberFormat`. Returns an em-dash (`\"—\"`) for null/undefined so an\n * absent value renders as a readable placeholder rather than `\"NaN\"` or `\"null\"`.\n *\n * Defaults to two decimal places in {@link NUMBER_FALLBACK_LOCALE}.\n */\nexport function formatNumber(\n value: number | null | undefined,\n options?: { decimalPlaces?: number; locale?: string },\n): string {\n if (value == null) {\n return \"—\";\n }\n\n const dp = options?.decimalPlaces ?? 2;\n return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {\n minimumFractionDigits: dp,\n maximumFractionDigits: dp,\n }).format(value);\n}\n\n/**\n * Formats a RATIO as a percentage string with a fixed number of decimal places -\n * e.g. `0.1234` -> `\"12.34%\"`. The input is a ratio, not an already-scaled\n * percentage.\n *\n * Uses `toFixed` (no grouping) rather than `Intl` percent style so output is a\n * plain fixed-decimal percentage, matching what callers expect on compact labels.\n * Defaults to two decimal places.\n */\nexport function formatPercent(ratio: number, options?: { decimalPlaces?: number }): string {\n return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.cjs"}
@@ -1,19 +1,3 @@
1
- // src/number/format.ts
2
- var NUMBER_FALLBACK_LOCALE = "en-GB";
3
- function formatNumber(value, options) {
4
- if (value == null) {
5
- return "\u2014";
6
- }
7
- const dp = options?.decimalPlaces ?? 2;
8
- return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {
9
- minimumFractionDigits: dp,
10
- maximumFractionDigits: dp
11
- }).format(value);
12
- }
13
- function formatPercent(ratio, options) {
14
- return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;
15
- }
16
-
17
- export { NUMBER_FALLBACK_LOCALE, formatNumber, formatPercent };
1
+ export { NUMBER_FALLBACK_LOCALE, formatNumber, formatPercent } from '../chunk-ZQCGDT7D.js';
18
2
  //# sourceMappingURL=index.js.map
19
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/number/format.ts"],"names":[],"mappings":";AAgBO,IAAM,sBAAA,GAAyB;AAS/B,SAAS,YAAA,CACd,OACA,OAAA,EACQ;AACR,EAAA,IAAI,SAAS,IAAA,EAAM;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,EAAA,GAAK,SAAS,aAAA,IAAiB,CAAA;AACrC,EAAA,OAAO,IAAI,IAAA,CAAK,YAAA,CAAa,OAAA,EAAS,UAAU,sBAAA,EAAwB;AAAA,IACtE,qBAAA,EAAuB,EAAA;AAAA,IACvB,qBAAA,EAAuB;AAAA,GACxB,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACjB;AAWO,SAAS,aAAA,CAAc,OAAe,OAAA,EAA8C;AACzF,EAAA,OAAO,IAAI,KAAA,GAAQ,GAAA,EAAK,QAAQ,OAAA,EAAS,aAAA,IAAiB,CAAC,CAAC,CAAA,CAAA,CAAA;AAC9D","file":"index.js","sourcesContent":["/**\n * Framework-agnostic number DISPLAY formatting.\n *\n * Pure helpers for rendering plain numeric values and ratios as strings, with a\n * deterministic `en-GB` fallback locale so output is machine-stable regardless\n * of the host OS locale - the same principle as the money and date seams.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/number` sub-path.\n */\n\n/**\n * Deterministic fallback locale. Matches the money/date seams so numbers render\n * consistently when no explicit locale is supplied, and so output does not drift\n * with the OS locale of whatever host renders it.\n */\nexport const NUMBER_FALLBACK_LOCALE = \"en-GB\";\n\n/**\n * Formats a numeric value with grouping and a fixed number of decimal places via\n * `Intl.NumberFormat`. Returns an em-dash (`\"—\"`) for null/undefined so an\n * absent value renders as a readable placeholder rather than `\"NaN\"` or `\"null\"`.\n *\n * Defaults to two decimal places in {@link NUMBER_FALLBACK_LOCALE}.\n */\nexport function formatNumber(\n value: number | null | undefined,\n options?: { decimalPlaces?: number; locale?: string },\n): string {\n if (value == null) {\n return \"—\";\n }\n\n const dp = options?.decimalPlaces ?? 2;\n return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {\n minimumFractionDigits: dp,\n maximumFractionDigits: dp,\n }).format(value);\n}\n\n/**\n * Formats a RATIO as a percentage string with a fixed number of decimal places -\n * e.g. `0.1234` -> `\"12.34%\"`. The input is a ratio, not an already-scaled\n * percentage.\n *\n * Uses `toFixed` (no grouping) rather than `Intl` percent style so output is a\n * plain fixed-decimal percentage, matching what callers expect on compact labels.\n * Defaults to two decimal places.\n */\nexport function formatPercent(ratio: number, options?: { decimalPlaces?: number }): string {\n return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,127 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var mutative = require('mutative');
3
+ var chunkP5FSVRZO_cjs = require('../chunk-P5FSVRZO.cjs');
4
4
 
5
- // src/patch/index.ts
6
- function generateJsonPatch(original, modified) {
7
- if (!isSameType(original, modified)) {
8
- if (original === modified) return [];
9
- return [{ op: "replace", path: "", value: modified }];
10
- }
11
- const [, patches] = mutative.create(
12
- original,
13
- (draft) => {
14
- applyChanges(draft, modified);
15
- },
16
- {
17
- enablePatches: {
18
- pathAsArray: false
19
- }
20
- }
21
- );
22
- return patches.map(
23
- (patch) => ({
24
- op: patch.op,
25
- path: patch.path,
26
- // explicitly check for value existence to satisfy strict null checks if needed
27
- ...patch.op !== "remove" ? { value: patch.value } : {}
28
- })
29
- );
30
- }
31
- function applyChanges(draft, modified) {
32
- if (isDeepEqual(draft, modified)) {
33
- return;
34
- }
35
- if (Array.isArray(draft) && Array.isArray(modified)) {
36
- const draftArray = draft;
37
- draftArray.length = 0;
38
- draftArray.push(...modified);
39
- return;
40
- }
41
- if (isPlainObject(draft) && isPlainObject(modified)) {
42
- const draftObj = draft;
43
- const modifiedObj = modified;
44
- for (const key of Object.keys(draftObj)) {
45
- if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {
46
- delete draftObj[key];
47
- }
48
- }
49
- for (const key of Object.keys(modifiedObj)) {
50
- if (!isDeepEqual(draftObj[key], modifiedObj[key])) {
51
- draftObj[key] = modifiedObj[key];
52
- }
53
- }
54
- return;
55
- }
56
- }
57
- function isDeepEqual(a, b) {
58
- if (a === b) return true;
59
- if (a === null || a === void 0 || b === null || b === void 0) {
60
- return a === b;
61
- }
62
- if (typeof a !== typeof b) return false;
63
- if (a instanceof Date && b instanceof Date) {
64
- return a.getTime() === b.getTime();
65
- }
66
- if (Array.isArray(a) && Array.isArray(b)) {
67
- if (a.length !== b.length) return false;
68
- for (let i = 0; i < a.length; i++) {
69
- if (!isDeepEqual(a[i], b[i])) return false;
70
- }
71
- return true;
72
- }
73
- if (typeof a === "object" && typeof b === "object") {
74
- const objA = a;
75
- const objB = b;
76
- const keysA = Object.keys(objA);
77
- const keysB = Object.keys(objB);
78
- if (keysA.length !== keysB.length) return false;
79
- for (const key of keysA) {
80
- if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
81
- if (!isDeepEqual(objA[key], objB[key])) return false;
82
- }
83
- return true;
84
- }
85
- return false;
86
- }
87
- function isPlainObject(value) {
88
- return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
89
- }
90
- function isSameType(a, b) {
91
- if (Array.isArray(a)) return Array.isArray(b);
92
- if (Array.isArray(b)) return false;
93
- if (a instanceof Date) return b instanceof Date;
94
- if (a && typeof a === "object" && b && typeof b === "object") return true;
95
- return typeof a === typeof b;
96
- }
97
- function applyJsonPatch(original, patches) {
98
- const rootReplacement = patches.find((patch) => patch.path === "" && patch.op === "replace");
99
- if (rootReplacement) {
100
- return rootReplacement.value;
101
- }
102
- const nonRootPatches = patches.filter((patch) => patch.path !== "");
103
- if (nonRootPatches.length === 0) {
104
- return original;
105
- }
106
- return mutative.apply(original, nonRootPatches);
107
- }
108
- function isNullOrEmpty(patches) {
109
- return !patches || patches.length === 0;
110
- }
111
- function hasPatches(patches) {
112
- return patches.length > 0;
113
- }
114
- function deepClone(obj) {
115
- if (typeof structuredClone === "function") {
116
- return structuredClone(obj);
117
- }
118
- return JSON.parse(JSON.stringify(obj));
119
- }
120
5
 
121
- exports.applyJsonPatch = applyJsonPatch;
122
- exports.deepClone = deepClone;
123
- exports.generateJsonPatch = generateJsonPatch;
124
- exports.hasPatches = hasPatches;
125
- exports.isNullOrEmpty = isNullOrEmpty;
6
+
7
+ Object.defineProperty(exports, "applyJsonPatch", {
8
+ enumerable: true,
9
+ get: function () { return chunkP5FSVRZO_cjs.applyJsonPatch; }
10
+ });
11
+ Object.defineProperty(exports, "deepClone", {
12
+ enumerable: true,
13
+ get: function () { return chunkP5FSVRZO_cjs.deepClone; }
14
+ });
15
+ Object.defineProperty(exports, "generateJsonPatch", {
16
+ enumerable: true,
17
+ get: function () { return chunkP5FSVRZO_cjs.generateJsonPatch; }
18
+ });
19
+ Object.defineProperty(exports, "hasPatches", {
20
+ enumerable: true,
21
+ get: function () { return chunkP5FSVRZO_cjs.hasPatches; }
22
+ });
23
+ Object.defineProperty(exports, "isNullOrEmpty", {
24
+ enumerable: true,
25
+ get: function () { return chunkP5FSVRZO_cjs.isNullOrEmpty; }
26
+ });
126
27
  //# sourceMappingURL=index.cjs.map
127
28
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/patch/index.ts"],"names":["create","apply"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * RFC 6902 JSON Patch helpers shared by the EthisysCore monolith and plugins.\n *\n * The wire shape (`JsonPatchOperation`) matches the backend SDK's\n * `EthisysCore.Plugin.Sdk.JsonPatchOperation` record, so a patch produced here by\n * {@link generateJsonPatch} is applied by `IEntityPatchService` on the server.\n */\nimport { apply, create } from \"mutative\";\n\n/** JSON Patch operation type. */\nexport type PatchOperation = \"add\" | \"remove\" | \"replace\";\n\n/**\n * A single JSON Patch operation (RFC 6902 compatible; mutative's format with a string path).\n */\nexport interface JsonPatchOperation {\n /** The operation to be performed. */\n op: PatchOperation;\n /** JSON Pointer path (RFC 6902 format). */\n path: string;\n /** The value for add/replace operations. */\n value?: unknown;\n /** Source pointer for move/copy operations (backend `From`); unused by the generate/apply helpers. */\n from?: string;\n}\n\n/** Array of JSON Patch operations. */\nexport type JsonPatch = JsonPatchOperation[];\n\n/**\n * Generates JSON Patch operations (RFC 6902).\n * Strictly typed, handles Date objects, and ignores key order.\n */\nexport function generateJsonPatch<T>(original: T, modified: T): JsonPatch {\n // If types are completely different (e.g. Array vs Object), strict replacement.\n if (!isSameType(original, modified)) {\n // If both are strictly equal (e.g. both null), returns empty\n if (original === modified) return [];\n // If different, replace root\n return [{ op: \"replace\", path: \"\", value: modified }];\n }\n\n const [, patches] = create(\n original,\n (draft) => {\n // We can safely cast draft to unknown here because we control the recursion\n applyChanges(draft, modified);\n },\n {\n enablePatches: {\n pathAsArray: false,\n },\n },\n );\n\n return patches.map(\n (patch): JsonPatchOperation => ({\n op: patch.op as \"add\" | \"remove\" | \"replace\",\n path: patch.path as string,\n // explicitly check for value existence to satisfy strict null checks if needed\n ...(patch.op !== \"remove\" ? { value: patch.value } : {}),\n }),\n );\n}\n\n/**\n * Recursively applies changes to the Mutative draft.\n * Uses 'unknown' and Type Guards instead of 'any'.\n */\nfunction applyChanges(draft: unknown, modified: unknown): void {\n // This handles Primitives, Dates, and identical References early\n if (isDeepEqual(draft, modified)) {\n return;\n }\n\n // We explicitly check isArray to safely access .length and .push\n if (Array.isArray(draft) && Array.isArray(modified)) {\n const draftArray = draft as Array<unknown>;\n // Clear and Replace approach (safer for DTOs than diffing indices)\n draftArray.length = 0;\n draftArray.push(...modified);\n return;\n }\n\n // We use type guards to ensure we can treat them as dictionaries\n if (isPlainObject(draft) && isPlainObject(modified)) {\n const draftObj = draft;\n const modifiedObj = modified;\n\n // A. Remove keys that don't exist in modified\n for (const key of Object.keys(draftObj)) {\n if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {\n delete draftObj[key];\n }\n }\n\n // B. Add or update top-level keys. We deliberately do NOT recurse into nested objects: the backend\n // patch applier (IEntityPatchService) supports only top-level property operations, so a changed\n // nested object is replaced wholesale (path \"/key\"), never diffed into \"/key/child\" paths that the\n // server would reject.\n for (const key of Object.keys(modifiedObj)) {\n if (!isDeepEqual(draftObj[key], modifiedObj[key])) {\n draftObj[key] = modifiedObj[key];\n }\n }\n return;\n }\n}\n\n/**\n * Handles: Primitives, Arrays, Dates, Objects (order-independent), null/undefined\n */\nfunction isDeepEqual(a: unknown, b: unknown): boolean {\n // Strict Reference & Primitive Check\n if (a === b) return true;\n\n // Null/Undefined Checks\n if (a === null || a === undefined || b === null || b === undefined) {\n return a === b;\n }\n\n // Type Mismatch\n if (typeof a !== typeof b) return false;\n\n // Date Objects\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n // Objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n // Treat as Record<string, unknown> safely\n const objA = a as Record<string, unknown>;\n const objB = b as Record<string, unknown>;\n\n const keysA = Object.keys(objA);\n const keysB = Object.keys(objB);\n\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;\n if (!isDeepEqual(objA[key], objB[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Type Guard: Checks if value is a plain object (not null, array, or Date)\n * Allows safe casting to Record<string, unknown>\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n/**\n * Helper: Checks if two values share a high-level type category\n */\nfunction isSameType(a: unknown, b: unknown): boolean {\n if (Array.isArray(a)) return Array.isArray(b);\n if (Array.isArray(b)) return false;\n if (a instanceof Date) return b instanceof Date;\n if (a && typeof a === \"object\" && b && typeof b === \"object\") return true;\n return typeof a === typeof b;\n}\n\n/**\n * Applies JSON Patch operations to an object using Mutative.\n * @param original - The original object\n * @param patches - Array of JSON Patch operations to apply\n * @returns The patched object (immutable)\n */\nexport function applyJsonPatch<T>(original: T, patches: JsonPatch): T {\n // Check for root replacement first - if found, it should be the only operation\n const rootReplacement = patches.find((patch) => patch.path === \"\" && patch.op === \"replace\");\n if (rootReplacement) {\n return rootReplacement.value as T;\n }\n\n // Filter out any root operations that might interfere and apply the rest\n const nonRootPatches = patches.filter((patch) => patch.path !== \"\");\n if (nonRootPatches.length === 0) {\n return original;\n }\n\n // Mutative's apply function expects patches in RFC 6902 format, which is what we have\n return apply(original as Record<string, unknown>, nonRootPatches) as T;\n}\n\n/**\n * Checks if a patch array is empty (null, undefined, or empty array).\n * @param patches - Array of patch operations or null/undefined\n * @returns True if patches is null, undefined, or empty array\n */\nexport function isNullOrEmpty(patches: JsonPatch | null | undefined): boolean {\n return !patches || patches.length === 0;\n}\n\n/**\n * Checks if a patch array has any operations.\n * @param patches - Array of patch operations\n * @returns True if patches array has operations, false if empty\n */\nexport function hasPatches(patches: JsonPatch): boolean {\n return patches.length > 0;\n}\n\n/**\n * Creates a deep clone of an object.\n * Useful for creating the \"modified\" version before generating patches.\n * @param obj - Object to clone\n * @returns Deep cloned object\n */\nexport function deepClone<T>(obj: T): T {\n // structuredClone preserves Dates (and undefined); fall back to JSON clone on legacy runtimes.\n if (typeof structuredClone === \"function\") {\n return structuredClone(obj);\n }\n return JSON.parse(JSON.stringify(obj));\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.cjs"}
@@ -1,121 +1,3 @@
1
- import { create, apply } from 'mutative';
2
-
3
- // src/patch/index.ts
4
- function generateJsonPatch(original, modified) {
5
- if (!isSameType(original, modified)) {
6
- if (original === modified) return [];
7
- return [{ op: "replace", path: "", value: modified }];
8
- }
9
- const [, patches] = create(
10
- original,
11
- (draft) => {
12
- applyChanges(draft, modified);
13
- },
14
- {
15
- enablePatches: {
16
- pathAsArray: false
17
- }
18
- }
19
- );
20
- return patches.map(
21
- (patch) => ({
22
- op: patch.op,
23
- path: patch.path,
24
- // explicitly check for value existence to satisfy strict null checks if needed
25
- ...patch.op !== "remove" ? { value: patch.value } : {}
26
- })
27
- );
28
- }
29
- function applyChanges(draft, modified) {
30
- if (isDeepEqual(draft, modified)) {
31
- return;
32
- }
33
- if (Array.isArray(draft) && Array.isArray(modified)) {
34
- const draftArray = draft;
35
- draftArray.length = 0;
36
- draftArray.push(...modified);
37
- return;
38
- }
39
- if (isPlainObject(draft) && isPlainObject(modified)) {
40
- const draftObj = draft;
41
- const modifiedObj = modified;
42
- for (const key of Object.keys(draftObj)) {
43
- if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {
44
- delete draftObj[key];
45
- }
46
- }
47
- for (const key of Object.keys(modifiedObj)) {
48
- if (!isDeepEqual(draftObj[key], modifiedObj[key])) {
49
- draftObj[key] = modifiedObj[key];
50
- }
51
- }
52
- return;
53
- }
54
- }
55
- function isDeepEqual(a, b) {
56
- if (a === b) return true;
57
- if (a === null || a === void 0 || b === null || b === void 0) {
58
- return a === b;
59
- }
60
- if (typeof a !== typeof b) return false;
61
- if (a instanceof Date && b instanceof Date) {
62
- return a.getTime() === b.getTime();
63
- }
64
- if (Array.isArray(a) && Array.isArray(b)) {
65
- if (a.length !== b.length) return false;
66
- for (let i = 0; i < a.length; i++) {
67
- if (!isDeepEqual(a[i], b[i])) return false;
68
- }
69
- return true;
70
- }
71
- if (typeof a === "object" && typeof b === "object") {
72
- const objA = a;
73
- const objB = b;
74
- const keysA = Object.keys(objA);
75
- const keysB = Object.keys(objB);
76
- if (keysA.length !== keysB.length) return false;
77
- for (const key of keysA) {
78
- if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
79
- if (!isDeepEqual(objA[key], objB[key])) return false;
80
- }
81
- return true;
82
- }
83
- return false;
84
- }
85
- function isPlainObject(value) {
86
- return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
87
- }
88
- function isSameType(a, b) {
89
- if (Array.isArray(a)) return Array.isArray(b);
90
- if (Array.isArray(b)) return false;
91
- if (a instanceof Date) return b instanceof Date;
92
- if (a && typeof a === "object" && b && typeof b === "object") return true;
93
- return typeof a === typeof b;
94
- }
95
- function applyJsonPatch(original, patches) {
96
- const rootReplacement = patches.find((patch) => patch.path === "" && patch.op === "replace");
97
- if (rootReplacement) {
98
- return rootReplacement.value;
99
- }
100
- const nonRootPatches = patches.filter((patch) => patch.path !== "");
101
- if (nonRootPatches.length === 0) {
102
- return original;
103
- }
104
- return apply(original, nonRootPatches);
105
- }
106
- function isNullOrEmpty(patches) {
107
- return !patches || patches.length === 0;
108
- }
109
- function hasPatches(patches) {
110
- return patches.length > 0;
111
- }
112
- function deepClone(obj) {
113
- if (typeof structuredClone === "function") {
114
- return structuredClone(obj);
115
- }
116
- return JSON.parse(JSON.stringify(obj));
117
- }
118
-
119
- export { applyJsonPatch, deepClone, generateJsonPatch, hasPatches, isNullOrEmpty };
1
+ export { applyJsonPatch, deepClone, generateJsonPatch, hasPatches, isNullOrEmpty } from '../chunk-OEDAX6WN.js';
120
2
  //# sourceMappingURL=index.js.map
121
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/patch/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * RFC 6902 JSON Patch helpers shared by the EthisysCore monolith and plugins.\n *\n * The wire shape (`JsonPatchOperation`) matches the backend SDK's\n * `EthisysCore.Plugin.Sdk.JsonPatchOperation` record, so a patch produced here by\n * {@link generateJsonPatch} is applied by `IEntityPatchService` on the server.\n */\nimport { apply, create } from \"mutative\";\n\n/** JSON Patch operation type. */\nexport type PatchOperation = \"add\" | \"remove\" | \"replace\";\n\n/**\n * A single JSON Patch operation (RFC 6902 compatible; mutative's format with a string path).\n */\nexport interface JsonPatchOperation {\n /** The operation to be performed. */\n op: PatchOperation;\n /** JSON Pointer path (RFC 6902 format). */\n path: string;\n /** The value for add/replace operations. */\n value?: unknown;\n /** Source pointer for move/copy operations (backend `From`); unused by the generate/apply helpers. */\n from?: string;\n}\n\n/** Array of JSON Patch operations. */\nexport type JsonPatch = JsonPatchOperation[];\n\n/**\n * Generates JSON Patch operations (RFC 6902).\n * Strictly typed, handles Date objects, and ignores key order.\n */\nexport function generateJsonPatch<T>(original: T, modified: T): JsonPatch {\n // If types are completely different (e.g. Array vs Object), strict replacement.\n if (!isSameType(original, modified)) {\n // If both are strictly equal (e.g. both null), returns empty\n if (original === modified) return [];\n // If different, replace root\n return [{ op: \"replace\", path: \"\", value: modified }];\n }\n\n const [, patches] = create(\n original,\n (draft) => {\n // We can safely cast draft to unknown here because we control the recursion\n applyChanges(draft, modified);\n },\n {\n enablePatches: {\n pathAsArray: false,\n },\n },\n );\n\n return patches.map(\n (patch): JsonPatchOperation => ({\n op: patch.op as \"add\" | \"remove\" | \"replace\",\n path: patch.path as string,\n // explicitly check for value existence to satisfy strict null checks if needed\n ...(patch.op !== \"remove\" ? { value: patch.value } : {}),\n }),\n );\n}\n\n/**\n * Recursively applies changes to the Mutative draft.\n * Uses 'unknown' and Type Guards instead of 'any'.\n */\nfunction applyChanges(draft: unknown, modified: unknown): void {\n // This handles Primitives, Dates, and identical References early\n if (isDeepEqual(draft, modified)) {\n return;\n }\n\n // We explicitly check isArray to safely access .length and .push\n if (Array.isArray(draft) && Array.isArray(modified)) {\n const draftArray = draft as Array<unknown>;\n // Clear and Replace approach (safer for DTOs than diffing indices)\n draftArray.length = 0;\n draftArray.push(...modified);\n return;\n }\n\n // We use type guards to ensure we can treat them as dictionaries\n if (isPlainObject(draft) && isPlainObject(modified)) {\n const draftObj = draft;\n const modifiedObj = modified;\n\n // A. Remove keys that don't exist in modified\n for (const key of Object.keys(draftObj)) {\n if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {\n delete draftObj[key];\n }\n }\n\n // B. Add or update top-level keys. We deliberately do NOT recurse into nested objects: the backend\n // patch applier (IEntityPatchService) supports only top-level property operations, so a changed\n // nested object is replaced wholesale (path \"/key\"), never diffed into \"/key/child\" paths that the\n // server would reject.\n for (const key of Object.keys(modifiedObj)) {\n if (!isDeepEqual(draftObj[key], modifiedObj[key])) {\n draftObj[key] = modifiedObj[key];\n }\n }\n return;\n }\n}\n\n/**\n * Handles: Primitives, Arrays, Dates, Objects (order-independent), null/undefined\n */\nfunction isDeepEqual(a: unknown, b: unknown): boolean {\n // Strict Reference & Primitive Check\n if (a === b) return true;\n\n // Null/Undefined Checks\n if (a === null || a === undefined || b === null || b === undefined) {\n return a === b;\n }\n\n // Type Mismatch\n if (typeof a !== typeof b) return false;\n\n // Date Objects\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n // Objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n // Treat as Record<string, unknown> safely\n const objA = a as Record<string, unknown>;\n const objB = b as Record<string, unknown>;\n\n const keysA = Object.keys(objA);\n const keysB = Object.keys(objB);\n\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;\n if (!isDeepEqual(objA[key], objB[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Type Guard: Checks if value is a plain object (not null, array, or Date)\n * Allows safe casting to Record<string, unknown>\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n/**\n * Helper: Checks if two values share a high-level type category\n */\nfunction isSameType(a: unknown, b: unknown): boolean {\n if (Array.isArray(a)) return Array.isArray(b);\n if (Array.isArray(b)) return false;\n if (a instanceof Date) return b instanceof Date;\n if (a && typeof a === \"object\" && b && typeof b === \"object\") return true;\n return typeof a === typeof b;\n}\n\n/**\n * Applies JSON Patch operations to an object using Mutative.\n * @param original - The original object\n * @param patches - Array of JSON Patch operations to apply\n * @returns The patched object (immutable)\n */\nexport function applyJsonPatch<T>(original: T, patches: JsonPatch): T {\n // Check for root replacement first - if found, it should be the only operation\n const rootReplacement = patches.find((patch) => patch.path === \"\" && patch.op === \"replace\");\n if (rootReplacement) {\n return rootReplacement.value as T;\n }\n\n // Filter out any root operations that might interfere and apply the rest\n const nonRootPatches = patches.filter((patch) => patch.path !== \"\");\n if (nonRootPatches.length === 0) {\n return original;\n }\n\n // Mutative's apply function expects patches in RFC 6902 format, which is what we have\n return apply(original as Record<string, unknown>, nonRootPatches) as T;\n}\n\n/**\n * Checks if a patch array is empty (null, undefined, or empty array).\n * @param patches - Array of patch operations or null/undefined\n * @returns True if patches is null, undefined, or empty array\n */\nexport function isNullOrEmpty(patches: JsonPatch | null | undefined): boolean {\n return !patches || patches.length === 0;\n}\n\n/**\n * Checks if a patch array has any operations.\n * @param patches - Array of patch operations\n * @returns True if patches array has operations, false if empty\n */\nexport function hasPatches(patches: JsonPatch): boolean {\n return patches.length > 0;\n}\n\n/**\n * Creates a deep clone of an object.\n * Useful for creating the \"modified\" version before generating patches.\n * @param obj - Object to clone\n * @returns Deep cloned object\n */\nexport function deepClone<T>(obj: T): T {\n // structuredClone preserves Dates (and undefined); fall back to JSON clone on legacy runtimes.\n if (typeof structuredClone === \"function\") {\n return structuredClone(obj);\n }\n return JSON.parse(JSON.stringify(obj));\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}