@woltz/rich-domain 1.2.0 → 1.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/CHANGELOG.md +33 -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 +117 -86
- package/dist/base-entity.js.map +1 -1
- package/dist/criteria.d.ts +3 -3
- package/dist/criteria.d.ts.map +1 -1
- 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/entity-changes.d.ts +84 -0
- package/dist/entity-changes.d.ts.map +1 -0
- package/dist/entity-changes.js +135 -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 +219 -0
- package/dist/entity-schema-registry.js.map +1 -0
- package/dist/history-tracker.d.ts +97 -0
- package/dist/history-tracker.d.ts.map +1 -0
- package/dist/history-tracker.js +805 -0
- package/dist/history-tracker.js.map +1 -0
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/repository/base-repository.d.ts +6 -32
- 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/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 -25
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/change-tracker.d.ts +186 -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/history-tracker.d.ts +11 -0
- package/dist/types/history-tracker.d.ts.map +1 -1
- package/dist/types/utils.d.ts +0 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/validation-error.d.ts.map +1 -1
- package/dist/validation-error.js +0 -3
- 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 -21
- 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 +140 -100
- package/src/criteria.ts +2 -1
- package/src/crypto.ts +31 -0
- package/src/entity-changes.ts +151 -0
- package/src/entity-schema-registry.ts +275 -0
- package/src/history-tracker.ts +1114 -0
- package/src/id.ts +17 -26
- package/src/index.ts +1 -0
- package/src/mapper.ts +4 -1
- package/src/repository/base-repository.ts +6 -37
- package/src/repository/unit-of-work.ts +0 -25
- package/src/types/change-tracker.ts +221 -0
- package/src/types/criteria.ts +6 -1
- package/src/types/history-tracker.ts +13 -0
- package/src/types/utils.ts +0 -9
- package/src/validation-error.ts +0 -4
- package/src/value-object.ts +84 -23
- 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/src/deep-proxy.ts +0 -447
- package/tests/entity.test.ts +0 -33
package/src/base-entity.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Base Entity Class with Standard Schema Validation
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
1
|
import { Id } from "./id";
|
|
6
|
-
import { DeepProxy } from "./deep-proxy";
|
|
7
2
|
import { ValidationError } from "./validation-error";
|
|
8
3
|
import { IDomainEvent } from ".";
|
|
9
4
|
import {
|
|
10
5
|
BaseProps,
|
|
11
|
-
SubscriptionConfig,
|
|
12
6
|
HistoryEntry,
|
|
13
7
|
DeepJsonResult,
|
|
14
8
|
EntityHooks,
|
|
@@ -19,6 +13,8 @@ import {
|
|
|
19
13
|
import { DomainEventBus } from "./domain-event-bus";
|
|
20
14
|
import { DEFAULT_VALIDATION_CONFIG } from "./constants";
|
|
21
15
|
import { DomainError } from "./exceptions";
|
|
16
|
+
import { HistoryTracker } from "./history-tracker";
|
|
17
|
+
import { AggregateChanges } from "./aggregate-changes";
|
|
22
18
|
|
|
23
19
|
// Helper to get static properties from constructor
|
|
24
20
|
function getStaticProperty<T>(
|
|
@@ -30,7 +26,7 @@ function getStaticProperty<T>(
|
|
|
30
26
|
|
|
31
27
|
export abstract class BaseEntity<T extends BaseProps> {
|
|
32
28
|
private _props: T;
|
|
33
|
-
private
|
|
29
|
+
private tracker: HistoryTracker;
|
|
34
30
|
private proxiedProps: T;
|
|
35
31
|
private snapshot: T | null = null;
|
|
36
32
|
private validationConfig: Required<ValidationConfig>;
|
|
@@ -74,8 +70,16 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
this._props = finalProps;
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
|
|
74
|
+
// Initialize tracker and proxy
|
|
75
|
+
this.tracker = new HistoryTracker(this._props, this.constructor.name);
|
|
76
|
+
|
|
77
|
+
// Setup validation on update BEFORE creating proxy
|
|
78
|
+
if (this.validationConfig.onUpdate) {
|
|
79
|
+
this.setupUpdateValidation();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.proxiedProps = this.tracker.createProxy();
|
|
79
83
|
|
|
80
84
|
// Execute rules (custom validations)
|
|
81
85
|
if (hooks?.rules) {
|
|
@@ -87,15 +91,14 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
87
91
|
hooks.onCreate(this as any);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
// Setup update validation
|
|
91
|
-
if (this.entitySchema && this.validationConfig.onUpdate) {
|
|
92
|
-
this.setupUpdateValidation();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
94
|
// Take initial snapshot for onBeforeUpdate
|
|
96
95
|
this.takeSnapshot();
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Validation
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
99
102
|
private validateProps(props: T): void {
|
|
100
103
|
if (!this.entitySchema) return;
|
|
101
104
|
|
|
@@ -119,7 +122,6 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
119
122
|
throw validationError;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
// If not throwing, store error for later retrieval
|
|
123
125
|
(this as any)._validationError = validationError;
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -128,90 +130,105 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
128
130
|
if (pathSegment === null || pathSegment === undefined) {
|
|
129
131
|
return "";
|
|
130
132
|
}
|
|
131
|
-
// Handle PropertyKey (string | number | symbol)
|
|
132
133
|
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
133
134
|
return String(pathSegment);
|
|
134
135
|
}
|
|
135
136
|
if (typeof pathSegment === "symbol") {
|
|
136
137
|
return pathSegment.toString();
|
|
137
138
|
}
|
|
138
|
-
// Handle object with 'key' property (Zod's PathSegment)
|
|
139
139
|
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
140
140
|
return String((pathSegment as { key: unknown }).key);
|
|
141
141
|
}
|
|
142
|
-
// Fallback
|
|
143
142
|
return String(pathSegment);
|
|
144
143
|
}
|
|
145
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Setup validation that runs on every property change.
|
|
147
|
+
* Uses the HistoryTracker's onChangeValidator callback.
|
|
148
|
+
*/
|
|
146
149
|
private setupUpdateValidation(): void {
|
|
147
150
|
const self = this;
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
this.tracker.setOnChangeValidator((path, oldValue, newValue) => {
|
|
153
|
+
// Temporarily apply the change to validate
|
|
154
|
+
const originalValue = self._props[path as keyof T];
|
|
155
|
+
(self._props as any)[path] = newValue;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Check onBeforeUpdate hook
|
|
159
|
+
if (self.entityHooks?.onBeforeUpdate && self.snapshot) {
|
|
160
|
+
const shouldContinue = self.entityHooks.onBeforeUpdate(
|
|
161
|
+
self as any,
|
|
162
|
+
self.snapshot
|
|
163
|
+
);
|
|
164
|
+
if (!shouldContinue) {
|
|
165
|
+
// Revert change
|
|
166
|
+
(self._props as any)[path] = originalValue;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
160
169
|
}
|
|
161
|
-
}
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
// Validate with schema
|
|
172
|
+
if (self.entitySchema) {
|
|
173
|
+
const result = self.entitySchema["~standard"].validate(self._props);
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
if (result instanceof Promise) {
|
|
176
|
+
console.warn(
|
|
177
|
+
"Async validation on update not supported. Consider using sync validation."
|
|
178
|
+
);
|
|
179
|
+
(self._props as any)[path] = originalValue;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
if (result.issues && result.issues.length > 0) {
|
|
184
|
+
const validationError = new ValidationError(
|
|
185
|
+
result.issues.map((issue) => ({
|
|
186
|
+
path: issue.path?.map((p) => self.extractPathKey(p)) || [],
|
|
187
|
+
message: issue.message,
|
|
188
|
+
}))
|
|
189
|
+
);
|
|
181
190
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
// Revert change before throwing
|
|
192
|
+
(self._props as any)[path] = originalValue;
|
|
193
|
+
|
|
194
|
+
if (self.validationConfig.throwOnError) {
|
|
195
|
+
throw validationError;
|
|
186
196
|
}
|
|
187
|
-
|
|
197
|
+
|
|
198
|
+
console.error("Validation failed on update:", validationError);
|
|
199
|
+
return false;
|
|
188
200
|
}
|
|
189
|
-
console.error("Validation failed on update:", validationError);
|
|
190
201
|
}
|
|
191
|
-
}
|
|
192
202
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
// Execute rules after schema validation
|
|
204
|
+
if (self.entityHooks?.rules) {
|
|
205
|
+
try {
|
|
206
|
+
self.entityHooks.rules(self as any);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Revert change before throwing
|
|
209
|
+
(self._props as any)[path] = originalValue;
|
|
210
|
+
|
|
211
|
+
if (self.validationConfig.throwOnError) {
|
|
212
|
+
throw error;
|
|
202
213
|
}
|
|
203
|
-
|
|
214
|
+
|
|
215
|
+
console.error("Rules validation failed on update:", error);
|
|
216
|
+
return false;
|
|
204
217
|
}
|
|
205
|
-
console.error("Rules validation failed on update:", error);
|
|
206
218
|
}
|
|
207
|
-
}
|
|
208
219
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
220
|
+
// Revert for now - the actual set will happen in the proxy
|
|
221
|
+
(self._props as any)[path] = originalValue;
|
|
212
222
|
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
// Update snapshot after successful validation
|
|
224
|
+
// Note: snapshot is updated after the change is applied
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// Revert on any error
|
|
228
|
+
(self._props as any)[path] = originalValue;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
215
232
|
}
|
|
216
233
|
|
|
217
234
|
private takeSnapshot(): void {
|
|
@@ -220,36 +237,28 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
220
237
|
|
|
221
238
|
private deepCloneProps(obj: any, seen: WeakSet<object> = new WeakSet()): any {
|
|
222
239
|
if (obj === null || obj === undefined) return obj;
|
|
223
|
-
|
|
224
|
-
// Primitives
|
|
225
240
|
if (typeof obj !== "object") return obj;
|
|
226
|
-
|
|
227
|
-
// Special cases - don't clone these, just return the reference
|
|
228
241
|
if (obj instanceof Id) return obj;
|
|
229
242
|
if (obj instanceof Date) return new Date(obj.getTime());
|
|
230
243
|
|
|
231
|
-
// Check for circular references
|
|
232
244
|
if (seen.has(obj)) {
|
|
233
|
-
return obj;
|
|
245
|
+
return obj;
|
|
234
246
|
}
|
|
235
247
|
|
|
236
|
-
// Handle BaseEntity instances - just keep the reference
|
|
237
248
|
if (obj instanceof BaseEntity) {
|
|
238
249
|
return obj;
|
|
239
250
|
}
|
|
240
251
|
|
|
241
|
-
// Handle ValueObject instances - just keep the reference (they're immutable)
|
|
242
252
|
if (
|
|
243
253
|
obj.constructor &&
|
|
244
254
|
obj.constructor.name !== "Object" &&
|
|
245
255
|
obj.constructor.name !== "Array"
|
|
246
256
|
) {
|
|
247
|
-
// Check if it has toJson method (likely a ValueObject or similar)
|
|
248
257
|
if (
|
|
249
258
|
typeof obj.toJson === "function" &&
|
|
250
259
|
typeof obj.equals === "function"
|
|
251
260
|
) {
|
|
252
|
-
return obj;
|
|
261
|
+
return obj;
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
264
|
|
|
@@ -259,7 +268,6 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
259
268
|
return obj.map((item) => this.deepCloneProps(item, seen));
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
// Plain objects only
|
|
263
271
|
if (obj.constructor === Object) {
|
|
264
272
|
const cloned: any = {};
|
|
265
273
|
for (const key in obj) {
|
|
@@ -270,10 +278,13 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
270
278
|
return cloned;
|
|
271
279
|
}
|
|
272
280
|
|
|
273
|
-
// For other object types (custom classes), just keep the reference
|
|
274
281
|
return obj;
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Identity
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
277
288
|
get id(): Id {
|
|
278
289
|
return this._props.id;
|
|
279
290
|
}
|
|
@@ -284,27 +295,20 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
284
295
|
|
|
285
296
|
/**
|
|
286
297
|
* Check equality with another entity by comparing IDs
|
|
287
|
-
* Entities are equal if they have the same ID, regardless of other properties
|
|
288
|
-
*
|
|
289
|
-
* @param other - Another entity or an ID to compare with
|
|
290
|
-
* @returns true if entities have the same ID
|
|
291
298
|
*/
|
|
292
299
|
equals(other: BaseEntity<T> | Id | string): boolean {
|
|
293
300
|
if (!other) {
|
|
294
301
|
return false;
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
// Compare with another entity
|
|
298
304
|
if (other instanceof BaseEntity) {
|
|
299
305
|
return this.id.equals(other.id);
|
|
300
306
|
}
|
|
301
307
|
|
|
302
|
-
// Compare with an ID
|
|
303
308
|
if (other instanceof Id) {
|
|
304
309
|
return this.id.equals(other);
|
|
305
310
|
}
|
|
306
311
|
|
|
307
|
-
// Compare with a string ID
|
|
308
312
|
if (typeof other === "string") {
|
|
309
313
|
return this.id.equals(other);
|
|
310
314
|
}
|
|
@@ -312,6 +316,10 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
312
316
|
return false;
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Props Access
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
315
323
|
public get props(): T {
|
|
316
324
|
return this.proxiedProps;
|
|
317
325
|
}
|
|
@@ -330,23 +338,47 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
330
338
|
return (this as any)._validationError;
|
|
331
339
|
}
|
|
332
340
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// Change Tracking
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Returns all detected changes as AggregateChanges.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* const changes = user.getChanges();
|
|
351
|
+
* const batch = changes.toBatchOperations();
|
|
352
|
+
*
|
|
353
|
+
* for (const del of batch.deletes) { ... }
|
|
354
|
+
* for (const create of batch.creates) { ... }
|
|
355
|
+
* for (const upd of batch.updates) { ... }
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
getChanges<TEntityMap = Record<string, any>>(): AggregateChanges<TEntityMap> {
|
|
359
|
+
return this.tracker.getChanges<TEntityMap>();
|
|
340
360
|
}
|
|
341
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Returns the change history (for debugging).
|
|
364
|
+
*/
|
|
342
365
|
getHistory(): HistoryEntry[] {
|
|
343
|
-
return this.
|
|
366
|
+
return this.tracker.getHistory();
|
|
344
367
|
}
|
|
345
368
|
|
|
346
|
-
|
|
347
|
-
|
|
369
|
+
/**
|
|
370
|
+
* Clears history and marks entity as "clean".
|
|
371
|
+
* Call this after successfully persisting to the database.
|
|
372
|
+
*/
|
|
373
|
+
markAsClean(): void {
|
|
374
|
+
this.tracker.markAsClean();
|
|
375
|
+
this.takeSnapshot();
|
|
348
376
|
}
|
|
349
377
|
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Domain Events
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
350
382
|
/**
|
|
351
383
|
* Add a domain event to this entity
|
|
352
384
|
*/
|
|
@@ -354,8 +386,12 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
354
386
|
this.domainEvents.push(event);
|
|
355
387
|
}
|
|
356
388
|
|
|
357
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Dispatch all events through the event bus
|
|
391
|
+
*/
|
|
392
|
+
public async dispatchAll(bus: DomainEventBus): Promise<void> {
|
|
358
393
|
await bus.publishAll(this.getUncommittedEvents());
|
|
394
|
+
this.clearEvents();
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
/**
|
|
@@ -379,6 +415,10 @@ export abstract class BaseEntity<T extends BaseProps> {
|
|
|
379
415
|
return this.domainEvents.length > 0;
|
|
380
416
|
}
|
|
381
417
|
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Serialization
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
382
422
|
toJson(): DeepJsonResult<T> {
|
|
383
423
|
return this.deepToJson(this._props) as DeepJsonResult<T>;
|
|
384
424
|
}
|
package/src/criteria.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
PathValue,
|
|
14
14
|
Search,
|
|
15
15
|
TypedFilter,
|
|
16
|
+
TypedOrder,
|
|
16
17
|
} from "./types";
|
|
17
18
|
import {
|
|
18
19
|
isValidOperatorForType,
|
|
@@ -278,7 +279,7 @@ export class Criteria<T = any> {
|
|
|
278
279
|
static fromObject<T>(
|
|
279
280
|
obj: {
|
|
280
281
|
filters?: TypedFilter<T>[];
|
|
281
|
-
orders?:
|
|
282
|
+
orders?: TypedOrder<T>[];
|
|
282
283
|
pagination?: Pagination;
|
|
283
284
|
search?: { fields: FieldPath<T>[]; value: string };
|
|
284
285
|
},
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const customCrypto = {
|
|
2
|
+
randomUUID: () => {
|
|
3
|
+
if (
|
|
4
|
+
typeof globalThis !== "undefined" &&
|
|
5
|
+
globalThis?.crypto &&
|
|
6
|
+
typeof globalThis.crypto.randomUUID === "function"
|
|
7
|
+
) {
|
|
8
|
+
return globalThis.crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const hexChars = "0123456789abcdef";
|
|
12
|
+
let uuid = "";
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < 36; i++) {
|
|
15
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
16
|
+
uuid += "-";
|
|
17
|
+
} else if (i === 14) {
|
|
18
|
+
uuid += "4";
|
|
19
|
+
} else if (i === 19) {
|
|
20
|
+
uuid += hexChars.charAt(Math.floor(Math.random() * 4) + 8);
|
|
21
|
+
} else {
|
|
22
|
+
uuid += hexChars.charAt(Math.floor(Math.random() * 16));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return uuid;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const UUID = customCrypto.randomUUID;
|
|
31
|
+
export default UUID;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Entity Changes
|
|
3
|
+
// Changes filtered by entity type
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Operation,
|
|
8
|
+
CreateOperation,
|
|
9
|
+
UpdateOperation,
|
|
10
|
+
DeleteOperation,
|
|
11
|
+
} from "./types/change-tracker";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents the changes filtered for a specific entity.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const changes = user.getChanges();
|
|
19
|
+
* const postChanges = changes.for('Post');
|
|
20
|
+
*
|
|
21
|
+
* if (postChanges.hasCreates()) {
|
|
22
|
+
* console.log('Created posts:', postChanges.creates);
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* if (postChanges.hasUpdates()) {
|
|
26
|
+
* postChanges.updates.forEach(({ entity, changed }) => {
|
|
27
|
+
* console.log(`Post ${entity.id} has changed:`, changed);
|
|
28
|
+
* });
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class EntityChanges<T = any> {
|
|
33
|
+
constructor(private readonly operations: Operation<T>[]) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns all created entities
|
|
37
|
+
*/
|
|
38
|
+
get creates(): T[] {
|
|
39
|
+
return this.operations
|
|
40
|
+
.filter((op): op is CreateOperation<T> => op.type === "create")
|
|
41
|
+
.map((op) => op.data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns all updated entities with their changed fields
|
|
46
|
+
*/
|
|
47
|
+
get updates(): Array<{ entity: T; changed: Record<string, any> }> {
|
|
48
|
+
return this.operations
|
|
49
|
+
.filter((op): op is UpdateOperation<T> => op.type === "update")
|
|
50
|
+
.map((op) => ({
|
|
51
|
+
entity: op.data,
|
|
52
|
+
changed: op.changedFields,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns all deleted entities
|
|
58
|
+
*/
|
|
59
|
+
get deletes(): T[] {
|
|
60
|
+
return this.operations
|
|
61
|
+
.filter((op): op is DeleteOperation<T> => op.type === "delete")
|
|
62
|
+
.map((op) => op.data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the IDs of the created entities
|
|
67
|
+
*/
|
|
68
|
+
get createIds(): string[] {
|
|
69
|
+
return this.operations
|
|
70
|
+
.filter((op): op is CreateOperation<T> => op.type === "create")
|
|
71
|
+
.map((op) => this.extractId(op.data))
|
|
72
|
+
.filter((id): id is string => id !== undefined);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the IDs of the updated entities
|
|
77
|
+
*/
|
|
78
|
+
get updateIds(): string[] {
|
|
79
|
+
return this.operations
|
|
80
|
+
.filter((op): op is UpdateOperation<T> => op.type === "update")
|
|
81
|
+
.map((op) => op.id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the IDs of the deleted entities
|
|
86
|
+
*/
|
|
87
|
+
get deleteIds(): string[] {
|
|
88
|
+
return this.operations
|
|
89
|
+
.filter((op): op is DeleteOperation<T> => op.type === "delete")
|
|
90
|
+
.map((op) => op.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Checks if there are any creates
|
|
95
|
+
*/
|
|
96
|
+
hasCreates(): boolean {
|
|
97
|
+
return this.creates.length > 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Checks if there are any updates
|
|
102
|
+
*/
|
|
103
|
+
hasUpdates(): boolean {
|
|
104
|
+
return this.updates.length > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Checks if there are any deletes
|
|
109
|
+
*/
|
|
110
|
+
hasDeletes(): boolean {
|
|
111
|
+
return this.deletes.length > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Checks if there is any change
|
|
116
|
+
*/
|
|
117
|
+
hasChanges(): boolean {
|
|
118
|
+
return this.operations.length > 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Checks if it is empty (no changes)
|
|
123
|
+
*/
|
|
124
|
+
isEmpty(): boolean {
|
|
125
|
+
return this.operations.length === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns the total number of operations
|
|
130
|
+
*/
|
|
131
|
+
get count(): number {
|
|
132
|
+
return this.operations.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns the raw operations (for advanced use cases)
|
|
137
|
+
*/
|
|
138
|
+
get rawOperations(): Operation<T>[] {
|
|
139
|
+
return [...this.operations];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extracts the ID from an entity
|
|
144
|
+
*/
|
|
145
|
+
private extractId(entity: any): string | undefined {
|
|
146
|
+
if (!entity) return undefined;
|
|
147
|
+
if (entity.id?.value) return entity.id.value;
|
|
148
|
+
if (typeof entity.id === "string") return entity.id;
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|