mongo-query-dsl 1.0.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/README.md +339 -0
- package/dist/examples/usage.d.ts +13 -0
- package/dist/examples/usage.d.ts.map +1 -0
- package/dist/examples/usage.js +162 -0
- package/dist/examples/usage.js.map +1 -0
- package/dist/src/__tests__/executor.test.d.ts +2 -0
- package/dist/src/__tests__/executor.test.d.ts.map +1 -0
- package/dist/src/__tests__/executor.test.js +602 -0
- package/dist/src/__tests__/executor.test.js.map +1 -0
- package/dist/src/__tests__/integration.test.d.ts +2 -0
- package/dist/src/__tests__/integration.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration.test.js +320 -0
- package/dist/src/__tests__/integration.test.js.map +1 -0
- package/dist/src/__tests__/parser.test.d.ts +2 -0
- package/dist/src/__tests__/parser.test.d.ts.map +1 -0
- package/dist/src/__tests__/parser.test.js +320 -0
- package/dist/src/__tests__/parser.test.js.map +1 -0
- package/dist/src/__tests__/resolver.test.d.ts +2 -0
- package/dist/src/__tests__/resolver.test.d.ts.map +1 -0
- package/dist/src/__tests__/resolver.test.js +178 -0
- package/dist/src/__tests__/resolver.test.js.map +1 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +44 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/executor.d.ts +32 -0
- package/dist/src/executor.d.ts.map +1 -0
- package/dist/src/executor.js +214 -0
- package/dist/src/executor.js.map +1 -0
- package/dist/src/index.d.ts +43 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +120 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/parser.d.ts +42 -0
- package/dist/src/parser.d.ts.map +1 -0
- package/dist/src/parser.js +194 -0
- package/dist/src/parser.js.map +1 -0
- package/dist/src/resolver.d.ts +22 -0
- package/dist/src/resolver.d.ts.map +1 -0
- package/dist/src/resolver.js +55 -0
- package/dist/src/resolver.js.map +1 -0
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/examples/usage.ts +190 -0
- package/jest.config.js +13 -0
- package/package.json +34 -0
- package/src/__tests__/executor.test.ts +694 -0
- package/src/__tests__/integration.test.ts +392 -0
- package/src/__tests__/parser.test.ts +377 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/errors.ts +47 -0
- package/src/executor.ts +276 -0
- package/src/index.ts +118 -0
- package/src/parser.ts +216 -0
- package/src/resolver.ts +58 -0
- package/src/types.ts +107 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { QueryExecutor } from "../executor";
|
|
2
|
+
import { DatabaseProvider, JsonQuery } from "../types";
|
|
3
|
+
|
|
4
|
+
// Mock database provider
|
|
5
|
+
class MockDatabaseProvider implements DatabaseProvider {
|
|
6
|
+
private mockData: Map<string, Map<string, Record<string, any>[]>> = new Map();
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
// Initialize with sample data
|
|
10
|
+
this.addData("myDb", "users", [
|
|
11
|
+
{
|
|
12
|
+
_id: "user_123",
|
|
13
|
+
name: "John Doe",
|
|
14
|
+
email: "john@example.com",
|
|
15
|
+
role: "admin",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
_id: "user_456",
|
|
19
|
+
name: "Jane Smith",
|
|
20
|
+
email: "jane@example.com",
|
|
21
|
+
role: "user",
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
this.addData("myDb", "documents", [
|
|
26
|
+
{
|
|
27
|
+
_id: "doc_123",
|
|
28
|
+
title: "Document 1",
|
|
29
|
+
userId: "user_123",
|
|
30
|
+
content: "Sample content",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
_id: "doc_456",
|
|
34
|
+
title: "Document 2",
|
|
35
|
+
userId: "user_456",
|
|
36
|
+
content: "Another content",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
this.addData("myDb", "metadata", [
|
|
41
|
+
{
|
|
42
|
+
_id: "meta_123",
|
|
43
|
+
ownerId: "user_123",
|
|
44
|
+
ownerType: "users",
|
|
45
|
+
ownerSource: "myDb",
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
this.addData("permissions", "admin", [
|
|
50
|
+
{ _id: "user_123", permissions: ["read", "write", "delete"] },
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addData(db: string, collection: string, documents: Record<string, any>[]) {
|
|
55
|
+
if (!this.mockData.has(db)) {
|
|
56
|
+
this.mockData.set(db, new Map());
|
|
57
|
+
}
|
|
58
|
+
const dbMap = this.mockData.get(db)!;
|
|
59
|
+
dbMap.set(collection, documents);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async findOne(
|
|
63
|
+
dbName: string,
|
|
64
|
+
collectionName: string,
|
|
65
|
+
query: Record<string, any>,
|
|
66
|
+
): Promise<Record<string, any> | null> {
|
|
67
|
+
const db = this.mockData.get(dbName);
|
|
68
|
+
if (!db) return null;
|
|
69
|
+
|
|
70
|
+
const collection = db.get(collectionName);
|
|
71
|
+
if (!collection) return null;
|
|
72
|
+
|
|
73
|
+
// Simple query matching
|
|
74
|
+
const doc = collection.find((doc) => {
|
|
75
|
+
return Object.entries(query).every(([key, value]) => doc[key] === value);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return doc || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async close(): Promise<void> {
|
|
82
|
+
// Mock close
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("QueryExecutor", () => {
|
|
87
|
+
let executor: QueryExecutor;
|
|
88
|
+
let mockProvider: MockDatabaseProvider;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
mockProvider = new MockDatabaseProvider();
|
|
92
|
+
executor = new QueryExecutor(mockProvider);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("Basic query execution", () => {
|
|
96
|
+
it("should execute simple query with no steps", async () => {
|
|
97
|
+
const query: JsonQuery = {
|
|
98
|
+
start: {
|
|
99
|
+
collection: "users",
|
|
100
|
+
db: "myDb",
|
|
101
|
+
where: { _id: "user_123" },
|
|
102
|
+
},
|
|
103
|
+
steps: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = await executor.execute(query);
|
|
107
|
+
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
expect(result.result).toEqual({
|
|
110
|
+
_id: "user_123",
|
|
111
|
+
name: "John Doe",
|
|
112
|
+
email: "john@example.com",
|
|
113
|
+
role: "admin",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return error when document not found", async () => {
|
|
118
|
+
const query: JsonQuery = {
|
|
119
|
+
start: {
|
|
120
|
+
collection: "users",
|
|
121
|
+
db: "myDb",
|
|
122
|
+
where: { _id: "nonexistent" },
|
|
123
|
+
},
|
|
124
|
+
steps: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await executor.execute(query);
|
|
128
|
+
|
|
129
|
+
expect(result.success).toBe(false);
|
|
130
|
+
expect(result.error).toBeDefined();
|
|
131
|
+
expect(result.error).toContain("Document not found");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("GET step execution", () => {
|
|
136
|
+
it("should extract single field", async () => {
|
|
137
|
+
const query: JsonQuery = {
|
|
138
|
+
output: "value",
|
|
139
|
+
start: {
|
|
140
|
+
collection: "users",
|
|
141
|
+
db: "myDb",
|
|
142
|
+
where: { _id: "user_123" },
|
|
143
|
+
},
|
|
144
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = await executor.execute(query);
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should extract multiple fields", async () => {
|
|
154
|
+
const query: JsonQuery = {
|
|
155
|
+
output: "value",
|
|
156
|
+
start: {
|
|
157
|
+
collection: "users",
|
|
158
|
+
db: "myDb",
|
|
159
|
+
where: { _id: "user_123" },
|
|
160
|
+
},
|
|
161
|
+
steps: [{ type: "get", fields: ["name", "email"] }],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await executor.execute(query);
|
|
165
|
+
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
expect(result.result).toEqual({
|
|
168
|
+
name: "John Doe",
|
|
169
|
+
email: "john@example.com",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should return undefined for missing fields", async () => {
|
|
174
|
+
const query: JsonQuery = {
|
|
175
|
+
start: {
|
|
176
|
+
collection: "users",
|
|
177
|
+
db: "myDb",
|
|
178
|
+
where: { _id: "user_123" },
|
|
179
|
+
},
|
|
180
|
+
steps: [{ type: "get", fields: ["name", "nonexistent"] }],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = await executor.execute(query);
|
|
184
|
+
|
|
185
|
+
expect(result.success).toBe(true);
|
|
186
|
+
expect(result.result).toEqual({
|
|
187
|
+
name: "John Doe",
|
|
188
|
+
nonexistent: undefined,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should support aliases and default values in GET", async () => {
|
|
193
|
+
const query: JsonQuery = {
|
|
194
|
+
start: {
|
|
195
|
+
collection: "users",
|
|
196
|
+
db: "myDb",
|
|
197
|
+
where: { _id: "user_123" },
|
|
198
|
+
},
|
|
199
|
+
steps: [
|
|
200
|
+
{
|
|
201
|
+
type: "get",
|
|
202
|
+
fields: [
|
|
203
|
+
"name as fullName",
|
|
204
|
+
"email",
|
|
205
|
+
"missingField as fallbackCandidate",
|
|
206
|
+
],
|
|
207
|
+
default: { fallbackCandidate: "N/A", source: "default" },
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await executor.execute(query);
|
|
213
|
+
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
expect(result.result).toEqual({
|
|
216
|
+
source: "default",
|
|
217
|
+
fullName: "John Doe",
|
|
218
|
+
email: "john@example.com",
|
|
219
|
+
fallbackCandidate: undefined,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should resolve :field references from previous documents in GET", async () => {
|
|
224
|
+
const query: JsonQuery = {
|
|
225
|
+
start: {
|
|
226
|
+
collection: "documents",
|
|
227
|
+
db: "myDb",
|
|
228
|
+
where: { _id: "doc_123" },
|
|
229
|
+
},
|
|
230
|
+
steps: [
|
|
231
|
+
{ type: "get", fields: ["title", "userId"] },
|
|
232
|
+
{ type: "lookup", collection: "users", db: "myDb", by: "userId" },
|
|
233
|
+
{ type: "get", fields: ["name", ":title", ":userId"] },
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await executor.execute(query);
|
|
238
|
+
|
|
239
|
+
expect(result.success).toBe(true);
|
|
240
|
+
expect(result.result).toEqual({
|
|
241
|
+
name: "John Doe",
|
|
242
|
+
title: "Document 1",
|
|
243
|
+
userId: "user_123",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should return single value for getOne step", async () => {
|
|
248
|
+
const query: JsonQuery = {
|
|
249
|
+
start: {
|
|
250
|
+
collection: "users",
|
|
251
|
+
db: "myDb",
|
|
252
|
+
where: { _id: "user_123" },
|
|
253
|
+
},
|
|
254
|
+
steps: [{ type: "getOne", fields: "email" }],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = await executor.execute(query);
|
|
258
|
+
|
|
259
|
+
expect(result.success).toBe(true);
|
|
260
|
+
expect(result.result).toBe("john@example.com");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("LOOKUP step execution", () => {
|
|
265
|
+
it("should execute simple lookup", async () => {
|
|
266
|
+
const query: JsonQuery = {
|
|
267
|
+
start: {
|
|
268
|
+
collection: "documents",
|
|
269
|
+
db: "myDb",
|
|
270
|
+
where: { _id: "doc_123" },
|
|
271
|
+
},
|
|
272
|
+
steps: [
|
|
273
|
+
{ type: "lookup", collection: "users", db: "myDb", by: "userId" },
|
|
274
|
+
{ type: "get", fields: ["name", "email"] },
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const result = await executor.execute(query);
|
|
279
|
+
|
|
280
|
+
expect(result.success).toBe(true);
|
|
281
|
+
expect(result.result).toEqual({
|
|
282
|
+
name: "John Doe",
|
|
283
|
+
email: "john@example.com",
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should execute lookup with WHERE conditions", async () => {
|
|
288
|
+
const query: JsonQuery = {
|
|
289
|
+
start: {
|
|
290
|
+
collection: "documents",
|
|
291
|
+
db: "myDb",
|
|
292
|
+
where: { _id: "doc_123" },
|
|
293
|
+
},
|
|
294
|
+
steps: [
|
|
295
|
+
{
|
|
296
|
+
type: "lookup",
|
|
297
|
+
collection: "users",
|
|
298
|
+
db: "myDb",
|
|
299
|
+
by: "userId",
|
|
300
|
+
where: { role: "admin" },
|
|
301
|
+
},
|
|
302
|
+
{ type: "get", fields: ["name"] },
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const result = await executor.execute(query);
|
|
307
|
+
|
|
308
|
+
expect(result.success).toBe(true);
|
|
309
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should resolve dynamic values inside LOOKUP WHERE conditions", async () => {
|
|
313
|
+
const query: JsonQuery = {
|
|
314
|
+
start: {
|
|
315
|
+
collection: "documents",
|
|
316
|
+
db: "myDb",
|
|
317
|
+
where: { _id: "doc_123" },
|
|
318
|
+
},
|
|
319
|
+
steps: [
|
|
320
|
+
{ type: "get", fields: ["userId"], default: { role: "admin" } },
|
|
321
|
+
{
|
|
322
|
+
type: "lookup",
|
|
323
|
+
collection: "users",
|
|
324
|
+
db: "myDb",
|
|
325
|
+
by: "userId",
|
|
326
|
+
where: { role: ":role" },
|
|
327
|
+
},
|
|
328
|
+
{ type: "get", fields: ["name", "role"] },
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const result = await executor.execute(query);
|
|
333
|
+
|
|
334
|
+
expect(result.success).toBe(true);
|
|
335
|
+
expect(result.result).toEqual({ name: "John Doe", role: "admin" });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should fail when lookup document not found", async () => {
|
|
339
|
+
const query: JsonQuery = {
|
|
340
|
+
start: {
|
|
341
|
+
collection: "documents",
|
|
342
|
+
db: "myDb",
|
|
343
|
+
where: { _id: "doc_123" },
|
|
344
|
+
},
|
|
345
|
+
steps: [
|
|
346
|
+
{
|
|
347
|
+
type: "lookup",
|
|
348
|
+
collection: "users",
|
|
349
|
+
db: "myDb",
|
|
350
|
+
by: "userId",
|
|
351
|
+
where: { role: "superadmin" }, // No user with this role
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const result = await executor.execute(query);
|
|
357
|
+
|
|
358
|
+
expect(result.success).toBe(false);
|
|
359
|
+
expect(result.error).toContain("Document not found");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should fail when lookup field not found in document", async () => {
|
|
363
|
+
const query: JsonQuery = {
|
|
364
|
+
start: {
|
|
365
|
+
collection: "documents",
|
|
366
|
+
db: "myDb",
|
|
367
|
+
where: { _id: "doc_123" },
|
|
368
|
+
},
|
|
369
|
+
steps: [
|
|
370
|
+
{
|
|
371
|
+
type: "lookup",
|
|
372
|
+
collection: "users",
|
|
373
|
+
db: "myDb",
|
|
374
|
+
by: "nonexistentField",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const result = await executor.execute(query);
|
|
380
|
+
|
|
381
|
+
expect(result.success).toBe(false);
|
|
382
|
+
expect(result.error).toContain("not found in current document");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("Dynamic value resolution in LOOKUP", () => {
|
|
387
|
+
it("should resolve dynamic collection name", async () => {
|
|
388
|
+
const query: JsonQuery = {
|
|
389
|
+
start: {
|
|
390
|
+
collection: "metadata",
|
|
391
|
+
db: "myDb",
|
|
392
|
+
where: { _id: "meta_123" },
|
|
393
|
+
},
|
|
394
|
+
steps: [
|
|
395
|
+
{
|
|
396
|
+
type: "lookup",
|
|
397
|
+
collection: ":ownerType",
|
|
398
|
+
db: "myDb",
|
|
399
|
+
by: "ownerId",
|
|
400
|
+
},
|
|
401
|
+
{ type: "get", fields: ["name"] },
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const result = await executor.execute(query);
|
|
406
|
+
|
|
407
|
+
expect(result.success).toBe(true);
|
|
408
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should resolve dynamic database name", async () => {
|
|
412
|
+
const query: JsonQuery = {
|
|
413
|
+
start: {
|
|
414
|
+
collection: "metadata",
|
|
415
|
+
db: "myDb",
|
|
416
|
+
where: { _id: "meta_123" },
|
|
417
|
+
},
|
|
418
|
+
steps: [
|
|
419
|
+
{
|
|
420
|
+
type: "lookup",
|
|
421
|
+
collection: "users",
|
|
422
|
+
db: ":ownerSource",
|
|
423
|
+
by: "ownerId",
|
|
424
|
+
},
|
|
425
|
+
{ type: "get", fields: ["email"] },
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const result = await executor.execute(query);
|
|
430
|
+
|
|
431
|
+
expect(result.success).toBe(true);
|
|
432
|
+
expect(result.result).toEqual({ email: "john@example.com" });
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("should resolve both dynamic collection and database names", async () => {
|
|
436
|
+
const query: JsonQuery = {
|
|
437
|
+
start: {
|
|
438
|
+
collection: "metadata",
|
|
439
|
+
db: "myDb",
|
|
440
|
+
where: { _id: "meta_123" },
|
|
441
|
+
},
|
|
442
|
+
steps: [
|
|
443
|
+
{
|
|
444
|
+
type: "lookup",
|
|
445
|
+
collection: ":ownerType",
|
|
446
|
+
db: ":ownerSource",
|
|
447
|
+
by: "ownerId",
|
|
448
|
+
},
|
|
449
|
+
{ type: "get", fields: ["name", "role"] },
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const result = await executor.execute(query);
|
|
454
|
+
|
|
455
|
+
expect(result.success).toBe(true);
|
|
456
|
+
expect(result.result).toEqual({ name: "John Doe", role: "admin" });
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("Output formats", () => {
|
|
461
|
+
it("should return value format by default", async () => {
|
|
462
|
+
const query: JsonQuery = {
|
|
463
|
+
start: {
|
|
464
|
+
collection: "users",
|
|
465
|
+
db: "myDb",
|
|
466
|
+
where: { _id: "user_123" },
|
|
467
|
+
},
|
|
468
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const result = await executor.execute(query);
|
|
472
|
+
|
|
473
|
+
expect(result.success).toBe(true);
|
|
474
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
475
|
+
expect(result.trace).toBeUndefined();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should return value format when explicitly set", async () => {
|
|
479
|
+
const query: JsonQuery = {
|
|
480
|
+
output: "value",
|
|
481
|
+
start: {
|
|
482
|
+
collection: "users",
|
|
483
|
+
db: "myDb",
|
|
484
|
+
where: { _id: "user_123" },
|
|
485
|
+
},
|
|
486
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const result = await executor.execute(query);
|
|
490
|
+
|
|
491
|
+
expect(result.success).toBe(true);
|
|
492
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
493
|
+
expect(result.trace).toBeUndefined();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("should return document format", async () => {
|
|
497
|
+
const query: JsonQuery = {
|
|
498
|
+
output: "document",
|
|
499
|
+
start: {
|
|
500
|
+
collection: "users",
|
|
501
|
+
db: "myDb",
|
|
502
|
+
where: { _id: "user_123" },
|
|
503
|
+
},
|
|
504
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const result = await executor.execute(query);
|
|
508
|
+
|
|
509
|
+
expect(result.success).toBe(true);
|
|
510
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
511
|
+
expect(result.trace).toBeUndefined();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should return trace format with execution trace", async () => {
|
|
515
|
+
const query: JsonQuery = {
|
|
516
|
+
output: "trace",
|
|
517
|
+
start: {
|
|
518
|
+
collection: "users",
|
|
519
|
+
db: "myDb",
|
|
520
|
+
where: { _id: "user_123" },
|
|
521
|
+
},
|
|
522
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const result = await executor.execute(query);
|
|
526
|
+
|
|
527
|
+
expect(result.success).toBe(true);
|
|
528
|
+
expect(result.result).toEqual({ name: "John Doe" });
|
|
529
|
+
expect(result.trace).toBeDefined();
|
|
530
|
+
expect(result.trace).toHaveLength(2); // start + get step
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("should include start step in trace", async () => {
|
|
534
|
+
const query: JsonQuery = {
|
|
535
|
+
output: "trace",
|
|
536
|
+
start: {
|
|
537
|
+
collection: "users",
|
|
538
|
+
db: "myDb",
|
|
539
|
+
where: { _id: "user_123" },
|
|
540
|
+
},
|
|
541
|
+
steps: [],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const result = await executor.execute(query);
|
|
545
|
+
|
|
546
|
+
expect(result.trace).toBeDefined();
|
|
547
|
+
expect(result.trace![0]).toMatchObject({
|
|
548
|
+
stepNumber: 0,
|
|
549
|
+
stepType: "start",
|
|
550
|
+
collection: "users",
|
|
551
|
+
db: "myDb",
|
|
552
|
+
query: { _id: "user_123" },
|
|
553
|
+
});
|
|
554
|
+
expect(result.trace![0].documentFound).toBeDefined();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("should include lookup step in trace", async () => {
|
|
558
|
+
const query: JsonQuery = {
|
|
559
|
+
output: "trace",
|
|
560
|
+
start: {
|
|
561
|
+
collection: "documents",
|
|
562
|
+
db: "myDb",
|
|
563
|
+
where: { _id: "doc_123" },
|
|
564
|
+
},
|
|
565
|
+
steps: [
|
|
566
|
+
{ type: "lookup", collection: "users", db: "myDb", by: "userId" },
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const result = await executor.execute(query);
|
|
571
|
+
|
|
572
|
+
expect(result.trace).toBeDefined();
|
|
573
|
+
expect(result.trace![1]).toMatchObject({
|
|
574
|
+
stepNumber: 1,
|
|
575
|
+
stepType: "lookup",
|
|
576
|
+
collection: "users",
|
|
577
|
+
db: "myDb",
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("should include get step in trace", async () => {
|
|
582
|
+
const query: JsonQuery = {
|
|
583
|
+
output: "trace",
|
|
584
|
+
start: {
|
|
585
|
+
collection: "users",
|
|
586
|
+
db: "myDb",
|
|
587
|
+
where: { _id: "user_123" },
|
|
588
|
+
},
|
|
589
|
+
steps: [{ type: "get", fields: ["name", "email"] }],
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const result = await executor.execute(query);
|
|
593
|
+
|
|
594
|
+
expect(result.trace).toBeDefined();
|
|
595
|
+
expect(result.trace![1]).toMatchObject({
|
|
596
|
+
stepNumber: 1,
|
|
597
|
+
stepType: "get",
|
|
598
|
+
fieldsExtracted: { name: "John Doe", email: "john@example.com" },
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("should include getOne step in trace", async () => {
|
|
603
|
+
const query: JsonQuery = {
|
|
604
|
+
output: "trace",
|
|
605
|
+
start: {
|
|
606
|
+
collection: "users",
|
|
607
|
+
db: "myDb",
|
|
608
|
+
where: { _id: "user_123" },
|
|
609
|
+
},
|
|
610
|
+
steps: [{ type: "getOne", fields: "email" }],
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const result = await executor.execute(query);
|
|
614
|
+
|
|
615
|
+
expect(result.success).toBe(true);
|
|
616
|
+
expect(result.result).toBe("john@example.com");
|
|
617
|
+
expect(result.trace).toBeDefined();
|
|
618
|
+
expect(result.trace![1]).toMatchObject({
|
|
619
|
+
stepType: "getOne",
|
|
620
|
+
fieldsExtracted: { email: "john@example.com" },
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe("Complex queries", () => {
|
|
626
|
+
it("should execute multi-step query", async () => {
|
|
627
|
+
const query: JsonQuery = {
|
|
628
|
+
start: {
|
|
629
|
+
collection: "documents",
|
|
630
|
+
db: "myDb",
|
|
631
|
+
where: { _id: "doc_123" },
|
|
632
|
+
},
|
|
633
|
+
steps: [
|
|
634
|
+
{ type: "get", fields: ["title", "userId"] },
|
|
635
|
+
{ type: "lookup", collection: "users", db: "myDb", by: "userId" },
|
|
636
|
+
{ type: "get", fields: ["name", "email"] },
|
|
637
|
+
],
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const result = await executor.execute(query);
|
|
641
|
+
|
|
642
|
+
expect(result.success).toBe(true);
|
|
643
|
+
expect(result.result).toEqual({
|
|
644
|
+
name: "John Doe",
|
|
645
|
+
email: "john@example.com",
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("should execute query with multiple lookups", async () => {
|
|
650
|
+
// Add proper test data for this scenario
|
|
651
|
+
mockProvider.addData("myDb", "roles", [
|
|
652
|
+
{ _id: "admin", roleId: "role_admin", name: "Administrator" },
|
|
653
|
+
]);
|
|
654
|
+
mockProvider.addData("permissions", "roles", [
|
|
655
|
+
{ _id: "role_admin", permissions: ["read", "write", "delete"] },
|
|
656
|
+
]);
|
|
657
|
+
|
|
658
|
+
const query: JsonQuery = {
|
|
659
|
+
output: "trace",
|
|
660
|
+
start: {
|
|
661
|
+
collection: "metadata",
|
|
662
|
+
db: "myDb",
|
|
663
|
+
where: { _id: "meta_123" },
|
|
664
|
+
},
|
|
665
|
+
steps: [
|
|
666
|
+
{
|
|
667
|
+
type: "lookup",
|
|
668
|
+
collection: ":ownerType",
|
|
669
|
+
db: ":ownerSource",
|
|
670
|
+
by: "ownerId",
|
|
671
|
+
},
|
|
672
|
+
{ type: "get", fields: ["role"] },
|
|
673
|
+
{ type: "lookup", collection: "roles", db: "myDb", by: "role" },
|
|
674
|
+
{ type: "get", fields: ["roleId"] },
|
|
675
|
+
{
|
|
676
|
+
type: "lookup",
|
|
677
|
+
collection: "roles",
|
|
678
|
+
db: "permissions",
|
|
679
|
+
by: "roleId",
|
|
680
|
+
},
|
|
681
|
+
{ type: "get", fields: ["permissions"] },
|
|
682
|
+
],
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const result = await executor.execute(query);
|
|
686
|
+
|
|
687
|
+
expect(result.success).toBe(true);
|
|
688
|
+
expect(result.result).toEqual({
|
|
689
|
+
permissions: ["read", "write", "delete"],
|
|
690
|
+
});
|
|
691
|
+
expect(result.trace).toHaveLength(7); // start + 6 steps
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|