@sekiban/dcb-domain 0.1.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.
@@ -0,0 +1,150 @@
1
+ import type { z } from "zod";
2
+ export type JsonPrimitive = null | boolean | number | string;
3
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
4
+ readonly [key: string]: JsonValue;
5
+ };
6
+ export type DomainBoundary = "http-command" | "queue" | "stored-event" | "wasm-restore" | "external-query";
7
+ export declare class DomainAuthoringError extends Error {
8
+ readonly code: string;
9
+ constructor(code: string, message: string, options?: ErrorOptions);
10
+ }
11
+ export declare class DomainRegistrationError extends DomainAuthoringError {
12
+ readonly collisions: readonly string[];
13
+ constructor(message: string, collisions?: readonly string[]);
14
+ }
15
+ declare const tagFamilyBrand: unique symbol;
16
+ declare const eventPayloadBrand: unique symbol;
17
+ declare const parsedBoundaryBrand: unique symbol;
18
+ declare const terminalDecisionBrand: unique symbol;
19
+ export interface Tag<Family extends string = string> {
20
+ readonly family: Family;
21
+ readonly group: Family;
22
+ readonly value: string;
23
+ readonly content: string;
24
+ readonly id: string;
25
+ readonly tag: string;
26
+ readonly [tagFamilyBrand]: Family;
27
+ }
28
+ export interface TagFamily<Family extends string = string> {
29
+ readonly family: Family;
30
+ readonly group: Family;
31
+ readonly of: (value: string) => Tag<Family>;
32
+ readonly create: (value: string) => Tag<Family>;
33
+ readonly [tagFamilyBrand]: Family;
34
+ }
35
+ export type TagInput<Family extends string = string> = Tag<Family> | string;
36
+ export declare function tagFamily<const Family extends string>(family: Family): TagFamily<Family>;
37
+ export declare function tag<const Family extends string>(family: Family): TagFamily<Family>;
38
+ export declare function tag<const Family extends string>(family: Family, value: string): Tag<Family>;
39
+ export declare const defineTag: typeof tag;
40
+ export declare function normalizeTag<Family extends string = string>(input: TagInput<Family>): Tag<Family>;
41
+ export type TagDeriver<Payload> = (payload: Payload) => readonly Tag[];
42
+ export type TagDeriverResult<Deriver> = Deriver extends (...args: never[]) => infer Result ? Result : never;
43
+ export type TagFamilyOf<Values> = Values extends readonly (infer Item)[] ? Item extends Tag<infer Family> ? Family : never : never;
44
+ export type TagFamilyOfDeriver<Deriver> = TagFamilyOf<TagDeriverResult<Deriver>> extends never ? string : TagFamilyOf<TagDeriverResult<Deriver>>;
45
+ export interface EventRecord<Payload = unknown, Family extends string = string> {
46
+ readonly eventType: string;
47
+ readonly eventName: string;
48
+ readonly payload: Payload;
49
+ readonly tags: readonly Tag<Family>[];
50
+ readonly ordinal: string;
51
+ }
52
+ export type EventPayload<Event extends {
53
+ readonly schema: z.ZodTypeAny;
54
+ }> = z.infer<Event["schema"]> & {
55
+ readonly [eventPayloadBrand]: Event;
56
+ };
57
+ export type EventOf<Event extends {
58
+ readonly schema: z.ZodTypeAny;
59
+ }> = EventPayload<Event>;
60
+ export type ParsedAt<Boundary extends DomainBoundary, Value> = Value & {
61
+ readonly [parsedBoundaryBrand]: Boundary;
62
+ };
63
+ export type RejectKind = "validation" | "not-found" | "conflict" | "forbidden" | "invalid-state" | "internal";
64
+ export interface Reject<Kind extends RejectKind = RejectKind, Details = unknown> {
65
+ readonly kind: "reject";
66
+ readonly reason: string;
67
+ readonly rejectKind: Kind;
68
+ readonly code: string;
69
+ readonly details?: Details;
70
+ readonly [terminalDecisionBrand]: "reject";
71
+ }
72
+ export interface Done<Value extends JsonValue = JsonValue> {
73
+ readonly kind: "done";
74
+ readonly value?: Value;
75
+ readonly [terminalDecisionBrand]: "done";
76
+ }
77
+ export interface None {
78
+ readonly kind: "none";
79
+ readonly reason?: string;
80
+ readonly [terminalDecisionBrand]: "none";
81
+ }
82
+ export type TerminalDecision<Value extends JsonValue = JsonValue> = Done<Value> | None | Reject;
83
+ export declare const V1_REJECT_ERROR_CODES: Readonly<Record<RejectKind, string>>;
84
+ export type FixedNow = string | number | bigint;
85
+ export interface TimeProvider {
86
+ readonly now: () => FixedNow;
87
+ }
88
+ export interface PortableSnapshot<State = unknown> {
89
+ readonly projectorId: string;
90
+ readonly tag: Tag;
91
+ readonly head: string | null;
92
+ readonly state: State;
93
+ readonly exists: boolean;
94
+ }
95
+ export interface ReadClaim {
96
+ readonly kind: "state" | "exists";
97
+ readonly projectorId?: string;
98
+ readonly tag: Tag;
99
+ readonly head: string | null;
100
+ }
101
+ export interface ReadSet {
102
+ readonly claims: readonly ReadClaimDeclaration[];
103
+ readonly tags: readonly Tag[];
104
+ readonly has: (kind: ReadClaimDeclaration["kind"], projectorId: string | undefined, tag: Tag) => boolean;
105
+ }
106
+ export interface ReadClaimDeclaration {
107
+ readonly kind: "state" | "exists";
108
+ readonly projectorId?: string;
109
+ readonly projector?: ProjectorLike;
110
+ readonly tag: Tag;
111
+ }
112
+ export interface CommitCandidateEvent {
113
+ readonly eventType: string;
114
+ readonly eventName: string;
115
+ readonly payload: unknown;
116
+ readonly tags: readonly Tag[];
117
+ readonly ordinal: string;
118
+ }
119
+ export interface DecisionLog {
120
+ readonly now: FixedNow;
121
+ readonly events: readonly CommitCandidateEvent[];
122
+ readonly readClaims: readonly ReadClaim[];
123
+ readonly terminal: TerminalDecision;
124
+ }
125
+ export interface CandidateEnvelope {
126
+ readonly kind: "candidate-envelope";
127
+ readonly now: FixedNow;
128
+ readonly events: readonly CommitCandidateEvent[];
129
+ readonly tags: readonly Tag[];
130
+ readonly readClaims: readonly ReadClaim[];
131
+ readonly decision: Done;
132
+ }
133
+ export interface SnapshotReader {
134
+ readonly read: (projector: ProjectorLike, tag: Tag) => Promise<PortableSnapshot> | PortableSnapshot;
135
+ readonly exists?: (tag: Tag) => Promise<boolean> | boolean;
136
+ /** Optional exact head for exists-only reads; null is the assert-empty head. */
137
+ readonly head?: (tag: Tag) => Promise<string | null> | string | null;
138
+ }
139
+ export interface ProjectorLike {
140
+ readonly id: string;
141
+ readonly version: number;
142
+ readonly tag: TagFamily;
143
+ readonly initialState: unknown | (() => unknown);
144
+ readonly subscribes: (eventType: string) => boolean;
145
+ readonly apply: (state: never, event: EventRecord) => unknown;
146
+ }
147
+ export declare function isJsonValue(value: unknown): value is JsonValue;
148
+ export declare function assertJsonValue(value: unknown, boundary?: string): JsonValue;
149
+ export declare function cloneAndFreeze<T>(value: T): T;
150
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,90 @@
1
+ export class DomainAuthoringError extends Error {
2
+ code;
3
+ constructor(code, message, options) {
4
+ super(message, options);
5
+ this.name = "DomainAuthoringError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export class DomainRegistrationError extends DomainAuthoringError {
10
+ collisions;
11
+ constructor(message, collisions = []) {
12
+ super("DOMAIN_REGISTRATION_INVALID", message);
13
+ this.name = "DomainRegistrationError";
14
+ this.collisions = Object.freeze([...collisions]);
15
+ }
16
+ }
17
+ export function tagFamily(family) {
18
+ if (family.length === 0)
19
+ throw new DomainAuthoringError("TAG_FAMILY_INVALID", "Tag family must not be empty");
20
+ const make = (value) => {
21
+ if (value.length === 0)
22
+ throw new DomainAuthoringError("TAG_VALUE_INVALID", "Tag value must not be empty");
23
+ const id = `${family}:${value}`;
24
+ return Object.freeze({
25
+ family,
26
+ group: family,
27
+ value,
28
+ content: value,
29
+ id,
30
+ tag: id,
31
+ });
32
+ };
33
+ return Object.freeze({ family, group: family, of: make, create: make });
34
+ }
35
+ export function tag(family, value) {
36
+ const familyValue = tagFamily(family);
37
+ return value === undefined ? familyValue : familyValue.of(value);
38
+ }
39
+ export const defineTag = tag;
40
+ export function normalizeTag(input) {
41
+ if (typeof input === "string") {
42
+ const separator = input.indexOf(":");
43
+ if (separator <= 0 || separator === input.length - 1) {
44
+ throw new DomainAuthoringError("TAG_INVALID", "A tag string must be family:value");
45
+ }
46
+ return tagFamily(input.slice(0, separator)).of(input.slice(separator + 1));
47
+ }
48
+ if (input.id.length === 0 || input.family.length === 0 || input.value.length === 0) {
49
+ throw new DomainAuthoringError("TAG_INVALID", "Tag family, value, and id are required");
50
+ }
51
+ return input;
52
+ }
53
+ export const V1_REJECT_ERROR_CODES = Object.freeze({
54
+ validation: "validation_error",
55
+ "not-found": "not_found",
56
+ conflict: "consistency_conflict",
57
+ forbidden: "forbidden",
58
+ "invalid-state": "invalid_state",
59
+ internal: "internal_error",
60
+ });
61
+ export function isJsonValue(value) {
62
+ if (value === null || typeof value === "string" || typeof value === "boolean")
63
+ return true;
64
+ if (typeof value === "number")
65
+ return Number.isFinite(value);
66
+ if (Array.isArray(value))
67
+ return value.every(isJsonValue);
68
+ if (typeof value !== "object")
69
+ return false;
70
+ const prototype = Object.getPrototypeOf(value);
71
+ if (prototype !== Object.prototype && prototype !== null)
72
+ return false;
73
+ return Object.values(value).every(isJsonValue);
74
+ }
75
+ export function assertJsonValue(value, boundary = "value") {
76
+ if (!isJsonValue(value))
77
+ throw new DomainAuthoringError("INVALID_JSON_VALUE", `Value was not JSON serializable at ${boundary}`);
78
+ return value;
79
+ }
80
+ export function cloneAndFreeze(value) {
81
+ if (Array.isArray(value)) {
82
+ value.forEach(cloneAndFreeze);
83
+ return Object.freeze(value);
84
+ }
85
+ if (typeof value === "object" && value !== null) {
86
+ Object.values(value).forEach(cloneAndFreeze);
87
+ return Object.freeze(value);
88
+ }
89
+ return value;
90
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sekiban/dcb-domain",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Runtime-free schema-first DCB domain authoring surface.",
6
+ "license": "Elastic-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/J-Tech-Japan/sekiban-dcb-ts.git",
10
+ "directory": "packages/dcb-domain"
11
+ },
12
+ "homepage": "https://github.com/J-Tech-Japan/sekiban-dcb-ts/tree/main/packages/dcb-domain#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/J-Tech-Japan/sekiban-dcb-ts/issues"
15
+ },
16
+ "keywords": [
17
+ "sekiban",
18
+ "dcb",
19
+ "domain",
20
+ "event-sourcing"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./testing": {
33
+ "types": "./dist/testing.d.ts",
34
+ "import": "./dist/testing.js"
35
+ }
36
+ },
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "zod": "4.4.3"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --format=esm --platform=neutral --outfile=dist/index.js && esbuild src/testing.ts --bundle --format=esm --platform=neutral --outfile=dist/testing.js",
51
+ "prepack": "node ../../scripts/dcb-domain-prepare-package.mjs && node ../../scripts/dcb-domain-fix-declarations.mjs",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit"
53
+ }
54
+ }