@cullet/erp-core 1.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 +39 -0
- package/README.md +110 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/exceptions/validation-field.d.ts +2 -0
- package/dist/exceptions/validation-field.js +2 -0
- package/dist/gate-engine-registry.js +260 -0
- package/dist/gate-engine-registry.js.map +1 -0
- package/dist/gate-types.d.ts +307 -0
- package/dist/gate-v1-payload.schema.js +677 -0
- package/dist/gate-v1-payload.schema.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/outcome.js +92 -0
- package/dist/outcome.js.map +1 -0
- package/dist/parse-gate-payload.d.ts +136 -0
- package/dist/policies/engines/index.d.ts +3 -0
- package/dist/policies/engines/index.js +2 -0
- package/dist/policies/engines/v1/gate/index.d.ts +115 -0
- package/dist/policies/engines/v1/gate/index.js +84 -0
- package/dist/policies/engines/v1/gate/index.js.map +1 -0
- package/dist/policies/index.d.ts +4 -0
- package/dist/policies/index.js +5 -0
- package/dist/policy-service.d.ts +541 -0
- package/dist/policy-service.js +992 -0
- package/dist/policy-service.js.map +1 -0
- package/dist/validation-code.js +210 -0
- package/dist/validation-code.js.map +1 -0
- package/dist/validation-error.d.ts +686 -0
- package/dist/validation-error.js +757 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/validation-field.d.ts +12 -0
- package/dist/validation-field.js +23 -0
- package/dist/validation-field.js.map +1 -0
- package/meta.json +66 -0
- package/package.json +59 -0
- package/src/core/application/commands/command.ts +28 -0
- package/src/core/application/commands/index.ts +2 -0
- package/src/core/application/commands/requested-by.ts +97 -0
- package/src/core/application/index.ts +31 -0
- package/src/core/application/policy-error-mapper.ts +60 -0
- package/src/core/application/ports/index.ts +14 -0
- package/src/core/application/ports/logger.port.ts +10 -0
- package/src/core/application/ports/metrics.port.ts +9 -0
- package/src/core/application/ports/policy-port.ts +20 -0
- package/src/core/application/ports/repository.port.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +14 -0
- package/src/core/application/ports/tracer.port.ts +16 -0
- package/src/core/application/queries/index.ts +2 -0
- package/src/core/application/queries/query.ts +53 -0
- package/src/core/application/temporal/index.ts +11 -0
- package/src/core/application/temporal/temporal-context.ts +41 -0
- package/src/core/application/temporal/temporal-use-case.ts +41 -0
- package/src/core/application/use-case.ts +24 -0
- package/src/core/config/core-config.instance.ts +14 -0
- package/src/core/config/core-config.ts +87 -0
- package/src/core/config/index.ts +12 -0
- package/src/core/config/policy-reporter.ts +60 -0
- package/src/core/config/silent-policy-reporter.ts +5 -0
- package/src/core/domain/entity.ts +69 -0
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +16 -0
- package/src/core/domain/rulesets/ruleset-registry.ts +71 -0
- package/src/core/domain/rulesets/ruleset.contracts.ts +15 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +13 -0
- package/src/core/domain/temporal/index.ts +18 -0
- package/src/core/domain/temporal/temporal-range.ts +53 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +47 -0
- package/src/core/domain/temporal/transaction-time.ts +60 -0
- package/src/core/domain/temporal/valid-time.ts +57 -0
- package/src/core/domain/value-object.ts +30 -0
- package/src/core/errors/app-error.ts +85 -0
- package/src/core/errors/authentication-error.ts +171 -0
- package/src/core/errors/authorization-error.ts +204 -0
- package/src/core/errors/business-rule-violation-error.ts +28 -0
- package/src/core/errors/conflict-error.ts +246 -0
- package/src/core/errors/error-codes.ts +60 -0
- package/src/core/errors/idempotency-error.ts +372 -0
- package/src/core/errors/index.ts +99 -0
- package/src/core/errors/integration-error.ts +157 -0
- package/src/core/errors/legacy-incompatible-error.ts +26 -0
- package/src/core/errors/not-found-error.ts +27 -0
- package/src/core/errors/serialization-error.ts +233 -0
- package/src/core/errors/temporal-error.ts +145 -0
- package/src/core/errors/types.ts +74 -0
- package/src/core/errors/unexpected-error.ts +22 -0
- package/src/core/errors/utils/index.ts +4 -0
- package/src/core/errors/utils/json-safe.ts +93 -0
- package/src/core/errors/validation-error.ts +34 -0
- package/src/core/exceptions/business-rule-violation-exception.ts +12 -0
- package/src/core/exceptions/domain-exception.ts +9 -0
- package/src/core/exceptions/entity-not-found-exception.ts +12 -0
- package/src/core/exceptions/invalid-state-transition-exception.ts +15 -0
- package/src/core/exceptions/invariant-violation-exception.ts +9 -0
- package/src/core/exceptions/validation-code.ts +39 -0
- package/src/core/exceptions/validation-exception.ts +39 -0
- package/src/core/exceptions/validation-field.ts +32 -0
- package/src/core/index.ts +15 -0
- package/src/core/policies/asof/asof.ts +68 -0
- package/src/core/policies/asof/index.ts +2 -0
- package/src/core/policies/catalog/catalog-types.ts +12 -0
- package/src/core/policies/catalog/catalog.instance.ts +21 -0
- package/src/core/policies/catalog/index.ts +11 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +220 -0
- package/src/core/policies/catalog/policy-catalog.ts +124 -0
- package/src/core/policies/catalog/policy-key.ts +78 -0
- package/src/core/policies/context/context-builder.ts +391 -0
- package/src/core/policies/context/context-registry.instance.ts +26 -0
- package/src/core/policies/context/context-registry.ts +95 -0
- package/src/core/policies/context/context-resolver.ts +31 -0
- package/src/core/policies/context/context-seed.ts +32 -0
- package/src/core/policies/context/index.ts +21 -0
- package/src/core/policies/context/path.ts +152 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +73 -0
- package/src/core/policies/defs/index.ts +20 -0
- package/src/core/policies/defs/policy-definition-repository.ts +8 -0
- package/src/core/policies/defs/policy-definition.ts +192 -0
- package/src/core/policies/defs/policy-payload.contracts.ts +13 -0
- package/src/core/policies/defs/policy-scope.ts +53 -0
- package/src/core/policies/defs/policy-semver.ts +83 -0
- package/src/core/policies/engines/compute-engine-registry.ts +57 -0
- package/src/core/policies/engines/compute-evaluator-registry.ts +28 -0
- package/src/core/policies/engines/compute-payload.ts +5 -0
- package/src/core/policies/engines/compute-registry.ts +67 -0
- package/src/core/policies/engines/compute-types.ts +51 -0
- package/src/core/policies/engines/condition-evaluator-reporter.ts +34 -0
- package/src/core/policies/engines/gate-engine-registry.ts +38 -0
- package/src/core/policies/engines/gate-payload.ts +5 -0
- package/src/core/policies/engines/gate-types.ts +63 -0
- package/src/core/policies/engines/index.ts +48 -0
- package/src/core/policies/engines/parse-compute-payload.ts +71 -0
- package/src/core/policies/engines/parse-gate-payload.ts +68 -0
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +55 -0
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +75 -0
- package/src/core/policies/engines/v1/compute/index.ts +12 -0
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +54 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +592 -0
- package/src/core/policies/engines/v1/condition-schema.ts +50 -0
- package/src/core/policies/engines/v1/condition-types.ts +41 -0
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +157 -0
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +12 -0
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +46 -0
- package/src/core/policies/engines/v1/gate/index.ts +9 -0
- package/src/core/policies/engines/v1/index.ts +13 -0
- package/src/core/policies/index.ts +132 -0
- package/src/core/policies/package/policy-package.ts +17 -0
- package/src/core/policies/policy-ids.ts +60 -0
- package/src/core/policies/resolver/index.ts +1 -0
- package/src/core/policies/resolver/policy-resolver.ts +57 -0
- package/src/core/policies/service/index.ts +10 -0
- package/src/core/policies/service/policy-evaluation-error.ts +148 -0
- package/src/core/policies/service/policy-service.ts +493 -0
- package/src/core/policies/utils/date.ts +7 -0
- package/src/core/policies/utils/hash.ts +90 -0
- package/src/core/policies/utils/index.ts +3 -0
- package/src/core/policies/utils/result.ts +4 -0
- package/src/core/result/outcome.ts +158 -0
- package/src/core/result/result.ts +153 -0
- package/src/core/shared/aggregate-version.ts +11 -0
- package/src/core/shared/hashing.ts +38 -0
- package/src/core/shared/immutable.ts +46 -0
- package/src/core/shared/temporal-guards.ts +22 -0
- package/src/core/versioning/domain-event-contracts.ts +73 -0
- package/src/core/versioning/version.ts +34 -0
- package/src/errors/index.ts +1 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +31 -0
- package/src/examples/rulesets/entity/order-invariants-v1.ts +48 -0
- package/src/examples/rulesets/entity/order-invariants-v2.ts +28 -0
- package/src/examples/rulesets/entity/order.ts +162 -0
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +52 -0
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/cpf.ts +37 -0
- package/src/examples/rulesets/value-object/customer.ts +67 -0
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +37 -0
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/person-name.ts +39 -0
- package/src/exceptions/validation-field.ts +1 -0
- package/src/index.ts +11 -0
- package/src/policies/engines/index.ts +1 -0
- package/src/policies/engines/v1/gate/index.ts +1 -0
- package/src/policies/index.ts +1 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for reading/writing values at dot-separated paths
|
|
3
|
+
* in plain objects, e.g. "installment.dueDate".
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Result } from "../../result/result";
|
|
7
|
+
|
|
8
|
+
export class PolicyContextPath {
|
|
9
|
+
private static readonly FORBIDDEN_SEGMENTS = new Set([
|
|
10
|
+
"__proto__",
|
|
11
|
+
"prototype",
|
|
12
|
+
"constructor",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
private static resolve(
|
|
16
|
+
obj: Record<string, unknown>,
|
|
17
|
+
path: string,
|
|
18
|
+
): Result<
|
|
19
|
+
{
|
|
20
|
+
readonly found: boolean;
|
|
21
|
+
readonly value: unknown;
|
|
22
|
+
},
|
|
23
|
+
string
|
|
24
|
+
> {
|
|
25
|
+
const segmentsResult = PolicyContextPath.parseSegments(path);
|
|
26
|
+
if (segmentsResult.isErr()) {
|
|
27
|
+
return Result.err(segmentsResult.errorOrNull()!);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const segments = segmentsResult.getOrNull()!;
|
|
31
|
+
let current: unknown = obj;
|
|
32
|
+
|
|
33
|
+
for (const segment of segments) {
|
|
34
|
+
if (
|
|
35
|
+
current === null ||
|
|
36
|
+
current === undefined ||
|
|
37
|
+
typeof current !== "object"
|
|
38
|
+
) {
|
|
39
|
+
return Result.ok({ found: false, value: undefined });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const record = current as Record<string, unknown>;
|
|
43
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
44
|
+
return Result.ok({ found: false, value: undefined });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
current = record[segment];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Result.ok({ found: true, value: current });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static parseSegments(
|
|
54
|
+
path: string,
|
|
55
|
+
): Result<readonly string[], string> {
|
|
56
|
+
if (path.trim().length === 0) {
|
|
57
|
+
return Result.err("Path cannot be empty");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const segments = path.split(".");
|
|
61
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
62
|
+
return Result.err(`Path "${path}" contains an empty segment`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const forbiddenSegment = segments.find((segment) =>
|
|
66
|
+
PolicyContextPath.FORBIDDEN_SEGMENTS.has(segment),
|
|
67
|
+
);
|
|
68
|
+
if (forbiddenSegment !== undefined) {
|
|
69
|
+
return Result.err(
|
|
70
|
+
`Path "${path}" contains forbidden segment "${forbiddenSegment}"`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Result.ok(segments);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static isPlainRecord(
|
|
78
|
+
value: unknown,
|
|
79
|
+
): value is Record<string, unknown> {
|
|
80
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prototype = Object.getPrototypeOf(value);
|
|
85
|
+
return prototype === Object.prototype || prototype === null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static getOrAbsent(
|
|
89
|
+
obj: Record<string, unknown>,
|
|
90
|
+
path: string,
|
|
91
|
+
): Result<unknown, "absent"> {
|
|
92
|
+
const resolvedPath = PolicyContextPath.resolve(obj, path);
|
|
93
|
+
if (resolvedPath.isErr()) {
|
|
94
|
+
return Result.err("absent");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { found, value } = resolvedPath.getOrNull()!;
|
|
98
|
+
if (!found) {
|
|
99
|
+
return Result.err("absent");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Result.ok(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static has(obj: Record<string, unknown>, path: string): boolean {
|
|
106
|
+
const resolvedPath = PolicyContextPath.resolve(obj, path);
|
|
107
|
+
if (resolvedPath.isErr()) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return resolvedPath.getOrNull()!.found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static set(
|
|
115
|
+
obj: Record<string, unknown>,
|
|
116
|
+
path: string,
|
|
117
|
+
value: unknown,
|
|
118
|
+
): Result<void, string> {
|
|
119
|
+
const segmentsResult = PolicyContextPath.parseSegments(path);
|
|
120
|
+
if (segmentsResult.isErr()) {
|
|
121
|
+
return Result.err(segmentsResult.errorOrNull()!);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const segments = segmentsResult.getOrNull()!;
|
|
125
|
+
let current: Record<string, unknown> = obj;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
128
|
+
const seg = segments[i];
|
|
129
|
+
const next = current[seg];
|
|
130
|
+
|
|
131
|
+
if (next === undefined) {
|
|
132
|
+
const container = Object.create(null) as Record<string, unknown>;
|
|
133
|
+
current[seg] = container;
|
|
134
|
+
current = container;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!PolicyContextPath.isPlainRecord(next)) {
|
|
139
|
+
return Result.err(
|
|
140
|
+
`Cannot create nested path "${path}" because "${segments
|
|
141
|
+
.slice(0, i + 1)
|
|
142
|
+
.join(".")}" already contains a non-plain object`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
current = next;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
current[segments[segments.length - 1]] = value;
|
|
150
|
+
return Result.ok(undefined);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FindCandidatesParams,
|
|
3
|
+
PolicyDefinition,
|
|
4
|
+
} from "./policy-definition";
|
|
5
|
+
import type { PolicyDefinitionRepository } from "./policy-definition-repository";
|
|
6
|
+
import { PolicyScopeMatcher } from "./policy-scope";
|
|
7
|
+
|
|
8
|
+
export class InMemoryPolicyDefinitionRepository implements PolicyDefinitionRepository {
|
|
9
|
+
private readonly definitions: readonly PolicyDefinition[];
|
|
10
|
+
private readonly definitionsByPolicyKey: ReadonlyMap<
|
|
11
|
+
string,
|
|
12
|
+
readonly PolicyDefinition[]
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
constructor(definitions: readonly PolicyDefinition[]) {
|
|
16
|
+
this.definitions = definitions;
|
|
17
|
+
this.definitionsByPolicyKey = definitions.reduce<
|
|
18
|
+
Map<string, PolicyDefinition[]>
|
|
19
|
+
>((index, definition) => {
|
|
20
|
+
const existingDefinitions = index.get(definition.policyKey) ?? [];
|
|
21
|
+
existingDefinitions.push(definition);
|
|
22
|
+
index.set(definition.policyKey, existingDefinitions);
|
|
23
|
+
return index;
|
|
24
|
+
}, new Map<string, PolicyDefinition[]>());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
findCandidates(params: FindCandidatesParams): PolicyDefinition[] {
|
|
28
|
+
const {
|
|
29
|
+
policyKey,
|
|
30
|
+
kind,
|
|
31
|
+
payloadSchemaVersion,
|
|
32
|
+
asOf,
|
|
33
|
+
contextVersion,
|
|
34
|
+
scopeChain,
|
|
35
|
+
} = params;
|
|
36
|
+
|
|
37
|
+
const candidatesForPolicyKey =
|
|
38
|
+
this.definitionsByPolicyKey.get(policyKey) ?? [];
|
|
39
|
+
|
|
40
|
+
const candidates = candidatesForPolicyKey.filter((def) => {
|
|
41
|
+
// Match kind
|
|
42
|
+
if (def.kind !== kind) return false;
|
|
43
|
+
if (
|
|
44
|
+
payloadSchemaVersion !== undefined &&
|
|
45
|
+
def.payloadSchemaVersion !== payloadSchemaVersion
|
|
46
|
+
) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!def.enabled) return false;
|
|
51
|
+
|
|
52
|
+
// Only PUBLISHED
|
|
53
|
+
if (def.status !== "PUBLISHED") return false;
|
|
54
|
+
|
|
55
|
+
// Effective date range (effectiveFrom <= asOf, and effectiveTo is null or > asOf)
|
|
56
|
+
if (def.effectiveFrom > asOf) return false;
|
|
57
|
+
if (def.effectiveTo !== null && def.effectiveTo <= asOf) return false;
|
|
58
|
+
|
|
59
|
+
// Context version compatibility
|
|
60
|
+
if (contextVersion < def.contextVersionMin) return false;
|
|
61
|
+
if (contextVersion > def.contextVersionMax) return false;
|
|
62
|
+
|
|
63
|
+
// Scope must match one of the chain entries
|
|
64
|
+
if (!PolicyScopeMatcher.matchesChain(def.scope, scopeChain)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return candidates;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { InMemoryPolicyDefinitionRepository } from "./in-memory-policy-definition-repo";
|
|
2
|
+
export type {
|
|
3
|
+
BasePolicyDefinitionProps,
|
|
4
|
+
ComputePolicyDefinitionInput,
|
|
5
|
+
ComputePolicyDefinitionProps,
|
|
6
|
+
FindCandidatesParams,
|
|
7
|
+
GatePolicyDefinitionInput,
|
|
8
|
+
GatePolicyDefinitionProps,
|
|
9
|
+
PolicyDefinitionProps,
|
|
10
|
+
PolicyDefinitionStatus,
|
|
11
|
+
} from "./policy-definition";
|
|
12
|
+
export { PolicyDefinition } from "./policy-definition";
|
|
13
|
+
export type { PolicyDefinitionRepository } from "./policy-definition-repository";
|
|
14
|
+
export type {
|
|
15
|
+
AnyPolicyPayload,
|
|
16
|
+
PayloadForKey,
|
|
17
|
+
} from "./policy-payload.contracts";
|
|
18
|
+
export { comparePolicySemver, POLICY_SEMVER_PATTERN } from "./policy-semver";
|
|
19
|
+
export type { PolicyScope, ScopeChain } from "./policy-scope";
|
|
20
|
+
export { PolicyScopeMatcher } from "./policy-scope";
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../../exceptions/invariant-violation-exception";
|
|
2
|
+
import type { PolicyKind } from "../catalog";
|
|
3
|
+
import type { PolicyDefinitionId } from "../policy-ids";
|
|
4
|
+
|
|
5
|
+
import type { AnyPolicyPayload } from "./policy-payload.contracts";
|
|
6
|
+
import type { PolicyScope } from "./policy-scope";
|
|
7
|
+
import { POLICY_SEMVER_PATTERN } from "./policy-semver";
|
|
8
|
+
|
|
9
|
+
// ─── Status ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type PolicyDefinitionStatus = "DRAFT" | "PUBLISHED" | "RETIRED";
|
|
12
|
+
|
|
13
|
+
// ─── Definition props ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface BasePolicyDefinitionProps<
|
|
16
|
+
K extends string = string,
|
|
17
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
18
|
+
> {
|
|
19
|
+
readonly id: PolicyDefinitionId;
|
|
20
|
+
readonly policyKey: K;
|
|
21
|
+
readonly policyVersion: string; // semver
|
|
22
|
+
readonly payloadSchemaVersion: number;
|
|
23
|
+
readonly contextVersionMin: number;
|
|
24
|
+
readonly contextVersionMax: number;
|
|
25
|
+
readonly enabled: boolean;
|
|
26
|
+
readonly status: PolicyDefinitionStatus;
|
|
27
|
+
readonly scope: PolicyScope;
|
|
28
|
+
readonly effectiveFrom: Date;
|
|
29
|
+
readonly effectiveTo: Date | null;
|
|
30
|
+
readonly priority: number;
|
|
31
|
+
readonly payloadJson: P;
|
|
32
|
+
readonly payloadHash: string;
|
|
33
|
+
readonly createdAt: Date;
|
|
34
|
+
readonly publishedAt: Date | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GatePolicyDefinitionProps<
|
|
38
|
+
K extends string = string,
|
|
39
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
40
|
+
> extends BasePolicyDefinitionProps<K, P> {
|
|
41
|
+
readonly kind: "GATE";
|
|
42
|
+
readonly gateEngineVersion: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ComputePolicyDefinitionProps<
|
|
46
|
+
K extends string = string,
|
|
47
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
48
|
+
> extends BasePolicyDefinitionProps<K, P> {
|
|
49
|
+
readonly kind: "COMPUTE";
|
|
50
|
+
readonly computeEngineVersion: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type GatePolicyDefinitionInput<
|
|
54
|
+
K extends string = string,
|
|
55
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
56
|
+
> = Omit<GatePolicyDefinitionProps<K, P>, "kind" | "enabled"> & {
|
|
57
|
+
readonly enabled?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type ComputePolicyDefinitionInput<
|
|
61
|
+
K extends string = string,
|
|
62
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
63
|
+
> = Omit<ComputePolicyDefinitionProps<K, P>, "kind" | "enabled"> & {
|
|
64
|
+
readonly enabled?: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type PolicyDefinitionProps<
|
|
68
|
+
K extends string = string,
|
|
69
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
70
|
+
> = GatePolicyDefinitionProps<K, P> | ComputePolicyDefinitionProps<K, P>;
|
|
71
|
+
|
|
72
|
+
// ─── Definition row (as if loaded from database) ────────────────────────────
|
|
73
|
+
|
|
74
|
+
export class PolicyDefinition<
|
|
75
|
+
K extends string = string,
|
|
76
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
77
|
+
> {
|
|
78
|
+
private static assertPolicyVersionIsSemver(policyVersion: string): void {
|
|
79
|
+
if (!POLICY_SEMVER_PATTERN.test(policyVersion)) {
|
|
80
|
+
throw new InvariantViolationException(
|
|
81
|
+
`PolicyDefinition.policyVersion must be a valid semver. Received: "${policyVersion}"`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public readonly id: PolicyDefinitionId;
|
|
87
|
+
public readonly policyKey: K;
|
|
88
|
+
|
|
89
|
+
public readonly policyVersion: string;
|
|
90
|
+
public readonly payloadSchemaVersion: number;
|
|
91
|
+
public readonly contextVersionMin: number;
|
|
92
|
+
public readonly contextVersionMax: number;
|
|
93
|
+
public readonly gateEngineVersion?: number;
|
|
94
|
+
public readonly computeEngineVersion?: number;
|
|
95
|
+
|
|
96
|
+
public readonly enabled: boolean;
|
|
97
|
+
public readonly status: PolicyDefinitionStatus;
|
|
98
|
+
public readonly scope: PolicyScope;
|
|
99
|
+
public readonly effectiveFrom: Date;
|
|
100
|
+
public readonly effectiveTo: Date | null;
|
|
101
|
+
public readonly priority: number;
|
|
102
|
+
public readonly payloadJson: P;
|
|
103
|
+
public readonly payloadHash: string;
|
|
104
|
+
public readonly createdAt: Date;
|
|
105
|
+
public readonly publishedAt: Date | null;
|
|
106
|
+
public readonly kind: PolicyKind;
|
|
107
|
+
|
|
108
|
+
private constructor(props: PolicyDefinitionProps<K, P>) {
|
|
109
|
+
// The static factories are the supported API. This guard stays here because
|
|
110
|
+
// TypeScript privacy does not protect runtime-only construction paths.
|
|
111
|
+
this.id = props.id;
|
|
112
|
+
this.policyKey = props.policyKey;
|
|
113
|
+
PolicyDefinition.assertPolicyVersionIsSemver(props.policyVersion);
|
|
114
|
+
this.policyVersion = props.policyVersion;
|
|
115
|
+
this.payloadSchemaVersion = props.payloadSchemaVersion;
|
|
116
|
+
this.contextVersionMin = props.contextVersionMin;
|
|
117
|
+
this.contextVersionMax = props.contextVersionMax;
|
|
118
|
+
this.enabled = props.enabled;
|
|
119
|
+
this.status = props.status;
|
|
120
|
+
this.scope = props.scope;
|
|
121
|
+
this.effectiveFrom = props.effectiveFrom;
|
|
122
|
+
this.effectiveTo = props.effectiveTo;
|
|
123
|
+
this.priority = props.priority;
|
|
124
|
+
this.payloadJson = props.payloadJson;
|
|
125
|
+
this.payloadHash = props.payloadHash;
|
|
126
|
+
this.createdAt = props.createdAt;
|
|
127
|
+
this.publishedAt = props.publishedAt;
|
|
128
|
+
this.kind = props.kind;
|
|
129
|
+
|
|
130
|
+
if (props.kind === "GATE") {
|
|
131
|
+
if (props.gateEngineVersion === undefined) {
|
|
132
|
+
throw new InvariantViolationException(
|
|
133
|
+
`PolicyDefinition of kind 'GATE' must have a 'gateEngineVersion'. Policy key: ${props.policyKey}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
this.gateEngineVersion = props.gateEngineVersion;
|
|
137
|
+
} else {
|
|
138
|
+
if (props.computeEngineVersion === undefined) {
|
|
139
|
+
throw new InvariantViolationException(
|
|
140
|
+
`PolicyDefinition of kind 'COMPUTE' must have a 'computeEngineVersion'. Policy key: ${props.policyKey}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
this.computeEngineVersion = props.computeEngineVersion;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static gate<K extends string, P extends AnyPolicyPayload = AnyPolicyPayload>(
|
|
148
|
+
props: GatePolicyDefinitionInput<K, P>,
|
|
149
|
+
): PolicyDefinition<K, P> {
|
|
150
|
+
return new PolicyDefinition<K, P>({
|
|
151
|
+
...props,
|
|
152
|
+
enabled: props.enabled ?? true,
|
|
153
|
+
kind: "GATE",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static compute<
|
|
158
|
+
K extends string,
|
|
159
|
+
P extends AnyPolicyPayload = AnyPolicyPayload,
|
|
160
|
+
>(props: ComputePolicyDefinitionInput<K, P>): PolicyDefinition<K, P> {
|
|
161
|
+
return new PolicyDefinition<K, P>({
|
|
162
|
+
...props,
|
|
163
|
+
enabled: props.enabled ?? true,
|
|
164
|
+
kind: "COMPUTE",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isGate(): this is PolicyDefinition<K, P> & {
|
|
169
|
+
readonly kind: "GATE";
|
|
170
|
+
readonly gateEngineVersion: number;
|
|
171
|
+
} {
|
|
172
|
+
return this.kind === "GATE";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isCompute(): this is PolicyDefinition<K, P> & {
|
|
176
|
+
readonly kind: "COMPUTE";
|
|
177
|
+
readonly computeEngineVersion: number;
|
|
178
|
+
} {
|
|
179
|
+
return this.kind === "COMPUTE";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Query parameters ───────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export interface FindCandidatesParams {
|
|
186
|
+
readonly policyKey: string;
|
|
187
|
+
readonly kind: PolicyKind;
|
|
188
|
+
readonly payloadSchemaVersion?: number;
|
|
189
|
+
readonly asOf: Date;
|
|
190
|
+
readonly contextVersion: number;
|
|
191
|
+
readonly scopeChain: readonly PolicyScope[];
|
|
192
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComputePayload, GatePayload } from "../engines";
|
|
2
|
+
|
|
3
|
+
export type AnyPolicyPayload = GatePayload | ComputePayload;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Payload type associated with a policy key.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally NOT backed by global module augmentation: two modules registering
|
|
9
|
+
* different shapes for the same literal key would silently override each other and
|
|
10
|
+
* the last `declare module` wins. Per-key payload typing must be expressed
|
|
11
|
+
* explicitly at the definition site (see {@link PolicyDefinition} type params).
|
|
12
|
+
*/
|
|
13
|
+
export type PayloadForKey<_K extends string> = AnyPolicyPayload;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PolicyScopeLevel } from "../catalog";
|
|
2
|
+
import type { SchoolId, TenantId } from "../policy-ids";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scope attached to a PolicyDefinition — defines where it applies.
|
|
6
|
+
*/
|
|
7
|
+
export interface PolicyScope {
|
|
8
|
+
readonly level: PolicyScopeLevel;
|
|
9
|
+
readonly tenantId: TenantId | null;
|
|
10
|
+
readonly schoolId: SchoolId | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ordered list of scopes from most-specific to least-specific.
|
|
15
|
+
* Used by the resolver to match definitions.
|
|
16
|
+
*
|
|
17
|
+
* Example:
|
|
18
|
+
* ```
|
|
19
|
+
* [
|
|
20
|
+
* { level: 'SCHOOL', tenantId: 't1', schoolId: 's1' },
|
|
21
|
+
* { level: 'TENANT', tenantId: 't1', schoolId: null },
|
|
22
|
+
* { level: 'GLOBAL', tenantId: null, schoolId: null },
|
|
23
|
+
* ]
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export type ScopeChain = readonly PolicyScope[];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Numeric weight for scope specificity (higher = more specific).
|
|
30
|
+
*/
|
|
31
|
+
const SCOPE_WEIGHT: Record<PolicyScopeLevel, number> = {
|
|
32
|
+
SCHOOL: 3,
|
|
33
|
+
TENANT: 2,
|
|
34
|
+
GLOBAL: 1,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class PolicyScopeMatcher {
|
|
38
|
+
static weight(level: PolicyScopeLevel): number {
|
|
39
|
+
return SCOPE_WEIGHT[level];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static matchesChain(defScope: PolicyScope, chain: ScopeChain): boolean {
|
|
43
|
+
return chain.some((scope) => PolicyScopeMatcher.equals(defScope, scope));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static equals(a: PolicyScope, b: PolicyScope): boolean {
|
|
47
|
+
return (
|
|
48
|
+
a.level === b.level &&
|
|
49
|
+
a.tenantId === b.tenantId &&
|
|
50
|
+
a.schoolId === b.schoolId
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Semver without build metadata: comparisons are defined only over
|
|
2
|
+
// major.minor.patch and optional pre-release identifiers (semver §11).
|
|
3
|
+
export const POLICY_SEMVER_PATTERN =
|
|
4
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
5
|
+
|
|
6
|
+
interface ParsedSemver {
|
|
7
|
+
major: number;
|
|
8
|
+
minor: number;
|
|
9
|
+
patch: number;
|
|
10
|
+
preRelease: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseSemver(version: string): ParsedSemver {
|
|
14
|
+
const withoutBuild = version.split("+")[0];
|
|
15
|
+
const [core, preRelease = null] = withoutBuild.split("-") as [
|
|
16
|
+
string,
|
|
17
|
+
string | undefined,
|
|
18
|
+
];
|
|
19
|
+
const [major, minor, patch] = core.split(".").map(Number) as [
|
|
20
|
+
number,
|
|
21
|
+
number,
|
|
22
|
+
number,
|
|
23
|
+
];
|
|
24
|
+
return { major, minor, patch, preRelease: preRelease ?? null };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function comparePreRelease(a: string, b: string): number {
|
|
28
|
+
const aIds = a.split(".");
|
|
29
|
+
const bIds = b.split(".");
|
|
30
|
+
const len = Math.max(aIds.length, bIds.length);
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < len; i++) {
|
|
33
|
+
const ai = aIds[i];
|
|
34
|
+
const bi = bIds[i];
|
|
35
|
+
|
|
36
|
+
// Shorter pre-release has lower precedence when prefix matches (semver §11.4.4)
|
|
37
|
+
if (ai === undefined) return -1;
|
|
38
|
+
if (bi === undefined) return 1;
|
|
39
|
+
|
|
40
|
+
const aNum = Number(ai);
|
|
41
|
+
const bNum = Number(bi);
|
|
42
|
+
const aIsNum = !Number.isNaN(aNum);
|
|
43
|
+
const bIsNum = !Number.isNaN(bNum);
|
|
44
|
+
|
|
45
|
+
// Numeric identifiers always have lower precedence than alphanumeric (semver §11.4.1)
|
|
46
|
+
if (aIsNum && !bIsNum) return -1;
|
|
47
|
+
if (!aIsNum && bIsNum) return 1;
|
|
48
|
+
|
|
49
|
+
if (aIsNum && bIsNum) {
|
|
50
|
+
if (aNum !== bNum) return aNum - bNum;
|
|
51
|
+
} else {
|
|
52
|
+
if (ai < bi) return -1;
|
|
53
|
+
if (ai > bi) return 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compares two valid semver strings.
|
|
62
|
+
* Returns a negative number if a < b, 0 if a === b, positive if a > b.
|
|
63
|
+
*
|
|
64
|
+
* Callers are expected to pass only strings that already passed
|
|
65
|
+
* `POLICY_SEMVER_PATTERN` validation (i.e. values stored in PolicyDefinition).
|
|
66
|
+
*/
|
|
67
|
+
export function comparePolicySemver(a: string, b: string): number {
|
|
68
|
+
const av = parseSemver(a);
|
|
69
|
+
const bv = parseSemver(b);
|
|
70
|
+
|
|
71
|
+
if (av.major !== bv.major) return av.major - bv.major;
|
|
72
|
+
if (av.minor !== bv.minor) return av.minor - bv.minor;
|
|
73
|
+
if (av.patch !== bv.patch) return av.patch - bv.patch;
|
|
74
|
+
|
|
75
|
+
// Release > pre-release (semver §11.3)
|
|
76
|
+
if (av.preRelease === null && bv.preRelease !== null) return 1;
|
|
77
|
+
if (av.preRelease !== null && bv.preRelease === null) return -1;
|
|
78
|
+
if (av.preRelease !== null && bv.preRelease !== null) {
|
|
79
|
+
return comparePreRelease(av.preRelease, bv.preRelease);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Result } from "../../result/result";
|
|
2
|
+
|
|
3
|
+
import type { PolicyContext } from "./gate-types";
|
|
4
|
+
import { ComputePayloadParsers } from "./parse-compute-payload";
|
|
5
|
+
import type { ComputeEvaluatorRegistry } from "./compute-evaluator-registry";
|
|
6
|
+
import type { ComputeOutcome, VersionedComputeEngine } from "./compute-types";
|
|
7
|
+
|
|
8
|
+
export class ComputeEngineRegistry {
|
|
9
|
+
private readonly engines = new Map<number, VersionedComputeEngine>();
|
|
10
|
+
|
|
11
|
+
register(engine: VersionedComputeEngine): void {
|
|
12
|
+
this.engines.set(engine.version, engine);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(version: number): VersionedComputeEngine | undefined {
|
|
16
|
+
return this.engines.get(version);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
evaluate(
|
|
20
|
+
version: number,
|
|
21
|
+
params: {
|
|
22
|
+
readonly policyKey: string;
|
|
23
|
+
readonly payloadSchemaVersion: number;
|
|
24
|
+
readonly payload: unknown;
|
|
25
|
+
readonly context: PolicyContext;
|
|
26
|
+
readonly evaluators: ComputeEvaluatorRegistry;
|
|
27
|
+
},
|
|
28
|
+
): Result<ComputeOutcome, string> {
|
|
29
|
+
const engine = this.get(version);
|
|
30
|
+
if (engine === undefined) {
|
|
31
|
+
return Result.err(
|
|
32
|
+
`No compute engine registered for version "${version}"`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const evaluator = params.evaluators.get(params.policyKey, version);
|
|
37
|
+
if (evaluator === undefined) {
|
|
38
|
+
return Result.err(
|
|
39
|
+
`No compute evaluator registered for policy key "${params.policyKey}" and version "${version}"`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parsedPayloadResult = ComputePayloadParsers.parse(
|
|
44
|
+
params.payloadSchemaVersion,
|
|
45
|
+
params.payload,
|
|
46
|
+
);
|
|
47
|
+
if (parsedPayloadResult.isErr()) {
|
|
48
|
+
return Result.err(parsedPayloadResult.errorOrNull()!);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return engine.evaluate(
|
|
52
|
+
parsedPayloadResult.getOrNull()!,
|
|
53
|
+
params.context,
|
|
54
|
+
evaluator,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComputeEvaluator,
|
|
3
|
+
ComputeEvaluatorRegistration,
|
|
4
|
+
} from "./compute-types";
|
|
5
|
+
|
|
6
|
+
export class ComputeEvaluatorRegistry {
|
|
7
|
+
private readonly evaluators = new Map<string, ComputeEvaluator>();
|
|
8
|
+
|
|
9
|
+
private static toRegistryKey(policyKey: string, version: number): string {
|
|
10
|
+
return `${policyKey}::${version}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
register(registration: ComputeEvaluatorRegistration): void {
|
|
14
|
+
this.evaluators.set(
|
|
15
|
+
ComputeEvaluatorRegistry.toRegistryKey(
|
|
16
|
+
registration.policyKey,
|
|
17
|
+
registration.version,
|
|
18
|
+
),
|
|
19
|
+
registration.evaluator,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(policyKey: string, version: number): ComputeEvaluator | undefined {
|
|
24
|
+
return this.evaluators.get(
|
|
25
|
+
ComputeEvaluatorRegistry.toRegistryKey(policyKey, version),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|