@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/CHANGELOG.md +56 -23
- package/dist/criteria.d.ts +30 -14
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +151 -61
- package/dist/criteria.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.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/types/criteria.d.ts +27 -7
- package/dist/types/criteria.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/eslint.config.js +6 -0
- package/package.json +1 -1
- package/src/criteria.ts +263 -82
- package/src/index.ts +7 -2
- package/src/paginated-result.ts +7 -8
- package/src/types/criteria.ts +90 -17
- package/src/utils/criteria-operator-validation.ts +171 -0
- package/src/utils/helpers.ts +6 -0
- package/tests/criteria.test.ts +316 -1
package/src/types/criteria.ts
CHANGED
|
@@ -17,12 +17,65 @@ export const FILTER_OPERATORS = [
|
|
|
17
17
|
"isNotNull",
|
|
18
18
|
] as const;
|
|
19
19
|
|
|
20
|
+
export type FilterOperator = (typeof FILTER_OPERATORS)[number];
|
|
21
|
+
|
|
22
|
+
export type StringOperators =
|
|
23
|
+
| "equals"
|
|
24
|
+
| "notEquals"
|
|
25
|
+
| "contains"
|
|
26
|
+
| "startsWith"
|
|
27
|
+
| "endsWith"
|
|
28
|
+
| "in"
|
|
29
|
+
| "notIn"
|
|
30
|
+
| "isNull"
|
|
31
|
+
| "isNotNull";
|
|
32
|
+
|
|
33
|
+
export type NumberOperators =
|
|
34
|
+
| "equals"
|
|
35
|
+
| "notEquals"
|
|
36
|
+
| "greaterThan"
|
|
37
|
+
| "greaterThanOrEqual"
|
|
38
|
+
| "lessThan"
|
|
39
|
+
| "lessThanOrEqual"
|
|
40
|
+
| "in"
|
|
41
|
+
| "notIn"
|
|
42
|
+
| "between"
|
|
43
|
+
| "isNull"
|
|
44
|
+
| "isNotNull";
|
|
45
|
+
|
|
46
|
+
export type DateOperators =
|
|
47
|
+
| "equals"
|
|
48
|
+
| "notEquals"
|
|
49
|
+
| "greaterThan"
|
|
50
|
+
| "greaterThanOrEqual"
|
|
51
|
+
| "lessThan"
|
|
52
|
+
| "lessThanOrEqual"
|
|
53
|
+
| "in"
|
|
54
|
+
| "notIn"
|
|
55
|
+
| "between"
|
|
56
|
+
| "isNull"
|
|
57
|
+
| "isNotNull";
|
|
58
|
+
|
|
59
|
+
export type BooleanOperators = "equals" | "notEquals" | "isNull" | "isNotNull";
|
|
60
|
+
|
|
61
|
+
export type ArrayOperators = "in" | "notIn" | "isNull" | "isNotNull";
|
|
62
|
+
|
|
63
|
+
export type OperatorsForType<T> = T extends string
|
|
64
|
+
? StringOperators
|
|
65
|
+
: T extends number
|
|
66
|
+
? NumberOperators
|
|
67
|
+
: T extends Date
|
|
68
|
+
? DateOperators
|
|
69
|
+
: T extends boolean
|
|
70
|
+
? BooleanOperators
|
|
71
|
+
: T extends Array<any>
|
|
72
|
+
? ArrayOperators
|
|
73
|
+
: FilterOperator;
|
|
74
|
+
|
|
20
75
|
export type FilterValueFor<T> =
|
|
21
|
-
| T
|
|
22
|
-
| (T extends number | Date
|
|
23
|
-
|
|
24
|
-
: never)
|
|
25
|
-
| T[] // in, notIn
|
|
76
|
+
| T
|
|
77
|
+
| (T extends number | Date ? [T, T] : never)
|
|
78
|
+
| T[]
|
|
26
79
|
| null;
|
|
27
80
|
|
|
28
81
|
export type PathValue<
|
|
@@ -30,24 +83,34 @@ export type PathValue<
|
|
|
30
83
|
P extends string
|
|
31
84
|
> = P extends `${infer K}.${infer Rest}`
|
|
32
85
|
? K extends keyof T
|
|
33
|
-
?
|
|
86
|
+
? T[K] extends Array<infer U>
|
|
87
|
+
? PathValue<U, Rest>
|
|
88
|
+
: PathValue<T[K], Rest>
|
|
34
89
|
: never
|
|
35
90
|
: P extends keyof T
|
|
36
91
|
? T[P]
|
|
37
92
|
: never;
|
|
38
93
|
|
|
39
|
-
export type FilterOperator = (typeof FILTER_OPERATORS)[number];
|
|
40
|
-
|
|
41
94
|
export interface Filter<TField = string, TValue = unknown> {
|
|
42
95
|
field: TField;
|
|
43
|
-
operator: FilterOperator
|
|
96
|
+
operator: unknown extends TValue ? FilterOperator : OperatorsForType<TValue>;
|
|
44
97
|
value: TValue;
|
|
98
|
+
options?: CriteriaOptions;
|
|
45
99
|
}
|
|
46
100
|
|
|
47
101
|
export type TypedFilter<T> = {
|
|
48
|
-
[K in FieldPath<T>]:
|
|
102
|
+
[K in FieldPath<T>]: {
|
|
103
|
+
field: K;
|
|
104
|
+
operator: OperatorsForType<NonNullable<PathValue<T, K>>>;
|
|
105
|
+
value: FilterValueFor<NonNullable<PathValue<T, K>>>;
|
|
106
|
+
options?: CriteriaOptions;
|
|
107
|
+
};
|
|
49
108
|
}[FieldPath<T>];
|
|
50
109
|
|
|
110
|
+
export type CriteriaAdapter<Input, Output> = {
|
|
111
|
+
[K in FieldPath<Input>]?: FieldPath<Output>;
|
|
112
|
+
};
|
|
113
|
+
|
|
51
114
|
export type OrderDirection = "asc" | "desc";
|
|
52
115
|
|
|
53
116
|
export interface Order {
|
|
@@ -75,10 +138,20 @@ export interface PaginationMeta {
|
|
|
75
138
|
hasPrevious: boolean;
|
|
76
139
|
}
|
|
77
140
|
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
export interface CriteriaOptions {
|
|
142
|
+
quantifier?: "some" | "every" | "none";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type ExcludeBuiltInKeys<T> = Exclude<keyof T, keyof any[] | number | symbol>;
|
|
146
|
+
|
|
147
|
+
export type FieldPath<T> = T extends Primitive
|
|
148
|
+
? never
|
|
149
|
+
: {
|
|
150
|
+
[K in ExcludeBuiltInKeys<T> & string]: NonNullable<T[K]> extends Primitive
|
|
151
|
+
? K
|
|
152
|
+
: NonNullable<T[K]> extends Array<infer U>
|
|
153
|
+
? U extends Primitive
|
|
154
|
+
? K
|
|
155
|
+
: K | `${K}.${FieldPath<U>}`
|
|
156
|
+
: K | `${K}.${FieldPath<NonNullable<T[K]>>}`;
|
|
157
|
+
}[ExcludeBuiltInKeys<T> & string];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayOperators,
|
|
3
|
+
BooleanOperators,
|
|
4
|
+
DateOperators,
|
|
5
|
+
FILTER_OPERATORS,
|
|
6
|
+
FilterOperator,
|
|
7
|
+
NumberOperators,
|
|
8
|
+
StringOperators,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
export function isValidOperatorForType(
|
|
12
|
+
value: unknown,
|
|
13
|
+
operator: FilterOperator
|
|
14
|
+
): boolean {
|
|
15
|
+
// Handle null/undefined
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return ["isNull", "isNotNull", "equals", "notEquals"].includes(operator);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Special case: between operator with array [min, max]
|
|
21
|
+
if (operator === "between" && Array.isArray(value) && value.length === 2) {
|
|
22
|
+
// Validate based on the type of the first element
|
|
23
|
+
const elementType = typeof value[0];
|
|
24
|
+
if (elementType === "number" || value[0] instanceof Date) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const valueType = typeof value;
|
|
31
|
+
|
|
32
|
+
// String operators
|
|
33
|
+
if (valueType === "string") {
|
|
34
|
+
const validOps: StringOperators[] = [
|
|
35
|
+
"equals",
|
|
36
|
+
"notEquals",
|
|
37
|
+
"contains",
|
|
38
|
+
"startsWith",
|
|
39
|
+
"endsWith",
|
|
40
|
+
"in",
|
|
41
|
+
"notIn",
|
|
42
|
+
"isNull",
|
|
43
|
+
"isNotNull",
|
|
44
|
+
];
|
|
45
|
+
return validOps.includes(operator as StringOperators);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Number operators
|
|
49
|
+
if (valueType === "number") {
|
|
50
|
+
const validOps: NumberOperators[] = [
|
|
51
|
+
"equals",
|
|
52
|
+
"notEquals",
|
|
53
|
+
"greaterThan",
|
|
54
|
+
"greaterThanOrEqual",
|
|
55
|
+
"lessThan",
|
|
56
|
+
"lessThanOrEqual",
|
|
57
|
+
"in",
|
|
58
|
+
"notIn",
|
|
59
|
+
"between",
|
|
60
|
+
"isNull",
|
|
61
|
+
"isNotNull",
|
|
62
|
+
];
|
|
63
|
+
return validOps.includes(operator as NumberOperators);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Boolean operators
|
|
67
|
+
if (valueType === "boolean") {
|
|
68
|
+
const validOps: BooleanOperators[] = [
|
|
69
|
+
"equals",
|
|
70
|
+
"notEquals",
|
|
71
|
+
"isNull",
|
|
72
|
+
"isNotNull",
|
|
73
|
+
];
|
|
74
|
+
return validOps.includes(operator as BooleanOperators);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Date operators
|
|
78
|
+
if (value instanceof Date) {
|
|
79
|
+
const validOps: DateOperators[] = [
|
|
80
|
+
"equals",
|
|
81
|
+
"notEquals",
|
|
82
|
+
"greaterThan",
|
|
83
|
+
"greaterThanOrEqual",
|
|
84
|
+
"lessThan",
|
|
85
|
+
"lessThanOrEqual",
|
|
86
|
+
"in",
|
|
87
|
+
"notIn",
|
|
88
|
+
"between",
|
|
89
|
+
"isNull",
|
|
90
|
+
"isNotNull",
|
|
91
|
+
];
|
|
92
|
+
return validOps.includes(operator as DateOperators);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Array operators
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
const validOps: ArrayOperators[] = ["in", "notIn", "isNull", "isNotNull"];
|
|
98
|
+
return validOps.includes(operator as ArrayOperators);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For unknown types, allow all operators
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getValidOperatorsForType(value: unknown): FilterOperator[] {
|
|
106
|
+
if (value === null || value === undefined) {
|
|
107
|
+
return ["isNull", "isNotNull", "equals", "notEquals"];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const valueType = typeof value;
|
|
111
|
+
|
|
112
|
+
if (valueType === "string" && Number.isNaN(Number(value))) {
|
|
113
|
+
return [
|
|
114
|
+
"equals",
|
|
115
|
+
"notEquals",
|
|
116
|
+
"contains",
|
|
117
|
+
"startsWith",
|
|
118
|
+
"endsWith",
|
|
119
|
+
"in",
|
|
120
|
+
"notIn",
|
|
121
|
+
"isNull",
|
|
122
|
+
"isNotNull",
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (valueType === "number") {
|
|
127
|
+
return [
|
|
128
|
+
"equals",
|
|
129
|
+
"notEquals",
|
|
130
|
+
"greaterThan",
|
|
131
|
+
"greaterThanOrEqual",
|
|
132
|
+
"lessThan",
|
|
133
|
+
"lessThanOrEqual",
|
|
134
|
+
"in",
|
|
135
|
+
"notIn",
|
|
136
|
+
"between",
|
|
137
|
+
"isNull",
|
|
138
|
+
"isNotNull",
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (valueType === "boolean") {
|
|
143
|
+
return ["equals", "notEquals", "isNull", "isNotNull"];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (value instanceof Date) {
|
|
147
|
+
return [
|
|
148
|
+
"equals",
|
|
149
|
+
"notEquals",
|
|
150
|
+
"greaterThan",
|
|
151
|
+
"greaterThanOrEqual",
|
|
152
|
+
"lessThan",
|
|
153
|
+
"lessThanOrEqual",
|
|
154
|
+
"in",
|
|
155
|
+
"notIn",
|
|
156
|
+
"between",
|
|
157
|
+
"isNull",
|
|
158
|
+
"isNotNull",
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
return ["in", "notIn", "isNull", "isNotNull"];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [...FILTER_OPERATORS];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isOperator(value: string): value is FilterOperator {
|
|
170
|
+
return FILTER_OPERATORS.includes(value as FilterOperator);
|
|
171
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function parseQueryValue(value: string): any {
|
|
2
|
+
if (!isNaN(Number(value))) return Number(value); // number
|
|
3
|
+
if (value === "true" || value === "false") return value === "true"; // boolean
|
|
4
|
+
if (!isNaN(Date.parse(value))) return new Date(value); // Date
|
|
5
|
+
return value; // string
|
|
6
|
+
}
|
package/tests/criteria.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Pagination } from "../src";
|
|
2
2
|
import { Criteria } from "../src/criteria";
|
|
3
3
|
import { PaginatedResult } from "../src/paginated-result";
|
|
4
|
+
import { CriteriaAdapter } from "../src/types";
|
|
4
5
|
import { Post } from "./utils";
|
|
5
6
|
|
|
6
7
|
interface TestUser {
|
|
@@ -12,6 +13,17 @@ interface TestUser {
|
|
|
12
13
|
createdAt: Date;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
interface UserWithPostsDto {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
posts: { title: string; content: string }[];
|
|
20
|
+
leads: {
|
|
21
|
+
contact: {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
const testUsers: TestUser[] = [
|
|
16
28
|
{
|
|
17
29
|
id: "1",
|
|
@@ -56,6 +68,20 @@ const testUsers: TestUser[] = [
|
|
|
56
68
|
];
|
|
57
69
|
|
|
58
70
|
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
|
+
});
|
|
59
85
|
describe("Fluent API", () => {
|
|
60
86
|
it("should create empty criteria", () => {
|
|
61
87
|
const criteria = Criteria.create<TestUser>();
|
|
@@ -297,7 +323,7 @@ describe("Criteria", () => {
|
|
|
297
323
|
|
|
298
324
|
expect(result.data).toHaveLength(2);
|
|
299
325
|
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
|
|
300
|
-
expect(result.meta.total).toBe(3);
|
|
326
|
+
expect(result.meta.total).toBe(3);
|
|
301
327
|
expect(result.meta.totalPages).toBe(2);
|
|
302
328
|
});
|
|
303
329
|
});
|
|
@@ -437,4 +463,293 @@ describe("Criteria", () => {
|
|
|
437
463
|
expect(criteria.getPagination()?.limit).toBe(2);
|
|
438
464
|
});
|
|
439
465
|
});
|
|
466
|
+
|
|
467
|
+
describe("Quantifiers", () => {
|
|
468
|
+
describe("Fluent API methods", () => {
|
|
469
|
+
it("should create filter with whereSome", () => {
|
|
470
|
+
const criteria = Criteria.create<UserWithPostsDto>().whereSome(
|
|
471
|
+
"posts.title",
|
|
472
|
+
"contains",
|
|
473
|
+
"test"
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const filters = criteria.getFilters();
|
|
477
|
+
|
|
478
|
+
expect(filters).toHaveLength(1);
|
|
479
|
+
expect(filters[0].field).toBe("posts.title");
|
|
480
|
+
expect(filters[0].operator).toBe("contains");
|
|
481
|
+
expect(filters[0].value).toBe("test");
|
|
482
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("should create filter with whereEvery", () => {
|
|
486
|
+
const criteria = Criteria.create<UserWithPostsDto>().whereEvery(
|
|
487
|
+
"posts.title",
|
|
488
|
+
"contains",
|
|
489
|
+
"test"
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const filters = criteria.getFilters();
|
|
493
|
+
expect(filters).toHaveLength(1);
|
|
494
|
+
expect(filters[0].options?.quantifier).toBe("every");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should create filter with whereNone", () => {
|
|
498
|
+
const criteria = Criteria.create<UserWithPostsDto>().whereNone(
|
|
499
|
+
"posts.title",
|
|
500
|
+
"contains",
|
|
501
|
+
"test"
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const filters = criteria.getFilters();
|
|
505
|
+
expect(filters).toHaveLength(1);
|
|
506
|
+
expect(filters[0].options?.quantifier).toBe("none");
|
|
507
|
+
});
|
|
508
|
+
|
|
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
|
+
it("should parse quantifier from query params with @some", () => {
|
|
524
|
+
const queryParams = {
|
|
525
|
+
"posts.title:contains@some": "test",
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const criteria =
|
|
529
|
+
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
530
|
+
const filters = criteria.getFilters();
|
|
531
|
+
|
|
532
|
+
expect(filters).toHaveLength(1);
|
|
533
|
+
expect(filters[0].field).toBe("posts.title");
|
|
534
|
+
expect(filters[0].operator).toBe("contains");
|
|
535
|
+
expect(filters[0].value).toBe("test");
|
|
536
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
537
|
+
});
|
|
538
|
+
|
|
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
|
+
it("should work with in operator and quantifier", () => {
|
|
566
|
+
const queryParams = {
|
|
567
|
+
"posts.title:in@some": "test1,test2,test3",
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const criteria =
|
|
571
|
+
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
572
|
+
const filters = criteria.getFilters();
|
|
573
|
+
|
|
574
|
+
expect(filters).toHaveLength(1);
|
|
575
|
+
expect(filters[0].operator).toBe("in");
|
|
576
|
+
expect(filters[0].value).toEqual(["test1", "test2", "test3"]);
|
|
577
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("should work with between operator and quantifier", () => {
|
|
581
|
+
const queryParams = {
|
|
582
|
+
"posts.likes:between@some": "10,100",
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const criteria =
|
|
586
|
+
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
587
|
+
const filters = criteria.getFilters();
|
|
588
|
+
|
|
589
|
+
expect(filters).toHaveLength(1);
|
|
590
|
+
expect(filters[0].operator).toBe("between");
|
|
591
|
+
expect(filters[0].value).toEqual([10, 100]);
|
|
592
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
593
|
+
});
|
|
594
|
+
|
|
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
|
+
it("should throw error for invalid quantifier", () => {
|
|
612
|
+
const queryParams = {
|
|
613
|
+
"posts.title:contains@invalid": "test",
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
expect(() => {
|
|
617
|
+
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
618
|
+
}).toThrow("Invalid quantifier");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("should handle multiple filters with mixed quantifiers", () => {
|
|
622
|
+
const queryParams = {
|
|
623
|
+
"posts.title:contains@some": "test",
|
|
624
|
+
"posts.content:equals@every": "content",
|
|
625
|
+
"name:contains": "John",
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const criteria =
|
|
629
|
+
Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
|
|
630
|
+
const filters = criteria.getFilters();
|
|
631
|
+
|
|
632
|
+
expect(filters).toHaveLength(3);
|
|
633
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
634
|
+
expect(filters[1].options?.quantifier).toBe("every");
|
|
635
|
+
expect(filters[2].options).toBeUndefined();
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe("fromObject with quantifier", () => {
|
|
640
|
+
it("should create criteria from object with quantifier", () => {
|
|
641
|
+
const criteria = Criteria.fromObject<UserWithPostsDto>({
|
|
642
|
+
filters: [
|
|
643
|
+
{
|
|
644
|
+
field: "posts.title",
|
|
645
|
+
operator: "contains",
|
|
646
|
+
value: "test",
|
|
647
|
+
options: { quantifier: "some" },
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const filters = criteria.getFilters();
|
|
653
|
+
expect(filters).toHaveLength(1);
|
|
654
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("should preserve quantifier when cloning", () => {
|
|
658
|
+
const original = Criteria.create<UserWithPostsDto>().whereSome(
|
|
659
|
+
"posts.title",
|
|
660
|
+
"contains",
|
|
661
|
+
"test"
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
const cloned = original.clone();
|
|
665
|
+
const filters = cloned.getFilters();
|
|
666
|
+
|
|
667
|
+
expect(filters).toHaveLength(1);
|
|
668
|
+
expect(filters[0].options?.quantifier).toBe("some");
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
describe("toJSON with quantifier", () => {
|
|
673
|
+
it("should serialize quantifier to JSON", () => {
|
|
674
|
+
const criteria = Criteria.create<UserWithPostsDto>()
|
|
675
|
+
.whereSome("posts.title", "contains", "test")
|
|
676
|
+
.whereEvery("posts.content", "equals", "content");
|
|
677
|
+
|
|
678
|
+
const json = criteria.toJSON();
|
|
679
|
+
|
|
680
|
+
expect(json.filters).toHaveLength(2);
|
|
681
|
+
expect(json.filters[0].options?.quantifier).toBe("some");
|
|
682
|
+
expect(json.filters[1].options?.quantifier).toBe("every");
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("Adapter", () => {
|
|
688
|
+
type UserInDatabase = {
|
|
689
|
+
id: string;
|
|
690
|
+
name: string;
|
|
691
|
+
user_posts: { title: string; content: string }[];
|
|
692
|
+
leads: {
|
|
693
|
+
contact: {
|
|
694
|
+
fullName: string;
|
|
695
|
+
};
|
|
696
|
+
};
|
|
697
|
+
};
|
|
698
|
+
const UserWithPostsAdapter: CriteriaAdapter<
|
|
699
|
+
UserWithPostsDto,
|
|
700
|
+
UserInDatabase
|
|
701
|
+
> = {
|
|
702
|
+
posts: "user_posts",
|
|
703
|
+
"leads.contact.name": "leads.contact.fullName",
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
let criteria: Criteria<UserWithPostsDto>;
|
|
707
|
+
beforeEach(() => {
|
|
708
|
+
criteria =
|
|
709
|
+
Criteria.create<UserWithPostsDto>().useAdapter(UserWithPostsAdapter);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("should resolve field path", () => {
|
|
713
|
+
const filters = criteria
|
|
714
|
+
.where("leads.contact.name", "contains", "John")
|
|
715
|
+
.getFilters();
|
|
716
|
+
|
|
717
|
+
expect(filters[0].field).toBe("leads.contact.fullName");
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("should resolve field path from query params", () => {
|
|
721
|
+
const queryParams = {
|
|
722
|
+
"posts:contains": "test",
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const result = Criteria.fromQueryParams<UserWithPostsDto>(
|
|
726
|
+
queryParams,
|
|
727
|
+
UserWithPostsAdapter
|
|
728
|
+
);
|
|
729
|
+
const filters = result.getFilters();
|
|
730
|
+
|
|
731
|
+
expect(filters).toHaveLength(1);
|
|
732
|
+
expect(filters[0].field).toBe("user_posts");
|
|
733
|
+
expect(filters[0].operator).toBe("contains");
|
|
734
|
+
expect(filters[0].value).toBe("test");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("should resolve field path from object", () => {
|
|
738
|
+
const criteria = Criteria.fromObject<UserWithPostsDto>(
|
|
739
|
+
{
|
|
740
|
+
filters: [
|
|
741
|
+
{
|
|
742
|
+
field: "posts",
|
|
743
|
+
operator: "in",
|
|
744
|
+
value: [{ title: "test", content: "test" }],
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
UserWithPostsAdapter
|
|
749
|
+
);
|
|
750
|
+
const filters = criteria.getFilters();
|
|
751
|
+
expect(filters).toHaveLength(1);
|
|
752
|
+
expect(filters[0].field).toBe("user_posts");
|
|
753
|
+
});
|
|
754
|
+
});
|
|
440
755
|
});
|