@woltz/rich-domain 1.9.0 → 1.9.2
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/dist/cjs/core/aggregate-changes.d.ts +14 -0
- package/dist/cjs/core/aggregate-changes.d.ts.map +1 -1
- package/dist/cjs/core/aggregate-changes.js +18 -0
- package/dist/cjs/core/aggregate-changes.js.map +1 -1
- package/dist/cjs/core/base-entity.d.ts +2 -0
- package/dist/cjs/core/base-entity.d.ts.map +1 -1
- package/dist/cjs/core/base-entity.js +39 -41
- package/dist/cjs/core/base-entity.js.map +1 -1
- package/dist/cjs/core/change-tracker.d.ts +8 -0
- package/dist/cjs/core/change-tracker.d.ts.map +1 -1
- package/dist/cjs/core/change-tracker.js +36 -6
- package/dist/cjs/core/change-tracker.js.map +1 -1
- package/dist/cjs/core/domain-event.d.ts +3 -0
- package/dist/cjs/core/domain-event.d.ts.map +1 -1
- package/dist/cjs/core/domain-event.js +8 -1
- package/dist/cjs/core/domain-event.js.map +1 -1
- package/dist/cjs/core/value-object.d.ts.map +1 -1
- package/dist/cjs/core/value-object.js +3 -5
- package/dist/cjs/core/value-object.js.map +1 -1
- package/dist/cjs/criteria.d.ts +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.d.ts +56 -3
- package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.js +61 -6
- package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/outbox-store.d.ts +91 -0
- package/dist/cjs/types/outbox-store.d.ts.map +1 -0
- package/dist/cjs/types/outbox-store.js +3 -0
- package/dist/cjs/types/outbox-store.js.map +1 -0
- package/dist/cjs/utils/helpers.d.ts +1 -0
- package/dist/cjs/utils/helpers.d.ts.map +1 -1
- package/dist/cjs/utils/helpers.js +4 -0
- package/dist/cjs/utils/helpers.js.map +1 -1
- package/dist/esm/core/aggregate-changes.d.ts +14 -0
- package/dist/esm/core/aggregate-changes.d.ts.map +1 -1
- package/dist/esm/core/aggregate-changes.js +18 -0
- package/dist/esm/core/aggregate-changes.js.map +1 -1
- package/dist/esm/core/base-entity.d.ts +2 -0
- package/dist/esm/core/base-entity.d.ts.map +1 -1
- package/dist/esm/core/base-entity.js +37 -39
- package/dist/esm/core/base-entity.js.map +1 -1
- package/dist/esm/core/change-tracker.d.ts +8 -0
- package/dist/esm/core/change-tracker.d.ts.map +1 -1
- package/dist/esm/core/change-tracker.js +36 -6
- package/dist/esm/core/change-tracker.js.map +1 -1
- package/dist/esm/core/domain-event.d.ts +3 -0
- package/dist/esm/core/domain-event.d.ts.map +1 -1
- package/dist/esm/core/domain-event.js +5 -1
- package/dist/esm/core/domain-event.js.map +1 -1
- package/dist/esm/core/value-object.d.ts.map +1 -1
- package/dist/esm/core/value-object.js +1 -3
- package/dist/esm/core/value-object.js.map +1 -1
- package/dist/esm/criteria.d.ts +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.d.ts +56 -3
- package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.js +61 -6
- package/dist/esm/repository/entity-schema-registry.js.map +1 -1
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/types/index.js.map +1 -1
- package/dist/esm/types/outbox-store.d.ts +91 -0
- package/dist/esm/types/outbox-store.d.ts.map +1 -0
- package/dist/esm/types/outbox-store.js +2 -0
- package/dist/esm/types/outbox-store.js.map +1 -0
- package/dist/esm/utils/helpers.d.ts +1 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +3 -0
- package/dist/esm/utils/helpers.js.map +1 -1
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/core/aggregate-changes.d.ts +14 -0
- package/dist/types/core/aggregate-changes.d.ts.map +1 -1
- package/dist/types/core/base-entity.d.ts +2 -0
- package/dist/types/core/base-entity.d.ts.map +1 -1
- package/dist/types/core/change-tracker.d.ts +8 -0
- package/dist/types/core/change-tracker.d.ts.map +1 -1
- package/dist/types/core/domain-event.d.ts +3 -0
- package/dist/types/core/domain-event.d.ts.map +1 -1
- package/dist/types/core/value-object.d.ts.map +1 -1
- package/dist/types/criteria.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/repository/entity-schema-registry.d.ts +56 -3
- package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/types/outbox-store.d.ts +91 -0
- package/dist/types/types/outbox-store.d.ts.map +1 -0
- package/dist/types/utils/helpers.d.ts +1 -0
- package/dist/types/utils/helpers.d.ts.map +1 -1
- package/package.json +68 -67
- package/src/constants.ts +82 -0
- package/src/core/aggregate-changes.ts +466 -0
- package/src/core/base-aggregate.ts +76 -0
- package/src/core/base-entity.ts +552 -0
- package/src/core/change-tracker.ts +1327 -0
- package/src/core/domain-event.ts +41 -0
- package/src/core/entity-changes.ts +146 -0
- package/src/core/entity.ts +13 -0
- package/src/core/id.ts +124 -0
- package/src/core/index.ts +9 -0
- package/src/core/value-object.ts +179 -0
- package/src/criteria.ts +574 -0
- package/src/exceptions.ts +549 -0
- package/src/index.ts +74 -0
- package/src/repository/base-repository.ts +81 -0
- package/src/repository/entity-schema-registry.ts +620 -0
- package/src/repository/index.ts +5 -0
- package/src/repository/mapper.ts +7 -0
- package/src/repository/paginated-result.ts +251 -0
- package/src/repository/unit-of-work.ts +76 -0
- package/src/types/change-tracker.ts +268 -0
- package/src/types/criteria.ts +197 -0
- package/src/types/domain-event.ts +29 -0
- package/src/types/domain.ts +41 -0
- package/src/types/event-bus.ts +17 -0
- package/src/types/index.ts +9 -0
- package/src/types/outbox-store.ts +97 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +24 -0
- package/src/utils/criteria-operator-validation.ts +209 -0
- package/src/utils/crypto.ts +31 -0
- package/src/utils/helpers.ts +50 -0
- package/src/validation-error.ts +219 -0
- package/dist/cjs/t.d.ts +0 -2
- package/dist/cjs/t.d.ts.map +0 -1
- package/dist/cjs/t.js +0 -96
- package/dist/cjs/t.js.map +0 -1
- package/dist/esm/t.d.ts +0 -2
- package/dist/esm/t.d.ts.map +0 -1
- package/dist/esm/t.js +0 -94
- package/dist/esm/t.js.map +0 -1
- package/dist/types/t.d.ts +0 -2
- package/dist/types/t.d.ts.map +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Primitive } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4
|
+
const FILTER_OPERATORS = [
|
|
5
|
+
"equals",
|
|
6
|
+
"notEquals",
|
|
7
|
+
"greaterThan",
|
|
8
|
+
"greaterThanOrEqual",
|
|
9
|
+
"lessThan",
|
|
10
|
+
"lessThanOrEqual",
|
|
11
|
+
"contains",
|
|
12
|
+
"startsWith",
|
|
13
|
+
"endsWith",
|
|
14
|
+
"in",
|
|
15
|
+
"notIn",
|
|
16
|
+
"between",
|
|
17
|
+
"isNull",
|
|
18
|
+
"isNotNull",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type FilterOperator = (typeof FILTER_OPERATORS)[number];
|
|
22
|
+
|
|
23
|
+
export type StringOperators =
|
|
24
|
+
| "equals"
|
|
25
|
+
| "notEquals"
|
|
26
|
+
| "contains"
|
|
27
|
+
| "startsWith"
|
|
28
|
+
| "endsWith"
|
|
29
|
+
| "in"
|
|
30
|
+
| "notIn"
|
|
31
|
+
| "isNull"
|
|
32
|
+
| "isNotNull";
|
|
33
|
+
|
|
34
|
+
export type NumberOperators =
|
|
35
|
+
| "equals"
|
|
36
|
+
| "notEquals"
|
|
37
|
+
| "greaterThan"
|
|
38
|
+
| "greaterThanOrEqual"
|
|
39
|
+
| "lessThan"
|
|
40
|
+
| "lessThanOrEqual"
|
|
41
|
+
| "in"
|
|
42
|
+
| "notIn"
|
|
43
|
+
| "between"
|
|
44
|
+
| "isNull"
|
|
45
|
+
| "isNotNull";
|
|
46
|
+
|
|
47
|
+
export type DateOperators =
|
|
48
|
+
| "equals"
|
|
49
|
+
| "notEquals"
|
|
50
|
+
| "greaterThan"
|
|
51
|
+
| "greaterThanOrEqual"
|
|
52
|
+
| "lessThan"
|
|
53
|
+
| "lessThanOrEqual"
|
|
54
|
+
| "in"
|
|
55
|
+
| "notIn"
|
|
56
|
+
| "between"
|
|
57
|
+
| "isNull"
|
|
58
|
+
| "isNotNull";
|
|
59
|
+
|
|
60
|
+
export type BooleanOperators = "equals" | "notEquals" | "isNull" | "isNotNull";
|
|
61
|
+
|
|
62
|
+
export type ArrayOperators = "in" | "notIn" | "isNull" | "isNotNull";
|
|
63
|
+
|
|
64
|
+
export type OperatorsForType<T> = T extends string
|
|
65
|
+
? StringOperators
|
|
66
|
+
: T extends number
|
|
67
|
+
? NumberOperators
|
|
68
|
+
: T extends Date
|
|
69
|
+
? DateOperators
|
|
70
|
+
: T extends boolean
|
|
71
|
+
? BooleanOperators
|
|
72
|
+
: T extends Array<any>
|
|
73
|
+
? ArrayOperators
|
|
74
|
+
: FilterOperator;
|
|
75
|
+
|
|
76
|
+
export type FilterValueFor<T> =
|
|
77
|
+
| T
|
|
78
|
+
| (T extends number | Date ? [T, T] : never)
|
|
79
|
+
| T[]
|
|
80
|
+
| null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve individual entity prop types for criteria:
|
|
84
|
+
* - Date → Date (preserved, so DateOperators are used)
|
|
85
|
+
* - Array<U> → recurse into elements
|
|
86
|
+
* - Id / ValueObject (value + equals) → unwrap to primitive value
|
|
87
|
+
* - Everything else → keep as-is
|
|
88
|
+
*/
|
|
89
|
+
type ResolveEntityProp<T> = T extends Date
|
|
90
|
+
? Date
|
|
91
|
+
: T extends Array<infer U>
|
|
92
|
+
? ResolveEntityProp<U>[]
|
|
93
|
+
: T extends { value: infer V; equals(...args: any[]): any }
|
|
94
|
+
? V
|
|
95
|
+
: T;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract the plain object type from an entity for criteria field resolution.
|
|
99
|
+
* - For entities (with `props`), resolves from raw props preserving Date types.
|
|
100
|
+
* - For objects with toJSON(), uses the return type of toJSON().
|
|
101
|
+
* - Otherwise, uses the type as is.
|
|
102
|
+
*/
|
|
103
|
+
type ExtractPlainType<T> = T extends { props: infer P }
|
|
104
|
+
? { [K in keyof P]: ResolveEntityProp<P[K]> }
|
|
105
|
+
: T extends { toJSON(): infer R }
|
|
106
|
+
? R
|
|
107
|
+
: T;
|
|
108
|
+
|
|
109
|
+
export type PathValue<
|
|
110
|
+
T,
|
|
111
|
+
P extends string,
|
|
112
|
+
> = P extends `${infer K}.${infer Rest}`
|
|
113
|
+
? K extends keyof ExtractPlainType<T>
|
|
114
|
+
? ExtractPlainType<T>[K] extends Array<infer U>
|
|
115
|
+
? PathValue<U, Rest>
|
|
116
|
+
: PathValue<ExtractPlainType<T>[K], Rest>
|
|
117
|
+
: never
|
|
118
|
+
: P extends keyof ExtractPlainType<T>
|
|
119
|
+
? ExtractPlainType<T>[P]
|
|
120
|
+
: never;
|
|
121
|
+
|
|
122
|
+
export interface Filter<TField = string, TValue = unknown> {
|
|
123
|
+
field: TField;
|
|
124
|
+
operator: unknown extends TValue ? FilterOperator : OperatorsForType<TValue>;
|
|
125
|
+
value: TValue;
|
|
126
|
+
options?: CriteriaOptions;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type TypedFilter<T> = {
|
|
130
|
+
[K in FieldPath<T>]: {
|
|
131
|
+
field: K;
|
|
132
|
+
operator: OperatorsForType<NonNullable<PathValue<T, K>>>;
|
|
133
|
+
value: FilterValueFor<NonNullable<PathValue<T, K>>>;
|
|
134
|
+
options?: CriteriaOptions;
|
|
135
|
+
};
|
|
136
|
+
}[FieldPath<T>];
|
|
137
|
+
|
|
138
|
+
export type CriteriaAdapter<Input, Output> = {
|
|
139
|
+
[K in FieldPath<Input>]?: FieldPath<Output>;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type OrderDirection = "asc" | "desc";
|
|
143
|
+
|
|
144
|
+
export interface Order {
|
|
145
|
+
field: string;
|
|
146
|
+
direction: OrderDirection;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type TypedOrder<T> = {
|
|
150
|
+
field: FieldPath<T>;
|
|
151
|
+
direction: OrderDirection;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export interface Pagination {
|
|
155
|
+
page: number;
|
|
156
|
+
limit: number;
|
|
157
|
+
offset: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type Search = string;
|
|
161
|
+
|
|
162
|
+
export type QueryParamsObject = {
|
|
163
|
+
filters?: Record<string, unknown>;
|
|
164
|
+
pagination?: Omit<Pagination, "offset">;
|
|
165
|
+
orderBy?: string[];
|
|
166
|
+
search?: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export interface PaginationMeta {
|
|
170
|
+
page: number;
|
|
171
|
+
limit: number;
|
|
172
|
+
total: number;
|
|
173
|
+
totalPages: number;
|
|
174
|
+
hasNext: boolean;
|
|
175
|
+
hasPrevious: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface CriteriaOptions {
|
|
179
|
+
quantifier?: "some" | "every" | "none";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type ExcludeBuiltInKeys<T> = Exclude<keyof T, keyof any[] | number | symbol>;
|
|
183
|
+
|
|
184
|
+
export type FieldPath<T> =
|
|
185
|
+
ExtractPlainType<T> extends Primitive
|
|
186
|
+
? never
|
|
187
|
+
: {
|
|
188
|
+
[K in ExcludeBuiltInKeys<ExtractPlainType<T>> & string]: NonNullable<
|
|
189
|
+
ExtractPlainType<T>[K]
|
|
190
|
+
> extends Primitive
|
|
191
|
+
? K
|
|
192
|
+
: NonNullable<ExtractPlainType<T>[K]> extends Array<infer U>
|
|
193
|
+
? U extends Primitive
|
|
194
|
+
? K
|
|
195
|
+
: K | `${K}.${FieldPath<U>}`
|
|
196
|
+
: `${K}.${FieldPath<NonNullable<ExtractPlainType<T>[K]>>}`;
|
|
197
|
+
}[ExcludeBuiltInKeys<ExtractPlainType<T>> & string];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for all domain events
|
|
3
|
+
*/
|
|
4
|
+
export interface IDomainEvent<P = unknown> {
|
|
5
|
+
/**
|
|
6
|
+
* Unique identifier for this event occurrence
|
|
7
|
+
*/
|
|
8
|
+
readonly eventId: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* When the event occurred
|
|
12
|
+
*/
|
|
13
|
+
readonly occurredOn: Date;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Name/type of the event (e.g., "UserCreated", "OrderPlaced")
|
|
17
|
+
*/
|
|
18
|
+
readonly eventName: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Queue name for the event (optional)
|
|
22
|
+
*/
|
|
23
|
+
readonly queueName?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Payload of the event
|
|
27
|
+
*/
|
|
28
|
+
readonly payload: P;
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Id } from "../core/index";
|
|
2
|
+
import { StandardSchema } from "./standard-schema.js";
|
|
3
|
+
|
|
4
|
+
export interface BaseProps {
|
|
5
|
+
id: Id;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface DomainValidation<T> {
|
|
9
|
+
schema: StandardSchema<T>;
|
|
10
|
+
config?: ValidationConfig;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type EntityValidation<T> = DomainValidation<T>;
|
|
14
|
+
export type VOValidation<T> = DomainValidation<T>;
|
|
15
|
+
|
|
16
|
+
export interface VOHooks<V, VO> {
|
|
17
|
+
onBeforeCreate?: (value: V) => void;
|
|
18
|
+
rules?: (valueObject: VO) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EntityHooks<T extends BaseProps, E> {
|
|
22
|
+
onBeforeCreate?: (props: T) => void;
|
|
23
|
+
onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
|
|
24
|
+
onCreate?: (entity: E) => void;
|
|
25
|
+
rules?: (entity: E) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ValidationConfig {
|
|
29
|
+
/** When true, validates the entity on creation. Default: true */
|
|
30
|
+
onCreate?: boolean;
|
|
31
|
+
/** When true, validates the entity on update. Default: true */
|
|
32
|
+
onUpdate?: boolean;
|
|
33
|
+
/** When true, throws a ValidationError when validation fails. Default: true */
|
|
34
|
+
throwOnError?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When `throwOnError` is `false`, controls whether invalid updates are kept on the entity.
|
|
37
|
+
* - `true` (default): apply mutations and refresh `validationErrors` (dirty / form mode).
|
|
38
|
+
* - `false`: block mutations when errors already exist, and revert updates that fail validation.
|
|
39
|
+
*/
|
|
40
|
+
persistInvalidMutations?: boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IDomainEvent } from "./domain-event";
|
|
2
|
+
|
|
3
|
+
export interface IDomainEventBus {
|
|
4
|
+
/**
|
|
5
|
+
* Publish a single domain event.
|
|
6
|
+
*
|
|
7
|
+
* @param event - The domain event to publish
|
|
8
|
+
*/
|
|
9
|
+
publish(event: IDomainEvent): Promise<void>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Publish multiple domain events.
|
|
13
|
+
*
|
|
14
|
+
* @param events - Array of domain events to publish
|
|
15
|
+
*/
|
|
16
|
+
publishAll(events: IDomainEvent[]): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./criteria.js";
|
|
2
|
+
export * from "./domain.js";
|
|
3
|
+
export * from "./standard-schema.js";
|
|
4
|
+
export * from "./utils.js";
|
|
5
|
+
export * from "./unit-of-work.js";
|
|
6
|
+
export * from "./domain-event.js";
|
|
7
|
+
export * from "./change-tracker.js";
|
|
8
|
+
export * from "./event-bus.js";
|
|
9
|
+
export * from "./outbox-store.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { IDomainEvent } from "./domain-event.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Possible statuses for an outbox entry.
|
|
5
|
+
*
|
|
6
|
+
* - `pending`: The event has been persisted but not yet published.
|
|
7
|
+
* - `published`: The event has been successfully published to the message broker.
|
|
8
|
+
* - `failed`: The event failed to publish after the maximum number of retries.
|
|
9
|
+
*/
|
|
10
|
+
export type OutboxStatus = "pending" | "published" | "failed";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The data shape of a single outbox entry returned by {@link IOutboxStore.fetchPending}.
|
|
14
|
+
* This is the flat wire format — no class logic, just data.
|
|
15
|
+
*/
|
|
16
|
+
export interface OutboxEntryData {
|
|
17
|
+
/** The event's own `eventId`, used as the primary key of the outbox table. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The event class name (e.g. `"OrderPlaced"`). */
|
|
20
|
+
eventName: string;
|
|
21
|
+
/** The event payload (the `P` generic from `DomainEvent<P>`). */
|
|
22
|
+
payload: unknown;
|
|
23
|
+
/** When the domain event was originally created. */
|
|
24
|
+
occurredOn: Date;
|
|
25
|
+
/** Current publish status. */
|
|
26
|
+
status: OutboxStatus;
|
|
27
|
+
/** Number of publish attempts so far. */
|
|
28
|
+
retries: number;
|
|
29
|
+
/** Error message from the most recent failed publish attempt, if any. */
|
|
30
|
+
lastError: string | null;
|
|
31
|
+
/** When this outbox row was inserted. */
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result returned by {@link IOutboxStore.fetchPending}.
|
|
37
|
+
*/
|
|
38
|
+
export interface OutboxFetchResult {
|
|
39
|
+
entries: OutboxEntryData[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimal, framework-agnostic contract for an outbox persistence store.
|
|
44
|
+
*
|
|
45
|
+
* Implementations exist for Prisma, Drizzle, TypeORM, and raw SQL.
|
|
46
|
+
* The interface lives in `@woltz/rich-domain` (core) so that the
|
|
47
|
+
* `@woltz/rich-domain-outbox` package can depend on it without
|
|
48
|
+
* coupling to any specific ORM or database.
|
|
49
|
+
*/
|
|
50
|
+
export interface IOutboxStore {
|
|
51
|
+
/**
|
|
52
|
+
* Persist one or more domain events to the outbox table.
|
|
53
|
+
*
|
|
54
|
+
* MUST be called within the same database transaction as the aggregate write
|
|
55
|
+
* so that both succeed or both roll back together.
|
|
56
|
+
*
|
|
57
|
+
* Each event's `eventId` becomes the primary key (`id`) of the outbox row.
|
|
58
|
+
* The `status` column is always set to `"pending"` initially.
|
|
59
|
+
*
|
|
60
|
+
* @param events - The domain events to persist.
|
|
61
|
+
* @param tx - Optional ORM-specific transaction context (e.g. a Prisma interactive tx client, a Drizzle tx, or a TypeORM EntityManager).
|
|
62
|
+
*/
|
|
63
|
+
save(events: IDomainEvent[], tx?: unknown): Promise<void>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch a batch of events whose status is still `"pending"`.
|
|
67
|
+
* Entries are returned in insertion order (oldest first) so that
|
|
68
|
+
* earlier events are retried before later ones.
|
|
69
|
+
*
|
|
70
|
+
* The publisher calls this periodically and attempts to publish each entry.
|
|
71
|
+
* On success it calls {@link markPublished}; on failure it calls {@link markFailed}.
|
|
72
|
+
*
|
|
73
|
+
* @param batchSize - Maximum number of entries to return. Default 50.
|
|
74
|
+
* @returns A result object containing the matching entries.
|
|
75
|
+
*/
|
|
76
|
+
fetchPending(batchSize?: number): Promise<OutboxFetchResult>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mark an outbox entry as successfully published.
|
|
80
|
+
*
|
|
81
|
+
* Mapping: `UPDATE outbox SET status = 'published' WHERE id = :eventId`
|
|
82
|
+
*
|
|
83
|
+
* @param id - The event's `eventId` (same value used as the outbox primary key).
|
|
84
|
+
*/
|
|
85
|
+
markPublished(id: string): Promise<void>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mark an outbox entry as failed, incrementing its retry counter
|
|
89
|
+
* and recording the error that caused the failure.
|
|
90
|
+
*
|
|
91
|
+
* Mapping: `UPDATE outbox SET status = 'failed', retries = retries + 1, lastError = :error WHERE id = :eventId`
|
|
92
|
+
*
|
|
93
|
+
* @param id - The event's `eventId`.
|
|
94
|
+
* @param error - The error message.
|
|
95
|
+
*/
|
|
96
|
+
markFailed(id: string, error: string): Promise<void>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface StandardSchemaIssue {
|
|
2
|
+
message: string;
|
|
3
|
+
path?: ReadonlyArray<unknown>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface StandardSchemaResult<T> {
|
|
7
|
+
value?: T;
|
|
8
|
+
issues?: ReadonlyArray<StandardSchemaIssue>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StandardSchemaProps<T> {
|
|
12
|
+
validate: (
|
|
13
|
+
value: unknown
|
|
14
|
+
) => StandardSchemaResult<T> | Promise<StandardSchemaResult<T>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StandardSchema<T = unknown> {
|
|
18
|
+
"~standard": StandardSchemaProps<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Aggregate } from "../core/index";
|
|
2
|
+
import { Repository } from "../repository/base-repository.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transaction context for Unit of Work
|
|
6
|
+
*/
|
|
7
|
+
export interface TransactionContext {
|
|
8
|
+
/**
|
|
9
|
+
* Commit all changes
|
|
10
|
+
*/
|
|
11
|
+
commit(): Promise<void>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rollback all changes
|
|
15
|
+
*/
|
|
16
|
+
rollback(): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if transaction is active
|
|
20
|
+
*/
|
|
21
|
+
isActive(): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Unit of Work interface
|
|
26
|
+
* Manages transactions across multiple repositories
|
|
27
|
+
*/
|
|
28
|
+
export interface IUnitOfWork {
|
|
29
|
+
/**
|
|
30
|
+
* Start a new transaction
|
|
31
|
+
*/
|
|
32
|
+
begin(): Promise<TransactionContext>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute work within a transaction
|
|
36
|
+
* Auto-commits on success, rolls back on error
|
|
37
|
+
*/
|
|
38
|
+
transaction<T>(work: (ctx: TransactionContext) => Promise<T>): Promise<T>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get repository within transaction context
|
|
42
|
+
*/
|
|
43
|
+
getRepository<TDomain extends Aggregate<any>>(
|
|
44
|
+
repository: new (...args: any[]) => Repository<TDomain>
|
|
45
|
+
): Repository<TDomain>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Id, ValueObject } from "../core/index";
|
|
2
|
+
|
|
3
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
4
|
+
|
|
5
|
+
export type DeepJsonResult<T> = T extends Id
|
|
6
|
+
? string
|
|
7
|
+
: T extends ValueObject<infer U>
|
|
8
|
+
? U
|
|
9
|
+
: T extends Date
|
|
10
|
+
? string
|
|
11
|
+
: T extends Array<infer U>
|
|
12
|
+
? DeepJsonResult<U>[]
|
|
13
|
+
: T extends { toJSON(): infer R }
|
|
14
|
+
? DeepJsonResult<R>
|
|
15
|
+
: T extends object
|
|
16
|
+
? { [K in keyof T]: DeepJsonResult<T[K]> }
|
|
17
|
+
: T extends JsonPrimitive
|
|
18
|
+
? T
|
|
19
|
+
: never;
|
|
20
|
+
|
|
21
|
+
export type Primitive = string | number | boolean | Date | null | undefined;
|
|
22
|
+
export type UnwrapArray<T> = T extends Array<infer U> ? U : never;
|
|
23
|
+
export type IsArray<T> = T extends Array<any> ? true : false;
|
|
24
|
+
export type NonUndefined<T> = T extends undefined ? never : T;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { FILTER_OPERATORS } from "../index.js";
|
|
2
|
+
import {
|
|
3
|
+
ArrayOperators,
|
|
4
|
+
BooleanOperators,
|
|
5
|
+
DateOperators,
|
|
6
|
+
FilterOperator,
|
|
7
|
+
NumberOperators,
|
|
8
|
+
StringOperators,
|
|
9
|
+
} from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
const FORCE_STRING_OPERATORS = new Set(["contains", "startsWith", "endsWith"]);
|
|
12
|
+
|
|
13
|
+
export function sanitizeFieldValue(
|
|
14
|
+
value: unknown,
|
|
15
|
+
operator: FilterOperator
|
|
16
|
+
): unknown {
|
|
17
|
+
if (value === null || value === undefined) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map((item) => sanitizeFieldValue(item, operator));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value instanceof Date) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stringValue = String(value).trim();
|
|
30
|
+
|
|
31
|
+
if (stringValue === "") {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (operator && FORCE_STRING_OPERATORS.has(operator)) {
|
|
36
|
+
return stringValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (stringValue === "true" || stringValue === "false") {
|
|
40
|
+
return stringValue === "true";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const numberValue = Number(stringValue);
|
|
44
|
+
if (!Number.isNaN(numberValue)) {
|
|
45
|
+
return numberValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const dateObj = new Date(stringValue);
|
|
49
|
+
if (!Number.isNaN(dateObj.getTime())) {
|
|
50
|
+
return dateObj;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return stringValue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isValidOperatorForType(
|
|
57
|
+
value: unknown,
|
|
58
|
+
operator: FilterOperator
|
|
59
|
+
): boolean {
|
|
60
|
+
const sanitizedValue = sanitizeFieldValue(value, operator);
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
operator === "between" &&
|
|
64
|
+
Array.isArray(sanitizedValue) &&
|
|
65
|
+
sanitizedValue.length === 2
|
|
66
|
+
) {
|
|
67
|
+
const elementType = typeof sanitizedValue[0];
|
|
68
|
+
if (elementType === "number" || sanitizedValue[0] instanceof Date) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valueType = typeof sanitizedValue;
|
|
75
|
+
|
|
76
|
+
if (valueType === "string") {
|
|
77
|
+
const validOps: StringOperators[] = [
|
|
78
|
+
"equals",
|
|
79
|
+
"notEquals",
|
|
80
|
+
"contains",
|
|
81
|
+
"startsWith",
|
|
82
|
+
"endsWith",
|
|
83
|
+
"in",
|
|
84
|
+
"notIn",
|
|
85
|
+
"isNull",
|
|
86
|
+
"isNotNull",
|
|
87
|
+
];
|
|
88
|
+
return validOps.includes(operator as StringOperators);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (valueType === "number") {
|
|
92
|
+
const validOps: NumberOperators[] = [
|
|
93
|
+
"equals",
|
|
94
|
+
"notEquals",
|
|
95
|
+
"greaterThan",
|
|
96
|
+
"greaterThanOrEqual",
|
|
97
|
+
"lessThan",
|
|
98
|
+
"lessThanOrEqual",
|
|
99
|
+
"in",
|
|
100
|
+
"notIn",
|
|
101
|
+
"between",
|
|
102
|
+
"isNull",
|
|
103
|
+
"isNotNull",
|
|
104
|
+
];
|
|
105
|
+
return validOps.includes(operator as NumberOperators);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (valueType === "boolean") {
|
|
109
|
+
const validOps: BooleanOperators[] = [
|
|
110
|
+
"equals",
|
|
111
|
+
"notEquals",
|
|
112
|
+
"isNull",
|
|
113
|
+
"isNotNull",
|
|
114
|
+
];
|
|
115
|
+
return validOps.includes(operator as BooleanOperators);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (sanitizedValue instanceof Date) {
|
|
119
|
+
const validOps: DateOperators[] = [
|
|
120
|
+
"equals",
|
|
121
|
+
"notEquals",
|
|
122
|
+
"greaterThan",
|
|
123
|
+
"greaterThanOrEqual",
|
|
124
|
+
"lessThan",
|
|
125
|
+
"lessThanOrEqual",
|
|
126
|
+
"in",
|
|
127
|
+
"notIn",
|
|
128
|
+
"between",
|
|
129
|
+
"isNull",
|
|
130
|
+
"isNotNull",
|
|
131
|
+
];
|
|
132
|
+
return validOps.includes(operator as DateOperators);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(sanitizedValue)) {
|
|
136
|
+
const validOps: ArrayOperators[] = ["in", "notIn", "isNull", "isNotNull"];
|
|
137
|
+
return validOps.includes(operator as ArrayOperators);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getValidOperatorsForType(value: unknown): FilterOperator[] {
|
|
144
|
+
if (value === null || value === undefined) {
|
|
145
|
+
return ["isNull", "isNotNull", "equals", "notEquals"];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const valueType = typeof value;
|
|
149
|
+
|
|
150
|
+
if (valueType === "string" && Number.isNaN(Number(value))) {
|
|
151
|
+
return [
|
|
152
|
+
"equals",
|
|
153
|
+
"notEquals",
|
|
154
|
+
"contains",
|
|
155
|
+
"startsWith",
|
|
156
|
+
"endsWith",
|
|
157
|
+
"in",
|
|
158
|
+
"notIn",
|
|
159
|
+
"isNull",
|
|
160
|
+
"isNotNull",
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (valueType === "number") {
|
|
165
|
+
return [
|
|
166
|
+
"equals",
|
|
167
|
+
"notEquals",
|
|
168
|
+
"greaterThan",
|
|
169
|
+
"greaterThanOrEqual",
|
|
170
|
+
"lessThan",
|
|
171
|
+
"lessThanOrEqual",
|
|
172
|
+
"in",
|
|
173
|
+
"notIn",
|
|
174
|
+
"between",
|
|
175
|
+
"isNull",
|
|
176
|
+
"isNotNull",
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (valueType === "boolean") {
|
|
181
|
+
return ["equals", "notEquals", "isNull", "isNotNull"];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (value instanceof Date) {
|
|
185
|
+
return [
|
|
186
|
+
"equals",
|
|
187
|
+
"notEquals",
|
|
188
|
+
"greaterThan",
|
|
189
|
+
"greaterThanOrEqual",
|
|
190
|
+
"lessThan",
|
|
191
|
+
"lessThanOrEqual",
|
|
192
|
+
"in",
|
|
193
|
+
"notIn",
|
|
194
|
+
"between",
|
|
195
|
+
"isNull",
|
|
196
|
+
"isNotNull",
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Array.isArray(value)) {
|
|
201
|
+
return ["in", "notIn", "isNull", "isNotNull"];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return [...FILTER_OPERATORS];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isOperator(value: string): value is FilterOperator {
|
|
208
|
+
return FILTER_OPERATORS.includes(value as FilterOperator);
|
|
209
|
+
}
|