sqlstack 1.0.17 → 1.0.18
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/cjs/binders/sqlBinder.d.ts.map +1 -1
- package/dist/cjs/binders/sqlBinder.js +556 -0
- package/dist/cjs/binders/sqlBinder.js.map +1 -1
- package/dist/cjs/core/errors.d.ts +5 -0
- package/dist/cjs/core/errors.d.ts.map +1 -1
- package/dist/cjs/core/errors.js +10 -1
- package/dist/cjs/core/errors.js.map +1 -1
- package/dist/cjs/decorators/query.d.ts.map +1 -1
- package/dist/cjs/decorators/query.js +8 -1
- package/dist/cjs/decorators/query.js.map +1 -1
- package/dist/esm/binders/sqlBinder.js +556 -0
- package/dist/esm/binders/sqlBinder.js.map +1 -1
- package/dist/esm/core/errors.js +8 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/decorators/query.js +9 -2
- package/dist/esm/decorators/query.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +297 -33
package/readme.md
CHANGED
|
@@ -262,56 +262,320 @@ await repo.listByIds({ ids });
|
|
|
262
262
|
- `null` → SQL `NULL`
|
|
263
263
|
- `undefined` → throws an error; always provide a value or omit the parameter
|
|
264
264
|
|
|
265
|
-
###
|
|
265
|
+
### :update() Syntax for Partial Updates
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
The `:update()` syntax simplifies UPDATE statements by automatically generating SET clauses. It supports both parameterized columns and literal SQL expressions, making partial updates easy without needing `COALESCE` or `IFNULL`.
|
|
268
|
+
|
|
269
|
+
**Key Features:**
|
|
270
|
+
- Automatically generates `SET` keyword (don't write it yourself)
|
|
271
|
+
- Literal expressions: `col = datetime('now')` → always included
|
|
272
|
+
- Parameterized columns: `col` → only included if defined in args
|
|
273
|
+
- Mixed usage: combine literals and parameterized columns
|
|
274
|
+
|
|
275
|
+
**Basic Usage:**
|
|
276
|
+
|
|
277
|
+
```sql
|
|
278
|
+
-- updateInvoice.sql
|
|
279
|
+
UPDATE invoices
|
|
280
|
+
:update(invoice_date, due_date, subtotal_cents)
|
|
281
|
+
WHERE id = :id
|
|
282
|
+
```
|
|
268
283
|
|
|
269
284
|
```ts
|
|
270
|
-
|
|
285
|
+
await repo.updateInvoice({
|
|
286
|
+
invoice_date: '2024-01-01',
|
|
287
|
+
id: 1
|
|
288
|
+
});
|
|
289
|
+
// Generates: UPDATE invoices SET invoice_date = ? WHERE id = ?
|
|
290
|
+
// due_date and subtotal_cents are skipped (undefined)
|
|
291
|
+
```
|
|
271
292
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
email = COALESCE(:email, email),
|
|
280
|
-
status = COALESCE(:status, status)
|
|
293
|
+
**Literal SQL Expressions:**
|
|
294
|
+
|
|
295
|
+
Use `=` to include literal SQL expressions that are always included:
|
|
296
|
+
|
|
297
|
+
```sql
|
|
298
|
+
UPDATE invoices
|
|
299
|
+
:update(updated_at = datetime('now'), invoice_date, due_date)
|
|
281
300
|
WHERE id = :id
|
|
282
|
-
`
|
|
283
|
-
})
|
|
284
|
-
async updateUserPartial(_a: { id: string; name?: string; email?: string; status?: string }): Promise<any> {
|
|
285
|
-
throw new SqlStackError("replaced");
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
301
|
```
|
|
289
302
|
|
|
290
|
-
Call with partial arguments:
|
|
291
303
|
```ts
|
|
292
|
-
await repo.
|
|
293
|
-
|
|
304
|
+
await repo.updateInvoice({
|
|
305
|
+
invoice_date: '2024-01-01',
|
|
306
|
+
id: 1
|
|
307
|
+
});
|
|
308
|
+
// Generates: UPDATE invoices SET updated_at = datetime('now'), invoice_date = ? WHERE id = ?
|
|
294
309
|
```
|
|
295
310
|
|
|
296
|
-
**
|
|
311
|
+
**Mixed Literal and Parameterized:**
|
|
297
312
|
|
|
298
|
-
|
|
313
|
+
```sql
|
|
314
|
+
UPDATE invoices
|
|
315
|
+
:update(
|
|
316
|
+
updated_at = datetime('now'),
|
|
317
|
+
invoice_date,
|
|
318
|
+
due_date,
|
|
319
|
+
status = 'active'
|
|
320
|
+
)
|
|
321
|
+
WHERE id = :id
|
|
322
|
+
```
|
|
299
323
|
|
|
300
324
|
```ts
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
325
|
+
await repo.updateInvoice({
|
|
326
|
+
invoice_date: '2024-01-01',
|
|
327
|
+
due_date: '2024-01-15',
|
|
328
|
+
id: 1
|
|
329
|
+
});
|
|
330
|
+
// Generates: UPDATE invoices SET updated_at = datetime('now'), invoice_date = ?, due_date = ?, status = 'active' WHERE id = ?
|
|
308
331
|
```
|
|
309
332
|
|
|
310
|
-
|
|
333
|
+
**Notes:**
|
|
334
|
+
- `:update()` only works with named parameters (not positional)
|
|
335
|
+
- Literal expressions are always included
|
|
336
|
+
- Parameterized columns are only included if defined and not `undefined`
|
|
337
|
+
- If all parameterized columns are undefined (and no literals), throws an error
|
|
338
|
+
- Case-insensitive: `:update`, `:UPDATE`, `:Update` all work
|
|
311
339
|
|
|
312
|
-
|
|
340
|
+
### :insert() Syntax for INSERT Statements
|
|
313
341
|
|
|
314
|
-
|
|
342
|
+
The `:insert()` syntax simplifies INSERT statements by automatically generating both the column list and VALUES clause. It supports both parameterized columns and literal SQL expressions, making INSERT statements much more concise.
|
|
343
|
+
|
|
344
|
+
**Key Features:**
|
|
345
|
+
- Automatically generates column list `(col1, col2, ...)` and VALUES clause `VALUES (?, ?, ...)`
|
|
346
|
+
- Literal expressions: `col = datetime('now')` → column name and literal value included
|
|
347
|
+
- Parameterized columns: `col` → only included if defined in args
|
|
348
|
+
- Mixed usage: combine literals and parameterized columns
|
|
349
|
+
|
|
350
|
+
**Basic Usage:**
|
|
351
|
+
|
|
352
|
+
```sql
|
|
353
|
+
-- createInvoice.sql
|
|
354
|
+
INSERT INTO invoices :insert(case_id, invoice_number, invoice_date, due_date)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
await repo.createInvoice({
|
|
359
|
+
case_id: 1,
|
|
360
|
+
invoice_number: 'INV-001',
|
|
361
|
+
invoice_date: '2024-01-01',
|
|
362
|
+
due_date: '2024-01-15'
|
|
363
|
+
});
|
|
364
|
+
// Generates: INSERT INTO invoices (case_id, invoice_number, invoice_date, due_date) VALUES (?, ?, ?, ?)
|
|
365
|
+
// Params: [1, 'INV-001', '2024-01-01', '2024-01-15']
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**With Undefined Columns:**
|
|
369
|
+
|
|
370
|
+
```sql
|
|
371
|
+
INSERT INTO invoices :insert(case_id, invoice_number, invoice_date, due_date)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
await repo.createInvoice({
|
|
376
|
+
case_id: 1,
|
|
377
|
+
invoice_number: 'INV-001'
|
|
378
|
+
});
|
|
379
|
+
// Generates: INSERT INTO invoices (case_id, invoice_number) VALUES (?, ?)
|
|
380
|
+
// invoice_date and due_date are skipped (undefined)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Literal SQL Expressions:**
|
|
384
|
+
|
|
385
|
+
Use `=` to include literal SQL expressions that are always included:
|
|
386
|
+
|
|
387
|
+
```sql
|
|
388
|
+
INSERT INTO invoices :insert(created_at = datetime('now'), case_id, invoice_number)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
await repo.createInvoice({
|
|
393
|
+
case_id: 1,
|
|
394
|
+
invoice_number: 'INV-001'
|
|
395
|
+
});
|
|
396
|
+
// Generates: INSERT INTO invoices (created_at, case_id, invoice_number) VALUES (datetime('now'), ?, ?)
|
|
397
|
+
// Params: [1, 'INV-001']
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Mixed Literal and Parameterized:**
|
|
401
|
+
|
|
402
|
+
```sql
|
|
403
|
+
INSERT INTO invoices :insert(
|
|
404
|
+
created_at = datetime('now'),
|
|
405
|
+
case_id,
|
|
406
|
+
invoice_number,
|
|
407
|
+
status = 'draft'
|
|
408
|
+
)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
await repo.createInvoice({
|
|
413
|
+
case_id: 1,
|
|
414
|
+
invoice_number: 'INV-001'
|
|
415
|
+
});
|
|
416
|
+
// Generates: INSERT INTO invoices (created_at, case_id, invoice_number, status) VALUES (datetime('now'), ?, ?, 'draft')
|
|
417
|
+
// Params: [1, 'INV-001']
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Notes:**
|
|
421
|
+
- `:insert()` only works with named parameters (not positional)
|
|
422
|
+
- Literal expressions are always included (both column name and value)
|
|
423
|
+
- Parameterized columns are only included if defined and not `undefined`
|
|
424
|
+
- If all parameterized columns are undefined (and no literals), throws an error
|
|
425
|
+
- Case-insensitive: `:insert`, `:INSERT`, `:Insert` all work
|
|
426
|
+
|
|
427
|
+
### :filter() Syntax for WHERE/HAVING Clauses
|
|
428
|
+
|
|
429
|
+
The `:filter()` syntax simplifies WHERE and HAVING clauses by automatically generating conditions. It supports simple column names (equality), literal comparisons, and parameterized comparisons, making dynamic filtering much easier.
|
|
430
|
+
|
|
431
|
+
**Key Features:**
|
|
432
|
+
- Simple column names → equality: `name` → `name = ?`
|
|
433
|
+
- Literal comparisons → included as-is: `age > 30` → `age > 30`
|
|
434
|
+
- Parameterized comparisons → bind parameters: `age > :min_age` → `age > ?`
|
|
435
|
+
- Mixed usage: combine all three types
|
|
436
|
+
- Works in both WHERE and HAVING clauses
|
|
437
|
+
- Conditions are joined with `AND`
|
|
438
|
+
|
|
439
|
+
**Basic Usage:**
|
|
440
|
+
|
|
441
|
+
```sql
|
|
442
|
+
-- findUsers.sql
|
|
443
|
+
SELECT * FROM users WHERE :filter(name, age)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
await repo.findUsers({ name: 'John', age: 30 });
|
|
448
|
+
// Generates: SELECT * FROM users WHERE name = ? AND age = ?
|
|
449
|
+
// Params: ['John', 30]
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**With Some Undefined Columns:**
|
|
453
|
+
|
|
454
|
+
```sql
|
|
455
|
+
SELECT * FROM users WHERE :filter(name, age, status)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
await repo.findUsers({ name: 'John' });
|
|
460
|
+
// Generates: SELECT * FROM users WHERE name = ?
|
|
461
|
+
// age and status are skipped (undefined)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Literal Comparisons:**
|
|
465
|
+
|
|
466
|
+
```sql
|
|
467
|
+
SELECT * FROM users WHERE :filter(age > 30, status = 'active')
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
await repo.findUsers({});
|
|
472
|
+
// Generates: SELECT * FROM users WHERE age > 30 AND status = 'active'
|
|
473
|
+
// Params: []
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Parameterized Comparisons:**
|
|
477
|
+
|
|
478
|
+
```sql
|
|
479
|
+
SELECT * FROM users WHERE :filter(age > :min_age, name LIKE :search)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
await repo.findUsers({ min_age: 30, search: '%John%' });
|
|
484
|
+
// Generates: SELECT * FROM users WHERE age > ? AND name LIKE ?
|
|
485
|
+
// Params: [30, '%John%']
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Mixed Usage:**
|
|
489
|
+
|
|
490
|
+
```sql
|
|
491
|
+
SELECT * FROM users WHERE :filter(name, age > :min_age, status = 'active')
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
await repo.findUsers({ name: 'John', min_age: 30 });
|
|
496
|
+
// Generates: SELECT * FROM users WHERE name = ? AND age > ? AND status = 'active'
|
|
497
|
+
// Params: ['John', 30]
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**With Array Parameters (IN clause):**
|
|
501
|
+
|
|
502
|
+
```sql
|
|
503
|
+
SELECT * FROM users WHERE :filter(id IN (:ids))
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
await repo.findUsers({ ids: [1, 2, 3] });
|
|
508
|
+
// Generates: SELECT * FROM users WHERE id IN (?, ?, ?)
|
|
509
|
+
// Params: [1, 2, 3]
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**With :or() Expansion:**
|
|
513
|
+
|
|
514
|
+
The `:or()` syntax expands array parameters into multiple OR conditions:
|
|
515
|
+
|
|
516
|
+
```sql
|
|
517
|
+
SELECT * FROM users WHERE :filter(name = :or(:names), age > :min_age)
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
await repo.findUsers({ names: ['John', 'Jane', 'Bob'], min_age: 30 });
|
|
522
|
+
// Generates: SELECT * FROM users WHERE (name = ? OR name = ? OR name = ?) AND age > ?
|
|
523
|
+
// Params: ['John', 'Jane', 'Bob', 30]
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Complex :or() Usage:**
|
|
527
|
+
|
|
528
|
+
```sql
|
|
529
|
+
SELECT * FROM users WHERE :filter(
|
|
530
|
+
(name = :or(:names)),
|
|
531
|
+
age > :min_age,
|
|
532
|
+
status = 'active',
|
|
533
|
+
(department = :or(:depts) OR role = :or(:roles))
|
|
534
|
+
)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
await repo.findUsers({
|
|
539
|
+
names: ['John', 'Jane'],
|
|
540
|
+
min_age: 25,
|
|
541
|
+
depts: ['Engineering'],
|
|
542
|
+
roles: ['Manager']
|
|
543
|
+
});
|
|
544
|
+
// Generates: SELECT * FROM users WHERE (name = ? OR name = ?) AND age > ? AND status = 'active' AND (department = ? OR role = ?)
|
|
545
|
+
// Params: ['John', 'Jane', 25, 'Engineering', 'Manager']
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**HAVING Clause:**
|
|
549
|
+
|
|
550
|
+
```sql
|
|
551
|
+
SELECT department, COUNT(*) as count
|
|
552
|
+
FROM employees
|
|
553
|
+
GROUP BY department
|
|
554
|
+
HAVING :filter(count > :min_count, department = 'Sales')
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
await repo.findDepartments({ min_count: 10 });
|
|
559
|
+
// Generates: SELECT department, COUNT(*) as count FROM employees GROUP BY department HAVING count > ? AND department = 'Sales'
|
|
560
|
+
// Params: [10]
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Supported Operators:**
|
|
564
|
+
|
|
565
|
+
- Comparison: `=`, `!=`, `<>`, `>`, `<`, `>=`, `<=`
|
|
566
|
+
- Pattern matching: `LIKE`
|
|
567
|
+
- Membership: `IN`
|
|
568
|
+
- Range: `BETWEEN`
|
|
569
|
+
- Null checks: `IS NULL`, `IS NOT NULL`
|
|
570
|
+
|
|
571
|
+
**Notes:**
|
|
572
|
+
- `:filter()` only works with named parameters (not positional)
|
|
573
|
+
- Literal expressions are always included
|
|
574
|
+
- Parameterized conditions are only included if the parameter is defined and not `undefined`
|
|
575
|
+
- If all parameterized conditions are undefined (and no literals), throws an error
|
|
576
|
+
- Case-insensitive: `:filter`, `:FILTER`, `:Filter` all work
|
|
577
|
+
- `:or()` expands arrays into multiple OR conditions
|
|
578
|
+
- Operators inside string literals are not treated as operators (e.g., `name = 'value > test'`)
|
|
315
579
|
|
|
316
580
|
## SQL File Resolution
|
|
317
581
|
|