@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,53 @@
|
|
|
1
|
+
import { UseCase } from "../use-case";
|
|
2
|
+
import { version } from "../../versioning/version";
|
|
3
|
+
import type { AppError } from "../../errors";
|
|
4
|
+
import type { Result } from "../../result/result";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Paginated result of a list query.
|
|
8
|
+
* Use as `Output` when the query returns a collection with pagination metadata.
|
|
9
|
+
*/
|
|
10
|
+
interface Page<T> {
|
|
11
|
+
readonly items: readonly T[];
|
|
12
|
+
readonly total: number;
|
|
13
|
+
readonly page: number;
|
|
14
|
+
readonly pageSize: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cache strategy declared by the query type.
|
|
19
|
+
* Infrastructure reads this value to decide whether and how to cache the result.
|
|
20
|
+
*/
|
|
21
|
+
type CacheStrategy =
|
|
22
|
+
| { readonly kind: "NO_CACHE" }
|
|
23
|
+
| { readonly kind: "TIME_TO_LIVE"; readonly ttlMs: number }
|
|
24
|
+
| { readonly kind: "STALE_WHILE_REVALIDATE"; readonly ttlMs: number };
|
|
25
|
+
|
|
26
|
+
type QueryOutput = object | string | number | boolean | bigint | symbol | null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Base for use cases that **only read state**, with no side effects, following CQS.
|
|
30
|
+
*
|
|
31
|
+
* - `Data extends QueryOutput` prevents a query from declaring a `void` or
|
|
32
|
+
* `undefined` success payload, while still allowing primitive projections
|
|
33
|
+
* and `null` when that makes sense for the read. Failures stay explicit in
|
|
34
|
+
* `Result<T, E>`.
|
|
35
|
+
* - Override `cacheStrategy()` to declare how infrastructure should cache the
|
|
36
|
+
* result. By default, no cache is applied.
|
|
37
|
+
*
|
|
38
|
+
* A Query must never mutate persisted data — its execution must be idempotent
|
|
39
|
+
* and safe to call multiple times with the same input.
|
|
40
|
+
*/
|
|
41
|
+
@version("1.0")
|
|
42
|
+
abstract class Query<
|
|
43
|
+
Input = void,
|
|
44
|
+
Data extends QueryOutput = never,
|
|
45
|
+
Failure = AppError,
|
|
46
|
+
> extends UseCase<Input, Result<Data, Failure>> {
|
|
47
|
+
protected cacheStrategy(): CacheStrategy {
|
|
48
|
+
return { kind: "NO_CACHE" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type { CacheStrategy, Page, QueryOutput };
|
|
53
|
+
export { Query };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
assertTemporalContext,
|
|
3
|
+
createTemporalContext,
|
|
4
|
+
type CreateTemporalContextInput,
|
|
5
|
+
type TemporalContext,
|
|
6
|
+
} from "./temporal-context";
|
|
7
|
+
export {
|
|
8
|
+
type TemporalUseCaseInput,
|
|
9
|
+
type TemporalizedContextSeed,
|
|
10
|
+
TemporalUseCase,
|
|
11
|
+
} from "./temporal-use-case";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { makeImmutable } from "../../shared/immutable";
|
|
2
|
+
import { assertValidDate } from "../../shared/temporal-guards";
|
|
3
|
+
|
|
4
|
+
interface TemporalContext {
|
|
5
|
+
readonly asOf: Date;
|
|
6
|
+
readonly requestedAt: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CreateTemporalContextInput {
|
|
10
|
+
readonly asOf?: Date;
|
|
11
|
+
readonly requestedAt?: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertTemporalContext(
|
|
15
|
+
temporalContext: TemporalContext,
|
|
16
|
+
fieldName: string = "temporalContext",
|
|
17
|
+
): void {
|
|
18
|
+
assertValidDate(`${fieldName}.asOf`, temporalContext.asOf);
|
|
19
|
+
assertValidDate(`${fieldName}.requestedAt`, temporalContext.requestedAt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createTemporalContext(
|
|
23
|
+
input: CreateTemporalContextInput = {},
|
|
24
|
+
): TemporalContext {
|
|
25
|
+
const requestedAt = input.requestedAt ?? new Date();
|
|
26
|
+
const asOf = input.asOf ?? requestedAt;
|
|
27
|
+
|
|
28
|
+
assertTemporalContext({ asOf, requestedAt });
|
|
29
|
+
|
|
30
|
+
return makeImmutable({
|
|
31
|
+
asOf,
|
|
32
|
+
requestedAt,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
assertTemporalContext,
|
|
38
|
+
createTemporalContext,
|
|
39
|
+
type CreateTemporalContextInput,
|
|
40
|
+
type TemporalContext,
|
|
41
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { UseCase } from "../use-case";
|
|
2
|
+
import type { ContextSeed } from "../../policies";
|
|
3
|
+
import type { Result } from "../../result/result";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createTemporalContext,
|
|
7
|
+
type TemporalContext,
|
|
8
|
+
} from "./temporal-context";
|
|
9
|
+
|
|
10
|
+
interface TemporalUseCaseInput {
|
|
11
|
+
readonly temporalContext?: TemporalContext;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type TemporalizedContextSeed<TSeed extends ContextSeed> = TSeed;
|
|
15
|
+
|
|
16
|
+
abstract class TemporalUseCase<
|
|
17
|
+
Input extends object,
|
|
18
|
+
Output extends Result<unknown, unknown>,
|
|
19
|
+
> extends UseCase<Input & TemporalUseCaseInput, Output> {
|
|
20
|
+
protected resolveTemporalContext(
|
|
21
|
+
input: Input & TemporalUseCaseInput,
|
|
22
|
+
): TemporalContext {
|
|
23
|
+
return createTemporalContext(input.temporalContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected buildPolicySeed<TSeed extends ContextSeed>(
|
|
27
|
+
seed: TSeed,
|
|
28
|
+
temporalContext: TemporalContext,
|
|
29
|
+
): TemporalizedContextSeed<TSeed> {
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
...seed,
|
|
32
|
+
fields: {
|
|
33
|
+
...seed.fields,
|
|
34
|
+
now: new Date(temporalContext.requestedAt.getTime()),
|
|
35
|
+
},
|
|
36
|
+
}) as TSeed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type { TemporalizedContextSeed, TemporalUseCaseInput };
|
|
41
|
+
export { TemporalUseCase };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ContractVersion, version } from "../versioning/version";
|
|
2
|
+
import type { Result } from "../result/result";
|
|
3
|
+
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
|
|
6
|
+
@version("1.0")
|
|
7
|
+
abstract class UseCase<
|
|
8
|
+
Input = void,
|
|
9
|
+
Output extends Result<unknown, unknown> = Result<void, never>,
|
|
10
|
+
> {
|
|
11
|
+
public static readonly CONTRACT_VERSION: ContractVersion;
|
|
12
|
+
|
|
13
|
+
public get contractVersion(): ContractVersion {
|
|
14
|
+
return UseCase.CONTRACT_VERSION;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async run(input: Input): Promise<Output> {
|
|
18
|
+
return await this.execute(input);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected abstract execute(input: Input): MaybePromise<Output>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { type MaybePromise, UseCase };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CoreConfig } from "./core-config";
|
|
2
|
+
import { SilentPolicyReporter } from "./silent-policy-reporter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared instance for the application composition root.
|
|
6
|
+
* Use `new CoreConfig()` for isolated runtimes, tests, or multi-tenant hosts.
|
|
7
|
+
*
|
|
8
|
+
* The default reporter is silent so the core has no runtime logging dependency.
|
|
9
|
+
* Hosts can swap in their own `PolicyReporter` via
|
|
10
|
+
* `coreConfig.configure({ observability: { reporter } })`.
|
|
11
|
+
*/
|
|
12
|
+
export const coreConfig = new CoreConfig({
|
|
13
|
+
observability: { reporter: new SilentPolicyReporter() },
|
|
14
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConditionEvaluationOptions,
|
|
3
|
+
ConditionEvaluatorReporter,
|
|
4
|
+
} from "../policies/engines/condition-evaluator-reporter";
|
|
5
|
+
import type { PolicyEvent, PolicyReporter } from "./policy-reporter";
|
|
6
|
+
import { SilentPolicyReporter } from "./silent-policy-reporter";
|
|
7
|
+
|
|
8
|
+
export interface CoreObservabilityConfig {
|
|
9
|
+
readonly reporter?: PolicyReporter;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CoreConfigOptions {
|
|
13
|
+
readonly observability?: CoreObservabilityConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reportSafely(
|
|
17
|
+
policyReporter: PolicyReporter,
|
|
18
|
+
event: PolicyEvent,
|
|
19
|
+
): void {
|
|
20
|
+
try {
|
|
21
|
+
policyReporter.report(event);
|
|
22
|
+
} catch {
|
|
23
|
+
// Falhas de telemetria nao podem interromper o fluxo de dominio.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Centraliza a configuracao pura do core sem depender de implementacoes
|
|
29
|
+
* concretas de logging, tracing ou report.
|
|
30
|
+
*/
|
|
31
|
+
export class CoreConfig {
|
|
32
|
+
readonly #initialReporter: PolicyReporter;
|
|
33
|
+
#currentReporter: PolicyReporter;
|
|
34
|
+
|
|
35
|
+
constructor(options: CoreConfigOptions = {}) {
|
|
36
|
+
const reporter =
|
|
37
|
+
options.observability?.reporter ?? new SilentPolicyReporter();
|
|
38
|
+
this.#initialReporter = reporter;
|
|
39
|
+
this.#currentReporter = reporter;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
configure(options: CoreConfigOptions): this {
|
|
43
|
+
const reporter = options.observability?.reporter;
|
|
44
|
+
if (reporter) {
|
|
45
|
+
this.#currentReporter = reporter;
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Restores the reporter captured at construction time.
|
|
51
|
+
reset(): this {
|
|
52
|
+
this.#currentReporter = this.#initialReporter;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getPolicyReporter(): PolicyReporter {
|
|
57
|
+
return this.#currentReporter;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getConditionEvaluationOptions(
|
|
61
|
+
engineVersion: number,
|
|
62
|
+
): ConditionEvaluationOptions {
|
|
63
|
+
return {
|
|
64
|
+
reporter: this.#bridgeToConditionReporter(this.#currentReporter),
|
|
65
|
+
engineVersion,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#bridgeToConditionReporter(
|
|
70
|
+
policyReporter: PolicyReporter,
|
|
71
|
+
): ConditionEvaluatorReporter {
|
|
72
|
+
return {
|
|
73
|
+
warn(report) {
|
|
74
|
+
reportSafely(policyReporter, {
|
|
75
|
+
kind: "condition-eval",
|
|
76
|
+
...report,
|
|
77
|
+
} satisfies PolicyEvent);
|
|
78
|
+
},
|
|
79
|
+
error(report) {
|
|
80
|
+
reportSafely(policyReporter, {
|
|
81
|
+
kind: "condition-eval",
|
|
82
|
+
...report,
|
|
83
|
+
} satisfies PolicyEvent);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { CoreConfigOptions, CoreObservabilityConfig } from "./core-config";
|
|
2
|
+
export { CoreConfig } from "./core-config";
|
|
3
|
+
export { coreConfig } from "./core-config.instance";
|
|
4
|
+
export type {
|
|
5
|
+
PolicyEvent,
|
|
6
|
+
PolicyReporter,
|
|
7
|
+
ConditionEvalEvent,
|
|
8
|
+
PolicyResolutionEvent,
|
|
9
|
+
PolicyEvaluationFailedEvent,
|
|
10
|
+
PolicyEvaluationCompletedEvent,
|
|
11
|
+
} from "./policy-reporter";
|
|
12
|
+
export { SilentPolicyReporter } from "./silent-policy-reporter";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ConditionEvaluationReport } from "../policies/engines/condition-evaluator-reporter";
|
|
2
|
+
import type {
|
|
3
|
+
PolicyDecisionId,
|
|
4
|
+
PolicyDefinitionId,
|
|
5
|
+
} from "../policies/policy-ids";
|
|
6
|
+
|
|
7
|
+
// ─── Event kinds ─────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type ConditionEvalEvent = ConditionEvaluationReport & {
|
|
10
|
+
readonly kind: "condition-eval";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface PolicyResolutionEvent {
|
|
14
|
+
readonly kind: "policy-resolution";
|
|
15
|
+
readonly level: "info";
|
|
16
|
+
readonly policyKey: string;
|
|
17
|
+
readonly definitionId: PolicyDefinitionId;
|
|
18
|
+
readonly policyVersion: string;
|
|
19
|
+
readonly engineKind: "GATE" | "COMPUTE";
|
|
20
|
+
readonly engineVersion: number;
|
|
21
|
+
readonly occurredAt: Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PolicyEvaluationFailedEvent {
|
|
25
|
+
readonly kind: "policy-evaluation-failed";
|
|
26
|
+
readonly level: "error";
|
|
27
|
+
readonly policyKey: string | undefined;
|
|
28
|
+
readonly errorKind: string;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly cause: string;
|
|
31
|
+
readonly occurredAt: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PolicyEvaluationCompletedEvent {
|
|
35
|
+
readonly kind: "policy-evaluation-completed";
|
|
36
|
+
readonly level: "info";
|
|
37
|
+
readonly policyKey: string;
|
|
38
|
+
readonly decisionId: PolicyDecisionId;
|
|
39
|
+
readonly engineKind: "GATE" | "COMPUTE";
|
|
40
|
+
readonly occurredAt: Date;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PolicyEvent =
|
|
44
|
+
| ConditionEvalEvent
|
|
45
|
+
| PolicyResolutionEvent
|
|
46
|
+
| PolicyEvaluationFailedEvent
|
|
47
|
+
| PolicyEvaluationCompletedEvent;
|
|
48
|
+
|
|
49
|
+
// ─── Reporter interface ───────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface PolicyReporter {
|
|
52
|
+
report(event: PolicyEvent): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Re-export only what external consumers need for event construction ───────
|
|
56
|
+
|
|
57
|
+
export type {
|
|
58
|
+
ConditionEvaluationReport,
|
|
59
|
+
ConditionEvaluationReportTag,
|
|
60
|
+
} from "../policies/engines/condition-evaluator-reporter";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../exceptions/invariant-violation-exception";
|
|
2
|
+
import { assertValidAggregateVersion } from "../shared/aggregate-version";
|
|
3
|
+
import { assertValidDate, cloneDate } from "../shared/temporal-guards";
|
|
4
|
+
import { type ContractVersion, version } from "../versioning/version";
|
|
5
|
+
|
|
6
|
+
interface EntityState<TIdentifier> {
|
|
7
|
+
readonly id: TIdentifier;
|
|
8
|
+
readonly createdAt: Date;
|
|
9
|
+
readonly updatedAt: Date;
|
|
10
|
+
readonly aggregateVersion: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@version("1.0")
|
|
14
|
+
abstract class Entity<TIdentifier> {
|
|
15
|
+
public static readonly CONTRACT_VERSION: ContractVersion;
|
|
16
|
+
|
|
17
|
+
private readonly _id: TIdentifier;
|
|
18
|
+
private readonly _createdAt: Date;
|
|
19
|
+
private _updatedAt: Date;
|
|
20
|
+
private _aggregateVersion: number;
|
|
21
|
+
|
|
22
|
+
protected constructor(state: EntityState<TIdentifier>) {
|
|
23
|
+
assertValidDate("createdAt", state.createdAt);
|
|
24
|
+
assertValidDate("updatedAt", state.updatedAt);
|
|
25
|
+
assertValidAggregateVersion(state.aggregateVersion);
|
|
26
|
+
|
|
27
|
+
if (state.updatedAt.getTime() < state.createdAt.getTime()) {
|
|
28
|
+
throw new InvariantViolationException(
|
|
29
|
+
"updatedAt cannot be earlier than createdAt",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this._id = state.id;
|
|
34
|
+
this._createdAt = cloneDate(state.createdAt);
|
|
35
|
+
this._updatedAt = cloneDate(state.updatedAt);
|
|
36
|
+
this._aggregateVersion = state.aggregateVersion;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public get id(): TIdentifier {
|
|
40
|
+
return this._id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public get createdAt(): Date {
|
|
44
|
+
return cloneDate(this._createdAt);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public get updatedAt(): Date {
|
|
48
|
+
return cloneDate(this._updatedAt);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public get aggregateVersion(): number {
|
|
52
|
+
return this._aggregateVersion;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public get contractVersion(): ContractVersion {
|
|
56
|
+
return Entity.CONTRACT_VERSION;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected markAsModified(updatedAt: Date = new Date()): number {
|
|
60
|
+
assertValidDate("updatedAt", updatedAt);
|
|
61
|
+
|
|
62
|
+
this._updatedAt = cloneDate(updatedAt);
|
|
63
|
+
this._aggregateVersion += 1;
|
|
64
|
+
|
|
65
|
+
return this._aggregateVersion;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { Entity, type EntityState };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Ruleset, type RulesetId } from "./ruleset.contracts";
|
|
2
|
+
import { DomainException } from "../../exceptions/domain-exception";
|
|
3
|
+
|
|
4
|
+
interface CreationRuleset<TData> extends Ruleset {
|
|
5
|
+
validate(data: TData): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type InvariantRuleset = Ruleset;
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
type CreationRuleset,
|
|
12
|
+
DomainException,
|
|
13
|
+
type InvariantRuleset,
|
|
14
|
+
type Ruleset,
|
|
15
|
+
type RulesetId,
|
|
16
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DomainException } from "../../exceptions/domain-exception";
|
|
2
|
+
import {
|
|
3
|
+
type Ruleset,
|
|
4
|
+
type RulesetRegistry as RulesetRegistryContract,
|
|
5
|
+
} from "./ruleset.contracts";
|
|
6
|
+
|
|
7
|
+
class RulesetRegistryError extends DomainException {}
|
|
8
|
+
|
|
9
|
+
function parseVersion(id: string): [number, number] {
|
|
10
|
+
const atIdx = id.lastIndexOf("@");
|
|
11
|
+
const versionStr = id.slice(atIdx + 1);
|
|
12
|
+
const [major, minor] = versionStr.split(".").map(Number);
|
|
13
|
+
return [major, minor];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class RulesetRegistry implements RulesetRegistryContract {
|
|
17
|
+
private readonly _store = new Map<string, Ruleset>();
|
|
18
|
+
private _sealed = false;
|
|
19
|
+
|
|
20
|
+
register(ruleset: Ruleset): void {
|
|
21
|
+
if (this._sealed) {
|
|
22
|
+
throw new RulesetRegistryError(
|
|
23
|
+
"Registry is sealed. No new rulesets can be registered.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (this._store.has(ruleset.id)) {
|
|
27
|
+
throw new RulesetRegistryError(
|
|
28
|
+
`Ruleset with id "${ruleset.id}" is already registered.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
this._store.set(ruleset.id, ruleset);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
seal(): void {
|
|
35
|
+
this._sealed = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get<T extends Ruleset>(id: string): T {
|
|
39
|
+
const ruleset = this._store.get(id);
|
|
40
|
+
if (!ruleset) {
|
|
41
|
+
const available = Array.from(this._store.keys()).join(", ");
|
|
42
|
+
throw new RulesetRegistryError(
|
|
43
|
+
`Ruleset "${id}" not found. Available: [${available}]`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return ruleset as T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getCurrent<T extends Ruleset>(prefix: string): T {
|
|
50
|
+
const matchingEntries = Array.from(this._store.entries()).filter(([key]) =>
|
|
51
|
+
key.startsWith(prefix + "@"),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (matchingEntries.length === 0) {
|
|
55
|
+
throw new RulesetRegistryError(
|
|
56
|
+
`No rulesets found with prefix "${prefix}".`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sorted = matchingEntries.sort(([a], [b]) => {
|
|
61
|
+
const [aMajor, aMinor] = parseVersion(a);
|
|
62
|
+
const [bMajor, bMinor] = parseVersion(b);
|
|
63
|
+
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
64
|
+
return aMinor - bMinor;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return sorted[sorted.length - 1][1] as T;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { RulesetRegistry };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type RulesetId = `${string}@${number}.${number}`;
|
|
2
|
+
|
|
3
|
+
interface Ruleset {
|
|
4
|
+
readonly id: RulesetId;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface RulesetRegistry {
|
|
9
|
+
register(ruleset: Ruleset): void;
|
|
10
|
+
seal(): void;
|
|
11
|
+
get<T extends Ruleset>(id: string): T;
|
|
12
|
+
getCurrent<T extends Ruleset>(prefix: string): T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { type Ruleset, type RulesetId, type RulesetRegistry };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DomainException } from "../../exceptions/domain-exception";
|
|
2
|
+
import { type Ruleset, type RulesetId } from "./ruleset.contracts";
|
|
3
|
+
|
|
4
|
+
interface ValueObjectRuleset<T> extends Ruleset {
|
|
5
|
+
validate(value: T): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
DomainException,
|
|
10
|
+
type Ruleset,
|
|
11
|
+
type RulesetId,
|
|
12
|
+
type ValueObjectRuleset,
|
|
13
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
assertTransactionTime,
|
|
3
|
+
createTransactionTime,
|
|
4
|
+
type CreateTransactionTimeInput,
|
|
5
|
+
type TransactionTime,
|
|
6
|
+
} from "./transaction-time";
|
|
7
|
+
export { contains, isClosed, isOpen, overlaps } from "./temporal-range";
|
|
8
|
+
export {
|
|
9
|
+
createTemporalSnapshot,
|
|
10
|
+
type CreateTemporalSnapshotInput,
|
|
11
|
+
type TemporalSnapshot,
|
|
12
|
+
} from "./temporal-snapshot";
|
|
13
|
+
export {
|
|
14
|
+
assertValidTime,
|
|
15
|
+
createValidTime,
|
|
16
|
+
type CreateValidTimeInput,
|
|
17
|
+
type ValidTime,
|
|
18
|
+
} from "./valid-time";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { assertValidDate } from "../../shared/temporal-guards";
|
|
2
|
+
|
|
3
|
+
import { assertValidTime, type ValidTime } from "./valid-time";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether a temporal range is "open" (has no end date).
|
|
7
|
+
* Useful for identifying information that is still in effect / active.
|
|
8
|
+
*/
|
|
9
|
+
function isOpen(range: ValidTime): boolean {
|
|
10
|
+
assertValidTime(range, "range");
|
|
11
|
+
|
|
12
|
+
return range.to === undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether a temporal range is "closed" (has a defined end date).
|
|
17
|
+
*/
|
|
18
|
+
function isClosed(range: ValidTime): boolean {
|
|
19
|
+
return !isOpen(range);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Checks whether a specific date is contained within the temporal range.
|
|
24
|
+
* The lower bound (`from`) is inclusive, while the upper bound (`to`) is exclusive.
|
|
25
|
+
*/
|
|
26
|
+
function contains(range: ValidTime, date: Date): boolean {
|
|
27
|
+
assertValidTime(range, "range");
|
|
28
|
+
assertValidDate("date", date);
|
|
29
|
+
|
|
30
|
+
const fromTime = range.from.getTime();
|
|
31
|
+
const toTime = range.to?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
32
|
+
const targetTime = date.getTime();
|
|
33
|
+
|
|
34
|
+
return targetTime >= fromTime && targetTime < toTime;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks whether two temporal ranges overlap at any point.
|
|
39
|
+
* Useful for validating whether a new rule conflicts with an existing one.
|
|
40
|
+
*/
|
|
41
|
+
function overlaps(left: ValidTime, right: ValidTime): boolean {
|
|
42
|
+
assertValidTime(left, "left");
|
|
43
|
+
assertValidTime(right, "right");
|
|
44
|
+
|
|
45
|
+
const leftFrom = left.from.getTime();
|
|
46
|
+
const leftTo = left.to?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
47
|
+
const rightFrom = right.from.getTime();
|
|
48
|
+
const rightTo = right.to?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
49
|
+
|
|
50
|
+
return leftFrom < rightTo && rightFrom < leftTo;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { contains, isClosed, isOpen, overlaps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type DeepReadonly, makeImmutable } from "../../shared/immutable";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createTransactionTime,
|
|
5
|
+
type CreateTransactionTimeInput,
|
|
6
|
+
type TransactionTime,
|
|
7
|
+
} from "./transaction-time";
|
|
8
|
+
import {
|
|
9
|
+
createValidTime,
|
|
10
|
+
type CreateValidTimeInput,
|
|
11
|
+
type ValidTime,
|
|
12
|
+
} from "./valid-time";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pairs a domain datum with its time dimensions (Business and Transaction).
|
|
16
|
+
* It is the representation of an entry in the append-only (historical) table.
|
|
17
|
+
*/
|
|
18
|
+
interface TemporalSnapshot<T> {
|
|
19
|
+
/** The data or state captured by the snapshot. */
|
|
20
|
+
readonly data: DeepReadonly<T>;
|
|
21
|
+
/** Period during which this datum was valid in the real world. */
|
|
22
|
+
readonly validTime: ValidTime;
|
|
23
|
+
/** Period during which this record was the state known to the system. */
|
|
24
|
+
readonly txTime: TransactionTime;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CreateTemporalSnapshotInput<T> {
|
|
28
|
+
readonly data: T;
|
|
29
|
+
readonly validTime: CreateValidTimeInput;
|
|
30
|
+
readonly txTime: CreateTransactionTimeInput;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createTemporalSnapshot<T>(
|
|
34
|
+
input: CreateTemporalSnapshotInput<T>,
|
|
35
|
+
): TemporalSnapshot<T> {
|
|
36
|
+
return makeImmutable({
|
|
37
|
+
data: input.data,
|
|
38
|
+
validTime: createValidTime(input.validTime),
|
|
39
|
+
txTime: createTransactionTime(input.txTime),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
createTemporalSnapshot,
|
|
45
|
+
type CreateTemporalSnapshotInput,
|
|
46
|
+
type TemporalSnapshot,
|
|
47
|
+
};
|