@cullet/erp-core 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +24 -24
  2. package/dist/gate-engine-registry.js.map +1 -1
  3. package/dist/gate-v1-payload.schema.js +5 -0
  4. package/dist/gate-v1-payload.schema.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/outcome.js.map +1 -1
  9. package/dist/policies/engines/v1/gate/index.d.ts +1 -0
  10. package/dist/policies/engines/v1/gate/index.js.map +1 -1
  11. package/dist/policy-service.js.map +1 -1
  12. package/dist/validation-code.js.map +1 -1
  13. package/dist/validation-error.js.map +1 -1
  14. package/dist/validation-field.js.map +1 -1
  15. package/meta.json +67 -67
  16. package/package.json +56 -56
  17. package/src/core/application/commands/command.ts +3 -3
  18. package/src/core/application/commands/requested-by.ts +60 -60
  19. package/src/core/application/index.ts +21 -21
  20. package/src/core/application/policy-error-mapper.ts +54 -54
  21. package/src/core/application/ports/index.ts +6 -6
  22. package/src/core/application/ports/logger.port.ts +4 -4
  23. package/src/core/application/ports/metrics.port.ts +3 -3
  24. package/src/core/application/ports/policy-port.ts +7 -7
  25. package/src/core/application/ports/repository.port.ts +3 -3
  26. package/src/core/application/ports/temporal-repository.port.ts +4 -4
  27. package/src/core/application/ports/tracer.port.ts +7 -7
  28. package/src/core/application/queries/query.ts +13 -13
  29. package/src/core/application/temporal/index.ts +7 -7
  30. package/src/core/application/temporal/temporal-context.ts +20 -20
  31. package/src/core/application/temporal/temporal-use-case.ts +22 -22
  32. package/src/core/application/use-case.ts +10 -10
  33. package/src/core/config/core-config.instance.ts +1 -1
  34. package/src/core/config/core-config.ts +59 -59
  35. package/src/core/config/index.ts +6 -6
  36. package/src/core/config/policy-reporter.ts +31 -31
  37. package/src/core/config/silent-policy-reporter.ts +1 -1
  38. package/src/core/domain/entity.ts +44 -44
  39. package/src/core/domain/rulesets/entity-ruleset.contracts.ts +6 -6
  40. package/src/core/domain/rulesets/ruleset-registry.ts +49 -49
  41. package/src/core/domain/rulesets/ruleset.contracts.ts +6 -6
  42. package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +5 -5
  43. package/src/core/domain/temporal/index.ts +11 -11
  44. package/src/core/domain/temporal/temporal-range.ts +16 -16
  45. package/src/core/domain/temporal/temporal-snapshot.ts +24 -24
  46. package/src/core/domain/temporal/transaction-time.ts +30 -30
  47. package/src/core/domain/temporal/valid-time.ts +29 -29
  48. package/src/core/domain/value-object.ts +16 -16
  49. package/src/core/errors/app-error.ts +64 -64
  50. package/src/core/errors/authentication-error.ts +134 -134
  51. package/src/core/errors/authorization-error.ts +154 -154
  52. package/src/core/errors/business-rule-violation-error.ts +15 -15
  53. package/src/core/errors/conflict-error.ts +202 -201
  54. package/src/core/errors/error-codes.ts +48 -48
  55. package/src/core/errors/idempotency-error.ts +324 -320
  56. package/src/core/errors/index.ts +50 -50
  57. package/src/core/errors/integration-error.ts +120 -120
  58. package/src/core/errors/legacy-incompatible-error.ts +13 -13
  59. package/src/core/errors/not-found-error.ts +14 -14
  60. package/src/core/errors/serialization-error.ts +167 -167
  61. package/src/core/errors/temporal-error.ts +109 -107
  62. package/src/core/errors/types.ts +35 -35
  63. package/src/core/errors/unexpected-error.ts +10 -10
  64. package/src/core/errors/utils/index.ts +2 -2
  65. package/src/core/errors/utils/json-safe.ts +53 -53
  66. package/src/core/errors/validation-error.ts +18 -18
  67. package/src/core/exceptions/business-rule-violation-exception.ts +6 -6
  68. package/src/core/exceptions/domain-exception.ts +5 -5
  69. package/src/core/exceptions/entity-not-found-exception.ts +6 -6
  70. package/src/core/exceptions/invalid-state-transition-exception.ts +8 -8
  71. package/src/core/exceptions/invariant-violation-exception.ts +3 -3
  72. package/src/core/exceptions/validation-code.ts +35 -29
  73. package/src/core/exceptions/validation-exception.ts +19 -19
  74. package/src/core/exceptions/validation-field.ts +20 -19
  75. package/src/core/index.ts +7 -7
  76. package/src/core/policies/asof/asof.ts +47 -47
  77. package/src/core/policies/catalog/catalog.instance.ts +8 -6
  78. package/src/core/policies/catalog/index.ts +4 -4
  79. package/src/core/policies/catalog/policy-catalog-entry.ts +199 -197
  80. package/src/core/policies/catalog/policy-catalog.ts +101 -101
  81. package/src/core/policies/catalog/policy-key.ts +51 -51
  82. package/src/core/policies/context/context-builder.ts +335 -313
  83. package/src/core/policies/context/context-registry.instance.ts +9 -9
  84. package/src/core/policies/context/context-registry.ts +61 -61
  85. package/src/core/policies/context/context-resolver.ts +12 -12
  86. package/src/core/policies/context/context-seed.ts +19 -15
  87. package/src/core/policies/context/index.ts +9 -9
  88. package/src/core/policies/context/path.ts +135 -128
  89. package/src/core/policies/defs/in-memory-policy-definition-repo.ts +56 -55
  90. package/src/core/policies/defs/index.ts +10 -10
  91. package/src/core/policies/defs/policy-definition-repository.ts +3 -3
  92. package/src/core/policies/defs/policy-definition.ts +143 -142
  93. package/src/core/policies/defs/policy-scope.ts +21 -19
  94. package/src/core/policies/defs/policy-semver.ts +51 -51
  95. package/src/core/policies/engines/compute-engine-registry.ts +43 -43
  96. package/src/core/policies/engines/compute-evaluator-registry.ts +20 -20
  97. package/src/core/policies/engines/compute-registry.ts +45 -45
  98. package/src/core/policies/engines/compute-types.ts +15 -15
  99. package/src/core/policies/engines/condition-evaluator-reporter.ts +17 -17
  100. package/src/core/policies/engines/gate-engine-registry.ts +27 -25
  101. package/src/core/policies/engines/gate-types.ts +28 -28
  102. package/src/core/policies/engines/index.ts +28 -28
  103. package/src/core/policies/engines/parse-compute-payload.ts +51 -45
  104. package/src/core/policies/engines/parse-gate-payload.ts +42 -42
  105. package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +39 -37
  106. package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +43 -41
  107. package/src/core/policies/engines/v1/compute/index.ts +6 -6
  108. package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +41 -39
  109. package/src/core/policies/engines/v1/condition-evaluator.ts +557 -504
  110. package/src/core/policies/engines/v1/condition-schema.ts +35 -35
  111. package/src/core/policies/engines/v1/condition-types.ts +21 -21
  112. package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +134 -130
  113. package/src/core/policies/engines/v1/gate/gate-types-v1.ts +5 -5
  114. package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +32 -30
  115. package/src/core/policies/engines/v1/gate/index.ts +4 -4
  116. package/src/core/policies/engines/v1/index.ts +8 -8
  117. package/src/core/policies/index.ts +72 -72
  118. package/src/core/policies/package/policy-package.ts +3 -3
  119. package/src/core/policies/policy-ids.ts +25 -25
  120. package/src/core/policies/resolver/policy-resolver.ts +34 -34
  121. package/src/core/policies/service/index.ts +6 -6
  122. package/src/core/policies/service/policy-evaluation-error.ts +141 -133
  123. package/src/core/policies/service/policy-service.ts +432 -419
  124. package/src/core/policies/utils/date.ts +3 -3
  125. package/src/core/policies/utils/hash.ts +61 -61
  126. package/src/core/result/outcome.ts +131 -126
  127. package/src/core/result/result.ts +130 -127
  128. package/src/core/shared/aggregate-version.ts +5 -5
  129. package/src/core/shared/hashing.ts +18 -18
  130. package/src/core/shared/immutable.ts +29 -29
  131. package/src/core/shared/temporal-guards.ts +9 -9
  132. package/src/core/versioning/domain-event-contracts.ts +40 -40
  133. package/src/core/versioning/version.ts +19 -19
  134. package/src/examples/rulesets/entity/order-creation-rules-v1.ts +18 -18
  135. package/src/examples/rulesets/entity/order-invariants-v1.ts +31 -29
  136. package/src/examples/rulesets/entity/order-invariants-v2.ts +12 -12
  137. package/src/examples/rulesets/entity/order.ts +127 -127
  138. package/src/examples/rulesets/value-object/cpf-rules-v1.ts +41 -39
  139. package/src/examples/rulesets/value-object/cpf-rules-v2.ts +14 -12
  140. package/src/examples/rulesets/value-object/cpf.ts +24 -24
  141. package/src/examples/rulesets/value-object/customer.ts +42 -39
  142. package/src/examples/rulesets/value-object/person-name-rules-v1.ts +27 -27
  143. package/src/examples/rulesets/value-object/person-name-rules-v2.ts +13 -13
  144. package/src/examples/rulesets/value-object/person-name.ts +29 -26
  145. package/src/index.ts +2 -2
  146. package/src/version.ts +1 -1
