@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +87 -21
  2. package/dist/aggregate-changes.d.ts +164 -0
  3. package/dist/aggregate-changes.d.ts.map +1 -0
  4. package/dist/aggregate-changes.js +281 -0
  5. package/dist/aggregate-changes.js.map +1 -0
  6. package/dist/base-entity.d.ts +32 -8
  7. package/dist/base-entity.d.ts.map +1 -1
  8. package/dist/base-entity.js +117 -86
  9. package/dist/base-entity.js.map +1 -1
  10. package/dist/criteria.d.ts +31 -15
  11. package/dist/criteria.d.ts.map +1 -1
  12. package/dist/criteria.js +151 -61
  13. package/dist/criteria.js.map +1 -1
  14. package/dist/crypto.d.ts +3 -0
  15. package/dist/crypto.d.ts.map +1 -0
  16. package/dist/crypto.js +29 -0
  17. package/dist/crypto.js.map +1 -0
  18. package/dist/entity-changes.d.ts +84 -0
  19. package/dist/entity-changes.d.ts.map +1 -0
  20. package/dist/entity-changes.js +135 -0
  21. package/dist/entity-changes.js.map +1 -0
  22. package/dist/entity-schema-registry.d.ts +148 -0
  23. package/dist/entity-schema-registry.d.ts.map +1 -0
  24. package/dist/entity-schema-registry.js +219 -0
  25. package/dist/entity-schema-registry.js.map +1 -0
  26. package/dist/history-tracker.d.ts +97 -0
  27. package/dist/history-tracker.d.ts.map +1 -0
  28. package/dist/history-tracker.js +805 -0
  29. package/dist/history-tracker.js.map +1 -0
  30. package/dist/id.d.ts +11 -10
  31. package/dist/id.d.ts.map +1 -1
  32. package/dist/id.js +4 -28
  33. package/dist/id.js.map +1 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/mapper.d.ts +1 -1
  39. package/dist/mapper.d.ts.map +1 -1
  40. package/dist/mapper.js.map +1 -1
  41. package/dist/paginated-result.d.ts.map +1 -1
  42. package/dist/paginated-result.js +7 -6
  43. package/dist/paginated-result.js.map +1 -1
  44. package/dist/repository/base-repository.d.ts +6 -32
  45. package/dist/repository/base-repository.d.ts.map +1 -1
  46. package/dist/repository/base-repository.js +0 -27
  47. package/dist/repository/base-repository.js.map +1 -1
  48. package/dist/repository/unit-of-work.d.ts +0 -25
  49. package/dist/repository/unit-of-work.d.ts.map +1 -1
  50. package/dist/repository/unit-of-work.js +0 -25
  51. package/dist/repository/unit-of-work.js.map +1 -1
  52. package/dist/types/change-tracker.d.ts +186 -0
  53. package/dist/types/change-tracker.d.ts.map +1 -0
  54. package/dist/types/change-tracker.js +2 -0
  55. package/dist/types/change-tracker.js.map +1 -0
  56. package/dist/types/criteria.d.ts +31 -7
  57. package/dist/types/criteria.d.ts.map +1 -1
  58. package/dist/types/history-tracker.d.ts +11 -0
  59. package/dist/types/history-tracker.d.ts.map +1 -1
  60. package/dist/types/utils.d.ts +0 -1
  61. package/dist/types/utils.d.ts.map +1 -1
  62. package/dist/utils/criteria-operator-validation.d.ts +5 -0
  63. package/dist/utils/criteria-operator-validation.d.ts.map +1 -0
  64. package/dist/utils/criteria-operator-validation.js +143 -0
  65. package/dist/utils/criteria-operator-validation.js.map +1 -0
  66. package/dist/utils/helpers.d.ts +2 -0
  67. package/dist/utils/helpers.d.ts.map +1 -0
  68. package/dist/utils/helpers.js +10 -0
  69. package/dist/utils/helpers.js.map +1 -0
  70. package/dist/validation-error.d.ts.map +1 -1
  71. package/dist/validation-error.js +0 -3
  72. package/dist/validation-error.js.map +1 -1
  73. package/dist/value-object.d.ts +57 -8
  74. package/dist/value-object.d.ts.map +1 -1
  75. package/dist/value-object.js +49 -21
  76. package/dist/value-object.js.map +1 -1
  77. package/eslint.config.js +6 -0
  78. package/package.json +2 -1
  79. package/src/aggregate-changes.ts +335 -0
  80. package/src/base-entity.ts +140 -100
  81. package/src/criteria.ts +264 -82
  82. package/src/crypto.ts +31 -0
  83. package/src/entity-changes.ts +151 -0
  84. package/src/entity-schema-registry.ts +275 -0
  85. package/src/history-tracker.ts +1114 -0
  86. package/src/id.ts +17 -26
  87. package/src/index.ts +8 -2
  88. package/src/mapper.ts +4 -1
  89. package/src/paginated-result.ts +7 -8
  90. package/src/repository/base-repository.ts +6 -37
  91. package/src/repository/unit-of-work.ts +0 -25
  92. package/src/types/change-tracker.ts +221 -0
  93. package/src/types/criteria.ts +95 -17
  94. package/src/types/history-tracker.ts +13 -0
  95. package/src/types/utils.ts +0 -9
  96. package/src/utils/criteria-operator-validation.ts +171 -0
  97. package/src/utils/helpers.ts +6 -0
  98. package/src/validation-error.ts +0 -4
  99. package/src/value-object.ts +84 -23
  100. package/tests/aggregate-changes.test.ts +284 -0
  101. package/tests/criteria.test.ts +366 -90
  102. package/tests/entity-equality.test.ts +38 -61
  103. package/tests/entity-schema-registry.test.ts +382 -0
  104. package/tests/entity-validation.test.ts +7 -94
  105. package/tests/history-tracker.spec.ts +349 -617
  106. package/tests/id.test.ts +41 -44
  107. package/tests/load-test/data.json +346041 -0
  108. package/tests/load-test/entities.ts +97 -0
  109. package/tests/load-test/generate-data.ts +81 -0
  110. package/tests/load-test/lead-to-domain.mapper.ts +24 -0
  111. package/tests/load-test/load.test.ts +38 -0
  112. package/tests/repository.test.ts +30 -54
  113. package/tests/to-json.test.ts +14 -18
  114. package/tests/utils.ts +138 -102
  115. package/tests/value-objects.test.ts +57 -29
  116. package/dist/deep-proxy.d.ts +0 -36
  117. package/dist/deep-proxy.d.ts.map +0 -1
  118. package/dist/deep-proxy.js +0 -384
  119. package/dist/deep-proxy.js.map +0 -1
  120. package/src/deep-proxy.ts +0 -447
  121. package/tests/entity.test.ts +0 -33
