@woltz/rich-domain 1.1.0 → 1.2.0

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