@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Tests: AggregateChanges
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { AggregateChanges } from "../src/aggregate-changes";
|
|
6
|
+
|
|
7
|
+
describe("AggregateChanges", () => {
|
|
8
|
+
let changes: AggregateChanges;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
changes = new AggregateChanges();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("isEmpty / hasChanges", () => {
|
|
15
|
+
it("should be empty initially", () => {
|
|
16
|
+
expect(changes.isEmpty()).toBe(true);
|
|
17
|
+
expect(changes.hasChanges()).toBe(false);
|
|
18
|
+
expect(changes.count).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should not be empty after adding operation", () => {
|
|
22
|
+
changes.addCreate("User", { id: "1", name: "Test" }, 0);
|
|
23
|
+
expect(changes.isEmpty()).toBe(false);
|
|
24
|
+
expect(changes.hasChanges()).toBe(true);
|
|
25
|
+
expect(changes.count).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("addCreate", () => {
|
|
30
|
+
it("should add create operation", () => {
|
|
31
|
+
const data = { id: "1", name: "Test User" };
|
|
32
|
+
changes.addCreate("User", data, 0);
|
|
33
|
+
|
|
34
|
+
expect(changes.hasCreates()).toBe(true);
|
|
35
|
+
expect(changes.creates()).toHaveLength(1);
|
|
36
|
+
expect(changes.creates()[0]).toMatchObject({
|
|
37
|
+
type: "create",
|
|
38
|
+
entity: "User",
|
|
39
|
+
data,
|
|
40
|
+
depth: 0,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should add create with parent info", () => {
|
|
45
|
+
const data = { id: "post-1", title: "Test Post" };
|
|
46
|
+
changes.addCreate("Post", data, 1, "user-1", "User");
|
|
47
|
+
|
|
48
|
+
const create = changes.creates()[0];
|
|
49
|
+
expect(create.parentId).toBe("user-1");
|
|
50
|
+
expect(create.parentEntity).toBe("User");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("addUpdate", () => {
|
|
55
|
+
it("should add update operation", () => {
|
|
56
|
+
const data = { id: "1", name: "Updated" };
|
|
57
|
+
const changedFields = { name: "Updated" };
|
|
58
|
+
changes.addUpdate("User", "1", data, changedFields, 0);
|
|
59
|
+
|
|
60
|
+
expect(changes.hasUpdates()).toBe(true);
|
|
61
|
+
expect(changes.updates()).toHaveLength(1);
|
|
62
|
+
expect(changes.updates()[0]).toMatchObject({
|
|
63
|
+
type: "update",
|
|
64
|
+
entity: "User",
|
|
65
|
+
id: "1",
|
|
66
|
+
changedFields,
|
|
67
|
+
depth: 0,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("addDelete", () => {
|
|
73
|
+
it("should add delete operation", () => {
|
|
74
|
+
const data = { id: "1", name: "To Delete" };
|
|
75
|
+
changes.addDelete("User", "1", data, 0);
|
|
76
|
+
|
|
77
|
+
expect(changes.hasDeletes()).toBe(true);
|
|
78
|
+
expect(changes.deletes()).toHaveLength(1);
|
|
79
|
+
expect(changes.deletes()[0]).toMatchObject({
|
|
80
|
+
type: "delete",
|
|
81
|
+
entity: "User",
|
|
82
|
+
id: "1",
|
|
83
|
+
depth: 0,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("ordering", () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
// Add operations in random order
|
|
91
|
+
changes.addCreate("Comment", { id: "c1" }, 2);
|
|
92
|
+
changes.addDelete("Like", "l1", { id: "l1" }, 3);
|
|
93
|
+
changes.addCreate("User", { id: "u1" }, 0);
|
|
94
|
+
changes.addDelete("Comment", "c2", { id: "c2" }, 2);
|
|
95
|
+
changes.addCreate("Post", { id: "p1" }, 1);
|
|
96
|
+
changes.addDelete("Post", "p2", { id: "p2" }, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should order creates by depth ASC (root → leaf)", () => {
|
|
100
|
+
const creates = changes.creates();
|
|
101
|
+
expect(creates[0].entity).toBe("User"); // depth 0
|
|
102
|
+
expect(creates[1].entity).toBe("Post"); // depth 1
|
|
103
|
+
expect(creates[2].entity).toBe("Comment"); // depth 2
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should order deletes by depth DESC (leaf → root)", () => {
|
|
107
|
+
const deletes = changes.deletes();
|
|
108
|
+
expect(deletes[0].entity).toBe("Like"); // depth 3
|
|
109
|
+
expect(deletes[1].entity).toBe("Comment"); // depth 2
|
|
110
|
+
expect(deletes[2].entity).toBe("Post"); // depth 1
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("operations iterator", () => {
|
|
115
|
+
it("should yield operations in correct order: deletes, creates, updates", () => {
|
|
116
|
+
changes.addCreate("Post", { id: "p1" }, 1);
|
|
117
|
+
changes.addUpdate("User", "u1", { id: "u1" }, { name: "New" }, 0);
|
|
118
|
+
changes.addDelete("Comment", "c1", { id: "c1" }, 2);
|
|
119
|
+
|
|
120
|
+
const ops = [...changes.operations()];
|
|
121
|
+
|
|
122
|
+
expect(ops[0].type).toBe("delete"); // deletes first
|
|
123
|
+
expect(ops[1].type).toBe("create"); // creates second
|
|
124
|
+
expect(ops[2].type).toBe("update"); // updates last
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("toBatchOperations", () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
// Creates
|
|
131
|
+
changes.addCreate("User", { id: "u1" }, 0);
|
|
132
|
+
changes.addCreate("Post", { id: "p1" }, 1, "u1", "User");
|
|
133
|
+
changes.addCreate("Post", { id: "p2" }, 1, "u1", "User");
|
|
134
|
+
changes.addCreate("Comment", { id: "c1" }, 2, "p1", "Post");
|
|
135
|
+
|
|
136
|
+
// Updates
|
|
137
|
+
changes.addUpdate("User", "u2", { id: "u2" }, { name: "Updated" }, 0);
|
|
138
|
+
changes.addUpdate("Post", "p3", { id: "p3" }, { title: "New Title" }, 1);
|
|
139
|
+
|
|
140
|
+
// Deletes
|
|
141
|
+
changes.addDelete("Comment", "c2", { id: "c2" }, 2);
|
|
142
|
+
changes.addDelete("Comment", "c3", { id: "c3" }, 2);
|
|
143
|
+
changes.addDelete("Post", "p4", { id: "p4" }, 1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should group deletes by entity and order by depth DESC", () => {
|
|
147
|
+
const batch = changes.toBatchOperations();
|
|
148
|
+
|
|
149
|
+
expect(batch.deletes).toHaveLength(2); // Comment and Post
|
|
150
|
+
|
|
151
|
+
// Comments should come first (depth 2)
|
|
152
|
+
expect(batch.deletes[0].entity).toBe("Comment");
|
|
153
|
+
expect(batch.deletes[0].ids).toEqual(["c2", "c3"]);
|
|
154
|
+
|
|
155
|
+
// Posts second (depth 1)
|
|
156
|
+
expect(batch.deletes[1].entity).toBe("Post");
|
|
157
|
+
expect(batch.deletes[1].ids).toEqual(["p4"]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should group creates by entity and order by depth ASC", () => {
|
|
161
|
+
const batch = changes.toBatchOperations();
|
|
162
|
+
|
|
163
|
+
expect(batch.creates).toHaveLength(3); // User, Post, Comment
|
|
164
|
+
|
|
165
|
+
// User first (depth 0)
|
|
166
|
+
expect(batch.creates[0].entity).toBe("User");
|
|
167
|
+
expect(batch.creates[0].items).toHaveLength(1);
|
|
168
|
+
|
|
169
|
+
// Post second (depth 1)
|
|
170
|
+
expect(batch.creates[1].entity).toBe("Post");
|
|
171
|
+
expect(batch.creates[1].items).toHaveLength(2);
|
|
172
|
+
|
|
173
|
+
// Comment last (depth 2)
|
|
174
|
+
expect(batch.creates[2].entity).toBe("Comment");
|
|
175
|
+
expect(batch.creates[2].items).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should group updates by entity", () => {
|
|
179
|
+
const batch = changes.toBatchOperations();
|
|
180
|
+
|
|
181
|
+
expect(batch.updates).toHaveLength(2); // User and Post
|
|
182
|
+
|
|
183
|
+
const userUpdates = batch.updates.find((u) => u.entity === "User");
|
|
184
|
+
expect(userUpdates?.items).toHaveLength(1);
|
|
185
|
+
expect(userUpdates?.items[0].id).toBe("u2");
|
|
186
|
+
|
|
187
|
+
const postUpdates = batch.updates.find((u) => u.entity === "Post");
|
|
188
|
+
expect(postUpdates?.items).toHaveLength(1);
|
|
189
|
+
expect(postUpdates?.items[0].id).toBe("p3");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should include parentId in create items", () => {
|
|
193
|
+
const batch = changes.toBatchOperations();
|
|
194
|
+
|
|
195
|
+
const postCreates = batch.creates.find((c) => c.entity === "Post");
|
|
196
|
+
expect(postCreates?.items[0].parentId).toBe("u1");
|
|
197
|
+
expect(postCreates?.items[1].parentId).toBe("u1");
|
|
198
|
+
|
|
199
|
+
const commentCreates = batch.creates.find((c) => c.entity === "Comment");
|
|
200
|
+
expect(commentCreates?.items[0].parentId).toBe("p1");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("for (filter by entity)", () => {
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
changes.addCreate("Post", { id: "p1", title: "Post 1" }, 1);
|
|
207
|
+
changes.addCreate("Post", { id: "p2", title: "Post 2" }, 1);
|
|
208
|
+
changes.addUpdate("Post", "p3", { id: "p3" }, { title: "Updated" }, 1);
|
|
209
|
+
changes.addDelete("Post", "p4", { id: "p4" }, 1);
|
|
210
|
+
changes.addCreate("Comment", { id: "c1" }, 2);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should filter creates by entity", () => {
|
|
214
|
+
const postChanges = changes.for("Post");
|
|
215
|
+
expect(postChanges.creates).toHaveLength(2);
|
|
216
|
+
expect(postChanges.creates[0].id).toBe("p1");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should filter updates by entity", () => {
|
|
220
|
+
const postChanges = changes.for("Post");
|
|
221
|
+
expect(postChanges.updates).toHaveLength(1);
|
|
222
|
+
expect(postChanges.updates[0].entity.id).toBe("p3");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should filter deletes by entity", () => {
|
|
226
|
+
const postChanges = changes.for("Post");
|
|
227
|
+
expect(postChanges.deletes).toHaveLength(1);
|
|
228
|
+
expect(postChanges.deletes[0].id).toBe("p4");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should return empty for non-existent entity", () => {
|
|
232
|
+
const userChanges = changes.for("User");
|
|
233
|
+
expect(userChanges.isEmpty()).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should have helper methods", () => {
|
|
237
|
+
const postChanges = changes.for("Post");
|
|
238
|
+
expect(postChanges.hasCreates()).toBe(true);
|
|
239
|
+
expect(postChanges.hasUpdates()).toBe(true);
|
|
240
|
+
expect(postChanges.hasDeletes()).toBe(true);
|
|
241
|
+
expect(postChanges.hasChanges()).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("getAffectedEntities", () => {
|
|
246
|
+
it("should return list of unique entities", () => {
|
|
247
|
+
changes.addCreate("User", { id: "u1" }, 0);
|
|
248
|
+
changes.addCreate("Post", { id: "p1" }, 1);
|
|
249
|
+
changes.addUpdate("Post", "p2", { id: "p2" }, {}, 1);
|
|
250
|
+
changes.addDelete("Comment", "c1", { id: "c1" }, 2);
|
|
251
|
+
|
|
252
|
+
const entities = changes.getAffectedEntities();
|
|
253
|
+
|
|
254
|
+
expect(entities).toContain("User");
|
|
255
|
+
expect(entities).toContain("Post");
|
|
256
|
+
expect(entities).toContain("Comment");
|
|
257
|
+
expect(entities).toHaveLength(3);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("clone", () => {
|
|
262
|
+
it("should create independent copy", () => {
|
|
263
|
+
changes.addCreate("User", { id: "u1" }, 0);
|
|
264
|
+
|
|
265
|
+
const cloned = changes.clone();
|
|
266
|
+
cloned.addCreate("Post", { id: "p1" }, 1);
|
|
267
|
+
|
|
268
|
+
expect(changes.count).toBe(1);
|
|
269
|
+
expect(cloned.count).toBe(2);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("clear", () => {
|
|
274
|
+
it("should remove all operations", () => {
|
|
275
|
+
changes.addCreate("User", { id: "u1" }, 0);
|
|
276
|
+
changes.addUpdate("Post", "p1", { id: "p1" }, {}, 1);
|
|
277
|
+
|
|
278
|
+
changes.clear();
|
|
279
|
+
|
|
280
|
+
expect(changes.isEmpty()).toBe(true);
|
|
281
|
+
expect(changes.count).toBe(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
package/tests/criteria.test.ts
CHANGED
|
@@ -24,6 +24,17 @@ interface UserWithPostsDto {
|
|
|
24
24
|
}[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
interface UserWithNestedObject {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
address: {
|
|
31
|
+
street: string;
|
|
32
|
+
city: string;
|
|
33
|
+
country: string;
|
|
34
|
+
};
|
|
35
|
+
tags: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
const testUsers: TestUser[] = [
|
|
28
39
|
{
|
|
29
40
|
id: "1",
|
|
@@ -68,26 +79,12 @@ const testUsers: TestUser[] = [
|
|
|
68
79
|
];
|
|
69
80
|
|
|
70
81
|
describe("Criteria", () => {
|
|
71
|
-
describe("Search", () => {
|
|
72
|
-
it("should search by all items ignoring pagination and limit", () => {
|
|
73
|
-
const criteria = Criteria.create<TestUser>()
|
|
74
|
-
.search(["name"], "Eve")
|
|
75
|
-
.paginate(3, 1);
|
|
76
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
77
|
-
expect(result.data).toHaveLength(1);
|
|
78
|
-
expect(result.data[0].name).toBe("Eve");
|
|
79
|
-
expect(result.meta.total).toBe(1);
|
|
80
|
-
expect(result.meta.totalPages).toBe(1);
|
|
81
|
-
expect(result.meta.page).toBe(1);
|
|
82
|
-
expect(result.meta.limit).toBe(1);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
82
|
describe("Fluent API", () => {
|
|
86
83
|
it("should create empty criteria", () => {
|
|
87
84
|
const criteria = Criteria.create<TestUser>();
|
|
88
85
|
expect(criteria.hasFilters()).toBe(false);
|
|
89
86
|
expect(criteria.hasOrders()).toBe(false);
|
|
90
|
-
expect(criteria.hasPagination()).toBe(true);
|
|
87
|
+
expect(criteria.hasPagination()).toBe(true);
|
|
91
88
|
});
|
|
92
89
|
|
|
93
90
|
it("should chain methods fluently", () => {
|
|
@@ -118,27 +115,6 @@ describe("Criteria", () => {
|
|
|
118
115
|
});
|
|
119
116
|
|
|
120
117
|
describe("Filtering", () => {
|
|
121
|
-
it("should filter by equals", () => {
|
|
122
|
-
const criteria = Criteria.create<TestUser>().whereEquals(
|
|
123
|
-
"status",
|
|
124
|
-
"active"
|
|
125
|
-
);
|
|
126
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
127
|
-
expect(result.data).toHaveLength(3);
|
|
128
|
-
expect(result.data.every((u) => u.status === "active")).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("should filter by notEquals", () => {
|
|
132
|
-
const criteria = Criteria.create<TestUser>().where(
|
|
133
|
-
"status",
|
|
134
|
-
"notEquals",
|
|
135
|
-
"active"
|
|
136
|
-
);
|
|
137
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
138
|
-
expect(result.data).toHaveLength(2);
|
|
139
|
-
expect(result.data.every((u) => u.status === "inactive")).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
118
|
it("should filter by search", () => {
|
|
143
119
|
const criteria = Criteria.create<TestUser>().search(["name"], "Bob");
|
|
144
120
|
|
|
@@ -147,6 +123,19 @@ describe("Criteria", () => {
|
|
|
147
123
|
expect(result.data[0].name).toBe("Bob");
|
|
148
124
|
});
|
|
149
125
|
|
|
126
|
+
it("should search by all items ignoring pagination and limit", () => {
|
|
127
|
+
const criteria = Criteria.create<TestUser>()
|
|
128
|
+
.search(["name"], "Eve")
|
|
129
|
+
.paginate(3, 1);
|
|
130
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
131
|
+
expect(result.data).toHaveLength(1);
|
|
132
|
+
expect(result.data[0].name).toBe("Eve");
|
|
133
|
+
expect(result.meta.total).toBe(1);
|
|
134
|
+
expect(result.meta.totalPages).toBe(1);
|
|
135
|
+
expect(result.meta.page).toBe(1);
|
|
136
|
+
expect(result.meta.limit).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
150
139
|
it("should filter by greaterThan", () => {
|
|
151
140
|
const criteria = Criteria.create<TestUser>().where(
|
|
152
141
|
"age",
|
|
@@ -228,6 +217,20 @@ describe("Criteria", () => {
|
|
|
228
217
|
expect(result.data).toHaveLength(2);
|
|
229
218
|
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
|
|
230
219
|
});
|
|
220
|
+
|
|
221
|
+
it("should filter, order, and paginate", () => {
|
|
222
|
+
const criteria = Criteria.create<TestUser>()
|
|
223
|
+
.whereEquals("status", "active")
|
|
224
|
+
.orderByDesc("age")
|
|
225
|
+
.paginate(1, 2);
|
|
226
|
+
|
|
227
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
228
|
+
|
|
229
|
+
expect(result.data).toHaveLength(2);
|
|
230
|
+
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
|
|
231
|
+
expect(result.meta.total).toBe(3);
|
|
232
|
+
expect(result.meta.totalPages).toBe(2);
|
|
233
|
+
});
|
|
231
234
|
});
|
|
232
235
|
|
|
233
236
|
describe("Ordering", () => {
|
|
@@ -274,35 +277,6 @@ describe("Criteria", () => {
|
|
|
274
277
|
expect(result.meta.hasPrevious).toBe(false);
|
|
275
278
|
});
|
|
276
279
|
|
|
277
|
-
it("should get second page", () => {
|
|
278
|
-
const criteria = Criteria.create<TestUser>().paginate(2, 2);
|
|
279
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
280
|
-
|
|
281
|
-
expect(result.data).toHaveLength(2);
|
|
282
|
-
expect(result.data.map((u) => u.name)).toEqual(["Charlie", "Diana"]);
|
|
283
|
-
expect(result.meta.page).toBe(2);
|
|
284
|
-
expect(result.meta.hasNext).toBe(true);
|
|
285
|
-
expect(result.meta.hasPrevious).toBe(true);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("should get last page", () => {
|
|
289
|
-
const criteria = Criteria.create<TestUser>().paginate(3, 2);
|
|
290
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
291
|
-
|
|
292
|
-
expect(result.data).toHaveLength(1);
|
|
293
|
-
expect(result.data[0].name).toBe("Eve");
|
|
294
|
-
expect(result.meta.hasNext).toBe(false);
|
|
295
|
-
expect(result.meta.hasPrevious).toBe(true);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("should handle empty page", () => {
|
|
299
|
-
const criteria = Criteria.create<TestUser>().paginate(10, 2);
|
|
300
|
-
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
301
|
-
|
|
302
|
-
expect(result.data).toHaveLength(0);
|
|
303
|
-
expect(result.meta.total).toBe(5);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
280
|
it("should apply limit shorthand", () => {
|
|
307
281
|
const criteria = Criteria.create<TestUser>().limit(3);
|
|
308
282
|
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
@@ -310,21 +284,17 @@ describe("Criteria", () => {
|
|
|
310
284
|
expect(result.data).toHaveLength(3);
|
|
311
285
|
expect(result.meta.page).toBe(1);
|
|
312
286
|
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
describe("Combined Operations", () => {
|
|
316
|
-
it("should filter, order, and paginate", () => {
|
|
317
|
-
const criteria = Criteria.create<TestUser>()
|
|
318
|
-
.whereEquals("status", "active")
|
|
319
|
-
.orderByDesc("age")
|
|
320
|
-
.paginate(1, 2);
|
|
321
287
|
|
|
322
|
-
|
|
288
|
+
it("should create pagination meta", () => {
|
|
289
|
+
const pagination = { page: 2, limit: 10, offset: 10 };
|
|
290
|
+
const meta = PaginatedResult.createMeta(pagination, 45);
|
|
323
291
|
|
|
324
|
-
expect(
|
|
325
|
-
expect(
|
|
326
|
-
expect(
|
|
327
|
-
expect(
|
|
292
|
+
expect(meta.page).toBe(2);
|
|
293
|
+
expect(meta.limit).toBe(10);
|
|
294
|
+
expect(meta.total).toBe(45);
|
|
295
|
+
expect(meta.totalPages).toBe(5);
|
|
296
|
+
expect(meta.hasNext).toBe(true);
|
|
297
|
+
expect(meta.hasPrevious).toBe(true);
|
|
328
298
|
});
|
|
329
299
|
});
|
|
330
300
|
|
|
@@ -374,12 +344,14 @@ describe("Criteria", () => {
|
|
|
374
344
|
new Post({
|
|
375
345
|
title: "Post 1",
|
|
376
346
|
content: "Content 1",
|
|
377
|
-
|
|
347
|
+
published: true,
|
|
348
|
+
comments: [],
|
|
378
349
|
}),
|
|
379
350
|
new Post({
|
|
380
351
|
title: "Post 2",
|
|
381
352
|
content: "Content 2",
|
|
382
|
-
|
|
353
|
+
published: true,
|
|
354
|
+
comments: [],
|
|
383
355
|
}),
|
|
384
356
|
];
|
|
385
357
|
|
|
@@ -421,30 +393,6 @@ describe("Criteria", () => {
|
|
|
421
393
|
});
|
|
422
394
|
});
|
|
423
395
|
|
|
424
|
-
describe("Helper Functions", () => {
|
|
425
|
-
it("should create pagination meta", () => {
|
|
426
|
-
const pagination = { page: 2, limit: 10, offset: 10 };
|
|
427
|
-
const meta = PaginatedResult.createMeta(pagination, 45);
|
|
428
|
-
|
|
429
|
-
expect(meta.page).toBe(2);
|
|
430
|
-
expect(meta.limit).toBe(10);
|
|
431
|
-
expect(meta.total).toBe(45);
|
|
432
|
-
expect(meta.totalPages).toBe(5);
|
|
433
|
-
expect(meta.hasNext).toBe(true);
|
|
434
|
-
expect(meta.hasPrevious).toBe(true);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("should create paginated result", () => {
|
|
438
|
-
const data = [{ id: "1" }, { id: "2" }];
|
|
439
|
-
const pagination = { page: 1, limit: 2, offset: 0 };
|
|
440
|
-
const result = PaginatedResult.create(data, pagination, data.length);
|
|
441
|
-
|
|
442
|
-
expect(result.data).toEqual(data);
|
|
443
|
-
expect(result.meta.total).toBe(data.length);
|
|
444
|
-
expect(result.meta.totalPages).toBe(1);
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
396
|
describe("Criteria from Query Params", () => {
|
|
449
397
|
it("should create criteria from query params", () => {
|
|
450
398
|
const queryParams = {
|
|
@@ -466,6 +414,19 @@ describe("Criteria", () => {
|
|
|
466
414
|
|
|
467
415
|
describe("Quantifiers", () => {
|
|
468
416
|
describe("Fluent API methods", () => {
|
|
417
|
+
it("should use quantifiers horthand methods", () => {
|
|
418
|
+
// CHECK: Devemos permitir o encadeamento de metodos quantifiers?
|
|
419
|
+
const criteria = Criteria.create<TestUser>()
|
|
420
|
+
.whereSome("name", "contains", "ali")
|
|
421
|
+
.whereEvery("name", "contains", "ali")
|
|
422
|
+
.whereNone("name", "contains", "ali");
|
|
423
|
+
|
|
424
|
+
const filters = criteria.getFilters().map((f) => f.options?.quantifier);
|
|
425
|
+
|
|
426
|
+
expect(filters).toHaveLength(3);
|
|
427
|
+
expect(filters).toEqual(["some", "every", "none"]);
|
|
428
|
+
});
|
|
429
|
+
|
|
469
430
|
it("should create filter with whereSome", () => {
|
|
470
431
|
const criteria = Criteria.create<UserWithPostsDto>().whereSome(
|
|
471
432
|
"posts.title",
|
|
@@ -506,20 +467,6 @@ describe("Criteria", () => {
|
|
|
506
467
|
expect(filters[0].options?.quantifier).toBe("none");
|
|
507
468
|
});
|
|
508
469
|
|
|
509
|
-
it("should create filter without quantifier using where", () => {
|
|
510
|
-
const criteria = Criteria.create<UserWithPostsDto>().where(
|
|
511
|
-
"posts.title",
|
|
512
|
-
"contains",
|
|
513
|
-
"test"
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
const filters = criteria.getFilters();
|
|
517
|
-
expect(filters).toHaveLength(1);
|
|
518
|
-
expect(filters[0].options).toBeUndefined();
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
describe("fromQueryParams with quantifier", () => {
|
|
523
470
|
it("should parse quantifier from query params with @some", () => {
|
|
524
471
|
const queryParams = {
|
|
525
472
|
"posts.title:contains@some": "test",
|
|
@@ -536,32 +483,6 @@ describe("Criteria", () => {
|
|
|
536
483
|
expect(filters[0].options?.quantifier).toBe("some");
|
|
537
484
|
});
|
|
538
485
|
|
|
539
|
-
it("should parse quantifier from query params with @every", () => {
|
|
540
|
-
const queryParams = {
|
|
541
|
-
"posts.title:equals@every": "test",
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
const criteria =
|
|
545
|
-
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
546
|
-
const filters = criteria.getFilters();
|
|
547
|
-
|
|
548
|
-
expect(filters).toHaveLength(1);
|
|
549
|
-
expect(filters[0].options?.quantifier).toBe("every");
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("should parse quantifier from query params with @none", () => {
|
|
553
|
-
const queryParams = {
|
|
554
|
-
"posts.title:contains@none": "spam",
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
const criteria =
|
|
558
|
-
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
559
|
-
const filters = criteria.getFilters();
|
|
560
|
-
|
|
561
|
-
expect(filters).toHaveLength(1);
|
|
562
|
-
expect(filters[0].options?.quantifier).toBe("none");
|
|
563
|
-
});
|
|
564
|
-
|
|
565
486
|
it("should work with in operator and quantifier", () => {
|
|
566
487
|
const queryParams = {
|
|
567
488
|
"posts.title:in@some": "test1,test2,test3",
|
|
@@ -592,22 +513,6 @@ describe("Criteria", () => {
|
|
|
592
513
|
expect(filters[0].options?.quantifier).toBe("some");
|
|
593
514
|
});
|
|
594
515
|
|
|
595
|
-
it("should maintain backward compatibility without quantifier", () => {
|
|
596
|
-
const queryParams = {
|
|
597
|
-
"posts.title:contains": "test",
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
const criteria =
|
|
601
|
-
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
602
|
-
const filters = criteria.getFilters();
|
|
603
|
-
|
|
604
|
-
expect(filters).toHaveLength(1);
|
|
605
|
-
expect(filters[0].field).toBe("posts.title");
|
|
606
|
-
expect(filters[0].operator).toBe("contains");
|
|
607
|
-
expect(filters[0].value).toBe("test");
|
|
608
|
-
expect(filters[0].options).toBeUndefined();
|
|
609
|
-
});
|
|
610
|
-
|
|
611
516
|
it("should throw error for invalid quantifier", () => {
|
|
612
517
|
const queryParams = {
|
|
613
518
|
"posts.title:contains@invalid": "test",
|
|
@@ -741,7 +646,7 @@ describe("Criteria", () => {
|
|
|
741
646
|
{
|
|
742
647
|
field: "posts",
|
|
743
648
|
operator: "in",
|
|
744
|
-
value: [{ title: "test"
|
|
649
|
+
value: [{ title: "test" }] as any,
|
|
745
650
|
},
|
|
746
651
|
],
|
|
747
652
|
},
|
|
@@ -751,5 +656,61 @@ describe("Criteria", () => {
|
|
|
751
656
|
expect(filters).toHaveLength(1);
|
|
752
657
|
expect(filters[0].field).toBe("user_posts");
|
|
753
658
|
});
|
|
659
|
+
|
|
660
|
+
it("should only allow nested paths in fromObject for object fields", () => {
|
|
661
|
+
// Only nested paths should work for object fields
|
|
662
|
+
const criteria = Criteria.fromObject<UserWithNestedObject>({
|
|
663
|
+
filters: [
|
|
664
|
+
{
|
|
665
|
+
field: "address.street",
|
|
666
|
+
operator: "equals",
|
|
667
|
+
value: "123 Main St",
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
field: "address.city",
|
|
671
|
+
operator: "equals",
|
|
672
|
+
value: "New York",
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
field: "id",
|
|
676
|
+
operator: "equals",
|
|
677
|
+
value: "user-1",
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
expect(criteria.getFilters()).toHaveLength(3);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe("Nested Object Field Paths", () => {
|
|
687
|
+
it("should only allow nested paths for object fields, not the root", () => {
|
|
688
|
+
const criteria = Criteria.create<UserWithNestedObject>();
|
|
689
|
+
|
|
690
|
+
// These should work - nested paths
|
|
691
|
+
criteria.whereEquals("address.city", "123 Main St");
|
|
692
|
+
criteria.whereEquals("address.city", "New York");
|
|
693
|
+
criteria.whereEquals("address.country", "USA");
|
|
694
|
+
|
|
695
|
+
// These should work - primitives and arrays
|
|
696
|
+
criteria.whereEquals("id", "user-1");
|
|
697
|
+
criteria.whereEquals("name", "John Doe");
|
|
698
|
+
criteria.where("tags", "in", ["tag1", "tag2"]);
|
|
699
|
+
|
|
700
|
+
const filters = criteria.getFilters();
|
|
701
|
+
expect(filters).toHaveLength(6);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should work with array of objects (keeps root + nested)", () => {
|
|
705
|
+
const criteria = Criteria.create<UserWithPostsDto>();
|
|
706
|
+
|
|
707
|
+
// These should work - array root and nested paths
|
|
708
|
+
criteria.where("posts", "in", []);
|
|
709
|
+
criteria.whereEquals("posts.title", "Test Post");
|
|
710
|
+
criteria.whereEquals("posts.content", "Test Content");
|
|
711
|
+
|
|
712
|
+
const filters = criteria.getFilters();
|
|
713
|
+
expect(filters).toHaveLength(3);
|
|
714
|
+
});
|
|
754
715
|
});
|
|
755
716
|
});
|