sqlite-zod-orm 3.8.0 → 3.9.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.
@@ -1,669 +0,0 @@
1
- import { z } from 'zod';
2
- import {
3
- type ASTNode, type WhereCallback, type TypedColumnProxy, type FunctionProxy, type Operators,
4
- compileAST, wrapNode, createColumnProxy, createFunctionProxy, op,
5
- } from './ast';
6
-
7
- // ---------- Internal Query Object (IQO) ----------
8
-
9
- type OrderDirection = 'asc' | 'desc';
10
- type WhereOperator = '$gt' | '$gte' | '$lt' | '$lte' | '$ne' | '$in';
11
-
12
- interface WhereCondition {
13
- field: string;
14
- operator: '=' | '>' | '>=' | '<' | '<=' | '!=' | 'IN';
15
- value: any;
16
- }
17
-
18
- interface JoinClause {
19
- table: string;
20
- fromCol: string;
21
- toCol: string;
22
- columns: string[]; // columns to SELECT from the joined table
23
- }
24
-
25
- interface IQO {
26
- selects: string[];
27
- wheres: WhereCondition[];
28
- whereOrs: WhereCondition[][]; // Each sub-array is an OR group
29
- whereAST: ASTNode | null;
30
- joins: JoinClause[];
31
- limit: number | null;
32
- offset: number | null;
33
- orderBy: { field: string; direction: OrderDirection }[];
34
- includes: string[];
35
- raw: boolean;
36
- }
37
-
38
- // ---------- Operator mapping ----------
39
-
40
- const OPERATOR_MAP: Record<WhereOperator, string> = {
41
- $gt: '>',
42
- $gte: '>=',
43
- $lt: '<',
44
- $lte: '<=',
45
- $ne: '!=',
46
- $in: 'IN',
47
- };
48
-
49
- // ---------- SQL Compilation ----------
50
-
51
- function transformValueForStorage(value: any): any {
52
- if (value instanceof Date) return value.toISOString();
53
- if (typeof value === 'boolean') return value ? 1 : 0;
54
- return value;
55
- }
56
-
57
- export function compileIQO(tableName: string, iqo: IQO): { sql: string; params: any[] } {
58
- const params: any[] = [];
59
-
60
- // SELECT clause
61
- const selectParts: string[] = [];
62
- if (iqo.selects.length > 0) {
63
- selectParts.push(...iqo.selects.map(s => `${tableName}.${s}`));
64
- } else {
65
- selectParts.push(`${tableName}.*`);
66
- }
67
- // Add columns from joins
68
- for (const j of iqo.joins) {
69
- if (j.columns.length > 0) {
70
- selectParts.push(...j.columns.map(c => `${j.table}.${c} AS ${j.table}_${c}`));
71
- } else {
72
- selectParts.push(`${j.table}.*`);
73
- }
74
- }
75
-
76
- let sql = `SELECT ${selectParts.join(', ')} FROM ${tableName}`;
77
-
78
- // JOIN clauses
79
- for (const j of iqo.joins) {
80
- sql += ` JOIN ${j.table} ON ${tableName}.${j.fromCol} = ${j.table}.${j.toCol}`;
81
- }
82
-
83
- // WHERE clause — AST-based takes precedence if set
84
- if (iqo.whereAST) {
85
- const compiled = compileAST(iqo.whereAST);
86
- sql += ` WHERE ${compiled.sql}`;
87
- params.push(...compiled.params);
88
- } else if (iqo.wheres.length > 0) {
89
- const hasJoins = iqo.joins.length > 0;
90
- // When joins exist, qualify bare column names with the main table
91
- const qualify = (field: string) =>
92
- hasJoins && !field.includes('.') ? `${tableName}.${field}` : field;
93
-
94
- const whereParts: string[] = [];
95
- for (const w of iqo.wheres) {
96
- if (w.operator === 'IN') {
97
- const arr = w.value as any[];
98
- if (arr.length === 0) {
99
- whereParts.push('1 = 0');
100
- } else {
101
- const placeholders = arr.map(() => '?').join(', ');
102
- whereParts.push(`${qualify(w.field)} IN (${placeholders})`);
103
- params.push(...arr.map(transformValueForStorage));
104
- }
105
- } else {
106
- whereParts.push(`${qualify(w.field)} ${w.operator} ?`);
107
- params.push(transformValueForStorage(w.value));
108
- }
109
- }
110
- sql += ` WHERE ${whereParts.join(' AND ')}`;
111
- }
112
-
113
- // Append OR groups (from $or)
114
- if (iqo.whereOrs.length > 0) {
115
- for (const orGroup of iqo.whereOrs) {
116
- const orParts: string[] = [];
117
- for (const w of orGroup) {
118
- if (w.operator === 'IN') {
119
- const arr = w.value as any[];
120
- if (arr.length === 0) {
121
- orParts.push('1 = 0');
122
- } else {
123
- orParts.push(`${w.field} IN (${arr.map(() => '?').join(', ')})`);
124
- params.push(...arr.map(transformValueForStorage));
125
- }
126
- } else {
127
- orParts.push(`${w.field} ${w.operator} ?`);
128
- params.push(transformValueForStorage(w.value));
129
- }
130
- }
131
- if (orParts.length > 0) {
132
- const orClause = `(${orParts.join(' OR ')})`;
133
- sql += sql.includes(' WHERE ') ? ` AND ${orClause}` : ` WHERE ${orClause}`;
134
- }
135
- }
136
- }
137
-
138
- // ORDER BY clause
139
- if (iqo.orderBy.length > 0) {
140
- const parts = iqo.orderBy.map(o => `${o.field} ${o.direction.toUpperCase()}`);
141
- sql += ` ORDER BY ${parts.join(', ')}`;
142
- }
143
-
144
- // LIMIT
145
- if (iqo.limit !== null) {
146
- sql += ` LIMIT ${iqo.limit}`;
147
- }
148
-
149
- // OFFSET
150
- if (iqo.offset !== null) {
151
- sql += ` OFFSET ${iqo.offset}`;
152
- }
153
-
154
- return { sql, params };
155
- }
156
-
157
- // ---------- QueryBuilder Class ----------
158
-
159
- /**
160
- * A Fluent Query Builder that accumulates query state via chaining
161
- * and only executes when a terminal method is called (.all(), .get())
162
- * or when it is `await`-ed (thenable).
163
- *
164
- * Supports two WHERE styles:
165
- * - Object-style: `.where({ name: 'Alice', age: { $gt: 18 } })`
166
- * - Callback-style (AST): `.where((c, f, op) => op.and(op.eq(c.name, 'Alice'), op.gt(c.age, 18)))`
167
- */
168
- export class QueryBuilder<T extends Record<string, any>> {
169
- private iqo: IQO;
170
- private tableName: string;
171
- private executor: (sql: string, params: any[], raw: boolean) => any[];
172
- private singleExecutor: (sql: string, params: any[], raw: boolean) => any | null;
173
- private joinResolver: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null;
174
- private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
175
- private revisionGetter: (() => string) | null;
176
- private eagerLoader: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null;
177
- private defaultPollInterval: number;
178
-
179
- constructor(
180
- tableName: string,
181
- executor: (sql: string, params: any[], raw: boolean) => any[],
182
- singleExecutor: (sql: string, params: any[], raw: boolean) => any | null,
183
- joinResolver?: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null,
184
- conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
185
- revisionGetter?: (() => string) | null,
186
- eagerLoader?: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null,
187
- pollInterval?: number,
188
- ) {
189
- this.tableName = tableName;
190
- this.executor = executor;
191
- this.singleExecutor = singleExecutor;
192
- this.joinResolver = joinResolver ?? null;
193
- this.conditionResolver = conditionResolver ?? null;
194
- this.revisionGetter = revisionGetter ?? null;
195
- this.eagerLoader = eagerLoader ?? null;
196
- this.defaultPollInterval = pollInterval ?? 500;
197
- this.iqo = {
198
- selects: [],
199
- wheres: [],
200
- whereOrs: [],
201
- whereAST: null,
202
- joins: [],
203
- limit: null,
204
- offset: null,
205
- orderBy: [],
206
- includes: [],
207
- raw: false,
208
- };
209
- }
210
-
211
- /**
212
- * Specify which columns to select.
213
- * If called with no arguments, defaults to `*`.
214
- */
215
- select(...cols: (keyof T & string)[]): this {
216
- this.iqo.selects.push(...cols);
217
- return this;
218
- }
219
-
220
- /**
221
- * Add WHERE conditions. Two calling styles:
222
- *
223
- * **Object-style** (simple equality and operators):
224
- * ```ts
225
- * .where({ name: 'Alice' })
226
- * .where({ age: { $gt: 18 } })
227
- * ```
228
- *
229
- * **Callback-style** (AST-based, full SQL expression power):
230
- * ```ts
231
- * .where((c, f, op) => op.and(
232
- * op.eq(f.lower(c.name), 'alice'),
233
- * op.gt(c.age, 18)
234
- * ))
235
- * ```
236
- */
237
- where(criteriaOrCallback: (Partial<Record<keyof T & string, any>> & { $or?: Partial<Record<keyof T & string, any>>[] }) | WhereCallback<T>): this {
238
- if (typeof criteriaOrCallback === 'function') {
239
- // Callback-style: evaluate with proxies to produce AST
240
- const ast = (criteriaOrCallback as WhereCallback<T>)(
241
- createColumnProxy<T>(),
242
- createFunctionProxy(),
243
- op,
244
- );
245
- // If we already have an AST, AND them together
246
- if (this.iqo.whereAST) {
247
- this.iqo.whereAST = { type: 'operator', op: 'AND', left: this.iqo.whereAST, right: ast };
248
- } else {
249
- this.iqo.whereAST = ast;
250
- }
251
- } else {
252
- // Resolve entity references: { author: tolstoy } → { authorId: tolstoy.id }
253
- const resolved = this.conditionResolver
254
- ? this.conditionResolver(criteriaOrCallback as Record<string, any>)
255
- : criteriaOrCallback;
256
-
257
- // Object-style: parse into IQO conditions
258
- for (const [key, value] of Object.entries(resolved)) {
259
- // Handle $or: [{ field1: val1 }, { field2: val2 }]
260
- if (key === '$or' && Array.isArray(value)) {
261
- const orConditions: WhereCondition[] = [];
262
- for (const branch of value as Record<string, any>[]) {
263
- const resolvedBranch = this.conditionResolver
264
- ? this.conditionResolver(branch)
265
- : branch;
266
- for (const [bKey, bValue] of Object.entries(resolvedBranch)) {
267
- if (typeof bValue === 'object' && bValue !== null && !Array.isArray(bValue) && !(bValue instanceof Date)) {
268
- for (const [opKey, operand] of Object.entries(bValue)) {
269
- const sqlOp = OPERATOR_MAP[opKey as WhereOperator];
270
- if (sqlOp) orConditions.push({ field: bKey, operator: sqlOp as WhereCondition['operator'], value: operand });
271
- }
272
- } else {
273
- orConditions.push({ field: bKey, operator: '=', value: bValue });
274
- }
275
- }
276
- }
277
- if (orConditions.length > 0) this.iqo.whereOrs.push(orConditions);
278
- continue;
279
- }
280
-
281
- if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
282
- for (const [opKey, operand] of Object.entries(value)) {
283
- const sqlOp = OPERATOR_MAP[opKey as WhereOperator];
284
- if (!sqlOp) throw new Error(`Unsupported query operator: '${opKey}' on field '${key}'.`);
285
- this.iqo.wheres.push({
286
- field: key,
287
- operator: sqlOp as WhereCondition['operator'],
288
- value: operand,
289
- });
290
- }
291
- } else {
292
- this.iqo.wheres.push({ field: key, operator: '=', value });
293
- }
294
- }
295
- }
296
- return this;
297
- }
298
-
299
- /** Set the maximum number of rows to return. */
300
- limit(n: number): this {
301
- this.iqo.limit = n;
302
- return this;
303
- }
304
-
305
- /** Set the offset for pagination. */
306
- offset(n: number): this {
307
- this.iqo.offset = n;
308
- return this;
309
- }
310
-
311
- /**
312
- * Add ORDER BY clauses.
313
- * ```ts
314
- * .orderBy('name', 'asc')
315
- * .orderBy('createdAt', 'desc')
316
- * ```
317
- */
318
- orderBy(field: keyof T & string, direction: OrderDirection = 'asc'): this {
319
- this.iqo.orderBy.push({ field, direction });
320
- return this;
321
- }
322
-
323
- /**
324
- * Join another table. Two calling styles:
325
- *
326
- * **Accessor-based** (auto-infers FK from relationships, type-safe columns):
327
- * ```ts
328
- * db.trees.select('name').join(db.forests, ['name']).all()
329
- * // → [{ name: 'Oak', forests_name: 'Sherwood' }]
330
- * ```
331
- *
332
- * **String-based** (manual FK):
333
- * ```ts
334
- * db.trees.select('name').join('forests', 'forestId', ['name']).all()
335
- * ```
336
- */
337
- join(
338
- accessor: { _tableName: string },
339
- columns?: string[],
340
- ): this;
341
- join(
342
- table: string,
343
- fk: string,
344
- columns?: string[],
345
- pk?: string,
346
- ): this;
347
- join(tableOrAccessor: string | { _tableName: string }, fkOrCols?: string | string[], colsOrPk?: string[] | string, pk?: string): this {
348
- let table: string;
349
- let fromCol: string;
350
- let toCol: string;
351
- let columns: string[];
352
-
353
- if (typeof tableOrAccessor === 'object' && '_tableName' in tableOrAccessor) {
354
- // Accessor-based: .join(db.forests, ['name', 'address'])
355
- table = tableOrAccessor._tableName;
356
- columns = Array.isArray(fkOrCols) ? fkOrCols : [];
357
-
358
- // Auto-resolve FK from relationships
359
- if (!this.joinResolver) throw new Error(`Cannot auto-resolve join: no relationship data available`);
360
- const resolved = this.joinResolver(this.tableName, table);
361
- if (!resolved) throw new Error(`No relationship found between '${this.tableName}' and '${table}'`);
362
- fromCol = resolved.fk;
363
- toCol = resolved.pk;
364
- } else {
365
- // String-based: .join('forests', 'forestId', ['name'], 'id')
366
- table = tableOrAccessor;
367
- fromCol = fkOrCols as string;
368
- columns = Array.isArray(colsOrPk) ? colsOrPk : [];
369
- toCol = (typeof colsOrPk === 'string' ? colsOrPk : pk) ?? 'id';
370
- }
371
-
372
- this.iqo.joins.push({ table, fromCol, toCol, columns });
373
- this.iqo.raw = true;
374
- return this;
375
- }
376
-
377
- /** Skip Zod parsing and return raw SQLite row objects. */
378
- raw(): this {
379
- this.iqo.raw = true;
380
- return this;
381
- }
382
-
383
- /**
384
- * Eagerly load a related entity and attach as an array property.
385
- *
386
- * ```ts
387
- * const authors = db.authors.select().with('books').all();
388
- * // authors[0].books → [{ title: 'War and Peace', ... }, ...]
389
- * ```
390
- *
391
- * Runs a single batched query (WHERE fk IN (...)) per relation,
392
- * avoiding the N+1 problem of lazy navigation.
393
- */
394
- with(...relations: string[]): this {
395
- this.iqo.includes.push(...relations);
396
- return this;
397
- }
398
-
399
- /** Internal: apply eager loads to a set of results */
400
- private _applyEagerLoads(results: T[]): T[] {
401
- if (this.iqo.includes.length === 0 || !this.eagerLoader || results.length === 0) {
402
- return results;
403
- }
404
-
405
- const parentIds = results.map((r: any) => r.id).filter((id: any) => typeof id === 'number');
406
- if (parentIds.length === 0) return results;
407
-
408
- for (const relation of this.iqo.includes) {
409
- const loaded = this.eagerLoader(this.tableName, relation, parentIds);
410
- if (!loaded) continue;
411
-
412
- for (const row of results as any[]) {
413
- row[loaded.key] = loaded.groups.get(row.id) ?? [];
414
- }
415
- }
416
-
417
- return results;
418
- }
419
-
420
- // ---------- Terminal / Execution Methods ----------
421
-
422
- /** Execute the query and return all matching rows. */
423
- all(): T[] {
424
- const { sql, params } = compileIQO(this.tableName, this.iqo);
425
- const results = this.executor(sql, params, this.iqo.raw);
426
- return this._applyEagerLoads(results);
427
- }
428
-
429
- /** Execute the query and return the first matching row, or null. */
430
- get(): T | null {
431
- this.iqo.limit = 1;
432
- const { sql, params } = compileIQO(this.tableName, this.iqo);
433
- const result = this.singleExecutor(sql, params, this.iqo.raw);
434
- if (!result) return null;
435
- const [loaded] = this._applyEagerLoads([result]);
436
- return loaded ?? null;
437
- }
438
-
439
- /** Execute the query and return the count of matching rows. */
440
- count(): number {
441
- const params: any[] = [];
442
- let sql = `SELECT COUNT(*) as count FROM ${this.tableName}`;
443
-
444
- // WHERE — AST takes precedence
445
- if (this.iqo.whereAST) {
446
- const compiled = compileAST(this.iqo.whereAST);
447
- sql += ` WHERE ${compiled.sql}`;
448
- params.push(...compiled.params);
449
- } else if (this.iqo.wheres.length > 0) {
450
- const whereParts: string[] = [];
451
- for (const w of this.iqo.wheres) {
452
- if (w.operator === 'IN') {
453
- const arr = w.value as any[];
454
- if (arr.length === 0) {
455
- whereParts.push('1 = 0');
456
- } else {
457
- const placeholders = arr.map(() => '?').join(', ');
458
- whereParts.push(`${w.field} IN (${placeholders})`);
459
- params.push(...arr.map(transformValueForStorage));
460
- }
461
- } else {
462
- whereParts.push(`${w.field} ${w.operator} ?`);
463
- params.push(transformValueForStorage(w.value));
464
- }
465
- }
466
- sql += ` WHERE ${whereParts.join(' AND ')}`;
467
- }
468
-
469
- const results = this.executor(sql, params, true);
470
- return (results[0] as any)?.count ?? 0;
471
- }
472
-
473
- // ---------- Subscribe (Smart Polling) ----------
474
-
475
- /**
476
- * Subscribe to query result changes using smart interval-based polling.
477
- *
478
- * Uses a lightweight fingerprint (`COUNT(*), MAX(id)`) combined with an
479
- * in-memory revision counter to detect ALL changes (inserts, updates, deletes)
480
- * with zero disk overhead.
481
- *
482
- * Uses a self-scheduling async loop: each callback (sync or async) completes
483
- * before the next poll starts. No overlapping polls.
484
- *
485
- * ```ts
486
- * const unsub = db.messages.select()
487
- * .where({ groupId: 1 })
488
- * .orderBy('id', 'desc')
489
- * .limit(20)
490
- * .subscribe((rows) => {
491
- * console.log('Messages updated:', rows);
492
- * }, { interval: 1000 });
493
- *
494
- * // Later: stop listening
495
- * unsub();
496
- * ```
497
- *
498
- * @param callback Called with the full result set whenever the data changes. Async callbacks are awaited.
499
- * @param options `interval` in ms (default 500). Set `immediate` to false to skip the first call.
500
- * @returns An unsubscribe function.
501
- */
502
- subscribe(
503
- callback: (rows: T[]) => void | Promise<void>,
504
- options: { interval?: number; immediate?: boolean } = {},
505
- ): () => void {
506
- const { interval = this.defaultPollInterval, immediate = true } = options;
507
-
508
- let lastRevision: string | null = null;
509
- let stopped = false;
510
-
511
- const poll = async () => {
512
- if (stopped) return;
513
- try {
514
- // Single check: revision combines in-memory counter (same-process)
515
- // + trigger-based seq from _satidb_changes (cross-process).
516
- // Both are table-specific — no false positives from other tables.
517
- const rev = this.revisionGetter?.() ?? '0';
518
-
519
- if (rev !== lastRevision) {
520
- lastRevision = rev;
521
- const rows = this.all();
522
- await callback(rows);
523
- }
524
- } catch {
525
- // Silently skip on error (table might be in transition)
526
- }
527
- if (!stopped) setTimeout(poll, interval);
528
- };
529
-
530
- if (immediate) {
531
- poll();
532
- } else {
533
- setTimeout(poll, interval);
534
- }
535
-
536
- return () => { stopped = true; };
537
- }
538
-
539
- /**
540
- * Stream new rows one at a time via a watermark (last seen id).
541
- *
542
- * Unlike `.subscribe()` (which gives you an array snapshot), `.each()`
543
- * calls your callback once per new row, in insertion order. The SQL
544
- * `WHERE id > watermark` is rebuilt each poll with the latest value,
545
- * so it's always O(new_rows) — not O(table_size).
546
- *
547
- * Composes with the query builder chain: any `.where()` conditions
548
- * are combined with the watermark clause.
549
- *
550
- * ```ts
551
- * // All new messages
552
- * const unsub = db.messages.select().each((msg) => {
553
- * console.log('New:', msg.text);
554
- * });
555
- *
556
- * // Only new messages by Alice
557
- * const unsub2 = db.messages.select()
558
- * .where({ author: 'Alice' })
559
- * .each((msg) => console.log(msg.text));
560
- * ```
561
- *
562
- * @param callback Called once per new row. Async callbacks are awaited.
563
- * @param options `interval` in ms (default: pollInterval).
564
- * @returns Unsubscribe function.
565
- */
566
- each(
567
- callback: (row: T) => void | Promise<void>,
568
- options: { interval?: number } = {},
569
- ): () => void {
570
- const { interval = this.defaultPollInterval } = options;
571
-
572
- // Compile the user's WHERE clause (if any) so we can combine with watermark
573
- const userWhere = this.buildWhereClause();
574
-
575
- // Initialize watermark to current max id, respecting user's WHERE clause
576
- const maxRows = this.executor(
577
- `SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ''}`,
578
- userWhere.params,
579
- true
580
- );
581
- let lastMaxId: number = (maxRows[0] as any)?._max ?? 0;
582
- let lastRevision = this.revisionGetter?.() ?? '0';
583
- let stopped = false;
584
-
585
- const poll = async () => {
586
- if (stopped) return;
587
-
588
- const rev = this.revisionGetter?.() ?? '0';
589
- if (rev !== lastRevision) {
590
- lastRevision = rev;
591
-
592
- // Combine user WHERE with watermark: WHERE (user_conditions) AND id > ?
593
- const params = [...userWhere.params, lastMaxId];
594
- const whereClause = userWhere.sql
595
- ? `WHERE ${userWhere.sql} AND id > ? ORDER BY id ASC`
596
- : `WHERE id > ? ORDER BY id ASC`;
597
- const sql = `SELECT * FROM ${this.tableName} ${whereClause}`;
598
-
599
- // raw=false → rows go through transform + get .update()/.delete() methods
600
- const newRows = this.executor(sql, params, false);
601
-
602
- for (const row of newRows) {
603
- if (stopped) return;
604
- await callback(row as T);
605
- lastMaxId = (row as any).id;
606
- }
607
- }
608
-
609
- if (!stopped) setTimeout(poll, interval);
610
- };
611
-
612
- setTimeout(poll, interval);
613
- return () => { stopped = true; };
614
- }
615
-
616
- /** Compile the IQO's WHERE conditions into a SQL fragment + params (without the WHERE keyword). */
617
- private buildWhereClause(): { sql: string; params: any[] } {
618
- const params: any[] = [];
619
-
620
- if (this.iqo.whereAST) {
621
- const compiled = compileAST(this.iqo.whereAST);
622
- return { sql: compiled.sql, params: compiled.params };
623
- }
624
-
625
- if (this.iqo.wheres.length > 0) {
626
- const whereParts: string[] = [];
627
- for (const w of this.iqo.wheres) {
628
- if (w.operator === 'IN') {
629
- const arr = w.value as any[];
630
- if (arr.length === 0) {
631
- whereParts.push('1 = 0');
632
- } else {
633
- const placeholders = arr.map(() => '?').join(', ');
634
- whereParts.push(`${w.field} IN (${placeholders})`);
635
- params.push(...arr.map(transformValueForStorage));
636
- }
637
- } else {
638
- whereParts.push(`${w.field} ${w.operator} ?`);
639
- params.push(transformValueForStorage(w.value));
640
- }
641
- }
642
- return { sql: whereParts.join(' AND '), params };
643
- }
644
-
645
- return { sql: '', params: [] };
646
- }
647
-
648
-
649
- // ---------- Thenable (async/await support) ----------
650
-
651
- /**
652
- * Implementing `.then()` makes the builder a "Thenable".
653
- * This means you can `await` a query builder directly:
654
- * ```ts
655
- * const users = await db.users.select().where({ level: 10 });
656
- * ```
657
- */
658
- then<TResult1 = T[], TResult2 = never>(
659
- onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
660
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
661
- ): Promise<TResult1 | TResult2> {
662
- try {
663
- const result = this.all();
664
- return Promise.resolve(result).then(onfulfilled, onrejected);
665
- } catch (err) {
666
- return Promise.reject(err).then(onfulfilled, onrejected);
667
- }
668
- }
669
- }