@woltz/rich-domain 1.2.0 → 1.2.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/CHANGELOG.md +58 -0
- package/dist/aggregate-changes.d.ts +164 -0
- package/dist/aggregate-changes.d.ts.map +1 -0
- package/dist/aggregate-changes.js +281 -0
- package/dist/aggregate-changes.js.map +1 -0
- package/dist/base-entity.d.ts +32 -8
- package/dist/base-entity.d.ts.map +1 -1
- package/dist/base-entity.js +86 -93
- package/dist/base-entity.js.map +1 -1
- package/dist/change-tracker.d.ts +97 -0
- package/dist/change-tracker.d.ts.map +1 -0
- package/dist/change-tracker.js +758 -0
- package/dist/change-tracker.js.map +1 -0
- package/dist/constants.d.ts +7 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +65 -0
- package/dist/constants.js.map +1 -1
- package/dist/criteria.d.ts +3 -3
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +6 -4
- package/dist/criteria.js.map +1 -1
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +29 -0
- package/dist/crypto.js.map +1 -0
- package/dist/domain-event.d.ts.map +1 -1
- package/dist/domain-event.js +0 -3
- package/dist/domain-event.js.map +1 -1
- package/dist/entity-changes.d.ts +84 -0
- package/dist/entity-changes.d.ts.map +1 -0
- package/dist/entity-changes.js +131 -0
- package/dist/entity-changes.js.map +1 -0
- package/dist/entity-schema-registry.d.ts +148 -0
- package/dist/entity-schema-registry.d.ts.map +1 -0
- package/dist/entity-schema-registry.js +213 -0
- package/dist/entity-schema-registry.js.map +1 -0
- package/dist/entity.d.ts +0 -6
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +0 -9
- package/dist/entity.js.map +1 -1
- package/dist/id.d.ts +11 -10
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +4 -28
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -11
- package/dist/index.js.map +1 -1
- package/dist/mapper.d.ts +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/mapper.js.map +1 -1
- package/dist/paginated-result.d.ts.map +1 -1
- package/dist/paginated-result.js +0 -15
- package/dist/paginated-result.js.map +1 -1
- package/dist/repository/base-repository.d.ts +7 -33
- package/dist/repository/base-repository.d.ts.map +1 -1
- package/dist/repository/base-repository.js +0 -27
- package/dist/repository/base-repository.js.map +1 -1
- package/dist/repository/index.d.ts.map +1 -1
- package/dist/repository/index.js +0 -6
- package/dist/repository/index.js.map +1 -1
- package/dist/repository/unit-of-work.d.ts +0 -25
- package/dist/repository/unit-of-work.d.ts.map +1 -1
- package/dist/repository/unit-of-work.js +0 -28
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/change-tracker.d.ts +196 -0
- package/dist/types/change-tracker.d.ts.map +1 -0
- package/dist/types/change-tracker.js +2 -0
- package/dist/types/change-tracker.js.map +1 -0
- package/dist/types/criteria.d.ts +5 -1
- package/dist/types/criteria.d.ts.map +1 -1
- package/dist/types/domain.d.ts +4 -6
- package/dist/types/domain.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/utils.d.ts +0 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/utils/criteria-operator-validation.d.ts +1 -0
- package/dist/utils/criteria-operator-validation.d.ts.map +1 -1
- package/dist/utils/criteria-operator-validation.js +39 -17
- package/dist/utils/criteria-operator-validation.js.map +1 -1
- package/dist/validation-error.d.ts.map +1 -1
- package/dist/validation-error.js +1 -6
- package/dist/validation-error.js.map +1 -1
- package/dist/value-object.d.ts +57 -8
- package/dist/value-object.d.ts.map +1 -1
- package/dist/value-object.js +49 -22
- package/dist/value-object.js.map +1 -1
- package/package.json +2 -1
- package/src/aggregate-changes.ts +335 -0
- package/src/base-entity.ts +102 -109
- package/src/change-tracker.ts +1062 -0
- package/src/constants.ts +75 -1
- package/src/criteria.ts +11 -4
- package/src/crypto.ts +31 -0
- package/src/domain-event.ts +0 -4
- package/src/entity-changes.ts +146 -0
- package/src/entity-schema-registry.ts +255 -0
- package/src/entity.ts +0 -11
- package/src/id.ts +17 -26
- package/src/index.ts +15 -19
- package/src/mapper.ts +4 -1
- package/src/paginated-result.ts +0 -21
- package/src/repository/base-repository.ts +7 -38
- package/src/repository/index.ts +0 -9
- package/src/repository/unit-of-work.ts +0 -29
- package/src/types/change-tracker.ts +233 -0
- package/src/types/criteria.ts +6 -1
- package/src/types/domain.ts +4 -8
- package/src/types/index.ts +1 -1
- package/src/types/utils.ts +0 -9
- package/src/utils/criteria-operator-validation.ts +57 -19
- package/src/validation-error.ts +1 -7
- package/src/value-object.ts +84 -24
- package/tests/aggregate-changes.test.ts +284 -0
- package/tests/criteria.test.ts +122 -161
- package/tests/entity-equality.test.ts +38 -61
- package/tests/entity-schema-registry.test.ts +382 -0
- package/tests/entity-validation.test.ts +7 -94
- package/tests/history-tracker.spec.ts +349 -617
- package/tests/id.test.ts +41 -44
- package/tests/load-test/data.json +346041 -0
- package/tests/load-test/entities.ts +97 -0
- package/tests/load-test/generate-data.ts +81 -0
- package/tests/load-test/lead-to-domain.mapper.ts +24 -0
- package/tests/load-test/load.test.ts +38 -0
- package/tests/repository.test.ts +30 -54
- package/tests/to-json.test.ts +14 -18
- package/tests/utils.ts +138 -102
- package/tests/value-objects.test.ts +57 -29
- package/dist/deep-proxy.d.ts +0 -36
- package/dist/deep-proxy.d.ts.map +0 -1
- package/dist/deep-proxy.js +0 -384
- package/dist/deep-proxy.js.map +0 -1
- package/dist/types/history-tracker.d.ts +0 -36
- package/dist/types/history-tracker.d.ts.map +0 -1
- package/dist/types/history-tracker.js +0 -2
- package/dist/types/history-tracker.js.map +0 -1
- package/src/deep-proxy.ts +0 -447
- package/src/types/history-tracker.ts +0 -45
- package/tests/entity.test.ts +0 -33
package/src/mapper.ts
CHANGED
package/src/paginated-result.ts
CHANGED
|
@@ -2,10 +2,6 @@ import { Id } from "./id";
|
|
|
2
2
|
import type { Criteria } from "./criteria";
|
|
3
3
|
import type { Pagination, PaginationMeta, Filter } from "./types";
|
|
4
4
|
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// Type Utilities
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
5
|
/**
|
|
10
6
|
* Infers the JSON result type from T
|
|
11
7
|
* - If T has toJson(), returns its return type
|
|
@@ -21,10 +17,6 @@ export type PaginatedJsonResult<T> = {
|
|
|
21
17
|
meta: PaginationMeta;
|
|
22
18
|
};
|
|
23
19
|
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// PaginatedResult Class
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
20
|
export class PaginatedResult<T> {
|
|
29
21
|
constructor(
|
|
30
22
|
public readonly data: T[],
|
|
@@ -78,13 +70,11 @@ export class PaginatedResult<T> {
|
|
|
78
70
|
total = result.length;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
// Apply filters
|
|
82
73
|
for (const filter of criteria.getFilters()) {
|
|
83
74
|
result = result.filter((item) => applyFilter(item, filter));
|
|
84
75
|
total = result.length;
|
|
85
76
|
}
|
|
86
77
|
|
|
87
|
-
// Apply ordering
|
|
88
78
|
for (const order of criteria.getOrders().reverse()) {
|
|
89
79
|
result.sort((a, b) => {
|
|
90
80
|
const aVal = getNestedValue(a, order.field);
|
|
@@ -98,7 +88,6 @@ export class PaginatedResult<T> {
|
|
|
98
88
|
});
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
// Apply pagination
|
|
102
91
|
const pagination = criteria.getPagination();
|
|
103
92
|
if (pagination && !criteria.hasSearch()) {
|
|
104
93
|
result = result.slice(
|
|
@@ -108,7 +97,6 @@ export class PaginatedResult<T> {
|
|
|
108
97
|
return PaginatedResult.create(result, pagination, total);
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
// No pagination - return all with default meta
|
|
112
100
|
return PaginatedResult.create(
|
|
113
101
|
result,
|
|
114
102
|
{ page: 1, limit: result.length, offset: 0 },
|
|
@@ -140,20 +128,16 @@ export class PaginatedResult<T> {
|
|
|
140
128
|
private deepSerialize(obj: any): any {
|
|
141
129
|
if (obj === null || obj === undefined) return obj;
|
|
142
130
|
|
|
143
|
-
// Id → string
|
|
144
131
|
if (obj instanceof Id) return obj.value;
|
|
145
132
|
|
|
146
|
-
// Arrays → map recursively
|
|
147
133
|
if (Array.isArray(obj)) {
|
|
148
134
|
return obj.map((item) => this.deepSerialize(item));
|
|
149
135
|
}
|
|
150
136
|
|
|
151
|
-
// Objects with toJson() method (Entity/Aggregate/ValueObject)
|
|
152
137
|
if (obj && typeof obj.toJson === "function") {
|
|
153
138
|
return obj.toJson();
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
// Plain objects → serialize properties recursively
|
|
157
141
|
if (typeof obj === "object") {
|
|
158
142
|
const result: any = {};
|
|
159
143
|
for (const key in obj) {
|
|
@@ -164,7 +148,6 @@ export class PaginatedResult<T> {
|
|
|
164
148
|
return result;
|
|
165
149
|
}
|
|
166
150
|
|
|
167
|
-
// Primitives
|
|
168
151
|
return obj;
|
|
169
152
|
}
|
|
170
153
|
|
|
@@ -190,10 +173,6 @@ export class PaginatedResult<T> {
|
|
|
190
173
|
}
|
|
191
174
|
}
|
|
192
175
|
|
|
193
|
-
// ============================================================================
|
|
194
|
-
// Helper Functions (moved from criteria.ts)
|
|
195
|
-
// ============================================================================
|
|
196
|
-
|
|
197
176
|
function applyFilter<T>(item: T, filter: Filter): boolean {
|
|
198
177
|
const value = getNestedValue(item, filter.field);
|
|
199
178
|
|
|
@@ -1,57 +1,26 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Base Repository - Abstract implementation with common logic
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
1
|
import type { Aggregate } from "../entity";
|
|
6
2
|
import type { Criteria } from "../criteria";
|
|
7
3
|
import { PaginatedResult } from "../paginated-result";
|
|
8
4
|
import { Mapper } from "../mapper";
|
|
9
5
|
|
|
10
|
-
/**
|
|
11
|
-
* Abstract base repository
|
|
12
|
-
* Implements common logic, delegates persistence to subclasses
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* class UserRepository extends BaseRepository<User, PrismaUser> {
|
|
17
|
-
* constructor(prisma: PrismaClient) {
|
|
18
|
-
* super(new UserMapper());
|
|
19
|
-
* this.prisma = prisma;
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* protected async insertOne(data: PrismaUser): Promise<PrismaUser> {
|
|
23
|
-
* return this.prisma.user.create({ data });
|
|
24
|
-
* }
|
|
25
|
-
*
|
|
26
|
-
* protected async updateOne(id: string, data: PrismaUser): Promise<PrismaUser> {
|
|
27
|
-
* return this.prisma.user.update({ where: { id }, data });
|
|
28
|
-
* }
|
|
29
|
-
*
|
|
30
|
-
* // ... implement other abstract methods
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
6
|
export abstract class ReadRepository<Agg extends Aggregate<any>> {
|
|
36
|
-
abstract find(criteria
|
|
7
|
+
abstract find(criteria?: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
|
|
37
8
|
abstract findById(id: string): Promise<Agg | null>;
|
|
38
|
-
abstract count(criteria
|
|
9
|
+
abstract count(criteria?: Criteria<Agg>): Promise<number>;
|
|
39
10
|
abstract exists(id: string): Promise<boolean>;
|
|
40
11
|
}
|
|
41
12
|
|
|
42
13
|
export abstract class WriteRepository<Agg extends Aggregate<any>> {
|
|
43
|
-
abstract
|
|
44
|
-
abstract update(entity: Agg): Promise<void>;
|
|
14
|
+
abstract save(entity: Agg): Promise<void>;
|
|
45
15
|
abstract delete(entity: Agg): Promise<void>;
|
|
46
16
|
}
|
|
47
17
|
|
|
48
18
|
export abstract class WriteAndRead<Agg extends Aggregate<any>> {
|
|
49
|
-
abstract find(criteria
|
|
19
|
+
abstract find(criteria?: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
|
|
50
20
|
abstract findById(id: string): Promise<Agg | null>;
|
|
51
|
-
abstract
|
|
52
|
-
abstract update(entity: Agg): Promise<void>;
|
|
21
|
+
abstract save(entity: Agg): Promise<void>;
|
|
53
22
|
abstract delete(entity: Agg): Promise<void>;
|
|
54
|
-
abstract count(criteria
|
|
23
|
+
abstract count(criteria?: Criteria<Agg>): Promise<number>;
|
|
55
24
|
abstract exists(id: string): Promise<boolean>;
|
|
56
25
|
}
|
|
57
26
|
|
|
@@ -60,5 +29,5 @@ export abstract class Repository<
|
|
|
60
29
|
> extends WriteAndRead<TDomain> {
|
|
61
30
|
protected abstract readonly mapperToDomain: Mapper<unknown, TDomain>;
|
|
62
31
|
protected abstract readonly mapperToPersistence: Mapper<TDomain, unknown>;
|
|
63
|
-
abstract get model(): any;
|
|
32
|
+
protected abstract get model(): any;
|
|
64
33
|
}
|
package/src/repository/index.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Repository Module - Clean exports
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
// Mapper
|
|
6
1
|
export { Mapper } from "../mapper";
|
|
7
|
-
|
|
8
|
-
// Base implementations
|
|
9
2
|
export * from "./base-repository";
|
|
10
|
-
|
|
11
|
-
// Unit of Work
|
|
12
3
|
export { UnitOfWork, BaseTransactionContext } from "./unit-of-work";
|
|
@@ -1,37 +1,8 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Unit of Work - Simple transaction management
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
1
|
import type { IUnitOfWork, TransactionContext } from "../types";
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
4
|
* Abstract Unit of Work
|
|
9
5
|
* 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
6
|
*/
|
|
36
7
|
export abstract class UnitOfWork implements IUnitOfWork {
|
|
37
8
|
protected currentContext: TransactionContext | null = null;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Entity } from "../entity";
|
|
2
|
+
import { ValueObject } from "../value-object";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base operation with common information.
|
|
6
|
+
*/
|
|
7
|
+
export interface BaseOperation {
|
|
8
|
+
/** Entity name in the domain (e.g., 'User', 'Post', 'Comment') */
|
|
9
|
+
entity: string;
|
|
10
|
+
/** Depth in the aggregate tree (0 = root, 1 = direct children, etc.) */
|
|
11
|
+
depth: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create operation.
|
|
16
|
+
*/
|
|
17
|
+
export interface CreateOperation<T = any> extends BaseOperation {
|
|
18
|
+
type: "create";
|
|
19
|
+
/** Entity data to be created */
|
|
20
|
+
data: T;
|
|
21
|
+
/** Parent ID (for FK) */
|
|
22
|
+
parentId?: string;
|
|
23
|
+
/** Parent entity name */
|
|
24
|
+
parentEntity?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Update operation.
|
|
29
|
+
*/
|
|
30
|
+
export interface UpdateOperation<T = any> extends BaseOperation {
|
|
31
|
+
type: "update";
|
|
32
|
+
/** Entity ID */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Current entity instance */
|
|
35
|
+
data: T;
|
|
36
|
+
/** Only fields that have changed */
|
|
37
|
+
changedFields: Record<string, any>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Delete operation.
|
|
42
|
+
*/
|
|
43
|
+
export interface DeleteOperation<T = any> extends BaseOperation {
|
|
44
|
+
type: "delete";
|
|
45
|
+
/** Entity ID to be deleted */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Entity data (for reference) */
|
|
48
|
+
data: T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Union of all possible operations.
|
|
53
|
+
*/
|
|
54
|
+
export type Operation<T = any> =
|
|
55
|
+
| CreateOperation<T>
|
|
56
|
+
| UpdateOperation<T>
|
|
57
|
+
| DeleteOperation<T>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Item for batch creation.
|
|
61
|
+
*/
|
|
62
|
+
export interface BatchCreateItem<T = any> {
|
|
63
|
+
/** Entity data */
|
|
64
|
+
data: T;
|
|
65
|
+
/** Parent ID (for FK) */
|
|
66
|
+
parentId?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Item for batch update.
|
|
71
|
+
*/
|
|
72
|
+
export interface BatchUpdateItem {
|
|
73
|
+
/** Entity ID */
|
|
74
|
+
id: string;
|
|
75
|
+
/** Fields that have changed */
|
|
76
|
+
changedFields: Record<string, any>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Grouped and ordered operations for batch execution.
|
|
81
|
+
*/
|
|
82
|
+
export interface BatchOperations {
|
|
83
|
+
/**
|
|
84
|
+
* Deletes grouped by entity, ordered by depth descending (leaf → root).
|
|
85
|
+
*/
|
|
86
|
+
deletes: Array<{
|
|
87
|
+
entity: string;
|
|
88
|
+
depth: number;
|
|
89
|
+
ids: string[];
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates grouped by entity, ordered by depth ascending (root → leaf).
|
|
94
|
+
*/
|
|
95
|
+
creates: Array<{
|
|
96
|
+
entity: string;
|
|
97
|
+
depth: number;
|
|
98
|
+
items: BatchCreateItem[];
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Updates grouped by entity.
|
|
103
|
+
*/
|
|
104
|
+
updates: Array<{
|
|
105
|
+
entity: string;
|
|
106
|
+
items: BatchUpdateItem[];
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Changes detected in a collection (1:N).
|
|
112
|
+
*/
|
|
113
|
+
export interface CollectionChanges<T = any> {
|
|
114
|
+
/** Created items */
|
|
115
|
+
created: T[];
|
|
116
|
+
/** Updated items with their changes */
|
|
117
|
+
updated: Array<{
|
|
118
|
+
entity: T;
|
|
119
|
+
changes: Record<string, { from: any; to: any }>;
|
|
120
|
+
}>;
|
|
121
|
+
/** Deleted items */
|
|
122
|
+
deleted: T[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Possible states for a 1:1 relationship.
|
|
127
|
+
*/
|
|
128
|
+
export type EntityChangeState =
|
|
129
|
+
| "created" // null → Entity
|
|
130
|
+
| "updated" // Entity(id:1) → Entity(id:1) with changes
|
|
131
|
+
| "deleted" // Entity → null
|
|
132
|
+
| "replaced" // Entity(id:1) → Entity(id:2)
|
|
133
|
+
| "unchanged"; // No changes
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Change in a 1:1 entity relationship.
|
|
137
|
+
*/
|
|
138
|
+
export interface EntityChange<T = any> {
|
|
139
|
+
/** State of the change */
|
|
140
|
+
state: EntityChangeState;
|
|
141
|
+
/** Current entity (null if deleted) */
|
|
142
|
+
current: T | null;
|
|
143
|
+
/** Previous entity (null if created) */
|
|
144
|
+
previous: T | null;
|
|
145
|
+
/** Field changes (if state === 'updated') */
|
|
146
|
+
changes?: Record<string, { from: any; to: any }>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Change in a primitive field.
|
|
151
|
+
*/
|
|
152
|
+
export interface FieldChange<T = any> {
|
|
153
|
+
from: T;
|
|
154
|
+
to: T;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extracts the props type from an Entity or ValueObject.
|
|
159
|
+
*/
|
|
160
|
+
export type ExtractProps<T> = T extends Entity<infer P>
|
|
161
|
+
? P
|
|
162
|
+
: T extends ValueObject<infer P>
|
|
163
|
+
? P
|
|
164
|
+
: never;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Keys of primitive properties (not Entity, ValueObject or Array).
|
|
168
|
+
*/
|
|
169
|
+
export type PrimitiveKeys<T> = {
|
|
170
|
+
[K in keyof T]: T[K] extends
|
|
171
|
+
| Entity<any>
|
|
172
|
+
| ValueObject<any>
|
|
173
|
+
| Array<any>
|
|
174
|
+
| undefined
|
|
175
|
+
? never
|
|
176
|
+
: K;
|
|
177
|
+
}[keyof T];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Keys of collections (arrays).
|
|
181
|
+
*/
|
|
182
|
+
export type CollectionKeys<T> = {
|
|
183
|
+
[K in keyof T]: T[K] extends Array<any> ? K : never;
|
|
184
|
+
}[keyof T];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Keys of single entities (1:1).
|
|
188
|
+
*/
|
|
189
|
+
export type SingleEntityKeys<T> = {
|
|
190
|
+
[K in keyof T]: T[K] extends Entity<any> | ValueObject<any> | null | undefined
|
|
191
|
+
? T[K] extends Array<any>
|
|
192
|
+
? never
|
|
193
|
+
: K
|
|
194
|
+
: never;
|
|
195
|
+
}[keyof T];
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Change history entry.
|
|
199
|
+
*/
|
|
200
|
+
export interface HistoryEntry {
|
|
201
|
+
path: string;
|
|
202
|
+
previousValue: any;
|
|
203
|
+
currentValue: any;
|
|
204
|
+
timestamp: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Metadata from a tracked entity/VO.
|
|
209
|
+
*/
|
|
210
|
+
export interface TrackedEntityMetadata {
|
|
211
|
+
/** Entity name */
|
|
212
|
+
entityName: string;
|
|
213
|
+
/** Depth in the tree */
|
|
214
|
+
depth: number;
|
|
215
|
+
/** Parent ID */
|
|
216
|
+
parentId?: string;
|
|
217
|
+
/** Parent entity name */
|
|
218
|
+
parentEntity?: string;
|
|
219
|
+
/** Path in the object (e.g., 'posts[0].comments[1]') */
|
|
220
|
+
path: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface TrackedItem {
|
|
224
|
+
entity: any;
|
|
225
|
+
metadata: TrackedEntityMetadata;
|
|
226
|
+
originalState: any;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface ArrayState {
|
|
230
|
+
cloned: any[];
|
|
231
|
+
original: any[];
|
|
232
|
+
metadata: TrackedEntityMetadata;
|
|
233
|
+
}
|
package/src/types/criteria.ts
CHANGED
|
@@ -118,6 +118,11 @@ export interface Order {
|
|
|
118
118
|
direction: OrderDirection;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export type TypedOrder<T> = {
|
|
122
|
+
field: FieldPath<T>;
|
|
123
|
+
direction: OrderDirection;
|
|
124
|
+
};
|
|
125
|
+
|
|
121
126
|
export interface Pagination {
|
|
122
127
|
page: number;
|
|
123
128
|
limit: number;
|
|
@@ -153,5 +158,5 @@ export type FieldPath<T> = T extends Primitive
|
|
|
153
158
|
? U extends Primitive
|
|
154
159
|
? K
|
|
155
160
|
: K | `${K}.${FieldPath<U>}`
|
|
156
|
-
:
|
|
161
|
+
: `${K}.${FieldPath<NonNullable<T[K]>>}`;
|
|
157
162
|
}[ExcludeBuiltInKeys<T> & string];
|
package/src/types/domain.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { ValidationConfig } from "..";
|
|
2
1
|
import { Id } from "../id";
|
|
3
2
|
import { StandardSchema } from "./standard-schema";
|
|
4
3
|
|
|
5
|
-
export type EntityId = string | number;
|
|
6
|
-
|
|
7
4
|
export interface BaseProps {
|
|
8
5
|
id: Id;
|
|
9
6
|
}
|
|
@@ -22,15 +19,14 @@ export interface VOHooks<T, E> {
|
|
|
22
19
|
rules?: (entity: E) => void;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
// Specialized hooks for entities (with BaseProps)
|
|
26
22
|
export interface EntityHooks<T extends BaseProps, E> {
|
|
27
23
|
onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
|
|
28
24
|
onCreate?: (entity: E) => void;
|
|
29
25
|
rules?: (entity: E) => void;
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
export interface ValidationConfig {
|
|
29
|
+
onCreate?: boolean;
|
|
30
|
+
onUpdate?: boolean;
|
|
31
|
+
throwOnError?: boolean;
|
|
36
32
|
}
|
package/src/types/index.ts
CHANGED
package/src/types/utils.ts
CHANGED
|
@@ -14,16 +14,7 @@ export type DeepJsonResult<T> = {
|
|
|
14
14
|
: T[K];
|
|
15
15
|
};
|
|
16
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
17
|
export type Primitive = string | number | boolean | Date | null | undefined;
|
|
26
|
-
|
|
27
18
|
export type UnwrapArray<T> = T extends Array<infer U> ? U : never;
|
|
28
19
|
export type IsArray<T> = T extends Array<any> ? true : false;
|
|
29
20
|
export type NonUndefined<T> = T extends undefined ? never : T;
|
|
@@ -8,28 +8,71 @@ import {
|
|
|
8
8
|
StringOperators,
|
|
9
9
|
} from "../types";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const FORCE_STRING_OPERATORS = new Set(["contains", "startsWith", "endsWith"]);
|
|
12
|
+
|
|
13
|
+
export function sanitizeFieldValue(
|
|
12
14
|
value: unknown,
|
|
13
15
|
operator: FilterOperator
|
|
14
|
-
):
|
|
15
|
-
// Handle null/undefined
|
|
16
|
+
): unknown {
|
|
16
17
|
if (value === null || value === undefined) {
|
|
17
|
-
return
|
|
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;
|
|
18
51
|
}
|
|
19
52
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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) {
|
|
25
69
|
return true;
|
|
26
70
|
}
|
|
27
71
|
return false;
|
|
28
72
|
}
|
|
29
73
|
|
|
30
|
-
const valueType = typeof
|
|
74
|
+
const valueType = typeof sanitizedValue;
|
|
31
75
|
|
|
32
|
-
// String operators
|
|
33
76
|
if (valueType === "string") {
|
|
34
77
|
const validOps: StringOperators[] = [
|
|
35
78
|
"equals",
|
|
@@ -45,7 +88,6 @@ export function isValidOperatorForType(
|
|
|
45
88
|
return validOps.includes(operator as StringOperators);
|
|
46
89
|
}
|
|
47
90
|
|
|
48
|
-
// Number operators
|
|
49
91
|
if (valueType === "number") {
|
|
50
92
|
const validOps: NumberOperators[] = [
|
|
51
93
|
"equals",
|
|
@@ -63,7 +105,6 @@ export function isValidOperatorForType(
|
|
|
63
105
|
return validOps.includes(operator as NumberOperators);
|
|
64
106
|
}
|
|
65
107
|
|
|
66
|
-
// Boolean operators
|
|
67
108
|
if (valueType === "boolean") {
|
|
68
109
|
const validOps: BooleanOperators[] = [
|
|
69
110
|
"equals",
|
|
@@ -74,8 +115,7 @@ export function isValidOperatorForType(
|
|
|
74
115
|
return validOps.includes(operator as BooleanOperators);
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
|
|
78
|
-
if (value instanceof Date) {
|
|
118
|
+
if (sanitizedValue instanceof Date) {
|
|
79
119
|
const validOps: DateOperators[] = [
|
|
80
120
|
"equals",
|
|
81
121
|
"notEquals",
|
|
@@ -92,13 +132,11 @@ export function isValidOperatorForType(
|
|
|
92
132
|
return validOps.includes(operator as DateOperators);
|
|
93
133
|
}
|
|
94
134
|
|
|
95
|
-
|
|
96
|
-
if (Array.isArray(value)) {
|
|
135
|
+
if (Array.isArray(sanitizedValue)) {
|
|
97
136
|
const validOps: ArrayOperators[] = ["in", "notIn", "isNull", "isNotNull"];
|
|
98
137
|
return validOps.includes(operator as ArrayOperators);
|
|
99
138
|
}
|
|
100
139
|
|
|
101
|
-
// For unknown types, allow all operators
|
|
102
140
|
return true;
|
|
103
141
|
}
|
|
104
142
|
|
|
@@ -168,4 +206,4 @@ export function getValidOperatorsForType(value: unknown): FilterOperator[] {
|
|
|
168
206
|
|
|
169
207
|
export function isOperator(value: string): value is FilterOperator {
|
|
170
208
|
return FILTER_OPERATORS.includes(value as FilterOperator);
|
|
171
|
-
}
|
|
209
|
+
}
|
package/src/validation-error.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Validation Error - Domain Validation Errors
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
1
|
export interface ValidationIssue {
|
|
6
2
|
path: string[];
|
|
7
3
|
message: string;
|
|
@@ -9,7 +5,7 @@ export interface ValidationIssue {
|
|
|
9
5
|
|
|
10
6
|
export class ValidationError extends Error {
|
|
11
7
|
public readonly issues: ValidationIssue[];
|
|
12
|
-
public readonly __isValidationError = true;
|
|
8
|
+
public readonly __isValidationError = true;
|
|
13
9
|
|
|
14
10
|
constructor(issues: ValidationIssue[], message?: string) {
|
|
15
11
|
const errorMessage =
|
|
@@ -18,7 +14,6 @@ export class ValidationError extends Error {
|
|
|
18
14
|
this.name = 'ValidationError';
|
|
19
15
|
this.issues = issues;
|
|
20
16
|
|
|
21
|
-
// Maintain proper stack trace
|
|
22
17
|
if (Error.captureStackTrace) {
|
|
23
18
|
Error.captureStackTrace(this, ValidationError);
|
|
24
19
|
}
|
|
@@ -31,7 +26,6 @@ export class ValidationError extends Error {
|
|
|
31
26
|
if (error instanceof ValidationError) {
|
|
32
27
|
return true;
|
|
33
28
|
}
|
|
34
|
-
// Check by duck typing for cross-module compatibility
|
|
35
29
|
return (
|
|
36
30
|
error instanceof Error &&
|
|
37
31
|
error.name === 'ValidationError' &&
|