turbine-orm 0.3.0 → 0.3.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/README.md +5 -5
- package/dist/cli/config.d.ts +1 -1
- package/dist/cli/config.js +2 -2
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +9 -9
- package/dist/cli/migrate.d.ts +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +21 -17
- package/dist/query.d.ts +13 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +145 -78
- package/package.json +3 -1
- package/dist/cli/config.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/migrate.js.map +0 -1
- package/dist/cli/ui.js.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/generate.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/introspect.js.map +0 -1
- package/dist/pipeline.js.map +0 -1
- package/dist/query.js.map +0 -1
- package/dist/schema-builder.js.map +0 -1
- package/dist/schema-sql.js.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/serverless.js.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/query.js
CHANGED
|
@@ -11,6 +11,28 @@
|
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
13
|
import { snakeToCamel, camelToSnake } from './schema.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Identifier quoting — prevents SQL injection via table/column names
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Quote a SQL identifier (table name, column name) using Postgres double-quote
|
|
19
|
+
* rules: wrap in double quotes, escape internal double quotes by doubling them.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* quoteIdent('users') → '"users"'
|
|
23
|
+
* quoteIdent('my"table') → '"my""table"'
|
|
24
|
+
* quoteIdent('user name') → '"user name"'
|
|
25
|
+
*/
|
|
26
|
+
export function quoteIdent(name) {
|
|
27
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Escape LIKE pattern metacharacters: %, _, and \.
|
|
31
|
+
* Must be used with `ESCAPE '\'` in the LIKE clause.
|
|
32
|
+
*/
|
|
33
|
+
function escapeLike(value) {
|
|
34
|
+
return value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
35
|
+
}
|
|
14
36
|
/** Known operator keys — used to detect operator objects vs plain values */
|
|
15
37
|
const OPERATOR_KEYS = new Set([
|
|
16
38
|
'gt', 'gte', 'lt', 'lte', 'not', 'in', 'notIn',
|
|
@@ -122,12 +144,13 @@ export class QueryInterface {
|
|
|
122
144
|
let sql = this.sqlCache.get(ck);
|
|
123
145
|
const params = whereKeys.map((k) => whereObj[k]);
|
|
124
146
|
if (!sql) {
|
|
125
|
-
const
|
|
147
|
+
const qt = quoteIdent(this.table);
|
|
148
|
+
const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} = $${i + 1}`);
|
|
126
149
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
127
150
|
const selectExpr = columnsList
|
|
128
|
-
? columnsList.map((c) => `${
|
|
129
|
-
: `${
|
|
130
|
-
sql = `SELECT ${selectExpr} FROM ${
|
|
151
|
+
? columnsList.map((c) => `${qt}.${quoteIdent(c)}`).join(', ')
|
|
152
|
+
: `${qt}.*`;
|
|
153
|
+
sql = `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
131
154
|
this.sqlCache.set(ck, sql);
|
|
132
155
|
}
|
|
133
156
|
return {
|
|
@@ -143,10 +166,11 @@ export class QueryInterface {
|
|
|
143
166
|
// General path: supports operators, null, OR, nested with
|
|
144
167
|
const { sql: whereSql, params } = this.buildWhere(args.where);
|
|
145
168
|
if (!args.with) {
|
|
169
|
+
const qt = quoteIdent(this.table);
|
|
146
170
|
const selectExpr = columnsList
|
|
147
|
-
? columnsList.map((c) => `${
|
|
148
|
-
: `${
|
|
149
|
-
const sql = `SELECT ${selectExpr} FROM ${
|
|
171
|
+
? columnsList.map((c) => `${qt}.${quoteIdent(c)}`).join(', ')
|
|
172
|
+
: `${qt}.*`;
|
|
173
|
+
const sql = `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
150
174
|
return {
|
|
151
175
|
sql,
|
|
152
176
|
params,
|
|
@@ -159,7 +183,7 @@ export class QueryInterface {
|
|
|
159
183
|
}
|
|
160
184
|
// Nested queries: build fresh each time (with clause affects params)
|
|
161
185
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, params, columnsList);
|
|
162
|
-
const sql = `SELECT ${selectClause} FROM ${this.table}${whereSql} LIMIT 1`;
|
|
186
|
+
const sql = `SELECT ${selectClause} FROM ${quoteIdent(this.table)}${whereSql} LIMIT 1`;
|
|
163
187
|
return {
|
|
164
188
|
sql,
|
|
165
189
|
params,
|
|
@@ -185,10 +209,11 @@ export class QueryInterface {
|
|
|
185
209
|
? this.buildWhere(args.where)
|
|
186
210
|
: { sql: '', params: [] };
|
|
187
211
|
const columnsList = this.resolveColumns(args?.select, args?.omit);
|
|
212
|
+
const qt = quoteIdent(this.table);
|
|
188
213
|
// Distinct support
|
|
189
214
|
let distinctPrefix = '';
|
|
190
215
|
if (args?.distinct && args.distinct.length > 0) {
|
|
191
|
-
const distinctCols = args.distinct.map((k) => this.
|
|
216
|
+
const distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
|
|
192
217
|
distinctPrefix = `DISTINCT ON (${distinctCols.join(', ')}) `;
|
|
193
218
|
}
|
|
194
219
|
let selectClause;
|
|
@@ -196,23 +221,23 @@ export class QueryInterface {
|
|
|
196
221
|
selectClause = this.buildSelectWithRelations(this.table, args.with, params, columnsList);
|
|
197
222
|
}
|
|
198
223
|
else if (columnsList) {
|
|
199
|
-
selectClause = columnsList.map((c) => `${
|
|
224
|
+
selectClause = columnsList.map((c) => `${qt}.${quoteIdent(c)}`).join(', ');
|
|
200
225
|
}
|
|
201
226
|
else {
|
|
202
|
-
selectClause = `${
|
|
227
|
+
selectClause = `${qt}.*`;
|
|
203
228
|
}
|
|
204
|
-
let sql = `SELECT ${distinctPrefix}${selectClause} FROM ${
|
|
229
|
+
let sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${whereSql}`;
|
|
205
230
|
// Cursor-based pagination: add WHERE condition for cursor
|
|
206
231
|
if (args?.cursor) {
|
|
207
232
|
const cursorEntries = Object.entries(args.cursor).filter(([, v]) => v !== undefined);
|
|
208
233
|
if (cursorEntries.length > 0) {
|
|
209
234
|
// Determine direction from orderBy (default 'asc')
|
|
210
235
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
211
|
-
const col = this.
|
|
236
|
+
const col = this.toSqlColumn(k);
|
|
212
237
|
const dir = args.orderBy?.[k] ?? 'asc';
|
|
213
238
|
const op = dir === 'desc' ? '<' : '>';
|
|
214
239
|
params.push(v);
|
|
215
|
-
return `${
|
|
240
|
+
return `${qt}.${col} ${op} $${params.length}`;
|
|
216
241
|
});
|
|
217
242
|
// Append to existing WHERE or create new one
|
|
218
243
|
if (whereSql) {
|
|
@@ -229,10 +254,12 @@ export class QueryInterface {
|
|
|
229
254
|
// take overrides limit when cursor pagination is used
|
|
230
255
|
const effectiveLimit = args?.take ?? args?.limit;
|
|
231
256
|
if (effectiveLimit !== undefined) {
|
|
232
|
-
|
|
257
|
+
params.push(Number(effectiveLimit));
|
|
258
|
+
sql += ` LIMIT $${params.length}`;
|
|
233
259
|
}
|
|
234
260
|
if (args?.offset !== undefined) {
|
|
235
|
-
|
|
261
|
+
params.push(Number(args.offset));
|
|
262
|
+
sql += ` OFFSET $${params.length}`;
|
|
236
263
|
}
|
|
237
264
|
return {
|
|
238
265
|
sql,
|
|
@@ -329,14 +356,19 @@ export class QueryInterface {
|
|
|
329
356
|
}
|
|
330
357
|
buildCreate(args) {
|
|
331
358
|
const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
|
|
332
|
-
const columns = entries.map(([k]) => this.
|
|
359
|
+
const columns = entries.map(([k]) => this.toSqlColumn(k));
|
|
333
360
|
const params = entries.map(([, v]) => v);
|
|
334
361
|
const placeholders = entries.map((_, i) => `$${i + 1}`);
|
|
335
|
-
const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
|
|
362
|
+
const sql = `INSERT INTO ${quoteIdent(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
|
|
336
363
|
return {
|
|
337
364
|
sql,
|
|
338
365
|
params,
|
|
339
|
-
transform: (result) =>
|
|
366
|
+
transform: (result) => {
|
|
367
|
+
const row = result.rows[0];
|
|
368
|
+
if (!row)
|
|
369
|
+
throw new Error('[turbine] Expected a row but query returned none');
|
|
370
|
+
return this.parseRow(row, this.table);
|
|
371
|
+
},
|
|
340
372
|
tag: `${this.table}.create`,
|
|
341
373
|
};
|
|
342
374
|
}
|
|
@@ -351,9 +383,10 @@ export class QueryInterface {
|
|
|
351
383
|
});
|
|
352
384
|
}
|
|
353
385
|
buildCreateMany(args) {
|
|
386
|
+
const qt = quoteIdent(this.table);
|
|
354
387
|
if (args.data.length === 0) {
|
|
355
388
|
return {
|
|
356
|
-
sql: `SELECT * FROM ${
|
|
389
|
+
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
357
390
|
params: [],
|
|
358
391
|
transform: () => [],
|
|
359
392
|
tag: `${this.table}.createMany`,
|
|
@@ -372,7 +405,8 @@ export class QueryInterface {
|
|
|
372
405
|
// Use actual Postgres types for array casts
|
|
373
406
|
const typeCasts = columns.map((col) => this.getColumnArrayType(col));
|
|
374
407
|
const unnestArgs = columnArrays.map((_, i) => `$${i + 1}::${typeCasts[i]}`);
|
|
375
|
-
|
|
408
|
+
const quotedColumns = columns.map((c) => quoteIdent(c));
|
|
409
|
+
let sql = `INSERT INTO ${qt} (${quotedColumns.join(', ')}) SELECT * FROM UNNEST(${unnestArgs.join(', ')})`;
|
|
376
410
|
// skipDuplicates: add ON CONFLICT DO NOTHING
|
|
377
411
|
if (args.skipDuplicates) {
|
|
378
412
|
sql += ` ON CONFLICT DO NOTHING`;
|
|
@@ -401,16 +435,21 @@ export class QueryInterface {
|
|
|
401
435
|
const params = [];
|
|
402
436
|
const setClauses = setEntries.map(([k, v]) => {
|
|
403
437
|
params.push(v);
|
|
404
|
-
return `${this.
|
|
438
|
+
return `${this.toSqlColumn(k)} = $${params.length}`;
|
|
405
439
|
});
|
|
406
440
|
// Build WHERE using the shared params array (continues numbering after SET params)
|
|
407
441
|
const whereClause = this.buildWhereClause(args.where, params);
|
|
408
442
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
409
|
-
const sql = `UPDATE ${this.table} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
|
|
443
|
+
const sql = `UPDATE ${quoteIdent(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
|
|
410
444
|
return {
|
|
411
445
|
sql,
|
|
412
446
|
params,
|
|
413
|
-
transform: (result) =>
|
|
447
|
+
transform: (result) => {
|
|
448
|
+
const row = result.rows[0];
|
|
449
|
+
if (!row)
|
|
450
|
+
throw new Error('[turbine] Expected a row but query returned none');
|
|
451
|
+
return this.parseRow(row, this.table);
|
|
452
|
+
},
|
|
414
453
|
tag: `${this.table}.update`,
|
|
415
454
|
};
|
|
416
455
|
}
|
|
@@ -426,11 +465,16 @@ export class QueryInterface {
|
|
|
426
465
|
}
|
|
427
466
|
buildDelete(args) {
|
|
428
467
|
const { sql: whereSql, params } = this.buildWhere(args.where);
|
|
429
|
-
const sql = `DELETE FROM ${this.table}${whereSql} RETURNING *`;
|
|
468
|
+
const sql = `DELETE FROM ${quoteIdent(this.table)}${whereSql} RETURNING *`;
|
|
430
469
|
return {
|
|
431
470
|
sql,
|
|
432
471
|
params,
|
|
433
|
-
transform: (result) =>
|
|
472
|
+
transform: (result) => {
|
|
473
|
+
const row = result.rows[0];
|
|
474
|
+
if (!row)
|
|
475
|
+
throw new Error('[turbine] Expected a row but query returned none');
|
|
476
|
+
return this.parseRow(row, this.table);
|
|
477
|
+
},
|
|
434
478
|
tag: `${this.table}.delete`,
|
|
435
479
|
};
|
|
436
480
|
}
|
|
@@ -447,29 +491,34 @@ export class QueryInterface {
|
|
|
447
491
|
buildUpsert(args) {
|
|
448
492
|
// Build the INSERT part from create data
|
|
449
493
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
450
|
-
const columns = createEntries.map(([k]) => this.
|
|
494
|
+
const columns = createEntries.map(([k]) => this.toSqlColumn(k));
|
|
451
495
|
const createParams = createEntries.map(([, v]) => v);
|
|
452
496
|
const placeholders = createEntries.map((_, i) => `$${i + 1}`);
|
|
453
497
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
454
498
|
const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
|
|
455
|
-
const conflictColumns = conflictKeys.map((k) => this.
|
|
499
|
+
const conflictColumns = conflictKeys.map((k) => this.toSqlColumn(k));
|
|
456
500
|
// Build the UPDATE SET part
|
|
457
501
|
const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
|
|
458
502
|
let paramIdx = createParams.length + 1;
|
|
459
503
|
const setClauses = updateEntries.map(([k]) => {
|
|
460
|
-
const clause = `${this.
|
|
504
|
+
const clause = `${this.toSqlColumn(k)} = $${paramIdx}`;
|
|
461
505
|
paramIdx++;
|
|
462
506
|
return clause;
|
|
463
507
|
});
|
|
464
508
|
const updateParams = updateEntries.map(([, v]) => v);
|
|
465
509
|
const params = [...createParams, ...updateParams];
|
|
466
|
-
const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
510
|
+
const sql = `INSERT INTO ${quoteIdent(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
467
511
|
` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${setClauses.join(', ')}` +
|
|
468
512
|
` RETURNING *`;
|
|
469
513
|
return {
|
|
470
514
|
sql,
|
|
471
515
|
params,
|
|
472
|
-
transform: (result) =>
|
|
516
|
+
transform: (result) => {
|
|
517
|
+
const row = result.rows[0];
|
|
518
|
+
if (!row)
|
|
519
|
+
throw new Error('[turbine] Expected a row but query returned none');
|
|
520
|
+
return this.parseRow(row, this.table);
|
|
521
|
+
},
|
|
473
522
|
tag: `${this.table}.upsert`,
|
|
474
523
|
};
|
|
475
524
|
}
|
|
@@ -489,12 +538,12 @@ export class QueryInterface {
|
|
|
489
538
|
const params = [];
|
|
490
539
|
const setClauses = setEntries.map(([k, v]) => {
|
|
491
540
|
params.push(v);
|
|
492
|
-
return `${this.
|
|
541
|
+
return `${this.toSqlColumn(k)} = $${params.length}`;
|
|
493
542
|
});
|
|
494
543
|
// Build WHERE using the shared params array (continues numbering after SET params)
|
|
495
544
|
const whereClause = this.buildWhereClause(args.where, params);
|
|
496
545
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
497
|
-
const sql = `UPDATE ${this.table} SET ${setClauses.join(', ')}${whereSql}`;
|
|
546
|
+
const sql = `UPDATE ${quoteIdent(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
498
547
|
return {
|
|
499
548
|
sql,
|
|
500
549
|
params,
|
|
@@ -514,7 +563,7 @@ export class QueryInterface {
|
|
|
514
563
|
}
|
|
515
564
|
buildDeleteMany(args) {
|
|
516
565
|
const { sql: whereSql, params } = this.buildWhere(args.where);
|
|
517
|
-
const sql = `DELETE FROM ${this.table}${whereSql}`;
|
|
566
|
+
const sql = `DELETE FROM ${quoteIdent(this.table)}${whereSql}`;
|
|
518
567
|
return {
|
|
519
568
|
sql,
|
|
520
569
|
params,
|
|
@@ -536,7 +585,7 @@ export class QueryInterface {
|
|
|
536
585
|
const { sql: whereSql, params } = args?.where
|
|
537
586
|
? this.buildWhere(args.where)
|
|
538
587
|
: { sql: '', params: [] };
|
|
539
|
-
const sql = `SELECT COUNT(*)::int AS count FROM ${this.table}${whereSql}`;
|
|
588
|
+
const sql = `SELECT COUNT(*)::int AS count FROM ${quoteIdent(this.table)}${whereSql}`;
|
|
540
589
|
return {
|
|
541
590
|
sql,
|
|
542
591
|
params,
|
|
@@ -555,7 +604,8 @@ export class QueryInterface {
|
|
|
555
604
|
});
|
|
556
605
|
}
|
|
557
606
|
buildGroupBy(args) {
|
|
558
|
-
const
|
|
607
|
+
const groupColsRaw = args.by.map((k) => this.toColumn(k));
|
|
608
|
+
const groupCols = groupColsRaw.map((c) => quoteIdent(c));
|
|
559
609
|
const { sql: whereSql, params } = args.where
|
|
560
610
|
? this.buildWhere(args.where)
|
|
561
611
|
: { sql: '', params: [] };
|
|
@@ -571,7 +621,7 @@ export class QueryInterface {
|
|
|
571
621
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
572
622
|
if (enabled) {
|
|
573
623
|
const col = this.toColumn(field);
|
|
574
|
-
selectExprs.push(`SUM(${col}) AS _sum_${col}`);
|
|
624
|
+
selectExprs.push(`SUM(${quoteIdent(col)}) AS _sum_${col}`);
|
|
575
625
|
}
|
|
576
626
|
}
|
|
577
627
|
}
|
|
@@ -580,7 +630,7 @@ export class QueryInterface {
|
|
|
580
630
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
581
631
|
if (enabled) {
|
|
582
632
|
const col = this.toColumn(field);
|
|
583
|
-
selectExprs.push(`AVG(${col})::float AS _avg_${col}`);
|
|
633
|
+
selectExprs.push(`AVG(${quoteIdent(col)})::float AS _avg_${col}`);
|
|
584
634
|
}
|
|
585
635
|
}
|
|
586
636
|
}
|
|
@@ -589,7 +639,7 @@ export class QueryInterface {
|
|
|
589
639
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
590
640
|
if (enabled) {
|
|
591
641
|
const col = this.toColumn(field);
|
|
592
|
-
selectExprs.push(`MIN(${col}) AS _min_${col}`);
|
|
642
|
+
selectExprs.push(`MIN(${quoteIdent(col)}) AS _min_${col}`);
|
|
593
643
|
}
|
|
594
644
|
}
|
|
595
645
|
}
|
|
@@ -598,11 +648,11 @@ export class QueryInterface {
|
|
|
598
648
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
599
649
|
if (enabled) {
|
|
600
650
|
const col = this.toColumn(field);
|
|
601
|
-
selectExprs.push(`MAX(${col}) AS _max_${col}`);
|
|
651
|
+
selectExprs.push(`MAX(${quoteIdent(col)}) AS _max_${col}`);
|
|
602
652
|
}
|
|
603
653
|
}
|
|
604
654
|
}
|
|
605
|
-
let sql = `SELECT ${selectExprs.join(', ')} FROM ${this.table}${whereSql} GROUP BY ${groupCols.join(', ')}`;
|
|
655
|
+
let sql = `SELECT ${selectExprs.join(', ')} FROM ${quoteIdent(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
|
|
606
656
|
// ORDER BY
|
|
607
657
|
if (args.orderBy) {
|
|
608
658
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
|
|
@@ -693,7 +743,7 @@ export class QueryInterface {
|
|
|
693
743
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
694
744
|
if (enabled) {
|
|
695
745
|
const col = this.toColumn(field);
|
|
696
|
-
selectExprs.push(`COUNT(${col})::int AS _count_${col}`);
|
|
746
|
+
selectExprs.push(`COUNT(${quoteIdent(col)})::int AS _count_${col}`);
|
|
697
747
|
}
|
|
698
748
|
}
|
|
699
749
|
}
|
|
@@ -702,7 +752,7 @@ export class QueryInterface {
|
|
|
702
752
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
703
753
|
if (enabled) {
|
|
704
754
|
const col = this.toColumn(field);
|
|
705
|
-
selectExprs.push(`SUM(${col}) AS _sum_${col}`);
|
|
755
|
+
selectExprs.push(`SUM(${quoteIdent(col)}) AS _sum_${col}`);
|
|
706
756
|
}
|
|
707
757
|
}
|
|
708
758
|
}
|
|
@@ -711,7 +761,7 @@ export class QueryInterface {
|
|
|
711
761
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
712
762
|
if (enabled) {
|
|
713
763
|
const col = this.toColumn(field);
|
|
714
|
-
selectExprs.push(`AVG(${col})::float AS _avg_${col}`);
|
|
764
|
+
selectExprs.push(`AVG(${quoteIdent(col)})::float AS _avg_${col}`);
|
|
715
765
|
}
|
|
716
766
|
}
|
|
717
767
|
}
|
|
@@ -720,7 +770,7 @@ export class QueryInterface {
|
|
|
720
770
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
721
771
|
if (enabled) {
|
|
722
772
|
const col = this.toColumn(field);
|
|
723
|
-
selectExprs.push(`MIN(${col}) AS _min_${col}`);
|
|
773
|
+
selectExprs.push(`MIN(${quoteIdent(col)}) AS _min_${col}`);
|
|
724
774
|
}
|
|
725
775
|
}
|
|
726
776
|
}
|
|
@@ -729,14 +779,14 @@ export class QueryInterface {
|
|
|
729
779
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
730
780
|
if (enabled) {
|
|
731
781
|
const col = this.toColumn(field);
|
|
732
|
-
selectExprs.push(`MAX(${col}) AS _max_${col}`);
|
|
782
|
+
selectExprs.push(`MAX(${quoteIdent(col)}) AS _max_${col}`);
|
|
733
783
|
}
|
|
734
784
|
}
|
|
735
785
|
}
|
|
736
786
|
if (selectExprs.length === 0) {
|
|
737
787
|
selectExprs.push('COUNT(*)::int AS _count');
|
|
738
788
|
}
|
|
739
|
-
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.table}${whereSql}`;
|
|
789
|
+
const sql = `SELECT ${selectExprs.join(', ')} FROM ${quoteIdent(this.table)}${whereSql}`;
|
|
740
790
|
return {
|
|
741
791
|
sql,
|
|
742
792
|
params,
|
|
@@ -830,13 +880,17 @@ export class QueryInterface {
|
|
|
830
880
|
}
|
|
831
881
|
return null;
|
|
832
882
|
}
|
|
833
|
-
/** Convert camelCase field name to snake_case column name */
|
|
883
|
+
/** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
|
|
834
884
|
toColumn(field) {
|
|
835
885
|
const mapped = this.tableMeta.columnMap[field];
|
|
836
886
|
if (mapped)
|
|
837
887
|
return mapped;
|
|
838
888
|
return camelToSnake(field);
|
|
839
889
|
}
|
|
890
|
+
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
891
|
+
toSqlColumn(field) {
|
|
892
|
+
return quoteIdent(this.toColumn(field));
|
|
893
|
+
}
|
|
840
894
|
/** Build WHERE clause from a where object (supports operators, NULL, OR) */
|
|
841
895
|
buildWhere(where) {
|
|
842
896
|
const params = [];
|
|
@@ -907,7 +961,8 @@ export class QueryInterface {
|
|
|
907
961
|
continue;
|
|
908
962
|
}
|
|
909
963
|
}
|
|
910
|
-
const
|
|
964
|
+
const rawColumn = this.toColumn(key);
|
|
965
|
+
const column = quoteIdent(rawColumn);
|
|
911
966
|
// Handle null → IS NULL
|
|
912
967
|
if (value === null) {
|
|
913
968
|
andClauses.push(`${column} IS NULL`);
|
|
@@ -915,7 +970,7 @@ export class QueryInterface {
|
|
|
915
970
|
}
|
|
916
971
|
// Handle JSONB filter operators (for json/jsonb columns)
|
|
917
972
|
if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
|
|
918
|
-
const colType = this.getColumnPgType(
|
|
973
|
+
const colType = this.getColumnPgType(rawColumn);
|
|
919
974
|
if (colType === 'json' || colType === 'jsonb') {
|
|
920
975
|
const jsonClauses = this.buildJsonFilterClauses(column, value, params);
|
|
921
976
|
andClauses.push(...jsonClauses);
|
|
@@ -924,7 +979,7 @@ export class QueryInterface {
|
|
|
924
979
|
}
|
|
925
980
|
// Handle Array filter operators (for array columns)
|
|
926
981
|
if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
|
|
927
|
-
const colType = this.getColumnPgType(
|
|
982
|
+
const colType = this.getColumnPgType(rawColumn);
|
|
928
983
|
if (colType.startsWith('_')) {
|
|
929
984
|
const arrayClauses = this.buildArrayFilterClauses(column, value, params, colType);
|
|
930
985
|
andClauses.push(...arrayClauses);
|
|
@@ -954,37 +1009,39 @@ export class QueryInterface {
|
|
|
954
1009
|
const targetMeta = this.schema.tables[targetTable];
|
|
955
1010
|
if (!targetMeta)
|
|
956
1011
|
return null;
|
|
1012
|
+
const qt = quoteIdent(targetTable);
|
|
1013
|
+
const qSelf = quoteIdent(this.table);
|
|
957
1014
|
const clauses = [];
|
|
958
1015
|
// Correlation: link child table to parent table
|
|
959
1016
|
let correlation;
|
|
960
1017
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
961
1018
|
// parent.pk = child.fk
|
|
962
|
-
correlation = `${
|
|
1019
|
+
correlation = `${qt}.${quoteIdent(relDef.foreignKey)} = ${qSelf}.${quoteIdent(relDef.referenceKey)}`;
|
|
963
1020
|
}
|
|
964
1021
|
else {
|
|
965
1022
|
// belongsTo: parent.fk = child.pk
|
|
966
|
-
correlation = `${
|
|
1023
|
+
correlation = `${qt}.${quoteIdent(relDef.referenceKey)} = ${qSelf}.${quoteIdent(relDef.foreignKey)}`;
|
|
967
1024
|
}
|
|
968
1025
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
969
1026
|
if (filterObj.some !== undefined) {
|
|
970
1027
|
const subWhere = filterObj.some;
|
|
971
1028
|
const filterClause = this.buildSubWhereForRelation(targetTable, subWhere, params);
|
|
972
1029
|
const fullWhere = filterClause ? `${correlation} AND ${filterClause}` : correlation;
|
|
973
|
-
clauses.push(`EXISTS (SELECT 1 FROM ${
|
|
1030
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${fullWhere})`);
|
|
974
1031
|
}
|
|
975
1032
|
// "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
976
1033
|
if (filterObj.none !== undefined) {
|
|
977
1034
|
const subWhere = filterObj.none;
|
|
978
1035
|
const filterClause = this.buildSubWhereForRelation(targetTable, subWhere, params);
|
|
979
1036
|
const fullWhere = filterClause ? `${correlation} AND ${filterClause}` : correlation;
|
|
980
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1037
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${fullWhere})`);
|
|
981
1038
|
}
|
|
982
1039
|
// "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND NOT (filter))
|
|
983
1040
|
if (filterObj.every !== undefined) {
|
|
984
1041
|
const subWhere = filterObj.every;
|
|
985
1042
|
const filterClause = this.buildSubWhereForRelation(targetTable, subWhere, params);
|
|
986
1043
|
if (filterClause) {
|
|
987
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1044
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation} AND NOT (${filterClause}))`);
|
|
988
1045
|
}
|
|
989
1046
|
else {
|
|
990
1047
|
// "every" with empty filter = true (all match trivially)
|
|
@@ -1000,22 +1057,24 @@ export class QueryInterface {
|
|
|
1000
1057
|
const meta = this.schema.tables[targetTable];
|
|
1001
1058
|
if (!meta)
|
|
1002
1059
|
return null;
|
|
1060
|
+
const qt = quoteIdent(targetTable);
|
|
1003
1061
|
const conditions = [];
|
|
1004
1062
|
for (const [field, value] of Object.entries(subWhere)) {
|
|
1005
1063
|
if (value === undefined)
|
|
1006
1064
|
continue;
|
|
1007
1065
|
const col = meta.columnMap[field] ?? camelToSnake(field);
|
|
1066
|
+
const qCol = `${qt}.${quoteIdent(col)}`;
|
|
1008
1067
|
if (value === null) {
|
|
1009
|
-
conditions.push(`${
|
|
1068
|
+
conditions.push(`${qCol} IS NULL`);
|
|
1010
1069
|
continue;
|
|
1011
1070
|
}
|
|
1012
1071
|
if (isWhereOperator(value)) {
|
|
1013
|
-
const opClauses = this.buildOperatorClauses(
|
|
1072
|
+
const opClauses = this.buildOperatorClauses(qCol, value, params);
|
|
1014
1073
|
conditions.push(...opClauses);
|
|
1015
1074
|
continue;
|
|
1016
1075
|
}
|
|
1017
1076
|
params.push(value);
|
|
1018
|
-
conditions.push(`${
|
|
1077
|
+
conditions.push(`${qCol} = $${params.length}`);
|
|
1019
1078
|
}
|
|
1020
1079
|
return conditions.length > 0 ? conditions.join(' AND ') : null;
|
|
1021
1080
|
}
|
|
@@ -1059,23 +1118,26 @@ export class QueryInterface {
|
|
|
1059
1118
|
clauses.push(`${column} != ALL($${params.length})`);
|
|
1060
1119
|
}
|
|
1061
1120
|
if (op.contains !== undefined) {
|
|
1062
|
-
params.push(`%${op.contains}%`);
|
|
1063
|
-
clauses.push(`${column} LIKE $${params.length}`);
|
|
1121
|
+
params.push(`%${escapeLike(op.contains)}%`);
|
|
1122
|
+
clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
|
|
1064
1123
|
}
|
|
1065
1124
|
if (op.startsWith !== undefined) {
|
|
1066
|
-
params.push(`${op.startsWith}%`);
|
|
1067
|
-
clauses.push(`${column} LIKE $${params.length}`);
|
|
1125
|
+
params.push(`${escapeLike(op.startsWith)}%`);
|
|
1126
|
+
clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
|
|
1068
1127
|
}
|
|
1069
1128
|
if (op.endsWith !== undefined) {
|
|
1070
|
-
params.push(`%${op.endsWith}`);
|
|
1071
|
-
clauses.push(`${column} LIKE $${params.length}`);
|
|
1129
|
+
params.push(`%${escapeLike(op.endsWith)}`);
|
|
1130
|
+
clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
|
|
1072
1131
|
}
|
|
1073
1132
|
return clauses;
|
|
1074
1133
|
}
|
|
1075
1134
|
/** Build ORDER BY clause from an object */
|
|
1076
1135
|
buildOrderBy(orderBy) {
|
|
1077
1136
|
return Object.entries(orderBy)
|
|
1078
|
-
.map(([key, dir]) =>
|
|
1137
|
+
.map(([key, dir]) => {
|
|
1138
|
+
const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
1139
|
+
return `${this.toSqlColumn(key)} ${safeDir}`;
|
|
1140
|
+
})
|
|
1079
1141
|
.join(', ');
|
|
1080
1142
|
}
|
|
1081
1143
|
/** Parse a flat row: convert snake_case to camelCase + Date coercion */
|
|
@@ -1153,8 +1215,9 @@ export class QueryInterface {
|
|
|
1153
1215
|
if (!meta)
|
|
1154
1216
|
throw new Error(`[turbine] Unknown table "${table}"`);
|
|
1155
1217
|
const cols = columnsList ?? meta.allColumns;
|
|
1218
|
+
const qtbl = quoteIdent(table);
|
|
1156
1219
|
const baseCols = cols
|
|
1157
|
-
.map((col) => `${
|
|
1220
|
+
.map((col) => `${qtbl}.${quoteIdent(col)}`)
|
|
1158
1221
|
.join(', ');
|
|
1159
1222
|
const relationSelects = [];
|
|
1160
1223
|
const aliasCounter = { n: 0 };
|
|
@@ -1166,7 +1229,7 @@ export class QueryInterface {
|
|
|
1166
1229
|
}
|
|
1167
1230
|
// The main table is not aliased, so pass table name as parentRef
|
|
1168
1231
|
const subquery = this.buildRelationSubquery(relDef, relSpec, params, table, aliasCounter);
|
|
1169
|
-
relationSelects.push(`(${subquery}) AS ${relName}`);
|
|
1232
|
+
relationSelects.push(`(${subquery}) AS ${quoteIdent(relName)}`);
|
|
1170
1233
|
}
|
|
1171
1234
|
return [baseCols, ...relationSelects].join(', ');
|
|
1172
1235
|
}
|
|
@@ -1202,7 +1265,7 @@ export class QueryInterface {
|
|
|
1202
1265
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
1203
1266
|
}
|
|
1204
1267
|
// Build json_build_object pairs for resolved columns
|
|
1205
|
-
const jsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${alias}.${col}`);
|
|
1268
|
+
const jsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${alias}.${quoteIdent(col)}`);
|
|
1206
1269
|
// Nested relations?
|
|
1207
1270
|
if (spec !== true && spec.with) {
|
|
1208
1271
|
for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
|
|
@@ -1217,6 +1280,9 @@ export class QueryInterface {
|
|
|
1217
1280
|
}
|
|
1218
1281
|
}
|
|
1219
1282
|
const jsonObj = `json_build_object(${jsonPairs.join(', ')})`;
|
|
1283
|
+
// Quote parent ref — can be a table name or auto-generated alias
|
|
1284
|
+
const qParent = quoteIdent(parentRef);
|
|
1285
|
+
const qTarget = quoteIdent(targetTable);
|
|
1220
1286
|
// Build ORDER BY for json_agg
|
|
1221
1287
|
let orderClause = '';
|
|
1222
1288
|
if (spec !== true && spec.orderBy) {
|
|
@@ -1227,7 +1293,7 @@ export class QueryInterface {
|
|
|
1227
1293
|
throw new Error(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
1228
1294
|
}
|
|
1229
1295
|
const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
1230
|
-
return `${alias}.${col} ${safeDir}`;
|
|
1296
|
+
return `${alias}.${quoteIdent(col)} ${safeDir}`;
|
|
1231
1297
|
})
|
|
1232
1298
|
.join(', ');
|
|
1233
1299
|
orderClause = ` ORDER BY ${orders}`;
|
|
@@ -1237,10 +1303,10 @@ export class QueryInterface {
|
|
|
1237
1303
|
// For belongsTo: source has FK, so alias.pk = parentRef.fk (reversed)
|
|
1238
1304
|
let whereClause;
|
|
1239
1305
|
if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
|
|
1240
|
-
whereClause = `${alias}.${relDef.referenceKey} = ${
|
|
1306
|
+
whereClause = `${alias}.${quoteIdent(relDef.referenceKey)} = ${qParent}.${quoteIdent(relDef.foreignKey)}`;
|
|
1241
1307
|
}
|
|
1242
1308
|
else {
|
|
1243
|
-
whereClause = `${alias}.${relDef.foreignKey} = ${
|
|
1309
|
+
whereClause = `${alias}.${quoteIdent(relDef.foreignKey)} = ${qParent}.${quoteIdent(relDef.referenceKey)}`;
|
|
1244
1310
|
}
|
|
1245
1311
|
// Additional filters — properly parameterized
|
|
1246
1312
|
if (spec !== true && spec.where) {
|
|
@@ -1250,13 +1316,14 @@ export class QueryInterface {
|
|
|
1250
1316
|
throw new Error(`[turbine] Unknown column "${k}" in where for table "${targetTable}"`);
|
|
1251
1317
|
}
|
|
1252
1318
|
params.push(v);
|
|
1253
|
-
whereClause += ` AND ${alias}.${col} = $${params.length}`;
|
|
1319
|
+
whereClause += ` AND ${alias}.${quoteIdent(col)} = $${params.length}`;
|
|
1254
1320
|
}
|
|
1255
1321
|
}
|
|
1256
1322
|
// LIMIT
|
|
1257
1323
|
let limitClause = '';
|
|
1258
1324
|
if (spec !== true && spec.limit) {
|
|
1259
|
-
|
|
1325
|
+
params.push(Number(spec.limit));
|
|
1326
|
+
limitClause = ` LIMIT $${params.length}`;
|
|
1260
1327
|
}
|
|
1261
1328
|
if (relDef.type === 'hasMany') {
|
|
1262
1329
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -1265,9 +1332,9 @@ export class QueryInterface {
|
|
|
1265
1332
|
const innerAlias = `${alias}i`;
|
|
1266
1333
|
// Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
|
|
1267
1334
|
// Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
|
|
1268
|
-
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${c}`).join(', ')} FROM ${
|
|
1335
|
+
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${quoteIdent(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
1269
1336
|
// For the json_build_object, reference the inner alias — only include resolved columns
|
|
1270
|
-
const innerJsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${innerAlias}.${col}`);
|
|
1337
|
+
const innerJsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${innerAlias}.${quoteIdent(col)}`);
|
|
1271
1338
|
// Re-add nested relation subqueries referencing innerAlias
|
|
1272
1339
|
if (spec !== true && spec.with) {
|
|
1273
1340
|
for (const [nestedRelName] of Object.entries(spec.with)) {
|
|
@@ -1281,10 +1348,10 @@ export class QueryInterface {
|
|
|
1281
1348
|
const innerJsonObj = `json_build_object(${innerJsonPairs.join(', ')})`;
|
|
1282
1349
|
return `SELECT COALESCE(json_agg(${innerJsonObj}), '[]'::json) FROM (${innerSql}) ${innerAlias}`;
|
|
1283
1350
|
}
|
|
1284
|
-
return `SELECT COALESCE(json_agg(${jsonObj}${orderClause}), '[]'::json) FROM ${
|
|
1351
|
+
return `SELECT COALESCE(json_agg(${jsonObj}${orderClause}), '[]'::json) FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
1285
1352
|
}
|
|
1286
1353
|
// belongsTo / hasOne — return single object
|
|
1287
|
-
return `SELECT ${jsonObj} FROM ${
|
|
1354
|
+
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
1288
1355
|
}
|
|
1289
1356
|
/**
|
|
1290
1357
|
* Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "TypeScript ORM with json_agg nested queries — 2-3x faster than Prisma, 1.5x faster than Drizzle",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
27
|
"!dist/test",
|
|
28
|
+
"!dist/**/*.js.map",
|
|
29
|
+
"!dist/examples.*",
|
|
28
30
|
"LICENSE",
|
|
29
31
|
"README.md"
|
|
30
32
|
],
|
package/dist/cli/config.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"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"}
|