@woltz/rich-domain 0.2.1
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/.github/workflows/ci.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.versionrc.json +21 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +712 -0
- package/commitlint.config.js +23 -0
- package/dist/base-entity.d.ts +67 -0
- package/dist/base-entity.d.ts.map +1 -0
- package/dist/base-entity.js +309 -0
- package/dist/base-entity.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/criteria.d.ts +60 -0
- package/dist/criteria.d.ts.map +1 -0
- package/dist/criteria.js +214 -0
- package/dist/criteria.js.map +1 -0
- package/dist/deep-proxy.d.ts +34 -0
- package/dist/deep-proxy.d.ts.map +1 -0
- package/dist/deep-proxy.js +297 -0
- package/dist/deep-proxy.js.map +1 -0
- package/dist/domain-event-bus.d.ts +57 -0
- package/dist/domain-event-bus.d.ts.map +1 -0
- package/dist/domain-event-bus.js +112 -0
- package/dist/domain-event-bus.js.map +1 -0
- package/dist/domain-event.d.ts +55 -0
- package/dist/domain-event.d.ts.map +1 -0
- package/dist/domain-event.js +42 -0
- package/dist/domain-event.js.map +1 -0
- package/dist/entity.d.ts +13 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +15 -0
- package/dist/entity.js.map +1 -0
- package/dist/filtering.d.ts +107 -0
- package/dist/filtering.d.ts.map +1 -0
- package/dist/filtering.js +202 -0
- package/dist/filtering.js.map +1 -0
- package/dist/id.d.ts +51 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +84 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ordering.d.ts +93 -0
- package/dist/ordering.d.ts.map +1 -0
- package/dist/ordering.js +154 -0
- package/dist/ordering.js.map +1 -0
- package/dist/paginated-result.d.ts +62 -0
- package/dist/paginated-result.d.ts.map +1 -0
- package/dist/paginated-result.js +201 -0
- package/dist/paginated-result.js.map +1 -0
- package/dist/pagination.d.ts +218 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +281 -0
- package/dist/pagination.js.map +1 -0
- package/dist/repository/base-repository.d.ts +77 -0
- package/dist/repository/base-repository.d.ts.map +1 -0
- package/dist/repository/base-repository.js +80 -0
- package/dist/repository/base-repository.js.map +1 -0
- package/dist/repository/in-memory-repository.d.ts +46 -0
- package/dist/repository/in-memory-repository.d.ts.map +1 -0
- package/dist/repository/in-memory-repository.js +85 -0
- package/dist/repository/in-memory-repository.js.map +1 -0
- package/dist/repository/index.d.ts +42 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +47 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/repository/mapper.d.ts +56 -0
- package/dist/repository/mapper.d.ts.map +1 -0
- package/dist/repository/mapper.js +15 -0
- package/dist/repository/mapper.js.map +1 -0
- package/dist/repository/types.d.ts +87 -0
- package/dist/repository/types.d.ts.map +1 -0
- package/dist/repository/types.js +6 -0
- package/dist/repository/types.js.map +1 -0
- package/dist/repository/unit-of-work.d.ts +70 -0
- package/dist/repository/unit-of-work.d.ts.map +1 -0
- package/dist/repository/unit-of-work.js +122 -0
- package/dist/repository/unit-of-work.js.map +1 -0
- package/dist/repository.d.ts +2 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +21 -0
- package/dist/repository.js.map +1 -0
- package/dist/specification.d.ts +102 -0
- package/dist/specification.d.ts.map +1 -0
- package/dist/specification.js +187 -0
- package/dist/specification.js.map +1 -0
- package/dist/types/criteria.d.ts +35 -0
- package/dist/types/criteria.d.ts.map +1 -0
- package/dist/types/criteria.js +17 -0
- package/dist/types/criteria.js.map +1 -0
- package/dist/types/domain.d.ts +30 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/domain.js +2 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/history-tracker.d.ts +36 -0
- package/dist/types/history-tracker.d.ts.map +1 -0
- package/dist/types/history-tracker.js +2 -0
- package/dist/types/history-tracker.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/repository.d.ts +43 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +2 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/standard-schema.d.ts +15 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/types/unit-of-work.d.ts +39 -0
- package/dist/types/unit-of-work.d.ts.map +1 -0
- package/dist/types/unit-of-work.js +2 -0
- package/dist/types/unit-of-work.js.map +1 -0
- package/dist/types/utils.d.ts +14 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +2 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/validation-error.d.ts +42 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +73 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/value-object.d.ts +47 -0
- package/dist/value-object.d.ts.map +1 -0
- package/dist/value-object.js +136 -0
- package/dist/value-object.js.map +1 -0
- package/eslint.config.js +51 -0
- package/jest.config.js +21 -0
- package/package.json +58 -0
- package/src/base-entity.ts +401 -0
- package/src/constants.ts +7 -0
- package/src/criteria.ts +291 -0
- package/src/deep-proxy.ts +339 -0
- package/src/domain-event-bus.ts +166 -0
- package/src/domain-event.ts +90 -0
- package/src/entity.ts +16 -0
- package/src/id.ts +94 -0
- package/src/index.ts +33 -0
- package/src/paginated-result.ts +274 -0
- package/src/repository/base-repository.ts +152 -0
- package/src/repository/in-memory-repository.ts +104 -0
- package/src/repository/index.ts +55 -0
- package/src/repository/mapper.ts +74 -0
- package/src/repository/unit-of-work.ts +148 -0
- package/src/types/criteria.ts +79 -0
- package/src/types/domain.ts +37 -0
- package/src/types/history-tracker.ts +45 -0
- package/src/types/index.ts +7 -0
- package/src/types/repository.ts +51 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +29 -0
- package/src/validation-error.ts +97 -0
- package/src/value-object.ts +187 -0
- package/tests/criteria.test.ts +432 -0
- package/tests/domain-events.test.ts +445 -0
- package/tests/entity-equality.test.ts +487 -0
- package/tests/entity-validation.test.ts +339 -0
- package/tests/entity.test.ts +33 -0
- package/tests/history-tracker.spec.ts +667 -0
- package/tests/id.test.ts +341 -0
- package/tests/repository.test.ts +641 -0
- package/tests/to-json.test.ts +91 -0
- package/tests/utils.ts +151 -0
- package/tests/value-object-validation.test.ts +228 -0
- package/tests/value-objects.test.ts +52 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Unit of Work - Simple transaction management
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { IUnitOfWork, TransactionContext } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Abstract Unit of Work
|
|
9
|
+
* Provides transaction management across multiple repositories
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Using transaction helper (recommended)
|
|
14
|
+
* await uow.transaction(async (ctx) => {
|
|
15
|
+
* const userRepo = uow.getRepository(UserRepository);
|
|
16
|
+
* const orderRepo = uow.getRepository(OrderRepository);
|
|
17
|
+
*
|
|
18
|
+
* await userRepo.save(user);
|
|
19
|
+
* await orderRepo.save(order);
|
|
20
|
+
*
|
|
21
|
+
* // Auto-commits on success, rolls back on error
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Manual control
|
|
25
|
+
* const ctx = await uow.begin();
|
|
26
|
+
* try {
|
|
27
|
+
* await userRepo.save(user);
|
|
28
|
+
* await orderRepo.save(order);
|
|
29
|
+
* await ctx.commit();
|
|
30
|
+
* } catch (error) {
|
|
31
|
+
* await ctx.rollback();
|
|
32
|
+
* throw error;
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export abstract class UnitOfWork implements IUnitOfWork {
|
|
37
|
+
protected currentContext: TransactionContext | null = null;
|
|
38
|
+
protected repositoryCache: Map<string, any> = new Map();
|
|
39
|
+
|
|
40
|
+
abstract begin(): Promise<TransactionContext>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute work within a transaction
|
|
44
|
+
* Auto-commits on success, rolls back on error
|
|
45
|
+
*/
|
|
46
|
+
async transaction<T>(
|
|
47
|
+
work: (ctx: TransactionContext) => Promise<T>
|
|
48
|
+
): Promise<T> {
|
|
49
|
+
const ctx = await this.begin();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await work(ctx);
|
|
53
|
+
await ctx.commit();
|
|
54
|
+
return result;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (ctx.isActive()) {
|
|
57
|
+
await ctx.rollback();
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
} finally {
|
|
61
|
+
this.currentContext = null;
|
|
62
|
+
this.repositoryCache.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get repository instance (cached per transaction)
|
|
68
|
+
*/
|
|
69
|
+
getRepository<TRepo>(
|
|
70
|
+
RepositoryClass: new (...args: any[]) => TRepo
|
|
71
|
+
): TRepo {
|
|
72
|
+
const key = RepositoryClass.name;
|
|
73
|
+
|
|
74
|
+
if (this.repositoryCache.has(key)) {
|
|
75
|
+
return this.repositoryCache.get(key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const repo = this.createRepository(RepositoryClass);
|
|
79
|
+
this.repositoryCache.set(key, repo);
|
|
80
|
+
return repo;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create repository instance - implement in subclass
|
|
85
|
+
*/
|
|
86
|
+
protected abstract createRepository<TRepo>(
|
|
87
|
+
RepositoryClass: new (...args: any[]) => TRepo
|
|
88
|
+
): TRepo;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Base Transaction Context
|
|
93
|
+
*/
|
|
94
|
+
export abstract class BaseTransactionContext implements TransactionContext {
|
|
95
|
+
protected _isActive = true;
|
|
96
|
+
|
|
97
|
+
abstract commit(): Promise<void>;
|
|
98
|
+
abstract rollback(): Promise<void>;
|
|
99
|
+
|
|
100
|
+
isActive(): boolean {
|
|
101
|
+
return this._isActive;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected markInactive(): void {
|
|
105
|
+
this._isActive = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* In-Memory Unit of Work (for testing)
|
|
111
|
+
*/
|
|
112
|
+
export class InMemoryUnitOfWork extends UnitOfWork {
|
|
113
|
+
private committed = false;
|
|
114
|
+
private rolledBack = false;
|
|
115
|
+
|
|
116
|
+
async begin(): Promise<TransactionContext> {
|
|
117
|
+
this.currentContext = new InMemoryTransactionContext();
|
|
118
|
+
this.committed = false;
|
|
119
|
+
this.rolledBack = false;
|
|
120
|
+
return this.currentContext;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected createRepository<TRepo>(
|
|
124
|
+
RepositoryClass: new (...args: any[]) => TRepo
|
|
125
|
+
): TRepo {
|
|
126
|
+
// For in-memory, just create a new instance
|
|
127
|
+
// In real implementation, pass transaction client
|
|
128
|
+
return new RepositoryClass();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isCommitted(): boolean {
|
|
132
|
+
return this.committed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
isRolledBack(): boolean {
|
|
136
|
+
return this.rolledBack;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class InMemoryTransactionContext extends BaseTransactionContext {
|
|
141
|
+
async commit(): Promise<void> {
|
|
142
|
+
this.markInactive();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async rollback(): Promise<void> {
|
|
146
|
+
this.markInactive();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Primitive } from "./utils";
|
|
2
|
+
|
|
3
|
+
export const FilterOperator = [
|
|
4
|
+
"equals",
|
|
5
|
+
"notEquals",
|
|
6
|
+
"greaterThan",
|
|
7
|
+
"greaterThanOrEqual",
|
|
8
|
+
"lessThan",
|
|
9
|
+
"lessThanOrEqual",
|
|
10
|
+
"contains",
|
|
11
|
+
"startsWith",
|
|
12
|
+
"endsWith",
|
|
13
|
+
"in",
|
|
14
|
+
"notIn",
|
|
15
|
+
"between",
|
|
16
|
+
"isNull",
|
|
17
|
+
"isNotNull",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type FilterValueFor<T> =
|
|
21
|
+
| T // equals, notEquals
|
|
22
|
+
| (T extends number | Date
|
|
23
|
+
? [T, T] // between
|
|
24
|
+
: never)
|
|
25
|
+
| T[] // in, notIn
|
|
26
|
+
| null;
|
|
27
|
+
|
|
28
|
+
export type PathValue<
|
|
29
|
+
T,
|
|
30
|
+
P extends string
|
|
31
|
+
> = P extends `${infer K}.${infer Rest}`
|
|
32
|
+
? K extends keyof T
|
|
33
|
+
? PathValue<T[K], Rest>
|
|
34
|
+
: never
|
|
35
|
+
: P extends keyof T
|
|
36
|
+
? T[P]
|
|
37
|
+
: never;
|
|
38
|
+
|
|
39
|
+
export type FilterOperator = (typeof FilterOperator)[number];
|
|
40
|
+
|
|
41
|
+
export interface Filter<TField = string, TValue = unknown> {
|
|
42
|
+
field: TField;
|
|
43
|
+
operator: FilterOperator;
|
|
44
|
+
value: TValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TypedFilter<T> = {
|
|
48
|
+
[K in FieldPath<T>]: Filter<K, FilterValueFor<PathValue<T, K>>>;
|
|
49
|
+
}[FieldPath<T>];
|
|
50
|
+
|
|
51
|
+
export type OrderDirection = "asc" | "desc";
|
|
52
|
+
|
|
53
|
+
export interface Order {
|
|
54
|
+
field: string;
|
|
55
|
+
direction: OrderDirection;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Pagination {
|
|
59
|
+
page: number;
|
|
60
|
+
limit: number;
|
|
61
|
+
offset: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PaginationMeta {
|
|
65
|
+
page: number;
|
|
66
|
+
limit: number;
|
|
67
|
+
total: number;
|
|
68
|
+
totalPages: number;
|
|
69
|
+
hasNext: boolean;
|
|
70
|
+
hasPrevious: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FieldPath<T> = {
|
|
74
|
+
[K in keyof T & string]: T[K] extends Primitive
|
|
75
|
+
? K
|
|
76
|
+
: T[K] extends Array<infer U>
|
|
77
|
+
? K | `${K}.${FieldPath<U>}`
|
|
78
|
+
: K | `${K}.${FieldPath<T[K]>}`;
|
|
79
|
+
}[keyof T & string];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StandardSchema, ValidationConfig } from "..";
|
|
2
|
+
import { Id } from "../id";
|
|
3
|
+
|
|
4
|
+
export type EntityId = string | number;
|
|
5
|
+
|
|
6
|
+
export interface BaseProps {
|
|
7
|
+
id: Id;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DomainValidation<T> {
|
|
11
|
+
schema: StandardSchema<T>;
|
|
12
|
+
config?: ValidationConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type EntityValidation<T> = DomainValidation<T>;
|
|
16
|
+
export type VOValidation<T> = DomainValidation<T>;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export interface VOHooks<T, E> {
|
|
20
|
+
onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
|
|
21
|
+
onCreate?: (entity: E) => void;
|
|
22
|
+
rules?: (entity: E) => void;
|
|
23
|
+
defaultValues?: Partial<T>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Specialized hooks for entities (with BaseProps)
|
|
27
|
+
export interface EntityHooks<T extends BaseProps, E> {
|
|
28
|
+
onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
|
|
29
|
+
onCreate?: (entity: E) => void;
|
|
30
|
+
rules?: (entity: E) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EntityConstructor<T extends BaseProps, E> {
|
|
34
|
+
new (props: T): E;
|
|
35
|
+
validation?: DomainValidation<T>;
|
|
36
|
+
hooks?: EntityHooks<T, E>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BaseProps } from "..";
|
|
2
|
+
import { IsArray, NonUndefined, UnwrapArray } from "./utils";
|
|
3
|
+
|
|
4
|
+
export interface ChangeEvent<T> {
|
|
5
|
+
previous: T | undefined;
|
|
6
|
+
current: T;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ArrayChangeEvent<T> {
|
|
11
|
+
toCreate: T[];
|
|
12
|
+
toUpdate: T[];
|
|
13
|
+
toDelete: T[];
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PropertySubscriber<T> = (event: ChangeEvent<T>) => void;
|
|
18
|
+
export type ArraySubscriber<T> = (event: ArrayChangeEvent<T>) => void;
|
|
19
|
+
|
|
20
|
+
export interface PropertySubscription<T> {
|
|
21
|
+
onChange: PropertySubscriber<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ArraySubscription<T> {
|
|
25
|
+
onChange: ArraySubscriber<T>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SubscriptionConfig<T extends BaseProps> = {
|
|
29
|
+
[K in keyof T]?: IsArray<NonUndefined<T[K]>> extends true
|
|
30
|
+
? ArraySubscription<UnwrapArray<NonUndefined<T[K]>>>
|
|
31
|
+
: PropertySubscription<NonUndefined<T[K]>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ValidationConfig {
|
|
35
|
+
onCreate?: boolean;
|
|
36
|
+
onUpdate?: boolean;
|
|
37
|
+
throwOnError?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HistoryEntry {
|
|
41
|
+
path: string;
|
|
42
|
+
previousValue: any;
|
|
43
|
+
currentValue: any;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Criteria } from "../criteria";
|
|
2
|
+
import { Aggregate } from "../entity";
|
|
3
|
+
import { Id } from "../id";
|
|
4
|
+
import { PaginatedResult } from "../paginated-result";
|
|
5
|
+
|
|
6
|
+
export interface IRepository<TDomain extends Aggregate<any>> {
|
|
7
|
+
/**
|
|
8
|
+
* Find by ID
|
|
9
|
+
*/
|
|
10
|
+
findById(id: Id): Promise<TDomain | null>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find using criteria (filtering, ordering, pagination)
|
|
14
|
+
*/
|
|
15
|
+
find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find all (with optional criteria)
|
|
19
|
+
*/
|
|
20
|
+
findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find one (first matching criteria)
|
|
24
|
+
*/
|
|
25
|
+
findOne(criteria: Criteria<TDomain>): Promise<TDomain | null>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Save (insert or update based on aggregate.isNew)
|
|
29
|
+
*/
|
|
30
|
+
save(aggregate: TDomain): Promise<void>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Delete aggregate
|
|
34
|
+
*/
|
|
35
|
+
delete(aggregate: TDomain): Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Delete by ID
|
|
39
|
+
*/
|
|
40
|
+
deleteById(id: Id): Promise<void>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if exists
|
|
44
|
+
*/
|
|
45
|
+
exists(id: Id): Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Count matching criteria
|
|
49
|
+
*/
|
|
50
|
+
count(criteria?: Criteria<TDomain>): Promise<number>;
|
|
51
|
+
}
|
|
@@ -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 "../entity";
|
|
2
|
+
import { IRepository } from "./repository";
|
|
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[]) => IRepository<TDomain>
|
|
45
|
+
): IRepository<TDomain>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Id } from "../id";
|
|
2
|
+
|
|
3
|
+
export type DeepJsonResult<T> = {
|
|
4
|
+
[K in keyof T]: T[K] extends Id
|
|
5
|
+
? string
|
|
6
|
+
: T[K] extends { toJson(): infer U }
|
|
7
|
+
? U
|
|
8
|
+
: T[K] extends Array<infer U>
|
|
9
|
+
? U extends { toJson(): infer V }
|
|
10
|
+
? V[]
|
|
11
|
+
: U extends Id
|
|
12
|
+
? string[]
|
|
13
|
+
: U[]
|
|
14
|
+
: T[K];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DeepKeyOf<T, K extends keyof T = keyof T> = K extends string
|
|
18
|
+
? T[K] extends Primitive
|
|
19
|
+
? K
|
|
20
|
+
: T[K] extends object
|
|
21
|
+
? `${K}` | `${K}.${DeepKeyOf<T[K]>}`
|
|
22
|
+
: never
|
|
23
|
+
: never;
|
|
24
|
+
|
|
25
|
+
export type Primitive = string | number | boolean | Date | null | undefined;
|
|
26
|
+
|
|
27
|
+
export type UnwrapArray<T> = T extends Array<infer U> ? U : never;
|
|
28
|
+
export type IsArray<T> = T extends Array<any> ? true : false;
|
|
29
|
+
export type NonUndefined<T> = T extends undefined ? never : T;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Validation Error - Domain Validation Errors
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface ValidationIssue {
|
|
6
|
+
path: string[];
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ValidationError extends Error {
|
|
11
|
+
public readonly issues: ValidationIssue[];
|
|
12
|
+
public readonly __isValidationError = true; // Brand for identification
|
|
13
|
+
|
|
14
|
+
constructor(issues: ValidationIssue[], message?: string) {
|
|
15
|
+
const errorMessage =
|
|
16
|
+
message || `Validation failed: ${issues.map(i => i.message).join(', ')}`;
|
|
17
|
+
super(errorMessage);
|
|
18
|
+
this.name = 'ValidationError';
|
|
19
|
+
this.issues = issues;
|
|
20
|
+
|
|
21
|
+
// Maintain proper stack trace
|
|
22
|
+
if (Error.captureStackTrace) {
|
|
23
|
+
Error.captureStackTrace(this, ValidationError);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if an error is a ValidationError (works across module boundaries)
|
|
29
|
+
*/
|
|
30
|
+
static isValidationError(error: unknown): error is ValidationError {
|
|
31
|
+
if (error instanceof ValidationError) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Check by duck typing for cross-module compatibility
|
|
35
|
+
return (
|
|
36
|
+
error instanceof Error &&
|
|
37
|
+
error.name === 'ValidationError' &&
|
|
38
|
+
'issues' in error &&
|
|
39
|
+
Array.isArray((error as any).issues)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all error messages as a simple array
|
|
45
|
+
*/
|
|
46
|
+
getMessages(): string[] {
|
|
47
|
+
return this.issues.map(i => i.message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get errors for a specific field path
|
|
52
|
+
*/
|
|
53
|
+
getErrorsForPath(path: string): ValidationIssue[] {
|
|
54
|
+
return this.issues.filter(i => i.path.join('.') === path);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a specific path has errors
|
|
59
|
+
*/
|
|
60
|
+
hasErrorsForPath(path: string): boolean {
|
|
61
|
+
return this.getErrorsForPath(path).length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert to a plain object for serialization
|
|
66
|
+
*/
|
|
67
|
+
toJSON(): { name: string; message: string; issues: ValidationIssue[] } {
|
|
68
|
+
return {
|
|
69
|
+
name: this.name,
|
|
70
|
+
message: this.message,
|
|
71
|
+
issues: this.issues,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Helper to create a single validation issue
|
|
78
|
+
*/
|
|
79
|
+
export function createValidationIssue(
|
|
80
|
+
path: string | string[],
|
|
81
|
+
message: string
|
|
82
|
+
): ValidationIssue {
|
|
83
|
+
return {
|
|
84
|
+
path: Array.isArray(path) ? path : path.split('.'),
|
|
85
|
+
message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Helper to throw a validation error with a single issue
|
|
91
|
+
*/
|
|
92
|
+
export function throwValidationError(
|
|
93
|
+
path: string | string[],
|
|
94
|
+
message: string
|
|
95
|
+
): never {
|
|
96
|
+
throw new ValidationError([createValidationIssue(path, message)]);
|
|
97
|
+
}
|