@cullet/erp-core 1.4.0 → 1.5.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.
- package/KIT_CONTEXT.md +6 -0
- package/dist/app-error.cjs +14 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +14 -6
- package/dist/app-error.js.map +1 -1
- package/dist/condition-evaluator.cjs +14 -0
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +14 -0
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/entity.cjs +2 -6
- package/dist/entity.cjs.map +1 -1
- package/dist/entity.js +2 -6
- package/dist/entity.js.map +1 -1
- package/dist/errors/index.cjs +2 -1
- package/dist/errors/index.js +2 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +2 -38
- package/dist/hashing.js.map +1 -1
- package/dist/index.cjs +3 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +2 -0
- package/dist/policies/engines/v1/gate/index.d.ts +2 -0
- package/dist/policy-service.cjs +2 -1
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.js +2 -1
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +3 -3
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +1 -1
- package/dist/requested-by.js.map +1 -1
- package/dist/stable-stringify.cjs +47 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +42 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-use-case.cjs +5 -0
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +6 -1
- package/dist/temporal-use-case.d.ts +6 -1
- package/dist/temporal-use-case.js +5 -0
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/use-case.cjs +2 -6
- package/dist/use-case.cjs.map +1 -1
- package/dist/use-case.js +2 -6
- package/dist/use-case.js.map +1 -1
- package/dist/uuid-identifier.cjs +2 -7
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.js +1 -6
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +18 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +13 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-error.d.cts +10 -5
- package/dist/validation-error.d.ts +10 -5
- package/dist/value-object.cjs +12 -3
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +9 -1
- package/dist/value-object.d.ts +9 -1
- package/dist/value-object.js +12 -3
- package/dist/value-object.js.map +1 -1
- package/meta.json +4 -2
- package/package.json +1 -1
- package/src/core/application/commands/requested-by.ts +3 -4
- package/src/core/application/queries/query.ts +6 -1
- package/src/core/application/use-case.ts +1 -1
- package/src/core/domain/entity.ts +1 -1
- package/src/core/domain/uuid-identifier.ts +1 -8
- package/src/core/domain/value-object.ts +12 -3
- package/src/core/errors/utils/json-safe.ts +20 -11
- package/src/core/policies/engines/v1/condition-evaluator.ts +56 -1
- package/src/core/shared/hashing.ts +1 -55
- package/src/core/shared/stable-stringify.ts +57 -0
- package/src/core/shared/uuid.ts +11 -0
- package/src/version.ts +1 -1
|
@@ -54,11 +54,14 @@ declare function serializationErrorCode(direction: SerializationCodeDirection, c
|
|
|
54
54
|
/**
|
|
55
55
|
* Ensures metadata is JSON-serializable.
|
|
56
56
|
*
|
|
57
|
-
* **Allowed:**
|
|
57
|
+
* **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
|
|
58
|
+
*
|
|
59
|
+
* **Dropped (mirroring `JSON.stringify`):** object properties whose value is
|
|
60
|
+
* `undefined` (the key is omitted); an `undefined` array item becomes `null`.
|
|
58
61
|
*
|
|
59
62
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
60
|
-
* symbols,
|
|
61
|
-
* `JSON.stringify` throw).
|
|
63
|
+
* symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
|
|
64
|
+
* (which would otherwise make `JSON.stringify` throw).
|
|
62
65
|
*
|
|
63
66
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
64
67
|
*/
|
|
@@ -266,8 +269,7 @@ declare function translateUniqueViolationToDuplicate(input: {
|
|
|
266
269
|
cause?: unknown;
|
|
267
270
|
}): DuplicateError;
|
|
268
271
|
//#endregion
|
|
269
|
-
//#region src/core/shared/
|
|
270
|
-
declare function sha256Hex(value: string): string;
|
|
272
|
+
//#region src/core/shared/stable-stringify.d.ts
|
|
271
273
|
/**
|
|
272
274
|
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
273
275
|
* does not affect the output.
|
|
@@ -282,6 +284,9 @@ declare function sha256Hex(value: string): string;
|
|
|
282
284
|
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
283
285
|
*/
|
|
284
286
|
declare function stableStringify(value: unknown): string;
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/core/shared/hashing.d.ts
|
|
289
|
+
declare function sha256Hex(value: string): string;
|
|
285
290
|
declare function payloadHash(value: unknown): string;
|
|
286
291
|
//#endregion
|
|
287
292
|
//#region src/core/errors/idempotency-error.d.ts
|
|
@@ -54,11 +54,14 @@ declare function serializationErrorCode(direction: SerializationCodeDirection, c
|
|
|
54
54
|
/**
|
|
55
55
|
* Ensures metadata is JSON-serializable.
|
|
56
56
|
*
|
|
57
|
-
* **Allowed:**
|
|
57
|
+
* **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
|
|
58
|
+
*
|
|
59
|
+
* **Dropped (mirroring `JSON.stringify`):** object properties whose value is
|
|
60
|
+
* `undefined` (the key is omitted); an `undefined` array item becomes `null`.
|
|
58
61
|
*
|
|
59
62
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
60
|
-
* symbols,
|
|
61
|
-
* `JSON.stringify` throw).
|
|
63
|
+
* symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
|
|
64
|
+
* (which would otherwise make `JSON.stringify` throw).
|
|
62
65
|
*
|
|
63
66
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
64
67
|
*/
|
|
@@ -266,8 +269,7 @@ declare function translateUniqueViolationToDuplicate(input: {
|
|
|
266
269
|
cause?: unknown;
|
|
267
270
|
}): DuplicateError;
|
|
268
271
|
//#endregion
|
|
269
|
-
//#region src/core/shared/
|
|
270
|
-
declare function sha256Hex(value: string): string;
|
|
272
|
+
//#region src/core/shared/stable-stringify.d.ts
|
|
271
273
|
/**
|
|
272
274
|
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
273
275
|
* does not affect the output.
|
|
@@ -282,6 +284,9 @@ declare function sha256Hex(value: string): string;
|
|
|
282
284
|
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
283
285
|
*/
|
|
284
286
|
declare function stableStringify(value: unknown): string;
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/core/shared/hashing.d.ts
|
|
289
|
+
declare function sha256Hex(value: string): string;
|
|
285
290
|
declare function payloadHash(value: unknown): string;
|
|
286
291
|
//#endregion
|
|
287
292
|
//#region src/core/errors/idempotency-error.d.ts
|
package/dist/value-object.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_stable_stringify = require("./stable-stringify.cjs");
|
|
1
2
|
const require_immutable = require("./immutable.cjs");
|
|
2
3
|
const require_plugin = require("./plugin.cjs");
|
|
3
4
|
//#region src/core/domain/value-object.ts
|
|
@@ -14,6 +15,12 @@ var _ValueObject;
|
|
|
14
15
|
* any risk of a consumer mutating shared state. Subclasses seal the instance
|
|
15
16
|
* itself with {@link finalize} once their own fields are set.
|
|
16
17
|
*
|
|
18
|
+
* One exception: nested `Date` instances are cloned but NOT frozen —
|
|
19
|
+
* `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
|
|
20
|
+
* `value` can still mutate an inner `Date` in place. Store instants as ISO
|
|
21
|
+
* strings or epoch timestamps in the wrapped state (converting to `Date` only
|
|
22
|
+
* in an accessor) when that guarantee matters.
|
|
23
|
+
*
|
|
17
24
|
* @typeParam T - The shape of the wrapped data.
|
|
18
25
|
* @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
|
|
19
26
|
*/
|
|
@@ -37,7 +44,7 @@ let ValueObject = class ValueObject {
|
|
|
37
44
|
* `@version` decorator — used to detect state persisted under an older shape.
|
|
38
45
|
*/
|
|
39
46
|
get contractVersion() {
|
|
40
|
-
return
|
|
47
|
+
return this.constructor.CONTRACT_VERSION;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Freezes the instance shell, blocking reassignment of any own field.
|
|
@@ -66,11 +73,13 @@ let ValueObject = class ValueObject {
|
|
|
66
73
|
* same data are considered equal.
|
|
67
74
|
*
|
|
68
75
|
* Delegates to the registered equality {@link plugins}; with no plugin
|
|
69
|
-
* registered it falls back to comparing the serialized wrapped `value
|
|
76
|
+
* registered it falls back to comparing the serialized wrapped `value`,
|
|
77
|
+
* with object keys sorted so insertion order (e.g. code-built vs
|
|
78
|
+
* JSON-rehydrated) never affects the result.
|
|
70
79
|
* Subclasses may still override for a faster or domain-specific comparison.
|
|
71
80
|
*/
|
|
72
81
|
equals(other) {
|
|
73
|
-
return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) =>
|
|
82
|
+
return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => require_stable_stringify.stableStringify(a.value) === require_stable_stringify.stableStringify(b.value) });
|
|
74
83
|
}
|
|
75
84
|
};
|
|
76
85
|
ValueObject = _ValueObject = require_immutable.__decorate([require_immutable.version("1.0")], ValueObject);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-object.cjs","names":["PluginManager","makeImmutable","version"],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return ValueObject.CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value
|
|
1
|
+
{"version":3,"file":"value-object.cjs","names":["PluginManager","makeImmutable","stableStringify","version"],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { stableStringify } from \"../shared/stable-stringify.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * One exception: nested `Date` instances are cloned but NOT frozen —\n * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of\n * `value` can still mutate an inner `Date` in place. Store instants as ISO\n * strings or epoch timestamps in the wrapped state (converting to `Date` only\n * in an accessor) when that guarantee matters.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return (this.constructor as typeof ValueObject).CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`,\n * with object keys sorted so insertion order (e.g. code-built vs\n * JSON-rehydrated) never affects the result.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n stableStringify(a.value) === stableStringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAIA,eAAAA,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQC,kBAAAA,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAQ,KAAK,YAAmC;CACpD;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;;;CAaA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACVC,yBAAAA,gBAAgB,EAAE,KAAK,MAAMA,yBAAAA,gBAAgB,EAAE,KAAK,EAC5D,CAAC;CACL;AAOJ;2DA/ECC,kBAAAA,QAAQ,KAAK,CAAA,GAAA,WAAA"}
|
package/dist/value-object.d.cts
CHANGED
|
@@ -26,6 +26,12 @@ type ValueObjectPluginContract = {
|
|
|
26
26
|
* any risk of a consumer mutating shared state. Subclasses seal the instance
|
|
27
27
|
* itself with {@link finalize} once their own fields are set.
|
|
28
28
|
*
|
|
29
|
+
* One exception: nested `Date` instances are cloned but NOT frozen —
|
|
30
|
+
* `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
|
|
31
|
+
* `value` can still mutate an inner `Date` in place. Store instants as ISO
|
|
32
|
+
* strings or epoch timestamps in the wrapped state (converting to `Date` only
|
|
33
|
+
* in an accessor) when that guarantee matters.
|
|
34
|
+
*
|
|
29
35
|
* @typeParam T - The shape of the wrapped data.
|
|
30
36
|
* @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
|
|
31
37
|
*/
|
|
@@ -74,7 +80,9 @@ declare abstract class ValueObject<T, P> {
|
|
|
74
80
|
* same data are considered equal.
|
|
75
81
|
*
|
|
76
82
|
* Delegates to the registered equality {@link plugins}; with no plugin
|
|
77
|
-
* registered it falls back to comparing the serialized wrapped `value
|
|
83
|
+
* registered it falls back to comparing the serialized wrapped `value`,
|
|
84
|
+
* with object keys sorted so insertion order (e.g. code-built vs
|
|
85
|
+
* JSON-rehydrated) never affects the result.
|
|
78
86
|
* Subclasses may still override for a faster or domain-specific comparison.
|
|
79
87
|
*/
|
|
80
88
|
equals(other: this): boolean;
|
package/dist/value-object.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ type ValueObjectPluginContract = {
|
|
|
26
26
|
* any risk of a consumer mutating shared state. Subclasses seal the instance
|
|
27
27
|
* itself with {@link finalize} once their own fields are set.
|
|
28
28
|
*
|
|
29
|
+
* One exception: nested `Date` instances are cloned but NOT frozen —
|
|
30
|
+
* `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
|
|
31
|
+
* `value` can still mutate an inner `Date` in place. Store instants as ISO
|
|
32
|
+
* strings or epoch timestamps in the wrapped state (converting to `Date` only
|
|
33
|
+
* in an accessor) when that guarantee matters.
|
|
34
|
+
*
|
|
29
35
|
* @typeParam T - The shape of the wrapped data.
|
|
30
36
|
* @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
|
|
31
37
|
*/
|
|
@@ -74,7 +80,9 @@ declare abstract class ValueObject<T, P> {
|
|
|
74
80
|
* same data are considered equal.
|
|
75
81
|
*
|
|
76
82
|
* Delegates to the registered equality {@link plugins}; with no plugin
|
|
77
|
-
* registered it falls back to comparing the serialized wrapped `value
|
|
83
|
+
* registered it falls back to comparing the serialized wrapped `value`,
|
|
84
|
+
* with object keys sorted so insertion order (e.g. code-built vs
|
|
85
|
+
* JSON-rehydrated) never affects the result.
|
|
78
86
|
* Subclasses may still override for a faster or domain-specific comparison.
|
|
79
87
|
*/
|
|
80
88
|
equals(other: this): boolean;
|
package/dist/value-object.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as stableStringify } from "./stable-stringify.js";
|
|
1
2
|
import { i as version, n as __decorate, t as makeImmutable } from "./immutable.js";
|
|
2
3
|
import { t as PluginManager } from "./plugin.js";
|
|
3
4
|
//#region src/core/domain/value-object.ts
|
|
@@ -14,6 +15,12 @@ var _ValueObject;
|
|
|
14
15
|
* any risk of a consumer mutating shared state. Subclasses seal the instance
|
|
15
16
|
* itself with {@link finalize} once their own fields are set.
|
|
16
17
|
*
|
|
18
|
+
* One exception: nested `Date` instances are cloned but NOT frozen —
|
|
19
|
+
* `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
|
|
20
|
+
* `value` can still mutate an inner `Date` in place. Store instants as ISO
|
|
21
|
+
* strings or epoch timestamps in the wrapped state (converting to `Date` only
|
|
22
|
+
* in an accessor) when that guarantee matters.
|
|
23
|
+
*
|
|
17
24
|
* @typeParam T - The shape of the wrapped data.
|
|
18
25
|
* @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
|
|
19
26
|
*/
|
|
@@ -37,7 +44,7 @@ let ValueObject = class ValueObject {
|
|
|
37
44
|
* `@version` decorator — used to detect state persisted under an older shape.
|
|
38
45
|
*/
|
|
39
46
|
get contractVersion() {
|
|
40
|
-
return
|
|
47
|
+
return this.constructor.CONTRACT_VERSION;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Freezes the instance shell, blocking reassignment of any own field.
|
|
@@ -66,11 +73,13 @@ let ValueObject = class ValueObject {
|
|
|
66
73
|
* same data are considered equal.
|
|
67
74
|
*
|
|
68
75
|
* Delegates to the registered equality {@link plugins}; with no plugin
|
|
69
|
-
* registered it falls back to comparing the serialized wrapped `value
|
|
76
|
+
* registered it falls back to comparing the serialized wrapped `value`,
|
|
77
|
+
* with object keys sorted so insertion order (e.g. code-built vs
|
|
78
|
+
* JSON-rehydrated) never affects the result.
|
|
70
79
|
* Subclasses may still override for a faster or domain-specific comparison.
|
|
71
80
|
*/
|
|
72
81
|
equals(other) {
|
|
73
|
-
return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) =>
|
|
82
|
+
return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => stableStringify(a.value) === stableStringify(b.value) });
|
|
74
83
|
}
|
|
75
84
|
};
|
|
76
85
|
ValueObject = _ValueObject = __decorate([version("1.0")], ValueObject);
|
package/dist/value-object.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-object.js","names":[],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return ValueObject.CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value
|
|
1
|
+
{"version":3,"file":"value-object.js","names":[],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { stableStringify } from \"../shared/stable-stringify.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * One exception: nested `Date` instances are cloned but NOT frozen —\n * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of\n * `value` can still mutate an inner `Date` in place. Store instants as ISO\n * strings or epoch timestamps in the wrapped state (converting to `Date` only\n * in an accessor) when that guarantee matters.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return (this.constructor as typeof ValueObject).CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`,\n * with object keys sorted so insertion order (e.g. code-built vs\n * JSON-rehydrated) never affects the result.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n stableStringify(a.value) === stableStringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAI,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQ,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAQ,KAAK,YAAmC;CACpD;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;;;CAaA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACV,gBAAgB,EAAE,KAAK,MAAM,gBAAgB,EAAE,KAAK,EAC5D,CAAC;CACL;AAOJ;yCA/EC,QAAQ,KAAK,CAAA,GAAA,WAAA"}
|
package/meta.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "2",
|
|
3
3
|
"name": "erp-core",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"description": "Núcleo arquitetural para ERP em TypeScript — entidades, value objects, erros tipados e policies declaráveis sobre clean architecture.",
|
|
6
6
|
"compatibility": {
|
|
7
7
|
"engines": {
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"PermissionSet",
|
|
84
84
|
"RbacAuthorizer",
|
|
85
85
|
"AuthorizerPort",
|
|
86
|
+
"rbacContextFields",
|
|
86
87
|
"AbacRule",
|
|
87
88
|
"AbacPolicySet",
|
|
88
89
|
"AbacAuthorizer",
|
|
@@ -105,7 +106,8 @@
|
|
|
105
106
|
"1.1.0 - **New: `ResultRepository`, `UseCaseObservability`, and `./application` subpath**",
|
|
106
107
|
"1.2.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
|
|
107
108
|
"1.3.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
|
|
108
|
-
"1.4.0 - Add a zod-free ABAC module and the `./abac` subpath."
|
|
109
|
+
"1.4.0 - Add a zod-free ABAC module and the `./abac` subpath.",
|
|
110
|
+
"1.5.0 - Add a zod-free ABAC module and the `./abac` subpath."
|
|
109
111
|
],
|
|
110
112
|
"deprecated": false
|
|
111
113
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ValidationCode } from "../../exceptions/validation-code.js";
|
|
2
2
|
import { InvalidValueException } from "../../exceptions/validation-exception.js";
|
|
3
3
|
import { ValidationField } from "../../exceptions/validation-field.js";
|
|
4
|
+
import { UUID_PATTERN } from "../../shared/uuid.js";
|
|
4
5
|
|
|
5
6
|
// Identifies who triggered a Command.
|
|
6
7
|
// Two valid formats:
|
|
7
|
-
// - Human user: UUID
|
|
8
|
+
// - Human user: canonical RFC-4122 UUID, versions 1–5 — the same pattern
|
|
9
|
+
// `UuidIdentifier` enforces (e.g.: "550e8400-e29b-41d4-a716-446655440000")
|
|
8
10
|
// - System identity: "system:<job>" where <job> is [a-z][a-z0-9]*(-[a-z0-9]+)*
|
|
9
11
|
// (e.g.: "system:late-fee-job", "system:email-sender")
|
|
10
12
|
//
|
|
@@ -15,9 +17,6 @@ type RequestedByKind = "user" | "system";
|
|
|
15
17
|
|
|
16
18
|
const REQUESTED_BY_FIELD = ValidationField.of("requestedBy");
|
|
17
19
|
|
|
18
|
-
const UUID_PATTERN =
|
|
19
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
-
|
|
21
20
|
// system:<job> where <job> starts with a lowercase letter and may have hyphens between segments
|
|
22
21
|
const SYSTEM_IDENTITY_PATTERN = /^system:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
23
22
|
|
|
@@ -44,7 +44,12 @@ abstract class Query<
|
|
|
44
44
|
Data extends QueryOutput = never,
|
|
45
45
|
Failure = AppError,
|
|
46
46
|
> extends UseCase<Input, Result<Data, Failure>> {
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Declares how infrastructure should cache this query's result. Public so
|
|
49
|
+
* a caching adapter can actually read it off the instance; overrides must
|
|
50
|
+
* stay public.
|
|
51
|
+
*/
|
|
52
|
+
public cacheStrategy(): CacheStrategy {
|
|
48
53
|
return { kind: "NO_CACHE" };
|
|
49
54
|
}
|
|
50
55
|
}
|
|
@@ -34,7 +34,7 @@ abstract class UseCase<
|
|
|
34
34
|
declare public static readonly CONTRACT_VERSION: ContractVersion;
|
|
35
35
|
|
|
36
36
|
public get contractVersion(): ContractVersion {
|
|
37
|
-
return UseCase.CONTRACT_VERSION;
|
|
37
|
+
return (this.constructor as typeof UseCase).CONTRACT_VERSION;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
+
import { UUID_PATTERN } from "../shared/uuid.js";
|
|
1
2
|
import { ValueObject } from "./value-object.js";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical RFC-4122 UUID (versions 1–5), matched case-insensitively. Declared
|
|
5
|
-
* once here so the dozens of typed identifiers a domain accumulates never have
|
|
6
|
-
* to re-state the pattern.
|
|
7
|
-
*/
|
|
8
|
-
const UUID_PATTERN =
|
|
9
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10
|
-
|
|
11
4
|
/**
|
|
12
5
|
* Base class for UUID-backed identity value objects.
|
|
13
6
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PluginManager } from "../plugins/index.js";
|
|
2
2
|
import { type DeepReadonly, makeImmutable } from "../shared/immutable.js";
|
|
3
|
+
import { stableStringify } from "../shared/stable-stringify.js";
|
|
3
4
|
import { type ContractVersion, version } from "../versioning/version.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -28,6 +29,12 @@ type ValueObjectPluginContract = {
|
|
|
28
29
|
* any risk of a consumer mutating shared state. Subclasses seal the instance
|
|
29
30
|
* itself with {@link finalize} once their own fields are set.
|
|
30
31
|
*
|
|
32
|
+
* One exception: nested `Date` instances are cloned but NOT frozen —
|
|
33
|
+
* `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
|
|
34
|
+
* `value` can still mutate an inner `Date` in place. Store instants as ISO
|
|
35
|
+
* strings or epoch timestamps in the wrapped state (converting to `Date` only
|
|
36
|
+
* in an accessor) when that guarantee matters.
|
|
37
|
+
*
|
|
31
38
|
* @typeParam T - The shape of the wrapped data.
|
|
32
39
|
* @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
|
|
33
40
|
*/
|
|
@@ -61,7 +68,7 @@ abstract class ValueObject<T, P> {
|
|
|
61
68
|
* `@version` decorator — used to detect state persisted under an older shape.
|
|
62
69
|
*/
|
|
63
70
|
public get contractVersion(): ContractVersion {
|
|
64
|
-
return ValueObject.CONTRACT_VERSION;
|
|
71
|
+
return (this.constructor as typeof ValueObject).CONTRACT_VERSION;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -93,13 +100,15 @@ abstract class ValueObject<T, P> {
|
|
|
93
100
|
* same data are considered equal.
|
|
94
101
|
*
|
|
95
102
|
* Delegates to the registered equality {@link plugins}; with no plugin
|
|
96
|
-
* registered it falls back to comparing the serialized wrapped `value
|
|
103
|
+
* registered it falls back to comparing the serialized wrapped `value`,
|
|
104
|
+
* with object keys sorted so insertion order (e.g. code-built vs
|
|
105
|
+
* JSON-rehydrated) never affects the result.
|
|
97
106
|
* Subclasses may still override for a faster or domain-specific comparison.
|
|
98
107
|
*/
|
|
99
108
|
public equals(other: this): boolean {
|
|
100
109
|
return ValueObject.plugins.invoke("equals", [this, other], {
|
|
101
110
|
fallback: (a, b) =>
|
|
102
|
-
|
|
111
|
+
stableStringify(a.value) === stableStringify(b.value),
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -30,18 +30,23 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
|
|
|
30
30
|
|
|
31
31
|
const type = typeof value;
|
|
32
32
|
|
|
33
|
-
// Primitives
|
|
34
|
-
if (type === "
|
|
33
|
+
// Primitives (non-finite numbers are not JSON-representable — placeholder)
|
|
34
|
+
if (type === "number") {
|
|
35
|
+
return Number.isFinite(value)
|
|
36
|
+
? (value as number)
|
|
37
|
+
: NON_SERIALIZABLE_PLACEHOLDER;
|
|
38
|
+
}
|
|
39
|
+
if (type === "string" || type === "boolean") {
|
|
35
40
|
return value as JsonSafeValue;
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
// `undefined` only reaches here as an array item: object properties with
|
|
44
|
+
// undefined values are omitted by the plain-object branch below. Both
|
|
45
|
+
// mirror `JSON.stringify` semantics.
|
|
46
|
+
if (value === undefined) return null;
|
|
47
|
+
|
|
38
48
|
// Non-serializable primitives
|
|
39
|
-
if (
|
|
40
|
-
type === "bigint" ||
|
|
41
|
-
type === "function" ||
|
|
42
|
-
type === "symbol" ||
|
|
43
|
-
value === undefined
|
|
44
|
-
) {
|
|
49
|
+
if (type === "bigint" || type === "function" || type === "symbol") {
|
|
45
50
|
return NON_SERIALIZABLE_PLACEHOLDER;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -69,6 +74,7 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
|
|
|
69
74
|
seen.add(value);
|
|
70
75
|
const result: Record<string, JsonSafeValue> = {};
|
|
71
76
|
for (const [key, val] of Object.entries(value)) {
|
|
77
|
+
if (val === undefined) continue;
|
|
72
78
|
result[key] = sanitizeValue(val, seen);
|
|
73
79
|
}
|
|
74
80
|
seen.delete(value);
|
|
@@ -82,11 +88,14 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
|
|
|
82
88
|
/**
|
|
83
89
|
* Ensures metadata is JSON-serializable.
|
|
84
90
|
*
|
|
85
|
-
* **Allowed:**
|
|
91
|
+
* **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
|
|
92
|
+
*
|
|
93
|
+
* **Dropped (mirroring `JSON.stringify`):** object properties whose value is
|
|
94
|
+
* `undefined` (the key is omitted); an `undefined` array item becomes `null`.
|
|
86
95
|
*
|
|
87
96
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
88
|
-
* symbols,
|
|
89
|
-
* `JSON.stringify` throw).
|
|
97
|
+
* symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
|
|
98
|
+
* (which would otherwise make `JSON.stringify` throw).
|
|
90
99
|
*
|
|
91
100
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
92
101
|
*/
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { PolicyContextPath } from "../../context/index.js";
|
|
2
|
-
|
|
2
|
+
// Import the date util directly (not the utils barrel): the barrel re-exports
|
|
3
|
+
// PolicyHashing, whose node:crypto import would needlessly drag a Node-only
|
|
4
|
+
// API into the zod-free ./abac subpath.
|
|
5
|
+
import { PolicyDateUtils } from "../../utils/date.js";
|
|
3
6
|
import { Result } from "../../../result/result.js";
|
|
4
7
|
|
|
5
8
|
import type {
|
|
@@ -213,6 +216,32 @@ export class ConditionEvaluatorV1 {
|
|
|
213
216
|
return Result.err(message);
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
private static containsInvalidDate(value: unknown): boolean {
|
|
220
|
+
if (value instanceof Date) {
|
|
221
|
+
return !PolicyDateUtils.isValid(value);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(value)) {
|
|
224
|
+
return value.some((item) =>
|
|
225
|
+
ConditionEvaluatorV1.containsInvalidDate(item),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Valid Dates become their ISO string so equality/set operators compare
|
|
232
|
+
// instants instead of references; every other value passes through.
|
|
233
|
+
private static normalizeDateOperand(value: unknown): unknown {
|
|
234
|
+
if (value instanceof Date) {
|
|
235
|
+
return value.toISOString();
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
return value.map((item) =>
|
|
239
|
+
ConditionEvaluatorV1.normalizeDateOperand(item),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
216
245
|
private reportDateOperandError(
|
|
217
246
|
node: ConditionLeafNode,
|
|
218
247
|
actual: unknown,
|
|
@@ -527,6 +556,32 @@ export class ConditionEvaluatorV1 {
|
|
|
527
556
|
return this.reportInvalidSetOperand(node, actual);
|
|
528
557
|
}
|
|
529
558
|
|
|
559
|
+
if (
|
|
560
|
+
node.op === "eq" ||
|
|
561
|
+
node.op === "neq" ||
|
|
562
|
+
node.op === "in" ||
|
|
563
|
+
node.op === "notIn"
|
|
564
|
+
) {
|
|
565
|
+
// A Date operand would otherwise compare by reference and
|
|
566
|
+
// silently never match. Normalize valid Dates (either side,
|
|
567
|
+
// including set items) to their ISO string; an invalid Date is
|
|
568
|
+
// a context/configuration error, like the relational path.
|
|
569
|
+
if (
|
|
570
|
+
ConditionEvaluatorV1.containsInvalidDate(actual) ||
|
|
571
|
+
ConditionEvaluatorV1.containsInvalidDate(node.value)
|
|
572
|
+
) {
|
|
573
|
+
return this.reportDateOperandError(node, actual);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return Result.ok(
|
|
577
|
+
this.evaluateOperator(
|
|
578
|
+
ConditionEvaluatorV1.normalizeDateOperand(actual),
|
|
579
|
+
node.op,
|
|
580
|
+
ConditionEvaluatorV1.normalizeDateOperand(node.value),
|
|
581
|
+
),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
530
585
|
return Result.ok(
|
|
531
586
|
this.evaluateOperator(actual, node.op, node.value),
|
|
532
587
|
);
|
|
@@ -1,65 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"Cannot stably stringify a circular structure";
|
|
3
|
+
import { stableStringify } from "./stable-stringify.js";
|
|
5
4
|
|
|
6
5
|
function sha256Hex(value: string): string {
|
|
7
6
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
// `seen` holds the current traversal branch so true cycles are rejected while
|
|
11
|
-
// repeated (acyclic) references are still serialized — matching JSON.stringify.
|
|
12
|
-
function sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
if (seen.has(value)) {
|
|
15
|
-
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
16
|
-
}
|
|
17
|
-
seen.add(value);
|
|
18
|
-
const mapped = value.map((item) => sortKeysDeep(item, seen));
|
|
19
|
-
seen.delete(value);
|
|
20
|
-
return mapped;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (value instanceof Date) {
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (value && typeof value === "object") {
|
|
28
|
-
if (seen.has(value)) {
|
|
29
|
-
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
30
|
-
}
|
|
31
|
-
seen.add(value);
|
|
32
|
-
const record = value as Record<string, unknown>;
|
|
33
|
-
const ordered = Object.create(null) as Record<string, unknown>;
|
|
34
|
-
|
|
35
|
-
for (const key of Object.keys(record).sort()) {
|
|
36
|
-
ordered[key] = sortKeysDeep(record[key], seen);
|
|
37
|
-
}
|
|
38
|
-
seen.delete(value);
|
|
39
|
-
|
|
40
|
-
return ordered;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
48
|
-
* does not affect the output.
|
|
49
|
-
*
|
|
50
|
-
* Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
|
|
51
|
-
* functions and symbols are dropped, non-finite numbers become `null`, `bigint`
|
|
52
|
-
* throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
|
|
53
|
-
* collapse to the same string — for collision-resistant hashing use
|
|
54
|
-
* `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
|
|
55
|
-
* up front.
|
|
56
|
-
*
|
|
57
|
-
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
58
|
-
*/
|
|
59
|
-
function stableStringify(value: unknown): string {
|
|
60
|
-
return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
9
|
function payloadHash(value: unknown): string {
|
|
64
10
|
return sha256Hex(stableStringify(value));
|
|
65
11
|
}
|