@cullet/erp-core 1.4.0 → 2.0.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 +7 -1
- package/dist/abac/index.d.cts +2 -2
- package/dist/abac/index.d.ts +2 -2
- package/dist/aggregate-version.cjs +14 -0
- package/dist/aggregate-version.cjs.map +1 -0
- package/dist/aggregate-version.js +9 -0
- package/dist/aggregate-version.js.map +1 -0
- package/dist/app-error.cjs +21 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +21 -6
- package/dist/app-error.js.map +1 -1
- package/dist/application/index.cjs +8 -4
- package/dist/application/index.d.cts +2 -2
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +1 -2
- package/dist/authorization-error.cjs +67 -17
- package/dist/authorization-error.cjs.map +1 -1
- package/dist/authorization-error.d.cts +6 -2
- package/dist/authorization-error.d.ts +6 -2
- package/dist/authorization-error.js +50 -18
- package/dist/authorization-error.js.map +1 -1
- package/dist/authorizer.port.d.cts +18 -3
- package/dist/authorizer.port.d.ts +18 -3
- package/dist/composite-authorizer.d.cts +63 -10
- package/dist/composite-authorizer.d.ts +63 -10
- package/dist/condition-evaluator.cjs +117 -172
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +118 -167
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/core-config.d.cts +53 -6
- package/dist/core-config.d.ts +53 -6
- package/dist/decorate.cjs +14 -0
- package/dist/decorate.js +9 -0
- package/dist/domain/index.cjs +107 -2
- package/dist/domain/index.cjs.map +1 -0
- package/dist/domain/index.d.cts +25 -1
- package/dist/domain/index.d.ts +25 -1
- package/dist/domain/index.js +99 -3
- package/dist/domain/index.js.map +1 -0
- package/dist/domain-event-contracts.cjs +12 -8
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.d.cts +29 -4
- package/dist/domain-event-contracts.d.ts +29 -4
- package/dist/domain-event-contracts.js +11 -7
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +2 -1
- package/dist/domain-exception.cjs.map +1 -1
- package/dist/domain-exception.js +2 -1
- package/dist/domain-exception.js.map +1 -1
- package/dist/errors/index.cjs +3 -2
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +3 -2
- package/dist/exceptions/index.cjs +2 -2
- package/dist/exceptions/index.d.cts +1 -2
- package/dist/exceptions/index.d.ts +1 -2
- package/dist/exceptions/index.js +2 -2
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +1 -1
- package/dist/gate-engine-registry.d.ts +1 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +11 -6
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +11 -6
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +3 -39
- package/dist/hashing.js.map +1 -1
- package/dist/immutable.cjs +25 -12
- package/dist/immutable.cjs.map +1 -1
- package/dist/immutable.js +24 -11
- package/dist/immutable.js.map +1 -1
- package/dist/index.cjs +15 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +6 -6
- package/dist/invalid-state-transition-exception.cjs.map +1 -1
- package/dist/invalid-state-transition-exception.js +6 -6
- package/dist/invalid-state-transition-exception.js.map +1 -1
- package/dist/invariant-violation-exception.cjs +2 -2
- package/dist/invariant-violation-exception.cjs.map +1 -1
- package/dist/invariant-violation-exception.js +2 -2
- package/dist/invariant-violation-exception.js.map +1 -1
- package/dist/not-found-error.cjs +6 -4
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +6 -4
- package/dist/not-found-error.js.map +1 -1
- package/dist/outcome.cjs +5 -5
- package/dist/outcome.cjs.map +1 -1
- package/dist/outcome.js +5 -5
- package/dist/outcome.js.map +1 -1
- package/dist/parse-gate-payload.d.cts +1 -1
- package/dist/parse-gate-payload.d.ts +1 -1
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +23 -13
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -8
- package/dist/plugin.d.ts +22 -8
- package/dist/plugin.js +23 -13
- package/dist/plugin.js.map +1 -1
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +3 -13
- package/dist/policies/engines/v1/gate/index.d.ts +3 -13
- package/dist/policies/index.d.cts +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policy-bridge.cjs +30 -2
- package/dist/policy-bridge.cjs.map +1 -1
- package/dist/policy-bridge.d.cts +18 -0
- package/dist/policy-bridge.d.ts +18 -0
- package/dist/policy-bridge.js +30 -2
- package/dist/policy-bridge.js.map +1 -1
- package/dist/policy-service.cjs +41 -46
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +72 -9
- package/dist/policy-service.d.ts +72 -9
- package/dist/policy-service.js +43 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +4 -4
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +2 -2
- package/dist/requested-by.js.map +1 -1
- package/dist/result.cjs +29 -1
- package/dist/result.cjs.map +1 -1
- package/dist/result.d.cts +12 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.js +29 -1
- package/dist/result.js.map +1 -1
- package/dist/rule.cjs +94 -24
- package/dist/rule.cjs.map +1 -1
- package/dist/rule.js +94 -24
- package/dist/rule.js.map +1 -1
- package/dist/ruleset-registry.cjs +19 -0
- package/dist/ruleset-registry.cjs.map +1 -1
- package/dist/ruleset-registry.js +19 -0
- package/dist/ruleset-registry.js.map +1 -1
- package/dist/stable-stringify.cjs +87 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +76 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +174 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +82 -44
- package/dist/temporal-use-case.d.ts +82 -44
- package/dist/temporal-use-case.js +167 -15
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +4 -2
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +4 -2
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.cjs +141 -8
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.d.cts +26 -7
- package/dist/uuid-identifier.d.ts +26 -7
- package/dist/uuid-identifier.js +135 -8
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +23 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +18 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-code.cjs +9 -0
- package/dist/validation-code.cjs.map +1 -1
- package/dist/validation-code.d.cts +3 -0
- package/dist/validation-code.d.ts +3 -0
- package/dist/validation-code.js +9 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.cjs +42 -67
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +29 -8
- package/dist/validation-error.d.ts +29 -8
- package/dist/validation-error.js +41 -66
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +17 -6
- package/dist/validation-exception.cjs.map +1 -1
- package/dist/validation-exception.d.cts +33 -9
- package/dist/validation-exception.d.ts +33 -9
- package/dist/validation-exception.js +17 -6
- package/dist/validation-exception.js.map +1 -1
- package/dist/validation-field.cjs +3 -0
- package/dist/validation-field.cjs.map +1 -1
- package/dist/validation-field.d.cts +1 -0
- package/dist/validation-field.d.ts +1 -0
- package/dist/validation-field.js +3 -0
- package/dist/validation-field.js.map +1 -1
- package/dist/value-object-ruleset.contracts.d.cts +10 -0
- package/dist/value-object-ruleset.contracts.d.ts +10 -0
- package/dist/value-object.cjs +23 -4
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +25 -1
- package/dist/value-object.d.ts +25 -1
- package/dist/value-object.js +23 -4
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +27 -0
- package/dist/version.d.ts +27 -0
- package/dist/versioning/index.d.cts +2 -2
- package/dist/versioning/index.d.ts +2 -2
- package/meta.json +5 -2
- package/package.json +1 -1
- package/src/core/abac/authorizer.ts +60 -10
- package/src/core/abac/domain/policy-set.ts +52 -5
- package/src/core/abac/domain/rule.ts +28 -16
- package/src/core/abac/index.ts +7 -1
- package/src/core/application/commands/command.ts +14 -1
- package/src/core/application/commands/requested-by.ts +13 -5
- package/src/core/application/index.ts +2 -1
- package/src/core/application/policy-error-mapper.ts +6 -0
- package/src/core/application/ports/index.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +35 -2
- package/src/core/application/queries/index.ts +1 -1
- package/src/core/application/queries/query.ts +25 -3
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +45 -8
- package/src/core/config/core-config.ts +46 -25
- package/src/core/config/index.ts +1 -0
- package/src/core/config/policy-reporter.ts +32 -1
- package/src/core/domain/entity.ts +51 -8
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
- package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
- package/src/core/domain/temporal/half-open-interval.ts +34 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
- package/src/core/domain/temporal/transaction-time.ts +19 -15
- package/src/core/domain/temporal/valid-time.ts +20 -15
- package/src/core/domain/uuid-identifier.ts +6 -11
- package/src/core/domain/value-object.ts +29 -3
- package/src/core/errors/authentication-error.ts +5 -31
- package/src/core/errors/authorization-error.ts +12 -20
- package/src/core/errors/business-rule-violation-error.ts +4 -1
- package/src/core/errors/idempotency-error.ts +5 -2
- package/src/core/errors/index.ts +4 -0
- package/src/core/errors/integration-error.ts +4 -24
- package/src/core/errors/legacy-incompatible-error.ts +1 -0
- package/src/core/errors/not-found-error.ts +4 -1
- package/src/core/errors/temporal-error.ts +22 -20
- package/src/core/errors/unexpected-error.ts +5 -1
- package/src/core/errors/utils/factory-helpers.ts +70 -0
- package/src/core/errors/utils/index.ts +5 -0
- package/src/core/errors/utils/json-safe.ts +35 -12
- package/src/core/errors/validation-error.ts +4 -1
- package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
- package/src/core/exceptions/domain-exception.ts +9 -1
- package/src/core/exceptions/entity-not-found-exception.ts +5 -1
- package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
- package/src/core/exceptions/invariant-violation-exception.ts +2 -2
- package/src/core/exceptions/validation-code.ts +14 -0
- package/src/core/exceptions/validation-exception.ts +44 -5
- package/src/core/exceptions/validation-field.ts +11 -1
- package/src/core/plugins/plugin.ts +25 -15
- package/src/core/plugins/types.ts +4 -2
- package/src/core/policies/asof/asof.ts +12 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
- package/src/core/policies/catalog/policy-catalog.ts +5 -1
- package/src/core/policies/context/context-builder.ts +20 -0
- package/src/core/policies/context/context-resolver.ts +5 -0
- package/src/core/policies/context/context-seed.ts +11 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
- package/src/core/policies/engines/parse-gate-payload.ts +9 -0
- package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +120 -291
- package/src/core/policies/engines/v1/condition-schema.ts +23 -19
- package/src/core/policies/engines/v1/condition-types.ts +18 -11
- package/src/core/policies/index.ts +4 -6
- package/src/core/policies/service/policy-service.ts +46 -35
- package/src/core/policies/utils/hash.ts +5 -69
- package/src/core/policies/utils/result.ts +1 -1
- package/src/core/rbac/access-request.ts +12 -2
- package/src/core/rbac/authorizer.ts +8 -1
- package/src/core/rbac/domain/role.ts +20 -0
- package/src/core/rbac/domain/scope.ts +6 -1
- package/src/core/result/index.ts +5 -0
- package/src/core/result/outcome.ts +5 -7
- package/src/core/result/result.ts +34 -1
- package/src/core/shared/hashing.ts +6 -57
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +144 -0
- package/src/core/shared/uuid.ts +16 -0
- package/src/core/versioning/domain-event-contracts.ts +43 -15
- package/src/core/versioning/index.ts +1 -0
- package/src/core/versioning/version.ts +30 -1
- package/src/domain/index.ts +5 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
- package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
- package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
- package/src/result/index.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/domain-exception.d.cts +0 -7
- package/dist/domain-exception.d.ts +0 -7
- package/dist/entity.cjs +0 -126
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -115
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -96
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -91
- package/dist/use-case.js.map +0 -1
|
@@ -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,7 +74,22 @@ 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)) {
|
|
72
|
-
|
|
77
|
+
if (val === undefined) continue;
|
|
78
|
+
const sanitized = sanitizeValue(val, seen);
|
|
79
|
+
if (key === "__proto__") {
|
|
80
|
+
// Plain `result.__proto__ = x` would reparent `result` instead of
|
|
81
|
+
// creating an own data property, silently dropping the key. Define
|
|
82
|
+
// it explicitly so `__proto__` survives as data — matching how
|
|
83
|
+
// JSON.stringify/JSON.parse round-trip it.
|
|
84
|
+
Object.defineProperty(result, key, {
|
|
85
|
+
value: sanitized,
|
|
86
|
+
enumerable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
result[key] = sanitized;
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
94
|
seen.delete(value);
|
|
75
95
|
return result;
|
|
@@ -82,11 +102,14 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
|
|
|
82
102
|
/**
|
|
83
103
|
* Ensures metadata is JSON-serializable.
|
|
84
104
|
*
|
|
85
|
-
* **Allowed:**
|
|
105
|
+
* **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
|
|
106
|
+
*
|
|
107
|
+
* **Dropped (mirroring `JSON.stringify`):** object properties whose value is
|
|
108
|
+
* `undefined` (the key is omitted); an `undefined` array item becomes `null`.
|
|
86
109
|
*
|
|
87
110
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
88
|
-
* symbols,
|
|
89
|
-
* `JSON.stringify` throw).
|
|
111
|
+
* symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
|
|
112
|
+
* (which would otherwise make `JSON.stringify` throw).
|
|
90
113
|
*
|
|
91
114
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
92
115
|
*/
|
|
@@ -38,14 +38,17 @@ class ValidationError extends AppError {
|
|
|
38
38
|
field: field.value,
|
|
39
39
|
validationCode: code.value,
|
|
40
40
|
};
|
|
41
|
+
// `field`/`validationCode` spread last so caller metadata is merged
|
|
42
|
+
// over them without ever overwriting them by accident.
|
|
41
43
|
const mergedMetadata = options?.metadata
|
|
42
|
-
? { ...
|
|
44
|
+
? { ...options.metadata, ...baseMetadata }
|
|
43
45
|
: baseMetadata;
|
|
44
46
|
|
|
45
47
|
super(message, ErrorCodes.validation, {
|
|
46
48
|
...options,
|
|
47
49
|
metadata: mergedMetadata,
|
|
48
50
|
});
|
|
51
|
+
Object.freeze(this);
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
abstract class DomainException extends Error {
|
|
2
|
-
|
|
2
|
+
// ES2020's `Error` lib type does not declare `cause`; mirror the sibling
|
|
3
|
+
// `AppError` and carry it explicitly so a domain exception can wrap the
|
|
4
|
+
// lower-level failure that triggered it. Assigned rather than passed to
|
|
5
|
+
// `super` to stay independent of the ES2022 `Error(message, { cause })`
|
|
6
|
+
// overload the build target doesn't guarantee.
|
|
7
|
+
public readonly cause?: unknown;
|
|
8
|
+
|
|
9
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
3
10
|
super(message);
|
|
11
|
+
this.cause = options?.cause;
|
|
4
12
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
5
13
|
this.name = new.target.name;
|
|
6
14
|
}
|
|
@@ -4,8 +4,12 @@ class EntityNotFoundException extends DomainException {
|
|
|
4
4
|
constructor(
|
|
5
5
|
public readonly entityName: string,
|
|
6
6
|
public readonly identifier: string,
|
|
7
|
+
options?: { cause?: unknown },
|
|
7
8
|
) {
|
|
8
|
-
super(
|
|
9
|
+
super(
|
|
10
|
+
`${entityName} with identifier ${identifier} was not found.`,
|
|
11
|
+
options,
|
|
12
|
+
);
|
|
9
13
|
}
|
|
10
14
|
}
|
|
11
15
|
|
|
@@ -6,9 +6,12 @@ class InvalidStateTransitionException<
|
|
|
6
6
|
constructor(
|
|
7
7
|
public readonly from: TState,
|
|
8
8
|
public readonly to: TState,
|
|
9
|
-
message
|
|
9
|
+
message?: string,
|
|
10
|
+
options?: { cause?: unknown },
|
|
10
11
|
) {
|
|
11
|
-
|
|
12
|
+
// Compose a sensible default from `from`/`to` when no message is given,
|
|
13
|
+
// mirroring how EntityNotFoundException builds its own message.
|
|
14
|
+
super(message ?? `Cannot transition from ${from} to ${to}.`, options);
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DomainException } from "./domain-exception.js";
|
|
2
2
|
|
|
3
3
|
class InvariantViolationException extends DomainException {
|
|
4
|
-
constructor(message: string) {
|
|
5
|
-
super(message);
|
|
4
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
5
|
+
super(message, options);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -13,6 +13,20 @@ class ValidationCode {
|
|
|
13
13
|
return new ValidationCode(value);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
public toString(): string {
|
|
17
|
+
return this.value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public equals(other: ValidationCode): boolean {
|
|
21
|
+
return this.value === other.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Serialize to the bare code string, so JSON.stringify of a violation
|
|
25
|
+
// yields "blank" rather than { "value": "blank" }. Mirrors ValidationField.
|
|
26
|
+
public toJSON(): string {
|
|
27
|
+
return this.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
// Common, domain-friendly codes
|
|
17
31
|
public static readonly BLANK = new ValidationCode("blank");
|
|
18
32
|
public static readonly REQUIRED = new ValidationCode("required");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DomainException } from "./domain-exception.js";
|
|
2
|
+
import { InvariantViolationException } from "./invariant-violation-exception.js";
|
|
2
3
|
import { ValidationCode } from "./validation-code.js";
|
|
3
4
|
import { ValidationField } from "./validation-field.js";
|
|
4
5
|
|
|
@@ -8,27 +9,65 @@ export interface ValidationViolation {
|
|
|
8
9
|
readonly message: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
// Extensible base for a single-field validation failure. Prefer throwing the
|
|
13
|
+
// concrete InvalidValueException below so callers can `instanceof` the common
|
|
14
|
+
// case; extend this directly only when you need a distinct validation subtype.
|
|
11
15
|
class ValidationException extends DomainException {
|
|
12
16
|
constructor(
|
|
13
17
|
public readonly field: ValidationField,
|
|
14
18
|
public readonly code: ValidationCode,
|
|
15
19
|
message: string,
|
|
20
|
+
options?: { cause?: unknown },
|
|
16
21
|
) {
|
|
17
|
-
super(message);
|
|
22
|
+
super(message, options);
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
// Carries multiple validation violations from a single operation (e.g. bulk validation).
|
|
22
27
|
// Use when you need to report all field errors at once rather than failing on the first.
|
|
23
28
|
class MultipleValidationException extends DomainException {
|
|
24
|
-
constructor(
|
|
25
|
-
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly violations: readonly ValidationViolation[],
|
|
31
|
+
options?: { cause?: unknown },
|
|
32
|
+
) {
|
|
33
|
+
// An aggregate with no violations is a caller bug: it produces a
|
|
34
|
+
// messageless, contentless exception. Fail fast as a broken invariant.
|
|
35
|
+
if (violations.length === 0) {
|
|
36
|
+
throw new InvariantViolationException(
|
|
37
|
+
"MultipleValidationException requires at least one violation.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
super(violations.map((v) => v.message).join("; "), options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Number of accumulated violations (always >= 1).
|
|
44
|
+
get count(): number {
|
|
45
|
+
return this.violations.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The first accumulated violation.
|
|
49
|
+
get first(): ValidationViolation {
|
|
50
|
+
return this.violations[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Violations targeting the given field.
|
|
54
|
+
byField(field: ValidationField): readonly ValidationViolation[] {
|
|
55
|
+
return this.violations.filter((v) => v.field.equals(field));
|
|
26
56
|
}
|
|
27
57
|
}
|
|
28
58
|
|
|
59
|
+
// The canonical concrete validation exception, thrown across the domain
|
|
60
|
+
// (rbac/abac/policies/commands) when a single input value is invalid. Its
|
|
61
|
+
// identity is exactly that: the named, catchable "a value was invalid" case,
|
|
62
|
+
// versus ValidationException as the open base.
|
|
29
63
|
class InvalidValueException extends ValidationException {
|
|
30
|
-
constructor(
|
|
31
|
-
|
|
64
|
+
constructor(
|
|
65
|
+
field: ValidationField,
|
|
66
|
+
code: ValidationCode,
|
|
67
|
+
message: string,
|
|
68
|
+
options?: { cause?: unknown },
|
|
69
|
+
) {
|
|
70
|
+
super(field, code, message, options);
|
|
32
71
|
}
|
|
33
72
|
}
|
|
34
73
|
|
|
@@ -14,7 +14,11 @@ class ValidationField {
|
|
|
14
14
|
return new ValidationField(value);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Creates a nested field path: parent.nested('child') -> 'parent.child'
|
|
17
|
+
// Creates a nested field path: parent.nested('child') -> 'parent.child'.
|
|
18
|
+
// The separator is a literal '.' and segments are NOT escaped, so
|
|
19
|
+
// of('a.b') and of('a').nested('b') collapse to the same path and compare
|
|
20
|
+
// equal — a field name that itself contains '.' is indistinguishable from
|
|
21
|
+
// a nested path. Keep segment names dot-free if that distinction matters.
|
|
18
22
|
public nested(child: string | ValidationField): ValidationField {
|
|
19
23
|
const childValue =
|
|
20
24
|
child instanceof ValidationField ? child.value : child;
|
|
@@ -25,6 +29,12 @@ class ValidationField {
|
|
|
25
29
|
return this.value;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
// Serialize to the bare field path, so JSON.stringify of a violation yields
|
|
33
|
+
// "email" rather than { "value": "email" }. Mirrors ValidationCode.
|
|
34
|
+
public toJSON(): string {
|
|
35
|
+
return this.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
public equals(other: ValidationField): boolean {
|
|
29
39
|
return this.value === other.value;
|
|
30
40
|
}
|
|
@@ -9,11 +9,23 @@ import {
|
|
|
9
9
|
* Registry that holds a set of plugins implementing a shared {@link PluginContract}
|
|
10
10
|
* and resolves which of them answer a given extension point.
|
|
11
11
|
*
|
|
12
|
-
* Plugins are kept sorted by priority (descending)
|
|
13
|
-
* {@link invoke} either
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Plugins are kept sorted by priority (descending); ties keep registration
|
|
13
|
+
* order (the sort is stable). For a given method, {@link invoke} either
|
|
14
|
+
* delegates to the highest-priority enabled plugin (`'first'`) or folds every
|
|
15
|
+
* enabled plugin into a single value (`'pipeline'`). When no plugin is enabled,
|
|
16
|
+
* the supplied `fallback` runs instead, so the host always has defined
|
|
17
|
+
* behaviour even with an empty registry.
|
|
18
|
+
*
|
|
19
|
+
* `name` is a unique key: registering a plugin whose name already exists
|
|
20
|
+
* replaces the previous one, so register/enable/disable/unregister all address
|
|
21
|
+
* the same single plugin.
|
|
22
|
+
*
|
|
23
|
+
* Plugins run arbitrary host code and are trusted: {@link invoke} does NOT wrap
|
|
24
|
+
* them in try/catch. A throwing plugin propagates to the caller (and, in
|
|
25
|
+
* `'pipeline'` mode, discards the accumulated result). Error handling, logging
|
|
26
|
+
* and retries are the host's responsibility by design — the registry's only
|
|
27
|
+
* defined-behaviour guarantee is `fallback` for an empty registry. Every plugin
|
|
28
|
+
* is expected to implement the full contract `P` (the types enforce it).
|
|
17
29
|
*
|
|
18
30
|
* @typeParam P - The plugin contract this manager coordinates.
|
|
19
31
|
*/
|
|
@@ -21,8 +33,10 @@ class PluginManager<P extends PluginContract> {
|
|
|
21
33
|
/** Internal list, kept sorted by priority (desc). */
|
|
22
34
|
private plugins: (P & BasePlugin)[] = [];
|
|
23
35
|
|
|
24
|
-
/** Registers one or more plugins and re-sorts by priority. */
|
|
36
|
+
/** Registers one or more plugins (replacing any with the same name) and re-sorts by priority. */
|
|
25
37
|
public register(...plugins: (P & BasePlugin)[]): void {
|
|
38
|
+
const incoming = new Set(plugins.map((p) => p.name));
|
|
39
|
+
this.plugins = this.plugins.filter((p) => !incoming.has(p.name));
|
|
26
40
|
this.plugins.push(...plugins);
|
|
27
41
|
this.reorder();
|
|
28
42
|
}
|
|
@@ -63,17 +77,14 @@ class PluginManager<P extends PluginContract> {
|
|
|
63
77
|
opts: InvokeOptions<P, K>,
|
|
64
78
|
): ReturnType<P[K]> {
|
|
65
79
|
const { mode = "first", fallback, reducer } = opts;
|
|
66
|
-
const enabled = this.list()
|
|
67
|
-
P & BasePlugin
|
|
68
|
-
>;
|
|
80
|
+
const enabled = this.list();
|
|
69
81
|
|
|
70
82
|
if (enabled.length === 0) {
|
|
71
|
-
return
|
|
83
|
+
return fallback(...args);
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
if (mode === "first") {
|
|
75
|
-
|
|
76
|
-
return fn(...args);
|
|
87
|
+
return enabled[0][method](...args);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
const defaultReducer: PipelineReducer<ReturnType<P[K]>, P> = (
|
|
@@ -82,10 +93,9 @@ class PluginManager<P extends PluginContract> {
|
|
|
82
93
|
) => curr;
|
|
83
94
|
const red = reducer ?? defaultReducer;
|
|
84
95
|
|
|
85
|
-
let acc: ReturnType<P[K]> = enabled[0][method]
|
|
96
|
+
let acc: ReturnType<P[K]> = enabled[0][method](...args);
|
|
86
97
|
enabled.slice(1).forEach((plugin, idx) => {
|
|
87
|
-
|
|
88
|
-
acc = red(acc, res, plugin, idx + 1);
|
|
98
|
+
acc = red(acc, plugin[method](...args), plugin, idx + 1);
|
|
89
99
|
});
|
|
90
100
|
return acc;
|
|
91
101
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
interface BasePlugin {
|
|
7
7
|
/** Unique identifier, used for logs / debugging and for enable/disable. */
|
|
8
8
|
readonly name: string;
|
|
9
|
-
/** Weight; the highest priority wins (default = 0). */
|
|
9
|
+
/** Weight; the highest priority wins (default = 0). Ties keep registration order. */
|
|
10
10
|
readonly priority?: number;
|
|
11
11
|
/** Shortcut to disable a plugin without removing it. */
|
|
12
12
|
enabled?: boolean;
|
|
@@ -40,7 +40,9 @@ type PipelineReducer<R, P extends PluginContract> = (
|
|
|
40
40
|
interface InvokeOptions<P extends PluginContract, K extends keyof P> {
|
|
41
41
|
/**
|
|
42
42
|
* `'first'` → returns the first enabled plugin with the highest priority.
|
|
43
|
-
* `'pipeline'` →
|
|
43
|
+
* `'pipeline'` → fan-out + reduce: every enabled plugin runs against the
|
|
44
|
+
* same original `args` (results are NOT chained into the next plugin's
|
|
45
|
+
* input), and the results are folded together via `reducer`.
|
|
44
46
|
* default = `'first'`.
|
|
45
47
|
*/
|
|
46
48
|
mode?: "first" | "pipeline";
|
|
@@ -9,6 +9,18 @@ export interface DeriveAsOfOptions {
|
|
|
9
9
|
readonly maxFutureYears?: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Derives the `asOf` instant an evaluation is pinned to.
|
|
14
|
+
*
|
|
15
|
+
* With `asOfSource: "CALLER_PROVIDED"` the instant comes from the reserved
|
|
16
|
+
* `seed.fields.asOf` key (a `Date`); with `"NOW"` it is the evaluation clock.
|
|
17
|
+
*
|
|
18
|
+
* The bound is intentionally asymmetric: a caller-provided `asOf` may be at
|
|
19
|
+
* most `maxFutureYears` in the future (a fat-finger / clock-skew guard against
|
|
20
|
+
* selecting a not-yet-in-force policy), but is unbounded in the past — querying
|
|
21
|
+
* how a policy stood at any historical instant is a first-class use of a
|
|
22
|
+
* temporal engine, so back-dating is never capped.
|
|
23
|
+
*/
|
|
12
24
|
export class PolicyAsOfResolver {
|
|
13
25
|
private static resolveMaxFutureYears(
|
|
14
26
|
options: DeriveAsOfOptions,
|
|
@@ -195,7 +195,9 @@ export class PolicyCatalogEntry {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
equals(other: PolicyCatalogEntry): boolean {
|
|
198
|
-
|
|
198
|
+
// Full variant identity (key + kind + engine/schema versions), not just
|
|
199
|
+
// the key: two variants under the same key are distinct entries.
|
|
200
|
+
return this.toVariantKey() === other.toVariantKey();
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
toJSON(): PolicyCatalogEntryProps {
|
|
@@ -124,7 +124,11 @@ export class PolicyCatalog {
|
|
|
124
124
|
return Result.ok(exactMatch);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
family.length === 1 &&
|
|
129
|
+
family[0].kind === params.kind &&
|
|
130
|
+
!family[0].hasExplicitVersionSelector()
|
|
131
|
+
) {
|
|
128
132
|
return Result.ok(family[0]);
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -32,6 +32,13 @@ interface ContextResolverCircuitState {
|
|
|
32
32
|
readonly openUntilMs: number | null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// The circuit breaker is keyed by context path alone, so its state is shared
|
|
36
|
+
// across every seed a builder serves. This is intentional — the breaker guards
|
|
37
|
+
// the backend behind a path, not a single tenant's request — but it means
|
|
38
|
+
// failures resolving a path for one tenant can trip the breaker for others.
|
|
39
|
+
// Give each isolated blast radius its own PolicyContextBuilder if that scope is
|
|
40
|
+
// wrong for your host.
|
|
41
|
+
|
|
35
42
|
export interface PolicyContextBuilderOptions {
|
|
36
43
|
readonly defaultResolverResilience?: ContextResolverResilienceOptions;
|
|
37
44
|
readonly now?: () => number;
|
|
@@ -296,6 +303,12 @@ export class PolicyContextBuilder {
|
|
|
296
303
|
return execute();
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
// The timeout is a race, not a cancellation: on timeout the losing
|
|
307
|
+
// resolver keeps running to completion in the background (there is no
|
|
308
|
+
// AbortSignal in the resolver contract). Combined with retry, a slow
|
|
309
|
+
// resolver can therefore be in flight more than once at a time — safe
|
|
310
|
+
// for the idempotent reads resolvers are required to be (see
|
|
311
|
+
// ContextValueResolver), a hazard for any resolver with side effects.
|
|
299
312
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
300
313
|
|
|
301
314
|
try {
|
|
@@ -359,6 +372,13 @@ export class PolicyContextBuilder {
|
|
|
359
372
|
/**
|
|
360
373
|
* Resolves all required context paths and returns a flat context object.
|
|
361
374
|
* If any required path cannot be resolved, returns an error.
|
|
375
|
+
*
|
|
376
|
+
* Resolution is deliberately sequential and fail-fast: paths resolve in
|
|
377
|
+
* order and the first failure returns immediately, so later resolvers never
|
|
378
|
+
* run once one path fails. This trades the latency-hiding of parallel
|
|
379
|
+
* resolution for skipping downstream (potentially expensive) lookups on a
|
|
380
|
+
* doomed evaluation. It is a tested contract, not an oversight — flip it to
|
|
381
|
+
* `Promise.all` only if you also accept that every resolver always runs.
|
|
362
382
|
*/
|
|
363
383
|
async build(
|
|
364
384
|
requirements: readonly string[],
|
|
@@ -23,6 +23,11 @@ export interface ContextResolverResilienceOptions {
|
|
|
23
23
|
/**
|
|
24
24
|
* A resolver that knows how to produce a value for a specific context path.
|
|
25
25
|
* Designed as async to allow future DB/API lookups without breaking the interface.
|
|
26
|
+
*
|
|
27
|
+
* Resolvers MUST be idempotent, side-effect-free reads. The builder's timeout
|
|
28
|
+
* is a race with no cancellation, so a timed-out resolver keeps running; with
|
|
29
|
+
* retry enabled it may also be invoked several times for one path. Anything but
|
|
30
|
+
* a pure lookup here risks duplicated effects.
|
|
26
31
|
*/
|
|
27
32
|
export interface ContextValueResolver {
|
|
28
33
|
readonly path: string;
|
|
@@ -6,6 +6,17 @@ import type { SchoolId, TenantId } from "../policy-ids.js";
|
|
|
6
6
|
* Seed data provided by the use case / caller.
|
|
7
7
|
* Contains raw IDs and inputs that the context system will resolve into paths.
|
|
8
8
|
* Each resolver is responsible for validating and extracting what it needs from fields.
|
|
9
|
+
*
|
|
10
|
+
* `tenantId`/`schoolId` are required on every seed, including evaluations scoped
|
|
11
|
+
* at TENANT or GLOBAL level. The `schoolId`/`SCHOOL` vocabulary is deliberately
|
|
12
|
+
* school-first: erp-core is a school ERP core, and a full-control copy targeting
|
|
13
|
+
* another domain is expected to rename these ids to its own leaf scope.
|
|
14
|
+
*
|
|
15
|
+
* `fields` is an open bag, but two keys are reserved by the pipeline and carry
|
|
16
|
+
* meaning beyond "just data":
|
|
17
|
+
* - `now` (a `Date`) pins the evaluation clock;
|
|
18
|
+
* - `asOf` (a `Date`) supplies the instant for `asOfSource: "CALLER_PROVIDED"`.
|
|
19
|
+
* See {@link EvaluateInput} and {@link PolicyAsOfResolver}.
|
|
9
20
|
*/
|
|
10
21
|
export interface ContextSeed {
|
|
11
22
|
readonly tenantId: TenantId;
|
|
@@ -6,14 +6,12 @@ import type { PolicyDefinitionRepository } from "./policy-definition-repository.
|
|
|
6
6
|
import { PolicyScopeMatcher } from "./policy-scope.js";
|
|
7
7
|
|
|
8
8
|
export class InMemoryPolicyDefinitionRepository implements PolicyDefinitionRepository {
|
|
9
|
-
private readonly definitions: readonly PolicyDefinition[];
|
|
10
9
|
private readonly definitionsByPolicyKey: ReadonlyMap<
|
|
11
10
|
string,
|
|
12
11
|
readonly PolicyDefinition[]
|
|
13
12
|
>;
|
|
14
13
|
|
|
15
14
|
constructor(definitions: readonly PolicyDefinition[]) {
|
|
16
|
-
this.definitions = definitions;
|
|
17
15
|
this.definitionsByPolicyKey = definitions.reduce<
|
|
18
16
|
Map<string, PolicyDefinition[]>
|
|
19
17
|
>((index, definition) => {
|
|
@@ -8,6 +8,15 @@ export type GatePayloadParser = (
|
|
|
8
8
|
payload: unknown,
|
|
9
9
|
) => ResultType<GatePayload, string>;
|
|
10
10
|
|
|
11
|
+
// Versioning decision (deliberate asymmetry with compute):
|
|
12
|
+
// Gate parsers are keyed by GATE ENGINE VERSION, not by `payloadSchemaVersion`.
|
|
13
|
+
// A gate definition's `payloadSchemaVersion` therefore selects a catalog
|
|
14
|
+
// variant but has NO effect on parsing — under one gate engine version there is
|
|
15
|
+
// exactly one payload shape. Compute is the opposite: its parsers are keyed by
|
|
16
|
+
// `payloadSchemaVersion` (see parse-compute-payload.ts), because a compute
|
|
17
|
+
// engine version can host several payload schemas. If a second gate payload
|
|
18
|
+
// schema under one engine version ever becomes necessary, migrate gate parsing
|
|
19
|
+
// to be keyed by `payloadSchemaVersion` here to match compute.
|
|
11
20
|
export class GatePayloadParserRegistry {
|
|
12
21
|
private readonly parsers = new Map<number, GatePayloadParser>();
|
|
13
22
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Pure helpers for treating condition operands as instants. Extracted from
|
|
2
|
+
// condition-evaluator.ts to keep that file under the kit's size limit and to
|
|
3
|
+
// isolate the (zod-free) date logic the ./abac subpath re-uses. Import the date
|
|
4
|
+
// util directly (not the utils barrel): the barrel re-exports PolicyHashing,
|
|
5
|
+
// whose node:crypto import would needlessly drag a Node-only API into the
|
|
6
|
+
// zod-free ./abac subpath.
|
|
7
|
+
import { PolicyDateUtils } from "../../utils/date.js";
|
|
8
|
+
import { Result } from "../../../result/result.js";
|
|
9
|
+
|
|
10
|
+
import type { ConditionOp } from "./condition-types.js";
|
|
11
|
+
|
|
12
|
+
export type RelationalOperator = Extract<
|
|
13
|
+
ConditionOp,
|
|
14
|
+
"gt" | "gte" | "lt" | "lte"
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The one instant format condition date operands may use. Strict on purpose
|
|
19
|
+
* (UTC, milliseconds) so comparisons are unambiguous across time zones — and
|
|
20
|
+
* so a malformed operand fails loudly instead of comparing wrong.
|
|
21
|
+
*/
|
|
22
|
+
export const CANONICAL_UTC_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.SSSZ";
|
|
23
|
+
|
|
24
|
+
const ISO_8601_UTC_DATE_PATTERN =
|
|
25
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
26
|
+
// A value that is clearly meant to be a date-time but isn't canonical UTC
|
|
27
|
+
// (missing milliseconds, a non-UTC offset, a space separator…). Lets the
|
|
28
|
+
// caller report a "malformed date" error instead of a misleading "not a
|
|
29
|
+
// number" one.
|
|
30
|
+
const ISO_8601_DATE_LIKE_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses a value used as a date operand:
|
|
34
|
+
* - `Result.ok(Date)` for a valid Date or a canonical ISO-8601 UTC string;
|
|
35
|
+
* - `Result.err` when it is date-shaped but malformed (invalid Date, or a
|
|
36
|
+
* date-like string that isn't canonical UTC) — a configuration error worth
|
|
37
|
+
* surfacing rather than silently treating as "not a date";
|
|
38
|
+
* - `null` when the value isn't date-shaped at all, so the caller can fall
|
|
39
|
+
* through to a numeric comparison.
|
|
40
|
+
*/
|
|
41
|
+
export function parseComparableDate(
|
|
42
|
+
value: unknown,
|
|
43
|
+
): Result<Date, string> | null {
|
|
44
|
+
if (value instanceof Date) {
|
|
45
|
+
return PolicyDateUtils.isValid(value)
|
|
46
|
+
? Result.ok(value)
|
|
47
|
+
: Result.err("Invalid Date instance");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ISO_8601_UTC_DATE_PATTERN.test(value)) {
|
|
55
|
+
const parsed = new Date(value);
|
|
56
|
+
if (
|
|
57
|
+
!PolicyDateUtils.isValid(parsed) ||
|
|
58
|
+
parsed.toISOString() !== value
|
|
59
|
+
) {
|
|
60
|
+
return Result.err("Invalid ISO 8601 UTC date string");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Result.ok(parsed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ISO_8601_DATE_LIKE_PATTERN.test(value)) {
|
|
67
|
+
return Result.err(
|
|
68
|
+
`date operand must be canonical ISO 8601 UTC (${CANONICAL_UTC_DATE_FORMAT})`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function evaluateRelationalNumbers(
|
|
76
|
+
actual: number,
|
|
77
|
+
op: RelationalOperator,
|
|
78
|
+
expected: number,
|
|
79
|
+
): boolean {
|
|
80
|
+
switch (op) {
|
|
81
|
+
case "gt":
|
|
82
|
+
return actual > expected;
|
|
83
|
+
case "gte":
|
|
84
|
+
return actual >= expected;
|
|
85
|
+
case "lt":
|
|
86
|
+
return actual < expected;
|
|
87
|
+
case "lte":
|
|
88
|
+
return actual <= expected;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function containsInvalidDate(value: unknown): boolean {
|
|
93
|
+
if (value instanceof Date) {
|
|
94
|
+
return !PolicyDateUtils.isValid(value);
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
return value.some((item) => containsInvalidDate(item));
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Valid Dates become their ISO string so equality/set operators compare
|
|
103
|
+
// instants instead of references; every other value passes through.
|
|
104
|
+
export function normalizeDateOperand(value: unknown): unknown {
|
|
105
|
+
if (value instanceof Date) {
|
|
106
|
+
return value.toISOString();
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
return value.map((item) => normalizeDateOperand(item));
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|