@@ -1,8 +1,8 @@
1
1
  import { Result } from "../../result/result";
2
2
 
3
3
  import {
4
- ContextResolverRegistry,
5
- registerNamespacedContextResolversIn,
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
- namespace: string,
19
- resolvers: readonly ContextValueResolver[],
18
+ namespace: string,
19
+ resolvers: readonly ContextValueResolver[],
20
20
  ): Result<ContextResolverRegistry, string> {
21
- return registerNamespacedContextResolversIn(
22
- contextResolverRegistry,
23
- namespace,
24
- resolvers,
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
- readonly namespace?: string;
6
+ readonly namespace?: string;
7
7
  }
8
8
 
9
9
  interface RegisteredContextResolver {
10
- readonly namespace: string;
11
- readonly resolver: ContextValueResolver;
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
- const normalized = namespace?.trim();
17
+ const normalized = namespace?.trim();
18
18
 
19
- if (normalized === undefined || normalized.length === 0) {
20
- return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
21
- }
19
+ if (normalized === undefined || normalized.length === 0) {
20
+ return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
21
+ }
22
22
 
23
- return normalized;
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
- 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
- );
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
- this.resolvers.set(resolver.path, {
46
- namespace,
47
- resolver,
48
- });
49
-
50
- return Result.ok(undefined);
51
- }
52
-
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
- }
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
- return Result.ok(undefined);
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
- has(path: string): boolean {
72
- return this.resolvers.has(path);
73
- }
71
+ has(path: string): boolean {
72
+ return this.resolvers.has(path);
73
+ }
74
74
 
