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,392 @@
|
|
|
1
|
+
import { NoQL } from "../index";
|
|
2
|
+
import { ParseError } from "../errors";
|
|
3
|
+
import { DatabaseProvider, JsonQuery } from "../types";
|
|
4
|
+
|
|
5
|
+
// Mock database provider for testing NoQL integration
|
|
6
|
+
class MockNoQLDatabaseProvider implements DatabaseProvider {
|
|
7
|
+
private mockData: Map<string, Map<string, Record<string, any>[]>> = new Map();
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
// Initialize with sample data
|
|
11
|
+
this.addData("testDb", "users", [
|
|
12
|
+
{ _id: "user_1", name: "Alice", email: "alice@test.com", role: "admin" },
|
|
13
|
+
{ _id: "user_2", name: "Bob", email: "bob@test.com", role: "user" },
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
this.addData("testDb", "posts", [
|
|
17
|
+
{ _id: "post_1", title: "First Post", authorId: "user_1" },
|
|
18
|
+
{ _id: "post_2", title: "Second Post", authorId: "user_2" },
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addData(db: string, collection: string, documents: Record<string, any>[]) {
|
|
23
|
+
if (!this.mockData.has(db)) {
|
|
24
|
+
this.mockData.set(db, new Map());
|
|
25
|
+
}
|
|
26
|
+
const dbMap = this.mockData.get(db)!;
|
|
27
|
+
dbMap.set(collection, documents);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findOne(
|
|
31
|
+
dbName: string,
|
|
32
|
+
collectionName: string,
|
|
33
|
+
query: Record<string, any>,
|
|
34
|
+
): Promise<Record<string, any> | null> {
|
|
35
|
+
const db = this.mockData.get(dbName);
|
|
36
|
+
if (!db) return null;
|
|
37
|
+
|
|
38
|
+
const collection = db.get(collectionName);
|
|
39
|
+
if (!collection) return null;
|
|
40
|
+
|
|
41
|
+
const doc = collection.find((doc) => {
|
|
42
|
+
return Object.entries(query).every(([key, value]) => doc[key] === value);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return doc || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async close(): Promise<void> {
|
|
49
|
+
// Mock close
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Helper to create NoQL with mock provider
|
|
54
|
+
function createMockNoQL(): NoQL {
|
|
55
|
+
const mockProvider = new MockNoQLDatabaseProvider();
|
|
56
|
+
// Use private constructor trick via type casting
|
|
57
|
+
const noql = Object.create(NoQL.prototype);
|
|
58
|
+
noql["dbProvider"] = mockProvider;
|
|
59
|
+
noql["parser"] = new (require("../parser").DSLParser)();
|
|
60
|
+
return noql;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("NoQL Integration Tests", () => {
|
|
64
|
+
let noql: NoQL;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
noql = createMockNoQL();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
await noql.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("parseDSL", () => {
|
|
75
|
+
it("should parse simple DSL query", () => {
|
|
76
|
+
const dsl = 'FROM users IN testDb WHERE _id = "user_1"';
|
|
77
|
+
const result = noql.parseDSL(dsl);
|
|
78
|
+
|
|
79
|
+
expect(result).toMatchObject({
|
|
80
|
+
start: {
|
|
81
|
+
collection: "users",
|
|
82
|
+
db: "testDb",
|
|
83
|
+
where: { _id: "user_1" },
|
|
84
|
+
},
|
|
85
|
+
steps: [],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should parse DSL query with WITH clause", () => {
|
|
90
|
+
const dsl = `
|
|
91
|
+
WITH output: "trace"
|
|
92
|
+
FROM users IN testDb WHERE _id = "user_1"
|
|
93
|
+
`.trim();
|
|
94
|
+
|
|
95
|
+
const result = noql.parseDSL(dsl);
|
|
96
|
+
|
|
97
|
+
expect(result.output).toBe("trace");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should parse DSL query with steps", () => {
|
|
101
|
+
const dsl = `
|
|
102
|
+
FROM posts IN testDb WHERE _id = "post_1"
|
|
103
|
+
-> LOOKUP users IN testDb BY authorId
|
|
104
|
+
-> GET name, email
|
|
105
|
+
`.trim();
|
|
106
|
+
|
|
107
|
+
const result = noql.parseDSL(dsl);
|
|
108
|
+
|
|
109
|
+
expect(result.steps).toHaveLength(2);
|
|
110
|
+
expect(result.steps[0].type).toBe("lookup");
|
|
111
|
+
expect(result.steps[1].type).toBe("get");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should throw ParseError for invalid DSL", () => {
|
|
115
|
+
const dsl = "INVALID SYNTAX";
|
|
116
|
+
expect(() => noql.parseDSL(dsl)).toThrow(ParseError);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("executeJson", () => {
|
|
121
|
+
it("should execute simple JSON query", async () => {
|
|
122
|
+
const query: JsonQuery = {
|
|
123
|
+
start: {
|
|
124
|
+
collection: "users",
|
|
125
|
+
db: "testDb",
|
|
126
|
+
where: { _id: "user_1" },
|
|
127
|
+
},
|
|
128
|
+
steps: [],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await noql.executeJson(query);
|
|
132
|
+
|
|
133
|
+
expect(result.success).toBe(true);
|
|
134
|
+
expect(result.result).toMatchObject({
|
|
135
|
+
_id: "user_1",
|
|
136
|
+
name: "Alice",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should execute JSON query with GET step", async () => {
|
|
141
|
+
const query: JsonQuery = {
|
|
142
|
+
start: {
|
|
143
|
+
collection: "users",
|
|
144
|
+
db: "testDb",
|
|
145
|
+
where: { _id: "user_1" },
|
|
146
|
+
},
|
|
147
|
+
steps: [{ type: "get", fields: ["name", "email"] }],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await noql.executeJson(query);
|
|
151
|
+
|
|
152
|
+
expect(result.success).toBe(true);
|
|
153
|
+
expect(result.result).toEqual({
|
|
154
|
+
name: "Alice",
|
|
155
|
+
email: "alice@test.com",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should execute JSON query with LOOKUP step", async () => {
|
|
160
|
+
const query: JsonQuery = {
|
|
161
|
+
start: {
|
|
162
|
+
collection: "posts",
|
|
163
|
+
db: "testDb",
|
|
164
|
+
where: { _id: "post_1" },
|
|
165
|
+
},
|
|
166
|
+
steps: [
|
|
167
|
+
{ type: "lookup", collection: "users", db: "testDb", by: "authorId" },
|
|
168
|
+
{ type: "get", fields: ["name"] },
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const result = await noql.executeJson(query);
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.result).toEqual({ name: "Alice" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should return error for document not found", async () => {
|
|
179
|
+
const query: JsonQuery = {
|
|
180
|
+
start: {
|
|
181
|
+
collection: "users",
|
|
182
|
+
db: "testDb",
|
|
183
|
+
where: { _id: "nonexistent" },
|
|
184
|
+
},
|
|
185
|
+
steps: [],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = await noql.executeJson(query);
|
|
189
|
+
|
|
190
|
+
expect(result.success).toBe(false);
|
|
191
|
+
expect(result.error).toBeDefined();
|
|
192
|
+
expect(result.error).toContain("Document not found");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should execute query with trace output", async () => {
|
|
196
|
+
const query: JsonQuery = {
|
|
197
|
+
output: "trace",
|
|
198
|
+
start: {
|
|
199
|
+
collection: "users",
|
|
200
|
+
db: "testDb",
|
|
201
|
+
where: { _id: "user_1" },
|
|
202
|
+
},
|
|
203
|
+
steps: [{ type: "get", fields: ["name"] }],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = await noql.executeJson(query);
|
|
207
|
+
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
expect(result.trace).toBeDefined();
|
|
210
|
+
expect(result.trace).toHaveLength(2);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("executeDSL", () => {
|
|
215
|
+
it("should execute simple DSL query", async () => {
|
|
216
|
+
const dsl = 'FROM users IN testDb WHERE _id = "user_1"';
|
|
217
|
+
const result = await noql.executeDSL(dsl);
|
|
218
|
+
|
|
219
|
+
expect(result.success).toBe(true);
|
|
220
|
+
expect(result.result).toMatchObject({
|
|
221
|
+
_id: "user_1",
|
|
222
|
+
name: "Alice",
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should execute DSL query with GET step", async () => {
|
|
227
|
+
const dsl = `
|
|
228
|
+
FROM users IN testDb WHERE _id = "user_1"
|
|
229
|
+
-> GET name, email
|
|
230
|
+
`.trim();
|
|
231
|
+
|
|
232
|
+
const result = await noql.executeDSL(dsl);
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(true);
|
|
235
|
+
expect(result.result).toEqual({
|
|
236
|
+
name: "Alice",
|
|
237
|
+
email: "alice@test.com",
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should execute DSL query with LOOKUP step", async () => {
|
|
242
|
+
const dsl = `
|
|
243
|
+
FROM posts IN testDb WHERE _id = "post_1"
|
|
244
|
+
-> LOOKUP users IN testDb BY authorId
|
|
245
|
+
-> GET name, email
|
|
246
|
+
`.trim();
|
|
247
|
+
|
|
248
|
+
const result = await noql.executeDSL(dsl);
|
|
249
|
+
|
|
250
|
+
expect(result.success).toBe(true);
|
|
251
|
+
expect(result.result).toEqual({
|
|
252
|
+
name: "Alice",
|
|
253
|
+
email: "alice@test.com",
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should execute DSL query with output format", async () => {
|
|
258
|
+
const dsl = `
|
|
259
|
+
WITH output: "trace"
|
|
260
|
+
FROM users IN testDb WHERE _id = "user_1"
|
|
261
|
+
-> GET name
|
|
262
|
+
`.trim();
|
|
263
|
+
|
|
264
|
+
const result = await noql.executeDSL(dsl);
|
|
265
|
+
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(result.trace).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should return error for invalid DSL syntax", async () => {
|
|
271
|
+
const dsl = "INVALID SYNTAX";
|
|
272
|
+
const result = await noql.executeDSL(dsl);
|
|
273
|
+
|
|
274
|
+
expect(result.success).toBe(false);
|
|
275
|
+
expect(result.error).toBeDefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should return error for document not found", async () => {
|
|
279
|
+
const dsl = 'FROM users IN testDb WHERE _id = "nonexistent"';
|
|
280
|
+
const result = await noql.executeDSL(dsl);
|
|
281
|
+
|
|
282
|
+
expect(result.success).toBe(false);
|
|
283
|
+
expect(result.error).toContain("Document not found");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should execute complex multi-step DSL query", async () => {
|
|
287
|
+
const dsl = `
|
|
288
|
+
FROM posts IN testDb WHERE _id = "post_2"
|
|
289
|
+
-> LOOKUP users IN testDb BY authorId
|
|
290
|
+
-> GET name, role
|
|
291
|
+
`.trim();
|
|
292
|
+
|
|
293
|
+
const result = await noql.executeDSL(dsl);
|
|
294
|
+
|
|
295
|
+
expect(result.success).toBe(true);
|
|
296
|
+
expect(result.result).toEqual({
|
|
297
|
+
name: "Bob",
|
|
298
|
+
role: "user",
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should handle WHERE conditions with different value types", async () => {
|
|
303
|
+
const dsl = `
|
|
304
|
+
FROM users IN testDb WHERE role = "admin"
|
|
305
|
+
-> GET name
|
|
306
|
+
`.trim();
|
|
307
|
+
|
|
308
|
+
const result = await noql.executeDSL(dsl);
|
|
309
|
+
|
|
310
|
+
expect(result.success).toBe(true);
|
|
311
|
+
expect(result.result).toMatchObject({
|
|
312
|
+
name: "Alice",
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("End-to-end DSL to execution flow", () => {
|
|
318
|
+
it("should parse and execute in one flow", async () => {
|
|
319
|
+
const dsl = `
|
|
320
|
+
WITH output: "value"
|
|
321
|
+
FROM posts IN testDb WHERE _id = "post_1"
|
|
322
|
+
-> LOOKUP users IN testDb BY authorId
|
|
323
|
+
-> GET name, email
|
|
324
|
+
`.trim();
|
|
325
|
+
|
|
326
|
+
// First parse to verify structure
|
|
327
|
+
const parsed = noql.parseDSL(dsl);
|
|
328
|
+
expect(parsed.output).toBe("value");
|
|
329
|
+
expect(parsed.steps).toHaveLength(2);
|
|
330
|
+
|
|
331
|
+
// Then execute
|
|
332
|
+
const result = await noql.executeDSL(dsl);
|
|
333
|
+
expect(result.success).toBe(true);
|
|
334
|
+
expect(result.result).toEqual({
|
|
335
|
+
name: "Alice",
|
|
336
|
+
email: "alice@test.com",
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should handle document format output", async () => {
|
|
341
|
+
const dsl = `
|
|
342
|
+
WITH output: "document"
|
|
343
|
+
FROM users IN testDb WHERE _id = "user_2"
|
|
344
|
+
-> GET name
|
|
345
|
+
`.trim();
|
|
346
|
+
|
|
347
|
+
const result = await noql.executeDSL(dsl);
|
|
348
|
+
|
|
349
|
+
expect(result.success).toBe(true);
|
|
350
|
+
expect(result.result).toEqual({ name: "Bob" });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should provide detailed trace for debugging", async () => {
|
|
354
|
+
const dsl = `
|
|
355
|
+
WITH output: "trace"
|
|
356
|
+
FROM posts IN testDb WHERE _id = "post_1"
|
|
357
|
+
-> LOOKUP users IN testDb BY authorId
|
|
358
|
+
-> GET name
|
|
359
|
+
`.trim();
|
|
360
|
+
|
|
361
|
+
const result = await noql.executeDSL(dsl);
|
|
362
|
+
|
|
363
|
+
expect(result.success).toBe(true);
|
|
364
|
+
expect(result.trace).toBeDefined();
|
|
365
|
+
expect(result.trace!.length).toBeGreaterThan(0);
|
|
366
|
+
|
|
367
|
+
// Verify trace structure
|
|
368
|
+
const startStep = result.trace![0];
|
|
369
|
+
expect(startStep.stepType).toBe("start");
|
|
370
|
+
expect(startStep.collection).toBe("posts");
|
|
371
|
+
expect(startStep.documentFound).toBeDefined();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Error handling", () => {
|
|
376
|
+
it("should handle parse errors gracefully in executeDSL", async () => {
|
|
377
|
+
const dsl = "FROM users";
|
|
378
|
+
const result = await noql.executeDSL(dsl);
|
|
379
|
+
|
|
380
|
+
expect(result.success).toBe(false);
|
|
381
|
+
expect(result.error).toBeDefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should handle execution errors gracefully", async () => {
|
|
385
|
+
const dsl = 'FROM nonexistent IN testDb WHERE _id = "test"';
|
|
386
|
+
const result = await noql.executeDSL(dsl);
|
|
387
|
+
|
|
388
|
+
expect(result.success).toBe(false);
|
|
389
|
+
expect(result.error).toBeDefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|