package/src/criteria.ts CHANGED
@@ -1,68 +1,94 @@
1
1
  import { InvalidCriteriaError } from "./exceptions";
2
2
  import {
3
+ CriteriaAdapter,
4
+ CriteriaOptions,
3
5
  FieldPath,
4
6
  Filter,
5
- FILTER_OPERATORS,
6
7
  FilterOperator,
7
8
  FilterValueFor,
9
+ OperatorsForType,
8
10
  Order,
9
11
  OrderDirection,
10
12
  Pagination,
11
13
  PathValue,
12
14
  Search,
13
15
  TypedFilter,
16
+ TypedOrder,
14
17
  } from "./types";
15
-
16
- // ============================================================================
17
- // Filter Types
18
- // ============================================================================
18
+ import {
19
+ isValidOperatorForType,
20
+ getValidOperatorsForType,
21
+ isOperator,
22
+ } from "./utils/criteria-operator-validation";
23
+ import { parseQueryValue } from "./utils/helpers";
19
24
 
20
25
  export class Criteria<T = any> {
21
26
  private _filters: Filter<FieldPath<T>, any>[] = [];
22
27
  private _orders: Order[] = [];
23
28
  private _pagination: Pagination = { page: 1, limit: 20, offset: 0 };
24
- private _search?: Search<T>
29
+ private _search?: Search<T>;
30
+ private _adapter?: CriteriaAdapter<any, any>;
25
31
 
26
32
  private constructor() {}
27
33
 
28
- /**
29
- * Create a new Criteria instance
30
- */
31
34
  static create<T = any>(): Criteria<T> {
32
35
  return new Criteria<T>();
33
36
  }
34
37
 
35
- /**
36
- * Add a filter condition
37
- */
38
+ useAdapter<A extends CriteriaAdapter<any, any>>(map: A): this {
39
+ this._adapter = map;
40
+ return this;
41
+ }
42
+
43
+ getAdapter(): CriteriaAdapter<any, any> | undefined {
44
+ return this._adapter;
45
+ }
46
+
47
+ where<K extends FieldPath<T>>(
48
+ field: K,
49
+ operator: OperatorsForType<NonNullable<PathValue<T, K>>>,
50
+ value?: FilterValueFor<PathValue<T, K>>,
51
+ options?: CriteriaOptions
52
+ ): this;
53
+
38
54
  where<K extends FieldPath<T>>(
39
55
  field: K,
40
56
  operator: FilterOperator,
41
- value?: FilterValueFor<PathValue<T, K>>
57
+ value?: FilterValueFor<PathValue<T, K>>,
58
+ options?: CriteriaOptions
42
59
  ): this {
60
+ this.validateOperator(operator, value);
61
+
43
62
  this._filters.push({
44
- field,
63
+ field: this.resolveFieldPath(field),
45
64
  operator,
46
65
  value,
66
+ options,
47
67
  });
48
68
  return this;
49
69
  }
50
70
 
51
- // === Shorthand methods (tipados) ===
52
-
53
71
  whereEquals<K extends FieldPath<T>>(field: K, value: PathValue<T, K>): this {
54
- return this.where(field, "equals", value);
72
+ return this.where(
73
+ field,
74
+ "equals" as OperatorsForType<PathValue<T, K>>,
75
+ value
76
+ );
55
77
  }
56
78
 
57
79
  whereContains<K extends FieldPath<T>>(
58
80
  field: K,
59
81
  value: PathValue<T, K>
60
82
  ): this {
61
- return this.where(field, "contains", value);
83
+ return this.where(
84
+ field,
85
+ "contains" as OperatorsForType<PathValue<T, K>>,
86
+ value
87
+ );
62
88
  }
63
89
 
64
90
  whereIn<K extends FieldPath<T>>(field: K, values: PathValue<T, K>[]): this {
65
- return this.where(field, "in", values);
91
+ return this.where(field, "in" as OperatorsForType<PathValue<T, K>>, values);
66
92
  }
67
93
 
68
94
  whereBetween<K extends FieldPath<T>>(
@@ -70,28 +96,51 @@ export class Criteria<T = any> {
70
96
  min: PathValue<T, K>,
71
97
  max: PathValue<T, K>
72
98
  ): this {
73
- return this.where(field, "between", [min, max] as [
74
- PathValue<T, K>,
75
- PathValue<T, K>
76
- ]);
99
+ return this.where(
100
+ field,
101
+ "between" as OperatorsForType<PathValue<T, K>>,
102
+ [min, max] as [PathValue<T, K>, PathValue<T, K>]
103
+ );
77
104
  }
78
105
 
79
106
  whereNull<K extends FieldPath<T>>(field: K): this {
80
- return this.where(field, "isNull");
107
+ return this.where(field, "isNull" as OperatorsForType<PathValue<T, K>>);
81
108
  }
82
109
 
83
110
  whereNotNull<K extends FieldPath<T>>(field: K): this {
84
- return this.where(field, "isNotNull");
111
+ return this.where(field, "isNotNull" as OperatorsForType<PathValue<T, K>>);
85
112
  }
86
113
 
87
- // === OrderBy ===
114
+ whereSome<K extends FieldPath<T>>(
115
+ field: K,
116
+ operator: OperatorsForType<NonNullable<PathValue<T, K>>>,
117
+ value?: FilterValueFor<PathValue<T, K>>
118
+ ): this {
119
+ return this.where(field, operator, value, { quantifier: "some" });
120
+ }
121
+
122
+ whereEvery<K extends FieldPath<T>>(
123
+ field: K,
124
+ operator: OperatorsForType<NonNullable<PathValue<T, K>>>,
125
+ value?: FilterValueFor<PathValue<T, K>>
126
+ ): this {
127
+ return this.where(field, operator, value, { quantifier: "every" });
128
+ }
129
+
130
+ whereNone<K extends FieldPath<T>>(
131
+ field: K,
132
+ operator: OperatorsForType<NonNullable<PathValue<T, K>>>,
133
+ value?: FilterValueFor<PathValue<T, K>>
134
+ ): this {
135
+ return this.where(field, operator, value, { quantifier: "none" });
136
+ }
88
137
 
89
138
  orderBy<K extends FieldPath<T>>(
90
139
  field: K,
91
140
  direction: OrderDirection = "asc"
92
141
  ): this {
93
142
  this._orders.push({
94
- field: String(field),
143
+ field: this.resolveFieldPath(field),
95
144
  direction,
96
145
  });
97
146
  return this;
@@ -105,13 +154,9 @@ export class Criteria<T = any> {
105
154
  return this.orderBy(field, "desc");
106
155
  }
107
156
 
108
- // --------------------------------------------------------------------------
109
- // Search (tipado)
110
- // --------------------------------------------------------------------------
111
-
112
157
  search<K extends FieldPath<T>>(fields: K[], value: string): this {
113
158
  this._search = {
114
- fields,
159
+ fields: fields.map(this.resolveFieldPath),
115
160
  value,
116
161
  };
117
162
  return this;
@@ -122,11 +167,14 @@ export class Criteria<T = any> {
122
167
  }
123
168
 
124
169
  getSearch() {
125
- return this._search;
170
+ return this._search
171
+ ? {
172
+ fields: this._search.fields.map(this.resolveFieldPath),
173
+ value: this._search.value,
174
+ }
175
+ : undefined;
126
176
  }
127
177
 
128
- // === Pagination ===
129
-
130
178
  paginate(page: number, limit: number): this {
131
179
  if (page < 1) page = 1;
132
180
  if (limit < 1) limit = 10;
@@ -143,14 +191,20 @@ export class Criteria<T = any> {
143
191
  return this.paginate(1, limit);
144
192
  }
145
193
 
146
- // === Getters ===
147
-
148
194
  getFilters(): Filter[] {
149
- return this._filters;
195
+ return this._filters.map((filter) => ({
196
+ field: this.resolveFieldPath(filter.field),
197
+ operator: filter.operator,
198
+ value: filter.value,
199
+ options: filter.options,
200
+ }));
150
201
  }
151
202
 
152
203
  getOrders(): Order[] {
153
- return this._orders;
204
+ return this._orders.map((order) => ({
205
+ field: this.resolveFieldPath(order.field as FieldPath<T>),
206
+ direction: order.direction,
207
+ }));
154
208
  }
155
209
 
156
210
  getPagination(): Pagination {
@@ -169,47 +223,132 @@ export class Criteria<T = any> {
169
223
  return this._pagination !== undefined;
170
224
  }
171
225
 
172
- // === Utilities ===
173
-
174
226
  clone(): Criteria<T> {
175
227
  const cloned = Criteria.create<T>();
176
- cloned._filters = [...this._filters];
177
- cloned._orders = [...this._orders];
228
+ cloned._filters = [
229
+ ...this._filters.map((filter) => ({
230
+ field: this.resolveFieldPath(filter.field),
231
+ operator: filter.operator,
232
+ value: filter.value,
233
+ options: filter.options,
234
+ })),
235
+ ];
236
+ cloned._orders = [
237
+ ...this._orders.map((order) => ({
238
+ field: this.resolveFieldPath(order.field as FieldPath<T>),
239
+ direction: order.direction,
240
+ })),
241
+ ];
178
242
  cloned._pagination = { ...this._pagination };
243
+ cloned._search = this._search
244
+ ? {
245
+ fields: this._search.fields.map(this.resolveFieldPath),
246
+ value: this._search.value,
247
+ }
248
+ : undefined;
249
+
250
+ if (this._adapter) {
251
+ cloned.useAdapter(this._adapter);
252
+ }
253
+
179
254
  return cloned;
180
255
  }
181
256
 
182
257
  toJSON() {
183
258
  return {
184
- filters: this._filters,
185
- orders: this._orders,
259
+ filters: this._filters.map((filter) => ({
260
+ field: this.resolveFieldPath(filter.field),
261
+ operator: filter.operator,
262
+ value: filter.value,
263
+ options: filter.options,
264
+ })),
265
+ orders: this._orders.map((order) => ({
266
+ field: this.resolveFieldPath(order.field as FieldPath<T>),
267
+ direction: order.direction,
268
+ })),
186
269
  pagination: this._pagination,
187
- search: this._search,
270
+ search: this._search
271
+ ? {
272
+ fields: this._search.fields.map(this.resolveFieldPath),
273
+ value: this._search.value,
274
+ }
275
+ : undefined,
188
276
  };
189
277
  }
190
278
 
191
- static fromObject<T>(obj: {
192
- filters?: TypedFilter<T>[];
193
- orders?: Order[];
194
- pagination?: Pagination;
195
- search?: { fields: FieldPath<T>[]; value: string };
196
- }): Criteria<T> {
279
+ static fromObject<T>(
280
+ obj: {
281
+ filters?: TypedFilter<T>[];
282
+ orders?: TypedOrder<T>[];
283
+ pagination?: Pagination;
284
+ search?: { fields: FieldPath<T>[]; value: string };
285
+ },
286
+ adapter?: CriteriaAdapter<any, any>
287
+ ): Criteria<T> {
197
288
  const criteria = Criteria.create<T>();
198
- if (obj.filters) criteria._filters = [...obj.filters];
199
- if (obj.orders) criteria._orders = [...obj.orders];
289
+
290
+ if (adapter) {
291
+ criteria.useAdapter(adapter);
292
+ }
293
+
294
+ if (obj.filters) {
295
+ for (const filter of obj.filters) {
296
+ filter.field = criteria.resolveFieldPath(filter.field);
297
+ criteria.validateOperator(filter.operator, filter.value);
298
+ }
299
+ criteria._filters = [...obj.filters];
300
+ }
301
+ if (obj.orders)
302
+ criteria._orders = [
303
+ ...obj.orders.map((order) => ({
304
+ field: criteria.resolveFieldPath(order.field as FieldPath<T>),
305
+ direction: order.direction,
306
+ })),
307
+ ];
200
308
  if (obj.pagination) criteria._pagination = { ...obj.pagination };
201
- if (obj.search) criteria._search = { ...obj.search };
309
+ if (obj.search)
310
+ criteria._search = {
311
+ ...obj.search,
312
+ fields: obj.search.fields.map(criteria.resolveFieldPath),
313
+ };
202
314
 
203
315
  return criteria;
204
316
  }
205
317
 
206
- static fromQueryParams<T>(query: Record<string, any>): Criteria<T> {
318
+ protected resolveFieldPath(field: FieldPath<T>): FieldPath<T> {
319
+ if (!this?._adapter) return field;
320
+
321
+ if (this._adapter[field]) {
322
+ return this._adapter[field] as FieldPath<T>;
323
+ }
324
+
325
+ const parts = field.split(".");
326
+ for (let i = parts.length; i > 0; i--) {
327
+ const prefix = parts.slice(0, i).join(".");
328
+ if (this._adapter[prefix]) {
329
+ const rest = parts.slice(i).join(".");
330
+ return rest
331
+ ? (`${this._adapter[prefix]}.${rest}` as FieldPath<T>)
332
+ : (this._adapter[prefix] as FieldPath<T>);
333
+ }
334
+ }
335
+
336
+ return field;
337
+ }
338
+
339
+ static fromQueryParams<T>(
340
+ query: Record<string, any>,
341
+ adapter?: CriteriaAdapter<any, any>
342
+ ): Criteria<T> {
207
343
  const criteria = Criteria.create<T>();
208
344
 
345
+ if (adapter) {
346
+ criteria.useAdapter(adapter);
347
+ }
348
+
209
349
  for (const [key, value] of Object.entries(query)) {
210
- // Pagination
211
350
  if (key === "page") {
212
- continue; // We'll handle pagination after
351
+ continue;
213
352
  }
214
353
  if (key === "limit") {
215
354
  continue;
@@ -218,35 +357,78 @@ export class Criteria<T = any> {
218
357
  continue;
219
358
  }
220
359
 
221
- const [field, operatorRaw] = key.split(":");
360
+ const [field, operatorWithQuantifier] = key.split(":");
222
361
 
223
- if (!operatorRaw || !field) continue;
362
+ if (!operatorWithQuantifier || !field) continue;
363
+
364
+ const [operatorRaw, quantifierRaw] = operatorWithQuantifier.split("@");
224
365
  const operator = isOperator(operatorRaw) ? operatorRaw : null;
225
- if (!operator)
366
+ if (!operator) {
226
367
  throw new InvalidCriteriaError(`Invalid filter operator`, operatorRaw);
368
+ }
369
+
370
+ const validQuantifiers = ["some", "every", "none"];
371
+ const quantifier =
372
+ quantifierRaw && validQuantifiers.includes(quantifierRaw)
373
+ ? (quantifierRaw as CriteriaOptions["quantifier"])
374
+ : undefined;
375
+
376
+ if (quantifierRaw && !quantifier) {
377
+ throw new InvalidCriteriaError(
378
+ `Invalid quantifier. Valid values: ${validQuantifiers.join(", ")}`,
379
+ quantifierRaw
380
+ );
381
+ }
382
+
383
+ const options: CriteriaOptions | undefined = quantifier
384
+ ? { quantifier }
385
+ : undefined;
227
386
 
228
387
  let parsedValue: any = value;
229
388
 
389
+ const resolvedField = criteria.resolveFieldPath(field as FieldPath<T>);
390
+
230
391
  if (operator === "between") {
231
392
  parsedValue = value
232
393
  .split(",")
233
394
  .map((v: any) => parseQueryValue(v.trim()));
234
395
  if (parsedValue.length === 2) {
235
- criteria.whereBetween(field as any, parsedValue[0], parsedValue[1]);
396
+ criteria.where(
397
+ resolvedField,
398
+ "between" as OperatorsForType<PathValue<T, FieldPath<T>>>,
399
+ [parsedValue[0], parsedValue[1]] as [
400
+ PathValue<T, FieldPath<T>>,
401
+ PathValue<T, FieldPath<T>>
402
+ ],
403
+ options
404
+ );
236
405
  }
237
406
  continue;
238
407
  }
239
408
 
240
409
  if (operator === "in" || operator === "notIn") {
241
410
  parsedValue = value.split(",").map(parseQueryValue);
242
- criteria.where(field as any, operator, parsedValue);
411
+ criteria.where(
412
+ field as any,
413
+ operator as OperatorsForType<PathValue<T, FieldPath<T>>>,
414
+ parsedValue,
415
+ options
416
+ );
243
417
  continue;
244
418
  }
245
419
 
246
- criteria.where(field as FieldPath<T>, operator, parseQueryValue(value));
420
+ const parsedFinalValue = parseQueryValue(value);
421
+
422
+ criteria.validateOperator(operator, parsedFinalValue);
423
+
424
+ criteria.where(
425
+ field as FieldPath<T>,
426
+ operator as OperatorsForType<PathValue<T, FieldPath<T>>>,
427
+ parsedFinalValue,
428
+ options
429
+ );
247
430
  }
248
431
 
249
- // Pagination
250
432
  const page = query.page ? parseInt(query.page) : undefined;
251
433
  const limit = query.limit ? parseInt(query.limit) : undefined;
252
434
 
@@ -254,12 +436,14 @@ export class Criteria<T = any> {
254
436
  criteria.paginate(page, limit);
255
437
  }
256
438
 
257
- // Sorting
258
439
  if (query.orderBy) {
259
440
  const sortParts = query.orderBy.split(",");
260
441
  sortParts.forEach((part: string) => {
261
442
  const [field, direction] = part.split(":");
262
- criteria.orderBy(field as FieldPath<T>, (direction as OrderDirection) || "asc");
443
+ criteria.orderBy(
444
+ field as FieldPath<T>,
445
+ (direction as OrderDirection) || "asc"
446
+ );
263
447
  });
264
448
  }
265
449
 
@@ -268,24 +452,22 @@ export class Criteria<T = any> {
268
452
  .split(",")
269
453
  .filter(Boolean) as FieldPath<T>[];
270
454
 
271
- criteria.search(fields, query.search as string);
455
+ const resolvedFields = fields.map(criteria.resolveFieldPath);
456
+ criteria.search(resolvedFields, query.search as string);
272
457
  }
273
458
 
274
459
  return criteria;
275
460
  }
276
- }
277
-
278
- // ============================================================================
279
- // Helper Functions
280
- // ============================================================================
281
461
 
282
- function parseQueryValue(value: string): any {
283
- if (!isNaN(Number(value))) return Number(value); // number
284
- if (value === "true" || value === "false") return value === "true"; // boolean
285
- if (!isNaN(Date.parse(value))) return new Date(value); // Date
286
- return value; // string
287
- }
288
-
289
- function isOperator(value: string): value is FilterOperator {
290
- return FILTER_OPERATORS.includes(value as FilterOperator);
462
+ private validateOperator(operator: FilterOperator, value: any): void {
463
+ if (value !== undefined && !isValidOperatorForType(value, operator)) {
464
+ const validOps = getValidOperatorsForType(value);
465
+ throw new InvalidCriteriaError(
466
+ `Operator "${operator}" is not valid for type "${typeof value}". Valid operators: ${validOps.join(
467
+ ", "
468
+ )}`,
469
+ operator
470
+ );
471
+ }
472
+ }
291
473
  }
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
+ }