@woltz/rich-domain 1.1.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 +87 -21
- 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 +31 -15
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +151 -61
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +7 -6
- package/dist/paginated-result.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 +31 -7
- 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/utils/criteria-operator-validation.d.ts +5 -0
- package/dist/utils/criteria-operator-validation.d.ts.map +1 -0
- package/dist/utils/criteria-operator-validation.js +143 -0
- package/dist/utils/criteria-operator-validation.js.map +1 -0
- package/dist/utils/helpers.d.ts +2 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +10 -0
- package/dist/utils/helpers.js.map +1 -0
- 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/eslint.config.js +6 -0
- package/package.json +2 -1
- package/src/aggregate-changes.ts +335 -0
- package/src/base-entity.ts +140 -100
- package/src/criteria.ts +264 -82
- 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 +8 -2
- package/src/mapper.ts +4 -1
- package/src/paginated-result.ts +7 -8
- 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 +95 -17
- package/src/types/history-tracker.ts +13 -0
- package/src/types/utils.ts +0 -9
- package/src/utils/criteria-operator-validation.ts +171 -0
- package/src/utils/helpers.ts +6 -0
- 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 +366 -90
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-object.d.ts","sourceRoot":"","sources":["../src/value-object.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"value-object.d.ts","sourceRoot":"","sources":["../src/value-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,CAAC;AACjC,OAAO,EACL,OAAO,EAGP,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAYjB;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;AAE7D,8BAAsB,WAAW,CAAC,CAAC;IACjC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAG,CAAC,CAAC;IAC7B,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,WAAW,CAAC,CAAkB;IACtC,OAAO,CAAC,YAAY,CAAC,CAAoB;IACzC,OAAO,CAAC,YAAY,CAAsB;IAE1C,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACpD,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAE3C;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAC;gBAE9C,KAAK,EAAE,CAAC;IAqCpB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,cAAc;IAgBtB;;OAEG;IACH,IAAI,mBAAmB,IAAI,OAAO,CAEjC;IAED;;OAEG;IACH,IAAI,gBAAgB,IAAI,eAAe,GAAG,SAAS,CAElD;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO;IAKtC;;;;;;;;;;;;;;OAcG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAiB/B;;OAEG;IACH,cAAc,IAAI,OAAO;IAOzB;;OAEG;IACH,MAAM,CAAC,wBAAwB,CAAC,CAAC,KAAK,qBAAqB,CAAC,CAAC,CAAC,GAAG,SAAS;IAI1E;;OAEG;IACH,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAInD;;OAEG;IACH,oBAAoB,IAAI,YAAY,EAAE;IAItC;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;OAEG;IACH,oBAAoB,IAAI,OAAO;IAI/B,MAAM,IAAI,CAAC;IAIX;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;CAI3C"}
|
package/dist/value-object.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Value Object - Immutable Domain Objects
|
|
3
|
-
// ============================================================================
|
|
4
1
|
import { ValidationError } from "./validation-error";
|
|
5
2
|
import { DEFAULT_VALIDATION_CONFIG } from "./constants";
|
|
6
3
|
import { DomainError } from "./exceptions";
|
|
@@ -11,7 +8,6 @@ function getStaticProperty(instance, propertyName) {
|
|
|
11
8
|
export class ValueObject {
|
|
12
9
|
constructor(props) {
|
|
13
10
|
this.domainEvents = [];
|
|
14
|
-
// Get static configuration from subclass
|
|
15
11
|
const validation = getStaticProperty(this, "validation");
|
|
16
12
|
const hooks = getStaticProperty(this, "hooks");
|
|
17
13
|
this.domainHooks = hooks;
|
|
@@ -23,19 +19,14 @@ export class ValueObject {
|
|
|
23
19
|
...validation?.config,
|
|
24
20
|
};
|
|
25
21
|
let finalProps = { ...props };
|
|
26
|
-
// Validate schema on creation
|
|
27
22
|
if (this.domainSchema && this.validationConfig.onCreate) {
|
|
28
23
|
this.validateProps(finalProps);
|
|
29
24
|
}
|
|
30
|
-
// Set props (not frozen yet) so rules can access them
|
|
31
25
|
this.props = finalProps;
|
|
32
|
-
// Execute rules (custom validations) - after props is set but before freezing
|
|
33
26
|
if (hooks?.rules) {
|
|
34
27
|
hooks.rules(this);
|
|
35
28
|
}
|
|
36
|
-
// Now freeze the props for immutability
|
|
37
29
|
Object.freeze(this.props);
|
|
38
|
-
// Hook onCreate
|
|
39
30
|
if (hooks?.onCreate) {
|
|
40
31
|
hooks.onCreate(this);
|
|
41
32
|
}
|
|
@@ -55,7 +46,6 @@ export class ValueObject {
|
|
|
55
46
|
if (this.validationConfig.throwOnError) {
|
|
56
47
|
throw validationError;
|
|
57
48
|
}
|
|
58
|
-
// If not throwing, store error for later retrieval
|
|
59
49
|
this._validationError = validationError;
|
|
60
50
|
}
|
|
61
51
|
}
|
|
@@ -63,57 +53,95 @@ export class ValueObject {
|
|
|
63
53
|
if (pathSegment === null || pathSegment === undefined) {
|
|
64
54
|
return "";
|
|
65
55
|
}
|
|
66
|
-
// Handle PropertyKey (string | number | symbol)
|
|
67
56
|
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
68
57
|
return String(pathSegment);
|
|
69
58
|
}
|
|
70
59
|
if (typeof pathSegment === "symbol") {
|
|
71
60
|
return pathSegment.toString();
|
|
72
61
|
}
|
|
73
|
-
// Handle object with 'key' property (Zod's PathSegment)
|
|
74
62
|
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
75
63
|
return String(pathSegment.key);
|
|
76
64
|
}
|
|
77
|
-
// Fallback
|
|
78
65
|
return String(pathSegment);
|
|
79
66
|
}
|
|
80
67
|
/**
|
|
81
|
-
*
|
|
68
|
+
* Returns true if the value object has validation errors (when throwOnError is false).
|
|
82
69
|
*/
|
|
83
70
|
get hasValidationErrors() {
|
|
84
71
|
return !!this._validationError;
|
|
85
72
|
}
|
|
86
73
|
/**
|
|
87
|
-
*
|
|
74
|
+
* Returns the validation errors (when throwOnError is false).
|
|
88
75
|
*/
|
|
89
76
|
get validationErrors() {
|
|
90
77
|
return this._validationError;
|
|
91
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Compare this ValueObject with another for equality based on their properties.
|
|
81
|
+
*/
|
|
92
82
|
equals(other) {
|
|
93
83
|
if (!other || !(other instanceof ValueObject))
|
|
94
84
|
return false;
|
|
95
85
|
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
|
96
86
|
}
|
|
97
87
|
/**
|
|
98
|
-
*
|
|
88
|
+
* Returns the identity key for this Value Object.
|
|
89
|
+
* Used for identification in collections when identityKey is set.
|
|
90
|
+
*
|
|
91
|
+
* @returns String with the identity key or null if not defined
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const like = new Like({ postId: 'p1', userId: 'u1' });
|
|
96
|
+
* like.getIdentityKey(); // 'p1:u1'
|
|
97
|
+
*
|
|
98
|
+
* const tag = new TagReference({ tagId: 'tag-123' });
|
|
99
|
+
* tag.getIdentityKey(); // 'tag-123'
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
getIdentityKey() {
|
|
103
|
+
const keyDef = getStaticProperty(this, "identityKey");
|
|
104
|
+
if (!keyDef) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(keyDef)) {
|
|
108
|
+
return keyDef.map((k) => String(this.props[k])).join(":");
|
|
109
|
+
}
|
|
110
|
+
return String(this.props[keyDef]);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Returns true if this Value Object has an identity key defined.
|
|
114
|
+
*/
|
|
115
|
+
hasIdentityKey() {
|
|
116
|
+
return (getStaticProperty(this, "identityKey") !==
|
|
117
|
+
undefined);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns the identity key definition (if any).
|
|
121
|
+
*/
|
|
122
|
+
static getIdentityKeyDefinition() {
|
|
123
|
+
return this.identityKey;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Adds a domain event to this value object.
|
|
99
127
|
*/
|
|
100
128
|
addDomainEvent(event) {
|
|
101
129
|
this.domainEvents.push(event);
|
|
102
130
|
}
|
|
103
131
|
/**
|
|
104
|
-
*
|
|
132
|
+
* Returns all uncommitted domain events.
|
|
105
133
|
*/
|
|
106
134
|
getUncommittedEvents() {
|
|
107
135
|
return [...this.domainEvents];
|
|
108
136
|
}
|
|
109
137
|
/**
|
|
110
|
-
*
|
|
138
|
+
* Clears all domain events (call after publishing).
|
|
111
139
|
*/
|
|
112
140
|
clearEvents() {
|
|
113
141
|
this.domainEvents = [];
|
|
114
142
|
}
|
|
115
143
|
/**
|
|
116
|
-
*
|
|
144
|
+
* Returns true if the value object has uncommitted events.
|
|
117
145
|
*/
|
|
118
146
|
hasUncommittedEvents() {
|
|
119
147
|
return this.domainEvents.length > 0;
|
|
@@ -122,8 +150,8 @@ export class ValueObject {
|
|
|
122
150
|
return { ...this.props };
|
|
123
151
|
}
|
|
124
152
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
153
|
+
* Creates a new ValueObject with updated properties.
|
|
154
|
+
* ValueObjects are immutable, so this returns a new instance.
|
|
127
155
|
*/
|
|
128
156
|
clone(updates) {
|
|
129
157
|
const Constructor = this.constructor;
|
package/dist/value-object.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-object.js","sourceRoot":"","sources":["../src/value-object.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"value-object.js","sourceRoot":"","sources":["../src/value-object.ts"],"names":[],"mappings":"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"}
|
package/eslint.config.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import tsParser from "@typescript-eslint/parser";
|
|
2
2
|
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
3
8
|
|
|
4
9
|
export default [
|
|
5
10
|
{
|
|
@@ -19,6 +24,7 @@ export default [
|
|
|
19
24
|
ecmaVersion: 2020,
|
|
20
25
|
sourceType: "module",
|
|
21
26
|
project: "./tsconfig.json",
|
|
27
|
+
tsconfigRootDir: __dirname,
|
|
22
28
|
},
|
|
23
29
|
globals: {
|
|
24
30
|
console: "readonly",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woltz/rich-domain",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Rich Domain Library with Standard Schema validation support",
|
|
5
5
|
"homepage": "https://github.com/tarcisioandrade/rich-domain",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"typescript"
|
|
42
42
|
],
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@faker-js/faker": "^9.9.0",
|
|
44
45
|
"zod": "^3.24.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Operation,
|
|
3
|
+
CreateOperation,
|
|
4
|
+
UpdateOperation,
|
|
5
|
+
DeleteOperation,
|
|
6
|
+
BatchOperations,
|
|
7
|
+
BatchCreateItem,
|
|
8
|
+
BatchUpdateItem,
|
|
9
|
+
} from "./types/change-tracker";
|
|
10
|
+
import { EntityChanges } from "./entity-changes";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages and organizes the changes of an Aggregate.
|
|
14
|
+
*
|
|
15
|
+
* Responsibilities:
|
|
16
|
+
* - Stores all operations (create, update, delete)
|
|
17
|
+
* - Orders operations respecting FK dependencies
|
|
18
|
+
* - Groups operations by entity for batch execution
|
|
19
|
+
* - Provides query and iteration methods
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Define an entity map for type-safe operations
|
|
24
|
+
* type UserEntities = {
|
|
25
|
+
* User: User;
|
|
26
|
+
* Post: Post;
|
|
27
|
+
* Comment: Comment;
|
|
28
|
+
* };
|
|
29
|
+
*
|
|
30
|
+
* // Getting changes with types
|
|
31
|
+
* const changes = user.getChanges<UserEntities>();
|
|
32
|
+
*
|
|
33
|
+
* // Filtering by entity with autocompletion
|
|
34
|
+
* const postChanges = changes.for('Post'); // 'Post' autocompletes
|
|
35
|
+
* postChanges.creates.forEach(post => {
|
|
36
|
+
* console.log(post.title); // 'post' is typed as Post
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class AggregateChanges<TEntityMap = Record<string, any>> {
|
|
41
|
+
private ops: Operation[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(operations: Operation[] = []) {
|
|
44
|
+
this.ops = [...operations];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a create operation.
|
|
49
|
+
*/
|
|
50
|
+
addCreate<T>(
|
|
51
|
+
entity: string,
|
|
52
|
+
data: T,
|
|
53
|
+
depth: number,
|
|
54
|
+
parentId?: string,
|
|
55
|
+
parentEntity?: string
|
|
56
|
+
): void {
|
|
57
|
+
this.ops.push({
|
|
58
|
+
type: "create",
|
|
59
|
+
entity,
|
|
60
|
+
data,
|
|
61
|
+
depth,
|
|
62
|
+
parentId,
|
|
63
|
+
parentEntity,
|
|
64
|
+
} as CreateOperation<T>);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Adds an update operation.
|
|
69
|
+
*/
|
|
70
|
+
addUpdate<T>(
|
|
71
|
+
entity: string,
|
|
72
|
+
id: string,
|
|
73
|
+
data: T,
|
|
74
|
+
changedFields: Record<string, any>,
|
|
75
|
+
depth: number
|
|
76
|
+
): void {
|
|
77
|
+
this.ops.push({
|
|
78
|
+
type: "update",
|
|
79
|
+
entity,
|
|
80
|
+
id,
|
|
81
|
+
data,
|
|
82
|
+
changedFields,
|
|
83
|
+
depth,
|
|
84
|
+
} as UpdateOperation<T>);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Adds a delete operation.
|
|
89
|
+
*/
|
|
90
|
+
addDelete<T>(entity: string, id: string, data: T, depth: number): void {
|
|
91
|
+
this.ops.push({
|
|
92
|
+
type: "delete",
|
|
93
|
+
entity,
|
|
94
|
+
id,
|
|
95
|
+
data,
|
|
96
|
+
depth,
|
|
97
|
+
} as DeleteOperation<T>);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns all create operations, sorted by ascending depth (root → leaf).
|
|
102
|
+
*/
|
|
103
|
+
creates(): CreateOperation[] {
|
|
104
|
+
return this.ops
|
|
105
|
+
.filter((op): op is CreateOperation => op.type === "create")
|
|
106
|
+
.sort((a, b) => a.depth - b.depth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns all update operations.
|
|
111
|
+
*/
|
|
112
|
+
updates(): UpdateOperation[] {
|
|
113
|
+
return this.ops.filter((op): op is UpdateOperation => op.type === "update");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Returns all delete operations, sorted by descending depth (leaf → root).
|
|
118
|
+
*/
|
|
119
|
+
deletes(): DeleteOperation[] {
|
|
120
|
+
return this.ops
|
|
121
|
+
.filter((op): op is DeleteOperation => op.type === "delete")
|
|
122
|
+
.sort((a, b) => b.depth - a.depth);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Iterator that returns operations in the correct execution order:
|
|
127
|
+
* 1. Deletes (leaf → root)
|
|
128
|
+
* 2. Creates (root → leaf)
|
|
129
|
+
* 3. Updates
|
|
130
|
+
*/
|
|
131
|
+
*operations(): Generator<Operation> {
|
|
132
|
+
yield* this.deletes();
|
|
133
|
+
yield* this.creates();
|
|
134
|
+
yield* this.updates();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns all operations as an array in execution order.
|
|
139
|
+
*/
|
|
140
|
+
toArray(): Operation[] {
|
|
141
|
+
return [...this.operations()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Converts the changes into BatchOperations for optimized execution.
|
|
146
|
+
*
|
|
147
|
+
* Groups operations by entity and sorts by depth:
|
|
148
|
+
* - Deletes: depth DESC (leaf → root)
|
|
149
|
+
* - Creates: depth ASC (root → leaf)
|
|
150
|
+
* - Updates: grouped by entity
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const batch = changes.toBatchOperations();
|
|
155
|
+
*
|
|
156
|
+
* // Run deletes
|
|
157
|
+
* for (const del of batch.deletes) {
|
|
158
|
+
* await tx[del.entity].deleteMany({ where: { id: { in: del.ids } } });
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* // Run creates
|
|
162
|
+
* for (const create of batch.creates) {
|
|
163
|
+
* await tx[create.entity].createMany({ data: create.items });
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
toBatchOperations(): BatchOperations {
|
|
168
|
+
return {
|
|
169
|
+
deletes: this.groupDeletes(),
|
|
170
|
+
creates: this.groupCreates(),
|
|
171
|
+
updates: this.groupUpdates(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Groups deletes by entity, sorted by descending depth.
|
|
177
|
+
*/
|
|
178
|
+
private groupDeletes(): BatchOperations["deletes"] {
|
|
179
|
+
const deleteOps = this.deletes();
|
|
180
|
+
const grouped = new Map<string, { depth: number; ids: string[] }>();
|
|
181
|
+
|
|
182
|
+
for (const op of deleteOps) {
|
|
183
|
+
if (!grouped.has(op.entity)) {
|
|
184
|
+
grouped.set(op.entity, { depth: op.depth, ids: [] });
|
|
185
|
+
}
|
|
186
|
+
grouped.get(op.entity)!.ids.push(op.id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Array.from(grouped.entries())
|
|
190
|
+
.map(([entity, { depth, ids }]) => ({ entity, depth, ids }))
|
|
191
|
+
.sort((a, b) => b.depth - a.depth);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Groups creates by entity, sorted by ascending depth.
|
|
196
|
+
*/
|
|
197
|
+
private groupCreates(): BatchOperations["creates"] {
|
|
198
|
+
const createOps = this.creates();
|
|
199
|
+
const grouped = new Map<
|
|
200
|
+
string,
|
|
201
|
+
{ depth: number; items: BatchCreateItem[] }
|
|
202
|
+
>();
|
|
203
|
+
|
|
204
|
+
for (const op of createOps) {
|
|
205
|
+
if (!grouped.has(op.entity)) {
|
|
206
|
+
grouped.set(op.entity, { depth: op.depth, items: [] });
|
|
207
|
+
}
|
|
208
|
+
grouped.get(op.entity)!.items.push({
|
|
209
|
+
data: op.data,
|
|
210
|
+
parentId: op.parentId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return Array.from(grouped.entries())
|
|
215
|
+
.map(([entity, { depth, items }]) => ({ entity, depth, items }))
|
|
216
|
+
.sort((a, b) => a.depth - b.depth);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Groups updates by entity.
|
|
221
|
+
*/
|
|
222
|
+
private groupUpdates(): BatchOperations["updates"] {
|
|
223
|
+
const updateOps = this.updates();
|
|
224
|
+
const grouped = new Map<string, BatchUpdateItem[]>();
|
|
225
|
+
|
|
226
|
+
for (const op of updateOps) {
|
|
227
|
+
if (!grouped.has(op.entity)) {
|
|
228
|
+
grouped.set(op.entity, []);
|
|
229
|
+
}
|
|
230
|
+
grouped.get(op.entity)!.push({
|
|
231
|
+
id: op.id,
|
|
232
|
+
changedFields: op.changedFields,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return Array.from(grouped.entries()).map(([entity, items]) => ({
|
|
237
|
+
entity,
|
|
238
|
+
items,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Filters changes by entity name.
|
|
244
|
+
*
|
|
245
|
+
* @param entityName - Name of the entity (e.g., 'Post', 'Comment')
|
|
246
|
+
* @returns EntityChanges containing only the operations for this entity
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const postChanges = changes.for('Post');
|
|
251
|
+
*
|
|
252
|
+
* if (postChanges.hasCreates()) {
|
|
253
|
+
* postChanges.creates.forEach(post => {
|
|
254
|
+
* console.log('New post:', post.title);
|
|
255
|
+
* });
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
for<K extends keyof TEntityMap>(entityName: K): EntityChanges<TEntityMap[K]> {
|
|
260
|
+
const filtered = this.ops.filter((op) => op.entity === entityName);
|
|
261
|
+
return new EntityChanges<TEntityMap[K]>(filtered);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks if there are create operations.
|
|
266
|
+
*/
|
|
267
|
+
hasCreates(): boolean {
|
|
268
|
+
return this.ops.some((op) => op.type === "create");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks if there are update operations.
|
|
273
|
+
*/
|
|
274
|
+
hasUpdates(): boolean {
|
|
275
|
+
return this.ops.some((op) => op.type === "update");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Checks if there are delete operations.
|
|
280
|
+
*/
|
|
281
|
+
hasDeletes(): boolean {
|
|
282
|
+
return this.ops.some((op) => op.type === "delete");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Checks if there are any operations.
|
|
287
|
+
*/
|
|
288
|
+
hasChanges(): boolean {
|
|
289
|
+
return this.ops.length > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Checks if there are no operations.
|
|
294
|
+
*/
|
|
295
|
+
isEmpty(): boolean {
|
|
296
|
+
return this.ops.length === 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Returns the total number of operations.
|
|
301
|
+
*/
|
|
302
|
+
get count(): number {
|
|
303
|
+
return this.ops.length;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Returns the raw operations (for debug/testing).
|
|
308
|
+
*/
|
|
309
|
+
get rawOperations(): Operation[] {
|
|
310
|
+
return [...this.ops];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Lists all entities that have changes.
|
|
315
|
+
*/
|
|
316
|
+
getAffectedEntities(): string[] {
|
|
317
|
+
const entities = new Set<string>();
|
|
318
|
+
this.ops.forEach((op) => entities.add(op.entity));
|
|
319
|
+
return Array.from(entities);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Clears all operations.
|
|
324
|
+
*/
|
|
325
|
+
clear(): void {
|
|
326
|
+
this.ops = [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Creates a copy of the changes.
|
|
331
|
+
*/
|
|
332
|
+
clone(): AggregateChanges<TEntityMap> {
|
|
333
|
+
return new AggregateChanges<TEntityMap>([...this.ops]);
|
|
334
|
+
}
|
|
335
|
+
}
|