@ekodb/ekodb-client 0.7.0 → 0.8.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.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Unit tests for ekoDB TypeScript client QueryBuilder
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { QueryBuilder, SortOrder } from "./query-builder";
7
+
8
+ // ============================================================================
9
+ // Basic Tests
10
+ // ============================================================================
11
+
12
+ describe("QueryBuilder basics", () => {
13
+ it("creates empty query builder", () => {
14
+ const qb = new QueryBuilder();
15
+ expect(qb).toBeInstanceOf(QueryBuilder);
16
+ });
17
+
18
+ it("builds empty query", () => {
19
+ const query = new QueryBuilder().build();
20
+ expect(query).toEqual({});
21
+ });
22
+ });
23
+
24
+ // ============================================================================
25
+ // Equality Operators Tests
26
+ // ============================================================================
27
+
28
+ describe("QueryBuilder equality operators", () => {
29
+ it("builds eq filter", () => {
30
+ const query = new QueryBuilder().eq("status", "active").build();
31
+
32
+ expect(query.filter).toEqual({
33
+ type: "Condition",
34
+ content: {
35
+ field: "status",
36
+ operator: "Eq",
37
+ value: "active",
38
+ },
39
+ });
40
+ });
41
+
42
+ it("builds ne filter", () => {
43
+ const query = new QueryBuilder().ne("status", "deleted").build();
44
+
45
+ expect(query.filter.content.operator).toBe("Ne");
46
+ });
47
+ });
48
+
49
+ // ============================================================================
50
+ // Comparison Operators Tests
51
+ // ============================================================================
52
+
53
+ describe("QueryBuilder comparison operators", () => {
54
+ it("builds gt filter", () => {
55
+ const query = new QueryBuilder().gt("age", 18).build();
56
+
57
+ expect(query.filter.content.operator).toBe("Gt");
58
+ expect(query.filter.content.value).toBe(18);
59
+ });
60
+
61
+ it("builds gte filter", () => {
62
+ const query = new QueryBuilder().gte("score", 80).build();
63
+
64
+ expect(query.filter.content.operator).toBe("Gte");
65
+ });
66
+
67
+ it("builds lt filter", () => {
68
+ const query = new QueryBuilder().lt("price", 100.5).build();
69
+
70
+ expect(query.filter.content.operator).toBe("Lt");
71
+ });
72
+
73
+ it("builds lte filter", () => {
74
+ const query = new QueryBuilder().lte("count", 1000).build();
75
+
76
+ expect(query.filter.content.operator).toBe("Lte");
77
+ });
78
+ });
79
+
80
+ // ============================================================================
81
+ // Array Operators Tests
82
+ // ============================================================================
83
+
84
+ describe("QueryBuilder array operators", () => {
85
+ it("builds in filter", () => {
86
+ const query = new QueryBuilder()
87
+ .in("status", ["active", "pending"])
88
+ .build();
89
+
90
+ expect(query.filter.content.operator).toBe("In");
91
+ expect(query.filter.content.value).toEqual(["active", "pending"]);
92
+ });
93
+
94
+ it("builds nin filter", () => {
95
+ const query = new QueryBuilder()
96
+ .nin("role", ["blocked", "deleted"])
97
+ .build();
98
+
99
+ expect(query.filter.content.operator).toBe("NotIn");
100
+ });
101
+ });
102
+
103
+ // ============================================================================
104
+ // String Operators Tests
105
+ // ============================================================================
106
+
107
+ describe("QueryBuilder string operators", () => {
108
+ it("builds contains filter", () => {
109
+ const query = new QueryBuilder().contains("email", "@example.com").build();
110
+
111
+ expect(query.filter.content.operator).toBe("Contains");
112
+ expect(query.filter.content.value).toBe("@example.com");
113
+ });
114
+
115
+ it("builds startsWith filter", () => {
116
+ const query = new QueryBuilder().startsWith("name", "John").build();
117
+
118
+ expect(query.filter.content.operator).toBe("StartsWith");
119
+ });
120
+
121
+ it("builds endsWith filter", () => {
122
+ const query = new QueryBuilder().endsWith("filename", ".pdf").build();
123
+
124
+ expect(query.filter.content.operator).toBe("EndsWith");
125
+ });
126
+
127
+ it("builds regex filter", () => {
128
+ const query = new QueryBuilder().regex("phone", "^\\+1").build();
129
+
130
+ expect(query.filter.content.operator).toBe("Regex");
131
+ });
132
+ });
133
+
134
+ // ============================================================================
135
+ // Logical Operators Tests
136
+ // ============================================================================
137
+
138
+ describe("QueryBuilder logical operators", () => {
139
+ it("builds and filter", () => {
140
+ const conditions = [
141
+ {
142
+ type: "Condition",
143
+ content: { field: "status", operator: "Eq", value: "active" },
144
+ },
145
+ {
146
+ type: "Condition",
147
+ content: { field: "age", operator: "Gt", value: 18 },
148
+ },
149
+ ];
150
+
151
+ const query = new QueryBuilder().and(conditions).build();
152
+
153
+ expect(query.filter.type).toBe("Logical");
154
+ expect(query.filter.content.operator).toBe("And");
155
+ expect(query.filter.content.expressions).toHaveLength(2);
156
+ });
157
+
158
+ it("builds or filter", () => {
159
+ const conditions = [
160
+ {
161
+ type: "Condition",
162
+ content: { field: "role", operator: "Eq", value: "admin" },
163
+ },
164
+ {
165
+ type: "Condition",
166
+ content: { field: "role", operator: "Eq", value: "super_admin" },
167
+ },
168
+ ];
169
+
170
+ const query = new QueryBuilder().or(conditions).build();
171
+
172
+ expect(query.filter.content.operator).toBe("Or");
173
+ });
174
+
175
+ it("builds not filter", () => {
176
+ const condition = {
177
+ type: "Condition",
178
+ content: { field: "deleted", operator: "Eq", value: true },
179
+ };
180
+
181
+ const query = new QueryBuilder().not(condition).build();
182
+
183
+ expect(query.filter.content.operator).toBe("Not");
184
+ });
185
+ });
186
+
187
+ // ============================================================================
188
+ // Multiple Filters (Auto AND) Tests
189
+ // ============================================================================
190
+
191
+ describe("QueryBuilder multiple filters", () => {
192
+ it("combines multiple filters with AND logic", () => {
193
+ const query = new QueryBuilder()
194
+ .eq("status", "active")
195
+ .gt("age", 18)
196
+ .contains("email", "@company.com")
197
+ .build();
198
+
199
+ expect(query.filter.type).toBe("Logical");
200
+ expect(query.filter.content.operator).toBe("And");
201
+ expect(query.filter.content.expressions).toHaveLength(3);
202
+ });
203
+ });
204
+
205
+ // ============================================================================
206
+ // Sorting Tests
207
+ // ============================================================================
208
+
209
+ describe("QueryBuilder sorting", () => {
210
+ it("builds ascending sort", () => {
211
+ const query = new QueryBuilder().sortAsc("name").build();
212
+
213
+ expect(query.sort).toHaveLength(1);
214
+ expect(query.sort![0].field).toBe("name");
215
+ expect(query.sort![0].ascending).toBe(true);
216
+ });
217
+
218
+ it("builds descending sort", () => {
219
+ const query = new QueryBuilder().sortDesc("created_at").build();
220
+
221
+ expect(query.sort![0].ascending).toBe(false);
222
+ });
223
+
224
+ it("builds multiple sorts", () => {
225
+ const query = new QueryBuilder()
226
+ .sortDesc("created_at")
227
+ .sortAsc("name")
228
+ .build();
229
+
230
+ expect(query.sort).toHaveLength(2);
231
+ expect(query.sort![0].field).toBe("created_at");
232
+ expect(query.sort![0].ascending).toBe(false);
233
+ expect(query.sort![1].field).toBe("name");
234
+ expect(query.sort![1].ascending).toBe(true);
235
+ });
236
+ });
237
+
238
+ // ============================================================================
239
+ // Pagination Tests
240
+ // ============================================================================
241
+
242
+ describe("QueryBuilder pagination", () => {
243
+ it("builds limit", () => {
244
+ const query = new QueryBuilder().limit(10).build();
245
+
246
+ expect(query.limit).toBe(10);
247
+ });
248
+
249
+ it("builds skip", () => {
250
+ const query = new QueryBuilder().skip(20).build();
251
+
252
+ expect(query.skip).toBe(20);
253
+ });
254
+
255
+ it("builds page (convenience method)", () => {
256
+ // Page 2 with 20 items per page = skip 40
257
+ const query = new QueryBuilder().page(2, 20).build();
258
+
259
+ expect(query.limit).toBe(20);
260
+ expect(query.skip).toBe(40);
261
+ });
262
+
263
+ it("builds page 0", () => {
264
+ const query = new QueryBuilder().page(0, 10).build();
265
+
266
+ expect(query.skip).toBe(0);
267
+ expect(query.limit).toBe(10);
268
+ });
269
+ });
270
+
271
+ // ============================================================================
272
+ // Join Tests
273
+ // ============================================================================
274
+
275
+ describe("QueryBuilder join", () => {
276
+ it("builds join configuration", () => {
277
+ const joinConfig = {
278
+ collections: ["users"],
279
+ local_field: "user_id",
280
+ foreign_field: "id",
281
+ as_field: "user",
282
+ };
283
+
284
+ const query = new QueryBuilder().join(joinConfig).build();
285
+
286
+ expect(query.join).toEqual(joinConfig);
287
+ });
288
+ });
289
+
290
+ // ============================================================================
291
+ // Bypass Flags Tests
292
+ // ============================================================================
293
+
294
+ describe("QueryBuilder bypass flags", () => {
295
+ it("builds bypass_cache true", () => {
296
+ const query = new QueryBuilder().bypassCache(true).build();
297
+
298
+ expect(query.bypass_cache).toBe(true);
299
+ });
300
+
301
+ it("builds bypass_cache false (not included)", () => {
302
+ const query = new QueryBuilder().bypassCache(false).build();
303
+
304
+ expect(query.bypass_cache).toBeUndefined();
305
+ });
306
+
307
+ it("builds bypass_cache default (true)", () => {
308
+ const query = new QueryBuilder().bypassCache().build();
309
+
310
+ expect(query.bypass_cache).toBe(true);
311
+ });
312
+
313
+ it("builds bypass_ripple", () => {
314
+ const query = new QueryBuilder().bypassRipple(true).build();
315
+
316
+ expect(query.bypass_ripple).toBe(true);
317
+ });
318
+ });
319
+
320
+ // ============================================================================
321
+ // Chaining Tests
322
+ // ============================================================================
323
+
324
+ describe("QueryBuilder chaining", () => {
325
+ it("supports full method chaining", () => {
326
+ const query = new QueryBuilder()
327
+ .eq("status", "active")
328
+ .gt("age", 18)
329
+ .sortDesc("created_at")
330
+ .sortAsc("name")
331
+ .limit(10)
332
+ .skip(20)
333
+ .bypassCache(true)
334
+ .build();
335
+
336
+ // Check filter exists
337
+ expect(query.filter).toBeDefined();
338
+
339
+ // Check sort exists
340
+ expect(query.sort).toHaveLength(2);
341
+
342
+ // Check pagination
343
+ expect(query.limit).toBe(10);
344
+ expect(query.skip).toBe(20);
345
+
346
+ // Check bypass flag
347
+ expect(query.bypass_cache).toBe(true);
348
+ });
349
+
350
+ it("returns this for method chaining", () => {
351
+ const qb = new QueryBuilder();
352
+
353
+ expect(qb.eq("a", 1)).toBe(qb);
354
+ expect(qb.ne("b", 2)).toBe(qb);
355
+ expect(qb.gt("c", 3)).toBe(qb);
356
+ expect(qb.gte("d", 4)).toBe(qb);
357
+ expect(qb.lt("e", 5)).toBe(qb);
358
+ expect(qb.lte("f", 6)).toBe(qb);
359
+ expect(qb.in("g", [7])).toBe(qb);
360
+ expect(qb.nin("h", [8])).toBe(qb);
361
+ expect(qb.contains("i", "j")).toBe(qb);
362
+ expect(qb.startsWith("k", "l")).toBe(qb);
363
+ expect(qb.endsWith("m", "n")).toBe(qb);
364
+ expect(qb.regex("o", "p")).toBe(qb);
365
+ expect(qb.sortAsc("q")).toBe(qb);
366
+ expect(qb.sortDesc("r")).toBe(qb);
367
+ expect(qb.limit(1)).toBe(qb);
368
+ expect(qb.skip(0)).toBe(qb);
369
+ expect(qb.bypassCache()).toBe(qb);
370
+ expect(qb.bypassRipple()).toBe(qb);
371
+ });
372
+ });
373
+
374
+ // ============================================================================
375
+ // Raw Filter Tests
376
+ // ============================================================================
377
+
378
+ describe("QueryBuilder rawFilter", () => {
379
+ it("adds raw filter expression", () => {
380
+ const rawFilter = {
381
+ type: "Condition",
382
+ content: {
383
+ field: "custom",
384
+ operator: "CustomOp",
385
+ value: "custom_value",
386
+ },
387
+ };
388
+
389
+ const query = new QueryBuilder().rawFilter(rawFilter).build();
390
+
391
+ expect(query.filter).toEqual(rawFilter);
392
+ });
393
+ });
394
+
395
+ // ============================================================================
396
+ // SortOrder Enum Tests
397
+ // ============================================================================
398
+
399
+ describe("SortOrder enum", () => {
400
+ it("has correct values", () => {
401
+ expect(SortOrder.Asc).toBe("asc");
402
+ expect(SortOrder.Desc).toBe("desc");
403
+ });
404
+ });
package/src/search.ts CHANGED
@@ -53,6 +53,12 @@ export interface SearchQuery {
53
53
  text_weight?: number;
54
54
  /** Weight for vector search (0.0-1.0) */
55
55
  vector_weight?: number;
56
+
57
+ // Field projection parameters
58
+ /** Only return these fields (plus 'id') */
59
+ select_fields?: string[];
60
+ /** Exclude these fields from results */
61
+ exclude_fields?: string[];
56
62
  }
57
63
 
58
64
  /**
@@ -253,6 +259,22 @@ export class SearchQueryBuilder {
253
259
  return this;
254
260
  }
255
261
 
262
+ /**
263
+ * Select specific fields to return
264
+ */
265
+ selectFields(fields: string[]): this {
266
+ this.query.select_fields = fields;
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * Exclude specific fields from results
272
+ */
273
+ excludeFields(fields: string[]): this {
274
+ this.query.exclude_fields = fields;
275
+ return this;
276
+ }
277
+
256
278
  /**
257
279
  * Build the final SearchQuery object
258
280
  */