@cullet/erp-core 1.0.2 → 1.0.3
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/README.md +24 -24
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/outcome.js.map +1 -1
- package/dist/policies/engines/v1/gate/index.js.map +1 -1
- package/dist/policy-service.js.map +1 -1
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-field.js.map +1 -1
- package/meta.json +67 -67
- package/package.json +56 -56
- package/src/core/application/commands/command.ts +3 -3
- package/src/core/application/commands/requested-by.ts +60 -60
- package/src/core/application/index.ts +21 -21
- package/src/core/application/policy-error-mapper.ts +54 -54
- package/src/core/application/ports/index.ts +6 -6
- package/src/core/application/ports/logger.port.ts +4 -4
- package/src/core/application/ports/metrics.port.ts +3 -3
- package/src/core/application/ports/policy-port.ts +7 -7
- package/src/core/application/ports/repository.port.ts +3 -3
- package/src/core/application/ports/temporal-repository.port.ts +4 -4
- package/src/core/application/ports/tracer.port.ts +7 -7
- package/src/core/application/queries/query.ts +13 -13
- package/src/core/application/temporal/index.ts +7 -7
- package/src/core/application/temporal/temporal-context.ts +20 -20
- package/src/core/application/temporal/temporal-use-case.ts +22 -22
- package/src/core/application/use-case.ts +10 -10
- package/src/core/config/core-config.instance.ts +1 -1
- package/src/core/config/core-config.ts +59 -59
- package/src/core/config/index.ts +6 -6
- package/src/core/config/policy-reporter.ts +31 -31
- package/src/core/config/silent-policy-reporter.ts +1 -1
- package/src/core/domain/entity.ts +44 -44
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/ruleset-registry.ts +49 -49
- package/src/core/domain/rulesets/ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +5 -5
- package/src/core/domain/temporal/index.ts +11 -11
- package/src/core/domain/temporal/temporal-range.ts +16 -16
- package/src/core/domain/temporal/temporal-snapshot.ts +24 -24
- package/src/core/domain/temporal/transaction-time.ts +30 -30
- package/src/core/domain/temporal/valid-time.ts +29 -29
- package/src/core/domain/value-object.ts +16 -16
- package/src/core/errors/app-error.ts +64 -64
- package/src/core/errors/authentication-error.ts +134 -134
- package/src/core/errors/authorization-error.ts +154 -154
- package/src/core/errors/business-rule-violation-error.ts +15 -15
- package/src/core/errors/conflict-error.ts +202 -201
- package/src/core/errors/error-codes.ts +48 -48
- package/src/core/errors/idempotency-error.ts +324 -320
- package/src/core/errors/index.ts +50 -50
- package/src/core/errors/integration-error.ts +120 -120
- package/src/core/errors/legacy-incompatible-error.ts +13 -13
- package/src/core/errors/not-found-error.ts +14 -14
- package/src/core/errors/serialization-error.ts +167 -167
- package/src/core/errors/temporal-error.ts +109 -107
- package/src/core/errors/types.ts +35 -35
- package/src/core/errors/unexpected-error.ts +10 -10
- package/src/core/errors/utils/index.ts +2 -2
- package/src/core/errors/utils/json-safe.ts +53 -53
- package/src/core/errors/validation-error.ts +18 -18
- package/src/core/exceptions/business-rule-violation-exception.ts +6 -6
- package/src/core/exceptions/domain-exception.ts +5 -5
- package/src/core/exceptions/entity-not-found-exception.ts +6 -6
- package/src/core/exceptions/invalid-state-transition-exception.ts +8 -8
- package/src/core/exceptions/invariant-violation-exception.ts +3 -3
- package/src/core/exceptions/validation-code.ts +35 -29
- package/src/core/exceptions/validation-exception.ts +19 -19
- package/src/core/exceptions/validation-field.ts +20 -19
- package/src/core/index.ts +7 -7
- package/src/core/policies/asof/asof.ts +47 -47
- package/src/core/policies/catalog/catalog.instance.ts +8 -6
- package/src/core/policies/catalog/index.ts +4 -4
- package/src/core/policies/catalog/policy-catalog-entry.ts +199 -197
- package/src/core/policies/catalog/policy-catalog.ts +101 -101
- package/src/core/policies/catalog/policy-key.ts +51 -51
- package/src/core/policies/context/context-builder.ts +335 -313
- package/src/core/policies/context/context-registry.instance.ts +9 -9
- package/src/core/policies/context/context-registry.ts +61 -61
- package/src/core/policies/context/context-resolver.ts +12 -12
- package/src/core/policies/context/context-seed.ts +19 -15
- package/src/core/policies/context/index.ts +9 -9
- package/src/core/policies/context/path.ts +135 -128
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +56 -55
- package/src/core/policies/defs/index.ts +10 -10
- package/src/core/policies/defs/policy-definition-repository.ts +3 -3
- package/src/core/policies/defs/policy-definition.ts +143 -142
- package/src/core/policies/defs/policy-scope.ts +21 -19
- package/src/core/policies/defs/policy-semver.ts +51 -51
- package/src/core/policies/engines/compute-engine-registry.ts +43 -43
- package/src/core/policies/engines/compute-evaluator-registry.ts +20 -20
- package/src/core/policies/engines/compute-registry.ts +45 -45
- package/src/core/policies/engines/compute-types.ts +15 -15
- package/src/core/policies/engines/condition-evaluator-reporter.ts +17 -17
- package/src/core/policies/engines/gate-engine-registry.ts +27 -25
- package/src/core/policies/engines/gate-types.ts +28 -28
- package/src/core/policies/engines/index.ts +28 -28
- package/src/core/policies/engines/parse-compute-payload.ts +51 -45
- package/src/core/policies/engines/parse-gate-payload.ts +42 -42
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +39 -37
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +43 -41
- package/src/core/policies/engines/v1/compute/index.ts +6 -6
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +41 -39
- package/src/core/policies/engines/v1/condition-evaluator.ts +547 -505
- package/src/core/policies/engines/v1/condition-schema.ts +35 -35
- package/src/core/policies/engines/v1/condition-types.ts +21 -21
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +134 -130
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +5 -5
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +32 -30
- package/src/core/policies/engines/v1/gate/index.ts +4 -4
- package/src/core/policies/engines/v1/index.ts +8 -8
- package/src/core/policies/index.ts +72 -72
- package/src/core/policies/package/policy-package.ts +3 -3
- package/src/core/policies/policy-ids.ts +25 -25
- package/src/core/policies/resolver/policy-resolver.ts +34 -34
- package/src/core/policies/service/index.ts +6 -6
- package/src/core/policies/service/policy-evaluation-error.ts +141 -133
- package/src/core/policies/service/policy-service.ts +432 -419
- package/src/core/policies/utils/date.ts +3 -3
- package/src/core/policies/utils/hash.ts +61 -61
- package/src/core/result/outcome.ts +131 -126
- package/src/core/result/result.ts +130 -127
- package/src/core/shared/aggregate-version.ts +5 -5
- package/src/core/shared/hashing.ts +18 -18
- package/src/core/shared/immutable.ts +29 -29
- package/src/core/shared/temporal-guards.ts +9 -9
- package/src/core/versioning/domain-event-contracts.ts +40 -40
- package/src/core/versioning/version.ts +19 -19
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +18 -18
- package/src/examples/rulesets/entity/order-invariants-v1.ts +31 -29
- package/src/examples/rulesets/entity/order-invariants-v2.ts +12 -12
- package/src/examples/rulesets/entity/order.ts +127 -127
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +41 -39
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +14 -12
- package/src/examples/rulesets/value-object/cpf.ts +24 -24
- package/src/examples/rulesets/value-object/customer.ts +42 -39
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +27 -27
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +13 -13
- package/src/examples/rulesets/value-object/person-name.ts +29 -26
- package/src/index.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Result } from "../../result/result";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
ContextResolverRegistry,
|
|
5
|
+
registerNamespacedContextResolversIn,
|
|
6
6
|
} from "./context-registry";
|
|
7
7
|
|
|
8
8
|
import type { ContextValueResolver } from "./context-resolver";
|
|
@@ -15,12 +15,12 @@ import type { ContextValueResolver } from "./context-resolver";
|
|
|
15
15
|
export const contextResolverRegistry = new ContextResolverRegistry();
|
|
16
16
|
|
|
17
17
|
export function registerNamespacedContextResolvers(
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
namespace: string,
|
|
19
|
+
resolvers: readonly ContextValueResolver[],
|
|
20
20
|
): Result<ContextResolverRegistry, string> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
return registerNamespacedContextResolversIn(
|
|
22
|
+
contextResolverRegistry,
|
|
23
|
+
namespace,
|
|
24
|
+
resolvers,
|
|
25
|
+
);
|
|
26
26
|
}
|
|
@@ -3,93 +3,93 @@ import { Result } from "../../result/result";
|
|
|
3
3
|
import type { ContextValueResolver } from "./context-resolver";
|
|
4
4
|
|
|
5
5
|
export interface ContextResolverRegistrationOptions {
|
|
6
|
-
|
|
6
|
+
readonly namespace?: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface RegisteredContextResolver {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
readonly namespace: string;
|
|
11
|
+
readonly resolver: ContextValueResolver;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const DEFAULT_CONTEXT_RESOLVER_NAMESPACE = "default";
|
|
15
15
|
|
|
16
16
|
function normalizeNamespace(namespace?: string): string {
|
|
17
|
-
|
|
17
|
+
const normalized = namespace?.trim();
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
if (normalized === undefined || normalized.length === 0) {
|
|
20
|
+
return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
return normalized;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Registry of context value resolvers, keyed by path.
|
|
28
28
|
*/
|
|
29
29
|
export class ContextResolverRegistry {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
private readonly resolvers = new Map<string, RegisteredContextResolver>();
|
|
31
|
+
|
|
32
|
+
register(
|
|
33
|
+
resolver: ContextValueResolver,
|
|
34
|
+
options: ContextResolverRegistrationOptions = {},
|
|
35
|
+
): Result<void, string> {
|
|
36
|
+
const namespace = normalizeNamespace(options.namespace);
|
|
37
|
+
const existing = this.resolvers.get(resolver.path);
|
|
38
|
+
|
|
39
|
+
if (existing !== undefined && existing.namespace !== namespace) {
|
|
40
|
+
return Result.err(
|
|
41
|
+
`Resolver path "${resolver.path}" is already registered by namespace "${existing.namespace}" and cannot be replaced by "${namespace}".`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.resolvers.set(resolver.path, {
|
|
46
|
+
namespace,
|
|
47
|
+
resolver,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return Result.ok(undefined);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const r of resolvers) {
|
|
58
|
-
const registrationResult = this.register(r, options);
|
|
59
|
-
if (registrationResult.isErr()) {
|
|
60
|
-
return registrationResult;
|
|
61
|
-
}
|
|
53
|
+
registerAll(
|
|
54
|
+
resolvers: readonly ContextValueResolver[],
|
|
55
|
+
options: ContextResolverRegistrationOptions = {},
|
|
56
|
+
): Result<void, string> {
|
|
57
|
+
for (const r of resolvers) {
|
|
58
|
+
const registrationResult = this.register(r, options);
|
|
59
|
+
if (registrationResult.isErr()) {
|
|
60
|
+
return registrationResult;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Result.ok(undefined);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
get(path: string): ContextValueResolver | undefined {
|
|
68
|
-
return this.resolvers.get(path)?.resolver;
|
|
69
|
-
}
|
|
67
|
+
get(path: string): ContextValueResolver | undefined {
|
|
68
|
+
return this.resolvers.get(path)?.resolver;
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
has(path: string): boolean {
|
|
72
|
+
return this.resolvers.has(path);
|
|
73
|
+
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
getNamespace(path: string): string | undefined {
|
|
76
|
+
return this.resolvers.get(path)?.namespace;
|
|
77
|
+
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function registerNamespacedContextResolversIn(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
registry: ContextResolverRegistry,
|
|
82
|
+
namespace: string,
|
|
83
|
+
resolvers: readonly ContextValueResolver[],
|
|
84
84
|
): Result<ContextResolverRegistry, string> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
const registrationResult = registry.registerAll(resolvers, {
|
|
86
|
+
namespace,
|
|
87
|
+
});
|
|
88
|
+
if (registrationResult.isErr()) {
|
|
89
|
+
return Result.err(registrationResult.errorOrNull()!);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return Result.ok(registry);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export { DEFAULT_CONTEXT_RESOLVER_NAMESPACE };
|
|
@@ -3,21 +3,21 @@ import { Result } from "../../result/result";
|
|
|
3
3
|
import type { ContextSeed } from "./context-seed";
|
|
4
4
|
|
|
5
5
|
export interface ContextResolverRetryOptions {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
readonly maxAttempts?: number;
|
|
7
|
+
readonly initialDelayMs?: number;
|
|
8
|
+
readonly maxDelayMs?: number;
|
|
9
|
+
readonly backoffMultiplier?: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface ContextResolverCircuitBreakerOptions {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
readonly failureThreshold?: number;
|
|
14
|
+
readonly cooldownMs?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface ContextResolverResilienceOptions {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
readonly retry?: ContextResolverRetryOptions | false;
|
|
20
|
+
readonly circuitBreaker?: ContextResolverCircuitBreakerOptions | false;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -25,7 +25,7 @@ export interface ContextResolverResilienceOptions {
|
|
|
25
25
|
* Designed as async to allow future DB/API lookups without breaking the interface.
|
|
26
26
|
*/
|
|
27
27
|
export interface ContextValueResolver {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly resilience?: ContextResolverResilienceOptions;
|
|
30
|
+
resolve(seed: ContextSeed): Promise<Result<unknown, string>>;
|
|
31
31
|
}
|
|
@@ -8,25 +8,29 @@ import type { SchoolId, TenantId } from "../policy-ids";
|
|
|
8
8
|
* Each resolver is responsible for validating and extracting what it needs from fields.
|
|
9
9
|
*/
|
|
10
10
|
export interface ContextSeed {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
readonly tenantId: TenantId;
|
|
12
|
+
readonly schoolId: SchoolId;
|
|
13
|
+
readonly fields: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export class ContextSeedValidator {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static validate(seed: ContextSeed): Result<ContextSeed, string> {
|
|
22
|
-
if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) {
|
|
23
|
-
return Result.err("ContextSeed tenantId must be a non-empty string");
|
|
17
|
+
private static isBlankSeedId(value: string): boolean {
|
|
18
|
+
return value.trim().length === 0;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
static validate(seed: ContextSeed): Result<ContextSeed, string> {
|
|
22
|
+
if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) {
|
|
23
|
+
return Result.err(
|
|
24
|
+
"ContextSeed tenantId must be a non-empty string",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) {
|
|
29
|
+
return Result.err(
|
|
30
|
+
"ContextSeed schoolId must be a non-empty string",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return Result.ok(seed);
|
|
35
|
+
}
|
|
32
36
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
export { PolicyContextBuilder } from "./context-builder";
|
|
2
2
|
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
ContextResolverRegistry,
|
|
4
|
+
DEFAULT_CONTEXT_RESOLVER_NAMESPACE,
|
|
5
|
+
registerNamespacedContextResolversIn,
|
|
6
6
|
} from "./context-registry";
|
|
7
7
|
export {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
contextResolverRegistry,
|
|
9
|
+
registerNamespacedContextResolvers,
|
|
10
10
|
} from "./context-registry.instance";
|
|
11
11
|
export type { ContextResolverRegistrationOptions } from "./context-registry";
|
|
12
12
|
export type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
ContextResolverCircuitBreakerOptions,
|
|
14
|
+
ContextResolverResilienceOptions,
|
|
15
|
+
ContextResolverRetryOptions,
|
|
16
|
+
ContextValueResolver,
|
|
17
17
|
} from "./context-resolver";
|
|
18
18
|
export type { PolicyContextBuilderOptions } from "./context-builder";
|
|
19
19
|
export { ContextSeedValidator } from "./context-seed";
|
|
@@ -6,147 +6,154 @@
|
|
|
6
6
|
import { Result } from "../../result/result";
|
|
7
7
|
|
|
8
8
|
export class PolicyContextPath {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 });
|
|
28
51
|
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
typeof current !== "object"
|
|
38
|
-
) {
|
|
39
|
-
return Result.ok({ found: false, value: undefined });
|
|
40
|
-
}
|
|
60
|
+
const segments = path.split(".");
|
|
61
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
62
|
+
return Result.err(`Path "${path}" contains an empty segment`);
|
|
63
|
+
}
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
value: unknown,
|
|
79
|
-
): value is Record<string, unknown> {
|
|
80
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
81
|
-
return false;
|
|
74
|
+
return Result.ok(segments);
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
private static isPlainRecord(
|
|
78
|
+
value: unknown,
|
|
79
|
+
): value is Record<string, unknown> {
|
|
80
|
+
if (
|
|
81
|
+
typeof value !== "object" ||
|
|
82
|
+
value === null ||
|
|
83
|
+
Array.isArray(value)
|
|
84
|
+
) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const prototype = Object.getPrototypeOf(value);
|
|
89
|
+
return prototype === Object.prototype || prototype === null;
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
static getOrAbsent(
|
|
93
|
+
obj: Record<string, unknown>,
|
|
94
|
+
path: string,
|
|
95
|
+
): Result<unknown, "absent"> {
|
|
96
|
+
const resolvedPath = PolicyContextPath.resolve(obj, path);
|
|
97
|
+
if (resolvedPath.isErr()) {
|
|
98
|
+
return Result.err("absent");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { found, value } = resolvedPath.getOrNull()!;
|
|
102
|
+
if (!found) {
|
|
103
|
+
return Result.err("absent");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Result.ok(value);
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (resolvedPath.isErr()) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
109
|
+
static has(obj: Record<string, unknown>, path: string): boolean {
|
|
110
|
+
const resolvedPath = PolicyContextPath.resolve(obj, path);
|
|
111
|
+
if (resolvedPath.isErr()) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
110
114
|
|
|
111
|
-
|
|
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()!);
|
|
115
|
+
return resolvedPath.getOrNull()!.found;
|
|
122
116
|
}
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
118
|
+
static set(
|
|
119
|
+
obj: Record<string, unknown>,
|
|
120
|
+
path: string,
|
|
121
|
+
value: unknown,
|
|
122
|
+
): Result<void, string> {
|
|
123
|
+
const segmentsResult = PolicyContextPath.parseSegments(path);
|
|
124
|
+
if (segmentsResult.isErr()) {
|
|
125
|
+
return Result.err(segmentsResult.errorOrNull()!);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const segments = segmentsResult.getOrNull()!;
|
|
129
|
+
let current: Record<string, unknown> = obj;
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
132
|
+
const seg = segments[i];
|
|
133
|
+
const next = current[seg];
|
|
134
|
+
|
|
135
|
+
if (next === undefined) {
|
|
136
|
+
const container = Object.create(null) as Record<
|
|
137
|
+
string,
|
|
138
|
+
unknown
|
|
139
|
+
>;
|
|
140
|
+
current[seg] = container;
|
|
141
|
+
current = container;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!PolicyContextPath.isPlainRecord(next)) {
|
|
146
|
+
return Result.err(
|
|
147
|
+
`Cannot create nested path "${path}" because "${segments
|
|
148
|
+
.slice(0, i + 1)
|
|
149
|
+
.join(".")}" already contains a non-plain object`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
current = next;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
current[segments[segments.length - 1]] = value;
|
|
157
|
+
return Result.ok(undefined);
|
|
147
158
|
}
|
|
148
|
-
|
|
149
|
-
current[segments[segments.length - 1]] = value;
|
|
150
|
-
return Result.ok(undefined);
|
|
151
|
-
}
|
|
152
159
|
}
|