@spooky-sync/query-builder 0.0.0-canary.1
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/dist/index.d.mts +413 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +413 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +459 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +448 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +65 -0
- package/src/query-builder.test.ts +470 -0
- package/src/query-builder.ts +933 -0
- package/src/repro_relationship.test.ts +59 -0
- package/src/table-schema.ts +268 -0
- package/src/types.ts +216 -0
- package/tsconfig.json +22 -0
- package/tsdown.config.ts +11 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
2
|
+
import { QueryBuilder, buildQueryFromOptions } from './query-builder';
|
|
3
|
+
import { RecordId } from 'surrealdb';
|
|
4
|
+
import type { TableNames, TableModel, GetTable } from './table-schema';
|
|
5
|
+
|
|
6
|
+
// Schema for testing the new array-based API
|
|
7
|
+
const testSchema = {
|
|
8
|
+
tables: [
|
|
9
|
+
{
|
|
10
|
+
name: 'user' as const,
|
|
11
|
+
columns: {
|
|
12
|
+
id: { type: 'string' as const, optional: false },
|
|
13
|
+
username: { type: 'string' as const, optional: false },
|
|
14
|
+
email: { type: 'string' as const, optional: false },
|
|
15
|
+
created_at: { type: 'number' as const, optional: false },
|
|
16
|
+
},
|
|
17
|
+
primaryKey: ['id'] as const,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'thread' as const,
|
|
21
|
+
columns: {
|
|
22
|
+
id: { type: 'string' as const, optional: false },
|
|
23
|
+
title: { type: 'string' as const, optional: false },
|
|
24
|
+
content: { type: 'string' as const, optional: false },
|
|
25
|
+
author: { type: 'string' as const, optional: false },
|
|
26
|
+
comments: { type: 'string' as const, optional: true },
|
|
27
|
+
created_at: { type: 'number' as const, optional: false },
|
|
28
|
+
},
|
|
29
|
+
primaryKey: ['id'] as const,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'comment' as const,
|
|
33
|
+
columns: {
|
|
34
|
+
id: { type: 'string' as const, optional: false },
|
|
35
|
+
content: { type: 'string' as const, optional: false },
|
|
36
|
+
author: { type: 'string' as const, optional: false },
|
|
37
|
+
thread: { type: 'string' as const, optional: false },
|
|
38
|
+
created_at: { type: 'number' as const, optional: false },
|
|
39
|
+
},
|
|
40
|
+
primaryKey: ['id'] as const,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
relationships: [
|
|
44
|
+
{
|
|
45
|
+
from: 'thread' as const,
|
|
46
|
+
field: 'author' as const,
|
|
47
|
+
to: 'user' as const,
|
|
48
|
+
cardinality: 'one' as const,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
from: 'thread' as const,
|
|
52
|
+
field: 'comments' as const,
|
|
53
|
+
to: 'comment' as const,
|
|
54
|
+
cardinality: 'many' as const,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
from: 'comment' as const,
|
|
58
|
+
field: 'author' as const,
|
|
59
|
+
to: 'user' as const,
|
|
60
|
+
cardinality: 'one' as const,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
from: 'comment' as const,
|
|
64
|
+
field: 'thread_ref' as const,
|
|
65
|
+
to: 'thread' as const,
|
|
66
|
+
cardinality: 'one' as const,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
backends: {},
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
describe('QueryBuilder', () => {
|
|
73
|
+
it('should build basic SELECT query', () => {
|
|
74
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
75
|
+
|
|
76
|
+
const result = builder.build().run();
|
|
77
|
+
|
|
78
|
+
expect(result.query).toBe('SELECT * FROM user;');
|
|
79
|
+
expect(result.vars).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should build query with where conditions', () => {
|
|
83
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
84
|
+
|
|
85
|
+
builder.where({ username: 'john', email: 'john@example.com' });
|
|
86
|
+
const result = builder.build().run();
|
|
87
|
+
|
|
88
|
+
expect(result.query).toBe('SELECT * FROM user WHERE username = $username AND email = $email;');
|
|
89
|
+
expect(result.vars).toEqual({
|
|
90
|
+
username: 'john',
|
|
91
|
+
email: 'john@example.com',
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should build query with select fields', () => {
|
|
96
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
97
|
+
builder.select('username', 'email');
|
|
98
|
+
const result = builder.build().run();
|
|
99
|
+
|
|
100
|
+
expect(result.query).toBe('SELECT username, email FROM user;');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw error when calling select twice', () => {
|
|
104
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
105
|
+
builder.select('username');
|
|
106
|
+
expect(() => builder.select('email')).toThrow('Select can only be called once per query');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should build query with ordering, limit, and offset', () => {
|
|
110
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
111
|
+
builder.orderBy('created_at', 'desc').limit(10).offset(5);
|
|
112
|
+
const result = builder.build().run();
|
|
113
|
+
|
|
114
|
+
expect(result.query).toBe('SELECT * FROM user ORDER BY created_at desc LIMIT 10 START 5;');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should support method chaining', () => {
|
|
118
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
119
|
+
const result = builder
|
|
120
|
+
.where({ username: 'john' })
|
|
121
|
+
.select('username', 'email')
|
|
122
|
+
.orderBy('created_at', 'desc')
|
|
123
|
+
.limit(10)
|
|
124
|
+
.build()
|
|
125
|
+
.run();
|
|
126
|
+
|
|
127
|
+
expect(result.query).toBe(
|
|
128
|
+
'SELECT username, email FROM user WHERE username = $username ORDER BY created_at desc LIMIT 10;'
|
|
129
|
+
);
|
|
130
|
+
expect(result.vars).toEqual({ username: 'john' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should build LIVE SELECT query (ignores ORDER BY, LIMIT, START)', () => {
|
|
134
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
135
|
+
builder.where({ username: 'john' }).orderBy('created_at', 'desc').limit(10).offset(5);
|
|
136
|
+
const result = builder.build().selectLive();
|
|
137
|
+
|
|
138
|
+
expect(result.query).toBe('LIVE SELECT * FROM user WHERE username = $username;');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Relationship Queries', () => {
|
|
143
|
+
// Using testSchema from top-level scope
|
|
144
|
+
|
|
145
|
+
it('should build query with one-to-one relationship', () => {
|
|
146
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
147
|
+
builder.related('author');
|
|
148
|
+
const result = builder.build().run();
|
|
149
|
+
|
|
150
|
+
expect(result.query).toBe(
|
|
151
|
+
'SELECT *, (SELECT * FROM user WHERE id=$parent.author LIMIT 1)[0] AS author FROM thread;'
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should build query with one-to-many relationship', () => {
|
|
156
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
157
|
+
builder.related('comments');
|
|
158
|
+
const result = builder.build().run();
|
|
159
|
+
|
|
160
|
+
expect(result.query).toBe(
|
|
161
|
+
'SELECT *, (SELECT * FROM comment WHERE thread=$parent.id) AS comments FROM thread;'
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should build query with relationship modifiers', () => {
|
|
166
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
167
|
+
builder.related('comments', (q) => q.where({ author: 'user:123' }).limit(5));
|
|
168
|
+
const result = builder.build().run();
|
|
169
|
+
|
|
170
|
+
expect(result.query).toBe(
|
|
171
|
+
'SELECT *, (SELECT * FROM comment WHERE thread=$parent.id AND author = user:⟨123⟩ LIMIT 5) AS comments FROM thread;'
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should build query with nested relationships', () => {
|
|
176
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
177
|
+
builder.related('comments', (q) => q.related('author'));
|
|
178
|
+
const result = builder.build().run();
|
|
179
|
+
|
|
180
|
+
expect(result.query).toBe(
|
|
181
|
+
'SELECT *, (SELECT *, (SELECT * FROM user WHERE id=$parent.author LIMIT 1)[0] AS author FROM comment WHERE thread=$parent.id) AS comments FROM thread;'
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('buildQueryFromOptions', () => {
|
|
187
|
+
it('should build query from options', () => {
|
|
188
|
+
const result = buildQueryFromOptions<TableModel<(typeof testSchema)['tables'][0]>, boolean>(
|
|
189
|
+
'SELECT',
|
|
190
|
+
'user',
|
|
191
|
+
{
|
|
192
|
+
where: { username: 'john' },
|
|
193
|
+
select: ['username', 'email'],
|
|
194
|
+
orderBy: { username: 'desc' },
|
|
195
|
+
limit: 10,
|
|
196
|
+
},
|
|
197
|
+
testSchema
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(result.query).toBe(
|
|
201
|
+
'SELECT username, email FROM user WHERE username = $username ORDER BY username desc LIMIT 10;'
|
|
202
|
+
);
|
|
203
|
+
expect(result.vars).toEqual({ username: 'john' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should build LIVE SELECT query from options', () => {
|
|
207
|
+
const result = buildQueryFromOptions<TableModel<(typeof testSchema)['tables'][0]>, boolean>(
|
|
208
|
+
'LIVE SELECT',
|
|
209
|
+
'user',
|
|
210
|
+
{
|
|
211
|
+
where: { username: 'john' },
|
|
212
|
+
},
|
|
213
|
+
testSchema
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(result.query).toBe('LIVE SELECT * FROM user WHERE username = $username;');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('RecordId Parsing', () => {
|
|
221
|
+
it('should parse string IDs to RecordId', () => {
|
|
222
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
223
|
+
builder.where({ author: 'user:123', id: 'abc123' });
|
|
224
|
+
const result = builder.build().run();
|
|
225
|
+
|
|
226
|
+
expect(result.vars).toBeDefined();
|
|
227
|
+
expect(result.vars!.author).toBeInstanceOf(RecordId);
|
|
228
|
+
expect((result.vars!.author as RecordId).toString()).toBe('user:⟨123⟩');
|
|
229
|
+
expect(result.vars!.id).toBeInstanceOf(RecordId);
|
|
230
|
+
expect((result.vars!.id as RecordId).toString()).toBe('thread:abc123');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should not parse non-ID strings', () => {
|
|
234
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
235
|
+
builder.where({ username: 'john_doe' });
|
|
236
|
+
const result = builder.build().run();
|
|
237
|
+
|
|
238
|
+
expect(result.vars?.username).toBe('john_doe');
|
|
239
|
+
expect(result.vars?.username).not.toBeInstanceOf(RecordId);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('Edge Cases', () => {
|
|
244
|
+
it('should handle empty where object', () => {
|
|
245
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
246
|
+
builder.where({});
|
|
247
|
+
const result = builder.build().run();
|
|
248
|
+
|
|
249
|
+
expect(result.query).toBe('SELECT * FROM user;');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle special characters in strings', () => {
|
|
253
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
254
|
+
builder.where({ username: 'john"doe' });
|
|
255
|
+
const result = builder.build().run();
|
|
256
|
+
|
|
257
|
+
expect(result.vars?.username).toBe('john"doe');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should return options via getOptions()', () => {
|
|
261
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
262
|
+
builder.where({ username: 'john' }).limit(10);
|
|
263
|
+
const options = builder.getOptions();
|
|
264
|
+
|
|
265
|
+
expect(options.where).toEqual({ username: 'john' });
|
|
266
|
+
expect(options.limit).toBe(10);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('Type Tests', () => {
|
|
271
|
+
it('should enforce correct table names', () => {
|
|
272
|
+
// Valid table names should work
|
|
273
|
+
new QueryBuilder(testSchema, 'user');
|
|
274
|
+
new QueryBuilder(testSchema, 'thread');
|
|
275
|
+
new QueryBuilder(testSchema, 'comment');
|
|
276
|
+
|
|
277
|
+
// @ts-expect-error - invalid table name should not compile
|
|
278
|
+
new QueryBuilder(testSchema, 'invalid_table');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should enforce correct field names in where clause', () => {
|
|
282
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
283
|
+
|
|
284
|
+
// Valid fields should work
|
|
285
|
+
builder.where({ username: 'john' });
|
|
286
|
+
builder.where({ email: 'john@example.com' });
|
|
287
|
+
builder.where({ id: 'user:123' });
|
|
288
|
+
|
|
289
|
+
// @ts-expect-error - invalid field should not compile
|
|
290
|
+
builder.where({ invalid_field: 'value' });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should enforce correct field names in select', () => {
|
|
294
|
+
const builder = new QueryBuilder(testSchema, 'comment', (q) => q.selectQuery);
|
|
295
|
+
const builder2 = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
296
|
+
|
|
297
|
+
// Valid fields should work
|
|
298
|
+
builder.select('content', 'thread');
|
|
299
|
+
builder2.select('id', 'created_at');
|
|
300
|
+
|
|
301
|
+
const builder3 = new QueryBuilder(testSchema, 'user');
|
|
302
|
+
// @ts-expect-error - invalid field should not compile
|
|
303
|
+
builder3.select('invalid_field');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should enforce correct field names in orderBy', () => {
|
|
307
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
308
|
+
|
|
309
|
+
// Valid fields should work
|
|
310
|
+
builder.orderBy('username', 'asc');
|
|
311
|
+
builder.orderBy('created_at', 'desc');
|
|
312
|
+
|
|
313
|
+
// @ts-expect-error - invalid field should not compile
|
|
314
|
+
builder.orderBy('invalid_field', 'asc');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should enforce correct relationship field names', () => {
|
|
318
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
319
|
+
|
|
320
|
+
// Valid relationship fields should work (cardinality now required)
|
|
321
|
+
builder.related('author');
|
|
322
|
+
builder.related('comments');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should enforce relationship metadata types', () => {
|
|
326
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
327
|
+
|
|
328
|
+
// The related method should accept a modifier function with correct types (cardinality as 2nd param)
|
|
329
|
+
builder.related('comments', (q) => {
|
|
330
|
+
// Should be able to call methods on the related query builder
|
|
331
|
+
q.where({ content: 'test' });
|
|
332
|
+
q.limit(5);
|
|
333
|
+
return q;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
builder.related('author', 'one', (q) => {
|
|
337
|
+
// Should be able to call methods on the related query builder
|
|
338
|
+
q.where({ username: 'john' });
|
|
339
|
+
return q;
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should enforce correct types in where clause values', () => {
|
|
344
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
345
|
+
|
|
346
|
+
// String fields should accept strings
|
|
347
|
+
builder.where({ username: 'john' });
|
|
348
|
+
builder.where({ email: 'test@example.com' });
|
|
349
|
+
|
|
350
|
+
// Number fields should accept numbers
|
|
351
|
+
builder.where({ created_at: 123456 });
|
|
352
|
+
|
|
353
|
+
// ID fields should accept strings (will be parsed to RecordId)
|
|
354
|
+
builder.where({ id: 'user:123' });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should return correctly typed query result', () => {
|
|
358
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
359
|
+
const result = builder.build().run();
|
|
360
|
+
|
|
361
|
+
// Query should be a string
|
|
362
|
+
expectTypeOf(result.query).toBeString();
|
|
363
|
+
|
|
364
|
+
// Vars should be a record or undefined
|
|
365
|
+
expectTypeOf(result.vars).toEqualTypeOf<Record<string, unknown> | undefined>();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should enforce correct select return type', () => {
|
|
369
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
370
|
+
|
|
371
|
+
// Select should return the builder for chaining
|
|
372
|
+
const result = builder.select('username', 'email');
|
|
373
|
+
expectTypeOf(result).toMatchTypeOf<typeof builder>();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should enforce correct method chaining types', () => {
|
|
377
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
378
|
+
|
|
379
|
+
// All methods should return the builder for chaining
|
|
380
|
+
const result = builder
|
|
381
|
+
.where({ username: 'john' })
|
|
382
|
+
.select('username', 'email')
|
|
383
|
+
.orderBy('created_at', 'desc')
|
|
384
|
+
.limit(10)
|
|
385
|
+
.offset(5);
|
|
386
|
+
|
|
387
|
+
expectTypeOf(result).toMatchTypeOf<typeof builder>();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Schema Metadata Integration', () => {
|
|
392
|
+
// Using testSchema from top-level scope
|
|
393
|
+
type TestSchemaMetadata = typeof testSchema;
|
|
394
|
+
|
|
395
|
+
it('should accept testSchema in constructor', () => {
|
|
396
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
397
|
+
|
|
398
|
+
expect(builder).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should build query with metadata-driven relationships', () => {
|
|
402
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
403
|
+
|
|
404
|
+
builder.related('author', 'one');
|
|
405
|
+
const result = builder.build().run();
|
|
406
|
+
|
|
407
|
+
expect(result.query).toBe(
|
|
408
|
+
'SELECT *, (SELECT * FROM user WHERE id=$parent.author LIMIT 1)[0] AS author FROM thread;'
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should handle one-to-many relationships with metadata', () => {
|
|
413
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
414
|
+
|
|
415
|
+
builder.related('comments');
|
|
416
|
+
const result = builder.build().run();
|
|
417
|
+
|
|
418
|
+
expect(result.query).toBe(
|
|
419
|
+
'SELECT *, (SELECT * FROM comment WHERE thread=$parent.id) AS comments FROM thread;'
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('Update and Delete Queries', () => {
|
|
425
|
+
it('should build UPDATE query with patches', () => {
|
|
426
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
427
|
+
builder.where({ username: 'john' });
|
|
428
|
+
const patches = [{ op: 'replace', path: '/email', value: 'new@example.com' }];
|
|
429
|
+
const result = builder.build().buildUpdateQuery(patches);
|
|
430
|
+
|
|
431
|
+
expect(result.query).toBe(
|
|
432
|
+
`UPDATE user WHERE username = $username PATCH ${JSON.stringify(patches)};`
|
|
433
|
+
);
|
|
434
|
+
expect(result.vars).toEqual({ username: 'john' });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should build DELETE query', () => {
|
|
438
|
+
const builder = new QueryBuilder(testSchema, 'user', (q) => q.selectQuery);
|
|
439
|
+
builder.where({ username: 'john' });
|
|
440
|
+
const result = builder.build().buildDeleteQuery();
|
|
441
|
+
|
|
442
|
+
expect(result.query).toBe('DELETE FROM user WHERE username = $username;');
|
|
443
|
+
expect(result.vars).toEqual({ username: 'john' });
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('Subquery Filtering', () => {
|
|
448
|
+
it('should inject parent filter into subqueries', () => {
|
|
449
|
+
const builder = new QueryBuilder(testSchema, 'thread', (q) => q.selectQuery);
|
|
450
|
+
builder.related('comments');
|
|
451
|
+
const query = builder.build();
|
|
452
|
+
|
|
453
|
+
const innerQuery = query.innerQuery;
|
|
454
|
+
const subqueries = innerQuery.subqueries;
|
|
455
|
+
|
|
456
|
+
expect(subqueries).toHaveLength(1);
|
|
457
|
+
const commentSubquery = subqueries[0];
|
|
458
|
+
|
|
459
|
+
// Check that the subquery has the parent filter injected
|
|
460
|
+
// thread table has 'comments' field pointing to 'comment' table (one-to-many)
|
|
461
|
+
// So it should have WHERE $parentIds ∋ thread_ref (found via reverse relationship)
|
|
462
|
+
// And 'thread_ref' should NOT be in vars because it's a direct variable reference
|
|
463
|
+
expect(commentSubquery.selectQuery.query).toContain('$parentIds ∋ thread_ref');
|
|
464
|
+
expect(commentSubquery.selectQuery.vars).not.toEqual(
|
|
465
|
+
expect.objectContaining({
|
|
466
|
+
thread_ref: '$parentIds',
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
});
|