@kernl-sdk/turbopuffer 0.1.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-check-types.log +60 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +201 -0
- package/README.md +60 -0
- package/dist/__tests__/convert.test.d.ts +2 -0
- package/dist/__tests__/convert.test.d.ts.map +1 -0
- package/dist/__tests__/convert.test.js +346 -0
- package/dist/__tests__/filter.test.d.ts +8 -0
- package/dist/__tests__/filter.test.d.ts.map +1 -0
- package/dist/__tests__/filter.test.js +649 -0
- package/dist/__tests__/filters.integration.test.d.ts +8 -0
- package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
- package/dist/__tests__/filters.integration.test.js +502 -0
- package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/filters.integration.test.js +475 -0
- package/dist/__tests__/integration/integration.test.d.ts +2 -0
- package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/integration.test.js +329 -0
- package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
- package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
- package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/memory.integration.test.js +287 -0
- package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/query.integration.test.js +385 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +343 -0
- package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
- package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/lifecycle.integration.test.js +385 -0
- package/dist/__tests__/query.integration.test.d.ts +8 -0
- package/dist/__tests__/query.integration.test.d.ts.map +1 -0
- package/dist/__tests__/query.integration.test.js +423 -0
- package/dist/__tests__/query.test.d.ts +8 -0
- package/dist/__tests__/query.test.d.ts.map +1 -0
- package/dist/__tests__/query.test.js +472 -0
- package/dist/convert/document.d.ts +20 -0
- package/dist/convert/document.d.ts.map +1 -0
- package/dist/convert/document.js +72 -0
- package/dist/convert/filter.d.ts +15 -0
- package/dist/convert/filter.d.ts.map +1 -0
- package/dist/convert/filter.js +109 -0
- package/dist/convert/index.d.ts +8 -0
- package/dist/convert/index.d.ts.map +1 -0
- package/dist/convert/index.js +7 -0
- package/dist/convert/query.d.ts +22 -0
- package/dist/convert/query.d.ts.map +1 -0
- package/dist/convert/query.js +111 -0
- package/dist/convert/schema.d.ts +39 -0
- package/dist/convert/schema.d.ts.map +1 -0
- package/dist/convert/schema.js +124 -0
- package/dist/convert.d.ts +68 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +333 -0
- package/dist/handle.d.ts +34 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +72 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/search.d.ts +85 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +167 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
- package/src/__tests__/convert.test.ts +425 -0
- package/src/__tests__/filter.test.ts +730 -0
- package/src/__tests__/integration/filters.integration.test.ts +558 -0
- package/src/__tests__/integration/integration.test.ts +399 -0
- package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
- package/src/__tests__/integration/memory.integration.test.ts +353 -0
- package/src/__tests__/integration/query.integration.test.ts +471 -0
- package/src/__tests__/query.test.ts +636 -0
- package/src/convert/document.ts +95 -0
- package/src/convert/filter.ts +123 -0
- package/src/convert/index.ts +8 -0
- package/src/convert/query.ts +151 -0
- package/src/convert/schema.ts +163 -0
- package/src/handle.ts +104 -0
- package/src/index.ts +31 -0
- package/src/search.ts +207 -0
- package/src/types.ts +14 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive unit tests for filter conversion.
|
|
3
|
+
*
|
|
4
|
+
* Tests all filter operators across scalar types, logical composition,
|
|
5
|
+
* edge cases, and error conditions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { FILTER } from "../convert/filter";
|
|
11
|
+
|
|
12
|
+
describe("FILTER", () => {
|
|
13
|
+
// ============================================================
|
|
14
|
+
// EQUALITY OPERATORS
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
describe("equality operators", () => {
|
|
18
|
+
describe("$eq / shorthand equality", () => {
|
|
19
|
+
it("encodes string equality (shorthand)", () => {
|
|
20
|
+
expect(FILTER.encode({ status: "active" })).toEqual([
|
|
21
|
+
"status",
|
|
22
|
+
"Eq",
|
|
23
|
+
"active",
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("encodes string equality (explicit $eq)", () => {
|
|
28
|
+
expect(FILTER.encode({ status: { $eq: "active" } })).toEqual([
|
|
29
|
+
"status",
|
|
30
|
+
"Eq",
|
|
31
|
+
"active",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("encodes number equality", () => {
|
|
36
|
+
expect(FILTER.encode({ count: 42 })).toEqual(["count", "Eq", 42]);
|
|
37
|
+
expect(FILTER.encode({ count: { $eq: 42 } })).toEqual([
|
|
38
|
+
"count",
|
|
39
|
+
"Eq",
|
|
40
|
+
42,
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("encodes zero", () => {
|
|
45
|
+
expect(FILTER.encode({ count: 0 })).toEqual(["count", "Eq", 0]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("encodes negative numbers", () => {
|
|
49
|
+
expect(FILTER.encode({ temp: -10 })).toEqual(["temp", "Eq", -10]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("encodes floating point numbers", () => {
|
|
53
|
+
expect(FILTER.encode({ score: 3.14 })).toEqual(["score", "Eq", 3.14]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("encodes boolean true", () => {
|
|
57
|
+
expect(FILTER.encode({ enabled: true })).toEqual([
|
|
58
|
+
"enabled",
|
|
59
|
+
"Eq",
|
|
60
|
+
true,
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("encodes boolean false", () => {
|
|
65
|
+
expect(FILTER.encode({ enabled: false })).toEqual([
|
|
66
|
+
"enabled",
|
|
67
|
+
"Eq",
|
|
68
|
+
false,
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("encodes null", () => {
|
|
73
|
+
expect(FILTER.encode({ deletedAt: null })).toEqual([
|
|
74
|
+
"deletedAt",
|
|
75
|
+
"Eq",
|
|
76
|
+
null,
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("encodes empty string", () => {
|
|
81
|
+
expect(FILTER.encode({ name: "" })).toEqual(["name", "Eq", ""]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("$neq", () => {
|
|
86
|
+
it("encodes string not-equal", () => {
|
|
87
|
+
expect(FILTER.encode({ status: { $neq: "deleted" } })).toEqual([
|
|
88
|
+
"status",
|
|
89
|
+
"NotEq",
|
|
90
|
+
"deleted",
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("encodes number not-equal", () => {
|
|
95
|
+
expect(FILTER.encode({ count: { $neq: 0 } })).toEqual([
|
|
96
|
+
"count",
|
|
97
|
+
"NotEq",
|
|
98
|
+
0,
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("encodes boolean not-equal", () => {
|
|
103
|
+
expect(FILTER.encode({ active: { $neq: false } })).toEqual([
|
|
104
|
+
"active",
|
|
105
|
+
"NotEq",
|
|
106
|
+
false,
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("encodes null not-equal (field exists)", () => {
|
|
111
|
+
expect(FILTER.encode({ field: { $neq: null } })).toEqual([
|
|
112
|
+
"field",
|
|
113
|
+
"NotEq",
|
|
114
|
+
null,
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// COMPARISON OPERATORS
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
describe("comparison operators", () => {
|
|
125
|
+
describe("$gt (greater than)", () => {
|
|
126
|
+
it("encodes $gt with positive number", () => {
|
|
127
|
+
expect(FILTER.encode({ views: { $gt: 1000 } })).toEqual([
|
|
128
|
+
"views",
|
|
129
|
+
"Gt",
|
|
130
|
+
1000,
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("encodes $gt with zero", () => {
|
|
135
|
+
expect(FILTER.encode({ count: { $gt: 0 } })).toEqual([
|
|
136
|
+
"count",
|
|
137
|
+
"Gt",
|
|
138
|
+
0,
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("encodes $gt with negative number", () => {
|
|
143
|
+
expect(FILTER.encode({ temp: { $gt: -10 } })).toEqual([
|
|
144
|
+
"temp",
|
|
145
|
+
"Gt",
|
|
146
|
+
-10,
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("encodes $gt with float", () => {
|
|
151
|
+
expect(FILTER.encode({ rating: { $gt: 4.5 } })).toEqual([
|
|
152
|
+
"rating",
|
|
153
|
+
"Gt",
|
|
154
|
+
4.5,
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("$gte (greater than or equal)", () => {
|
|
160
|
+
it("encodes $gte with number", () => {
|
|
161
|
+
expect(FILTER.encode({ views: { $gte: 100 } })).toEqual([
|
|
162
|
+
"views",
|
|
163
|
+
"Gte",
|
|
164
|
+
100,
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("encodes $gte with zero", () => {
|
|
169
|
+
expect(FILTER.encode({ count: { $gte: 0 } })).toEqual([
|
|
170
|
+
"count",
|
|
171
|
+
"Gte",
|
|
172
|
+
0,
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("$lt (less than)", () => {
|
|
178
|
+
it("encodes $lt with number", () => {
|
|
179
|
+
expect(FILTER.encode({ age: { $lt: 18 } })).toEqual(["age", "Lt", 18]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("encodes $lt with negative number", () => {
|
|
183
|
+
expect(FILTER.encode({ balance: { $lt: -100 } })).toEqual([
|
|
184
|
+
"balance",
|
|
185
|
+
"Lt",
|
|
186
|
+
-100,
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("$lte (less than or equal)", () => {
|
|
192
|
+
it("encodes $lte with number", () => {
|
|
193
|
+
expect(FILTER.encode({ priority: { $lte: 5 } })).toEqual([
|
|
194
|
+
"priority",
|
|
195
|
+
"Lte",
|
|
196
|
+
5,
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("combined comparisons", () => {
|
|
202
|
+
it("encodes range filter (gt + lt on same field)", () => {
|
|
203
|
+
const result = FILTER.encode({
|
|
204
|
+
price: { $gt: 10, $lt: 100 },
|
|
205
|
+
});
|
|
206
|
+
expect(result).toEqual([
|
|
207
|
+
"And",
|
|
208
|
+
[
|
|
209
|
+
["price", "Gt", 10],
|
|
210
|
+
["price", "Lt", 100],
|
|
211
|
+
],
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("encodes inclusive range filter (gte + lte)", () => {
|
|
216
|
+
const result = FILTER.encode({
|
|
217
|
+
rating: { $gte: 1, $lte: 5 },
|
|
218
|
+
});
|
|
219
|
+
expect(result).toEqual([
|
|
220
|
+
"And",
|
|
221
|
+
[
|
|
222
|
+
["rating", "Gte", 1],
|
|
223
|
+
["rating", "Lte", 5],
|
|
224
|
+
],
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ============================================================
|
|
231
|
+
// SET MEMBERSHIP OPERATORS
|
|
232
|
+
// ============================================================
|
|
233
|
+
|
|
234
|
+
describe("set membership operators", () => {
|
|
235
|
+
describe("$in", () => {
|
|
236
|
+
it("encodes $in with string array", () => {
|
|
237
|
+
expect(
|
|
238
|
+
FILTER.encode({ status: { $in: ["active", "pending"] } }),
|
|
239
|
+
).toEqual(["status", "In", ["active", "pending"]]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("encodes $in with number array", () => {
|
|
243
|
+
expect(FILTER.encode({ priority: { $in: [1, 2, 3] } })).toEqual([
|
|
244
|
+
"priority",
|
|
245
|
+
"In",
|
|
246
|
+
[1, 2, 3],
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("encodes $in with single element", () => {
|
|
251
|
+
expect(FILTER.encode({ status: { $in: ["active"] } })).toEqual([
|
|
252
|
+
"status",
|
|
253
|
+
"In",
|
|
254
|
+
["active"],
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("encodes $in with empty array", () => {
|
|
259
|
+
expect(FILTER.encode({ status: { $in: [] } })).toEqual([
|
|
260
|
+
"status",
|
|
261
|
+
"In",
|
|
262
|
+
[],
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("encodes $in with mixed types", () => {
|
|
267
|
+
expect(FILTER.encode({ value: { $in: [1, "two", true] } })).toEqual([
|
|
268
|
+
"value",
|
|
269
|
+
"In",
|
|
270
|
+
[1, "two", true],
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("$nin", () => {
|
|
276
|
+
it("encodes $nin with string array", () => {
|
|
277
|
+
expect(
|
|
278
|
+
FILTER.encode({ status: { $nin: ["deleted", "archived"] } }),
|
|
279
|
+
).toEqual(["status", "NotIn", ["deleted", "archived"]]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("encodes $nin with number array", () => {
|
|
283
|
+
expect(FILTER.encode({ code: { $nin: [0, -1] } })).toEqual([
|
|
284
|
+
"code",
|
|
285
|
+
"NotIn",
|
|
286
|
+
[0, -1],
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("encodes $nin with empty array", () => {
|
|
291
|
+
expect(FILTER.encode({ status: { $nin: [] } })).toEqual([
|
|
292
|
+
"status",
|
|
293
|
+
"NotIn",
|
|
294
|
+
[],
|
|
295
|
+
]);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ============================================================
|
|
301
|
+
// STRING PATTERN OPERATORS
|
|
302
|
+
// ============================================================
|
|
303
|
+
|
|
304
|
+
describe("string pattern operators", () => {
|
|
305
|
+
describe("$startsWith", () => {
|
|
306
|
+
it("encodes $startsWith as Glob prefix", () => {
|
|
307
|
+
expect(FILTER.encode({ name: { $startsWith: "john" } })).toEqual([
|
|
308
|
+
"name",
|
|
309
|
+
"Glob",
|
|
310
|
+
"john*",
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("encodes $startsWith with empty string", () => {
|
|
315
|
+
expect(FILTER.encode({ name: { $startsWith: "" } })).toEqual([
|
|
316
|
+
"name",
|
|
317
|
+
"Glob",
|
|
318
|
+
"*",
|
|
319
|
+
]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("encodes $startsWith with special characters", () => {
|
|
323
|
+
expect(FILTER.encode({ path: { $startsWith: "/api/v1" } })).toEqual([
|
|
324
|
+
"path",
|
|
325
|
+
"Glob",
|
|
326
|
+
"/api/v1*",
|
|
327
|
+
]);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("$endsWith", () => {
|
|
332
|
+
it("encodes $endsWith as Glob suffix", () => {
|
|
333
|
+
expect(FILTER.encode({ email: { $endsWith: "@example.com" } })).toEqual([
|
|
334
|
+
"email",
|
|
335
|
+
"Glob",
|
|
336
|
+
"*@example.com",
|
|
337
|
+
]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("encodes $endsWith with empty string", () => {
|
|
341
|
+
expect(FILTER.encode({ name: { $endsWith: "" } })).toEqual([
|
|
342
|
+
"name",
|
|
343
|
+
"Glob",
|
|
344
|
+
"*",
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("$contains", () => {
|
|
350
|
+
it("encodes $contains for array membership", () => {
|
|
351
|
+
expect(FILTER.encode({ tags: { $contains: "important" } })).toEqual([
|
|
352
|
+
"tags",
|
|
353
|
+
"Contains",
|
|
354
|
+
"important",
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("encodes $contains with number", () => {
|
|
359
|
+
expect(FILTER.encode({ ids: { $contains: 42 } })).toEqual([
|
|
360
|
+
"ids",
|
|
361
|
+
"Contains",
|
|
362
|
+
42,
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ============================================================
|
|
369
|
+
// EXISTENCE OPERATORS
|
|
370
|
+
// ============================================================
|
|
371
|
+
|
|
372
|
+
describe("existence operators", () => {
|
|
373
|
+
describe("$exists: true (field is not null)", () => {
|
|
374
|
+
it("encodes $exists: true as NotEq null", () => {
|
|
375
|
+
expect(FILTER.encode({ optionalField: { $exists: true } })).toEqual([
|
|
376
|
+
"optionalField",
|
|
377
|
+
"NotEq",
|
|
378
|
+
null,
|
|
379
|
+
]);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("$exists: false (field is null)", () => {
|
|
384
|
+
it("encodes $exists: false as Eq null", () => {
|
|
385
|
+
expect(FILTER.encode({ optionalField: { $exists: false } })).toEqual([
|
|
386
|
+
"optionalField",
|
|
387
|
+
"Eq",
|
|
388
|
+
null,
|
|
389
|
+
]);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ============================================================
|
|
395
|
+
// LOGICAL OPERATORS
|
|
396
|
+
// ============================================================
|
|
397
|
+
|
|
398
|
+
describe("logical operators", () => {
|
|
399
|
+
describe("implicit AND (multiple fields)", () => {
|
|
400
|
+
it("encodes two fields as AND", () => {
|
|
401
|
+
const result = FILTER.encode({
|
|
402
|
+
status: "active",
|
|
403
|
+
views: { $gte: 100 },
|
|
404
|
+
});
|
|
405
|
+
expect(result).toEqual([
|
|
406
|
+
"And",
|
|
407
|
+
[
|
|
408
|
+
["status", "Eq", "active"],
|
|
409
|
+
["views", "Gte", 100],
|
|
410
|
+
],
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("encodes three fields as AND", () => {
|
|
415
|
+
const result = FILTER.encode({
|
|
416
|
+
a: 1,
|
|
417
|
+
b: 2,
|
|
418
|
+
c: 3,
|
|
419
|
+
});
|
|
420
|
+
expect(result).toEqual([
|
|
421
|
+
"And",
|
|
422
|
+
[
|
|
423
|
+
["a", "Eq", 1],
|
|
424
|
+
["b", "Eq", 2],
|
|
425
|
+
["c", "Eq", 3],
|
|
426
|
+
],
|
|
427
|
+
]);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("$and", () => {
|
|
432
|
+
it("encodes explicit $and with two conditions", () => {
|
|
433
|
+
const result = FILTER.encode({
|
|
434
|
+
$and: [{ status: "active" }, { views: { $gte: 100 } }],
|
|
435
|
+
});
|
|
436
|
+
expect(result).toEqual([
|
|
437
|
+
"And",
|
|
438
|
+
[
|
|
439
|
+
["status", "Eq", "active"],
|
|
440
|
+
["views", "Gte", 100],
|
|
441
|
+
],
|
|
442
|
+
]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("encodes $and with single condition (unwraps)", () => {
|
|
446
|
+
const result = FILTER.encode({
|
|
447
|
+
$and: [{ status: "active" }],
|
|
448
|
+
});
|
|
449
|
+
// Single item AND should be unwrapped
|
|
450
|
+
expect(result).toEqual(["status", "Eq", "active"]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("encodes $and with empty array", () => {
|
|
454
|
+
const result = FILTER.encode({ $and: [] });
|
|
455
|
+
expect(result).toEqual(["And", []]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("encodes nested $and inside $and", () => {
|
|
459
|
+
const result = FILTER.encode({
|
|
460
|
+
$and: [{ a: 1 }, { $and: [{ b: 2 }, { c: 3 }] }],
|
|
461
|
+
});
|
|
462
|
+
expect(result).toEqual([
|
|
463
|
+
"And",
|
|
464
|
+
[
|
|
465
|
+
["a", "Eq", 1],
|
|
466
|
+
["And", [["b", "Eq", 2], ["c", "Eq", 3]]],
|
|
467
|
+
],
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("$or", () => {
|
|
473
|
+
it("encodes $or with two conditions", () => {
|
|
474
|
+
const result = FILTER.encode({
|
|
475
|
+
$or: [{ status: "draft" }, { status: "review" }],
|
|
476
|
+
});
|
|
477
|
+
expect(result).toEqual([
|
|
478
|
+
"Or",
|
|
479
|
+
[
|
|
480
|
+
["status", "Eq", "draft"],
|
|
481
|
+
["status", "Eq", "review"],
|
|
482
|
+
],
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("encodes $or with empty array", () => {
|
|
487
|
+
const result = FILTER.encode({ $or: [] });
|
|
488
|
+
expect(result).toEqual(["Or", []]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("encodes $or with complex conditions", () => {
|
|
492
|
+
const result = FILTER.encode({
|
|
493
|
+
$or: [
|
|
494
|
+
{ status: "active", priority: { $gte: 5 } },
|
|
495
|
+
{ status: "urgent" },
|
|
496
|
+
],
|
|
497
|
+
});
|
|
498
|
+
expect(result).toEqual([
|
|
499
|
+
"Or",
|
|
500
|
+
[
|
|
501
|
+
["And", [["status", "Eq", "active"], ["priority", "Gte", 5]]],
|
|
502
|
+
["status", "Eq", "urgent"],
|
|
503
|
+
],
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("$not", () => {
|
|
509
|
+
it("encodes $not with simple equality", () => {
|
|
510
|
+
const result = FILTER.encode({
|
|
511
|
+
$not: { deleted: true },
|
|
512
|
+
});
|
|
513
|
+
expect(result).toEqual(["Not", ["deleted", "Eq", true]]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("encodes $not with comparison", () => {
|
|
517
|
+
const result = FILTER.encode({
|
|
518
|
+
$not: { views: { $lt: 100 } },
|
|
519
|
+
});
|
|
520
|
+
expect(result).toEqual(["Not", ["views", "Lt", 100]]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("encodes $not with compound filter", () => {
|
|
524
|
+
const result = FILTER.encode({
|
|
525
|
+
$not: { status: "deleted", archived: true },
|
|
526
|
+
});
|
|
527
|
+
expect(result).toEqual([
|
|
528
|
+
"Not",
|
|
529
|
+
["And", [["status", "Eq", "deleted"], ["archived", "Eq", true]]],
|
|
530
|
+
]);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("complex nested logical trees", () => {
|
|
535
|
+
it("encodes AND of ORs", () => {
|
|
536
|
+
const result = FILTER.encode({
|
|
537
|
+
$and: [
|
|
538
|
+
{ $or: [{ status: "active" }, { status: "pending" }] },
|
|
539
|
+
{ $or: [{ type: "article" }, { type: "post" }] },
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
expect(result).toEqual([
|
|
543
|
+
"And",
|
|
544
|
+
[
|
|
545
|
+
["Or", [["status", "Eq", "active"], ["status", "Eq", "pending"]]],
|
|
546
|
+
["Or", [["type", "Eq", "article"], ["type", "Eq", "post"]]],
|
|
547
|
+
],
|
|
548
|
+
]);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("encodes OR of ANDs", () => {
|
|
552
|
+
const result = FILTER.encode({
|
|
553
|
+
$or: [
|
|
554
|
+
{ status: "active", role: "admin" },
|
|
555
|
+
{ status: "active", role: "moderator" },
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
expect(result).toEqual([
|
|
559
|
+
"Or",
|
|
560
|
+
[
|
|
561
|
+
["And", [["status", "Eq", "active"], ["role", "Eq", "admin"]]],
|
|
562
|
+
["And", [["status", "Eq", "active"], ["role", "Eq", "moderator"]]],
|
|
563
|
+
],
|
|
564
|
+
]);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("encodes NOT of AND of ORs", () => {
|
|
568
|
+
const result = FILTER.encode({
|
|
569
|
+
$not: {
|
|
570
|
+
$and: [
|
|
571
|
+
{ $or: [{ a: 1 }, { b: 2 }] },
|
|
572
|
+
{ $or: [{ c: 3 }, { d: 4 }] },
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
expect(result).toEqual([
|
|
577
|
+
"Not",
|
|
578
|
+
[
|
|
579
|
+
"And",
|
|
580
|
+
[
|
|
581
|
+
["Or", [["a", "Eq", 1], ["b", "Eq", 2]]],
|
|
582
|
+
["Or", [["c", "Eq", 3], ["d", "Eq", 4]]],
|
|
583
|
+
],
|
|
584
|
+
],
|
|
585
|
+
]);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("encodes deeply nested tree (4 levels)", () => {
|
|
589
|
+
const result = FILTER.encode({
|
|
590
|
+
$and: [
|
|
591
|
+
{ active: true },
|
|
592
|
+
{
|
|
593
|
+
$or: [
|
|
594
|
+
{ role: "admin" },
|
|
595
|
+
{
|
|
596
|
+
$and: [
|
|
597
|
+
{ role: "user" },
|
|
598
|
+
{
|
|
599
|
+
$or: [{ verified: true }, { trusted: true }],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
});
|
|
607
|
+
expect(result).toEqual([
|
|
608
|
+
"And",
|
|
609
|
+
[
|
|
610
|
+
["active", "Eq", true],
|
|
611
|
+
[
|
|
612
|
+
"Or",
|
|
613
|
+
[
|
|
614
|
+
["role", "Eq", "admin"],
|
|
615
|
+
[
|
|
616
|
+
"And",
|
|
617
|
+
[
|
|
618
|
+
["role", "Eq", "user"],
|
|
619
|
+
["Or", [["verified", "Eq", true], ["trusted", "Eq", true]]],
|
|
620
|
+
],
|
|
621
|
+
],
|
|
622
|
+
],
|
|
623
|
+
],
|
|
624
|
+
],
|
|
625
|
+
]);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("encodes mixed logical and comparison operators", () => {
|
|
629
|
+
const result = FILTER.encode({
|
|
630
|
+
$and: [
|
|
631
|
+
{ status: { $in: ["active", "pending"] } },
|
|
632
|
+
{ views: { $gte: 100, $lt: 10000 } },
|
|
633
|
+
{
|
|
634
|
+
$or: [
|
|
635
|
+
{ premium: true },
|
|
636
|
+
{ $not: { trial: true } },
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
});
|
|
641
|
+
expect(result).toEqual([
|
|
642
|
+
"And",
|
|
643
|
+
[
|
|
644
|
+
["status", "In", ["active", "pending"]],
|
|
645
|
+
["And", [["views", "Gte", 100], ["views", "Lt", 10000]]],
|
|
646
|
+
[
|
|
647
|
+
"Or",
|
|
648
|
+
[
|
|
649
|
+
["premium", "Eq", true],
|
|
650
|
+
["Not", ["trial", "Eq", true]],
|
|
651
|
+
],
|
|
652
|
+
],
|
|
653
|
+
],
|
|
654
|
+
]);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ============================================================
|
|
660
|
+
// EDGE CASES
|
|
661
|
+
// ============================================================
|
|
662
|
+
|
|
663
|
+
describe("edge cases", () => {
|
|
664
|
+
it("handles field names with special characters", () => {
|
|
665
|
+
expect(FILTER.encode({ "field.nested": "value" })).toEqual([
|
|
666
|
+
"field.nested",
|
|
667
|
+
"Eq",
|
|
668
|
+
"value",
|
|
669
|
+
]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("handles field names with underscores", () => {
|
|
673
|
+
expect(FILTER.encode({ created_at: "2024-01-01" })).toEqual([
|
|
674
|
+
"created_at",
|
|
675
|
+
"Eq",
|
|
676
|
+
"2024-01-01",
|
|
677
|
+
]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("handles field names with numbers", () => {
|
|
681
|
+
expect(FILTER.encode({ field1: "value" })).toEqual([
|
|
682
|
+
"field1",
|
|
683
|
+
"Eq",
|
|
684
|
+
"value",
|
|
685
|
+
]);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("handles very long string values", () => {
|
|
689
|
+
const longString = "a".repeat(10000);
|
|
690
|
+
expect(FILTER.encode({ content: longString })).toEqual([
|
|
691
|
+
"content",
|
|
692
|
+
"Eq",
|
|
693
|
+
longString,
|
|
694
|
+
]);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("handles large numbers", () => {
|
|
698
|
+
expect(FILTER.encode({ bigNum: 9007199254740991 })).toEqual([
|
|
699
|
+
"bigNum",
|
|
700
|
+
"Eq",
|
|
701
|
+
9007199254740991,
|
|
702
|
+
]);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("ignores undefined values in filter object", () => {
|
|
706
|
+
const result = FILTER.encode({ a: 1, b: undefined, c: 3 });
|
|
707
|
+
expect(result).toEqual([
|
|
708
|
+
"And",
|
|
709
|
+
[
|
|
710
|
+
["a", "Eq", 1],
|
|
711
|
+
["c", "Eq", 3],
|
|
712
|
+
],
|
|
713
|
+
]);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ============================================================
|
|
718
|
+
// ERROR CASES
|
|
719
|
+
// ============================================================
|
|
720
|
+
|
|
721
|
+
describe("error cases", () => {
|
|
722
|
+
it("throws on empty filter object", () => {
|
|
723
|
+
expect(() => FILTER.encode({})).toThrow("Empty filter");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("throws on filter with only undefined values", () => {
|
|
727
|
+
expect(() => FILTER.encode({ a: undefined })).toThrow("Empty filter");
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
});
|