75
- getNamespace(path: string): string | undefined {
76
- return this.resolvers.get(path)?.namespace;
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
- registry: ContextResolverRegistry,
82
- namespace: string,
83
- resolvers: readonly ContextValueResolver[],
81
+ registry: ContextResolverRegistry,
82
+ namespace: string,
83
+ resolvers: readonly ContextValueResolver[],
84
84
  ): Result<ContextResolverRegistry, string> {
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);
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
- readonly maxAttempts?: number;
7
- readonly initialDelayMs?: number;
8
- readonly maxDelayMs?: number;
9
- readonly backoffMultiplier?: number;
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
- readonly failureThreshold?: number;
14
- readonly cooldownMs?: number;
13
+ readonly failureThreshold?: number;
14
+ readonly cooldownMs?: number;
15
15
  }
16
16
 
17
17
  export interface ContextResolverResilienceOptions {
18
- readonly timeoutMs?: number;
19
- readonly retry?: ContextResolverRetryOptions | false;
20
- readonly circuitBreaker?: ContextResolverCircuitBreakerOptions | false;
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
- readonly path: string;
29
- readonly resilience?: ContextResolverResilienceOptions;
30
- resolve(seed: ContextSeed): Promise<Result<unknown, string>>;
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
- readonly tenantId: TenantId;
12
- readonly schoolId: SchoolId;
13
- readonly fields: Record<string, unknown>;
11
+ readonly tenantId: TenantId;
12
+ readonly schoolId: SchoolId;
13
+ readonly fields: Record<string, unknown>;
14
14
  }
15
15
 
16
16
  export class ContextSeedValidator {
17
- private static isBlankSeedId(value: string): boolean {
18
- return value.trim().length === 0;
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
- if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) {
27
- return Result.err("ContextSeed schoolId must be a non-empty string");
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
- return Result.ok(seed);
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
- ContextResolverRegistry,
4
- DEFAULT_CONTEXT_RESOLVER_NAMESPACE,
5
- registerNamespacedContextResolversIn,
3
+ ContextResolverRegistry,
4
+ DEFAULT_CONTEXT_RESOLVER_NAMESPACE,
5
+ registerNamespacedContextResolversIn,
6
6
  } from "./context-registry";
7
7
  export {
8
- contextResolverRegistry,
9
- registerNamespacedContextResolvers,
8
+ contextResolverRegistry,
9
+ registerNamespacedContextResolvers,
10
10
  } from "./context-registry.instance";
11
11
  export type { ContextResolverRegistrationOptions } from "./context-registry";
12
12
  export type {
13
- ContextResolverCircuitBreakerOptions,
14
- ContextResolverResilienceOptions,
15
- ContextResolverRetryOptions,
16
- ContextValueResolver,
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
- 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()!);
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
- const segments = segmentsResult.getOrNull()!;
31
- let current: unknown = obj;
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
- 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
- }
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
- 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
- }
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
- 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;
74
+ return Result.ok(segments);
82
75
  }
83
76
 
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");
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
- const { found, value } = resolvedPath.getOrNull()!;
98
- if (!found) {
99
- return Result.err("absent");
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
- 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
- }
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
- 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()!);
115
+ return resolvedPath.getOrNull()!.found;
122
116
  }
123
117
 
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;
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
  }