@truto/sqlite-builder 1.0.4 โ†’ 2.0.2-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md DELETED
@@ -1,893 +0,0 @@
1
- # ๐Ÿ—๏ธ truto-sqlite-builder
2
-
3
- [![npm version](https://badge.fury.io/js/%40truto%2Fsqlite-builder.svg)](https://badge.fury.io/js/%40truto%2Fsqlite-builder)
4
- [![CI](https://github.com/trutohq/truto-sqlite-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/trutohq/truto-sqlite-builder/actions/workflows/ci.yml)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- **Safe, zero-dependency template-literal tag for SQLite queries in any JS environment.**
9
-
10
- `@truto/sqlite-builder` provides a secure and ergonomic way to build SQLite queries using tagged template literals. It prevents SQL injection attacks through parameterized queries while offering convenient helper functions for common SQL patterns.
11
-
12
- ## โœจ Features
13
-
14
- - ๐Ÿ”’ **Injection-safe**: All values are parameterized, preventing SQL injection
15
- - ๐Ÿšซ **Defense in depth**: Multiple security layers including stacked query detection
16
- - ๐Ÿชถ **Zero dependencies**: Pure TypeScript/JavaScript with no runtime dependencies
17
- - ๐ŸŒ **Universal**: Works in Bun, Node.js, Deno, and modern browsers
18
- - ๐ŸŽฏ **TypeScript-first**: Full type safety with excellent IDE support
19
- - ๐Ÿ”ง **Helper functions**: Built-in utilities for identifiers, IN clauses, and more
20
- - ๐Ÿ” **JSON Filter Language**: MongoDB-style JSON filters for WHERE clauses
21
- - ๐Ÿ”— **Qualified Filters**: Table/alias scoping for complex JOINs using `$alias` blocks
22
- - โšก **Lightweight**: Minimal bundle size with tree-shaking support
23
-
24
- ## ๐Ÿ“ฆ Installation
25
-
26
- ```bash
27
- bun add @truto/sqlite-builder
28
- ```
29
-
30
- ```bash
31
- npm install @truto/sqlite-builder
32
- ```
33
-
34
- ```bash
35
- yarn add @truto/sqlite-builder
36
- ```
37
-
38
- ```bash
39
- pnpm add @truto/sqlite-builder
40
- ```
41
-
42
- ## ๐Ÿš€ Quick Start
43
-
44
- ```typescript
45
- import sqlite3 from 'better-sqlite3'
46
- import { sql, compileFilter } from '@truto/sqlite-builder'
47
-
48
- const db = new sqlite3('database.db')
49
-
50
- // Simple query
51
- const name = 'Alice'
52
- const { text, values } = sql`SELECT * FROM users WHERE name = ${name}`
53
- const users = db.prepare(text).all(...values)
54
-
55
- // JSON Filter queries
56
- const filter = {
57
- name: { like: 'John%' },
58
- age: { gte: 18, lt: 65 },
59
- or: [{ email: { regex: '.*@example.com$' } }, { phone: { exists: false } }],
60
- }
61
-
62
- const { text: whereText, values: whereValues } = compileFilter(filter)
63
- const query = sql`
64
- SELECT * FROM users
65
- WHERE ${sql.raw(whereText)}
66
- `
67
-
68
- const results = db.prepare(query.text).all(...query.values)
69
-
70
- // Qualified filters for JOINs with alias blocks
71
- const joinFilter = {
72
- status: 'ACTIVE', // Main table
73
- $profiles: {
74
- // Profile table alias
75
- verified: true,
76
- 'settings.theme': 'dark', // JSON path in profile
77
- },
78
- $orders: {
79
- // Orders table alias
80
- total: { gt: 100 },
81
- },
82
- }
83
-
84
- const joinWhere = compileFilter(joinFilter)
85
- const joinQuery = sql`
86
- SELECT u.name, p.verified, o.total
87
- FROM users u
88
- JOIN profiles p ON u.id = p.user_id
89
- JOIN orders o ON u.id = o.user_id
90
- WHERE ${sql.raw(joinWhere.text)}
91
- `
92
- ```
93
-
94
- ## ๐Ÿ“– API Reference
95
-
96
- ### `sql` Tagged Template
97
-
98
- The main function for building SQL queries.
99
-
100
- ```typescript
101
- const query = sql`SELECT * FROM users WHERE id = ${userId}`
102
- // Returns: { text: "SELECT * FROM users WHERE id = ?", values: [userId] }
103
- ```
104
-
105
- **Parameters:**
106
-
107
- - Template strings and interpolated values
108
- - Returns a frozen `SqlQuery` object with `text` and `values` properties
109
-
110
- ### `sql.ident(identifier: string | readonly (string | SqlFragment)[])`
111
-
112
- Safely quotes SQL identifiers (table names, column names, etc.). Accepts single identifiers, arrays of identifiers, or mixed arrays containing both identifiers and SQL fragments.
113
-
114
- ```typescript
115
- // Single identifier
116
- const table = 'users'
117
- const query = sql`SELECT * FROM ${sql.ident(table)}`
118
- // Returns: { text: 'SELECT * FROM "users"', values: [] }
119
-
120
- // Qualified identifiers (table.column)
121
- const qualifiedQuery = sql`SELECT ${sql.ident('u.name')}, ${sql.ident('u.email')} FROM users u`
122
- // Returns: { text: 'SELECT "u"."name", "u"."email" FROM users u', values: [] }
123
-
124
- // Array of identifiers (useful for column lists)
125
- const columns = ['name', 'email', 'created_at']
126
- const selectQuery = sql`SELECT ${sql.ident(columns)} FROM users`
127
- // Returns: { text: 'SELECT "name", "email", "created_at" FROM users', values: [] }
128
-
129
- // Mixed arrays with qualified and simple identifiers
130
- const mixedColumns = ['u.id', 'name', 'u.email', 'p.title']
131
- const joinQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users u JOIN posts p ON u.id = p.user_id`
132
- // Returns: { text: 'SELECT "u"."id", "name", "u"."email", "p"."title" FROM users u JOIN posts p ON u.id = p.user_id', values: [] }
133
-
134
- // Mixed arrays with identifiers and SQL fragments
135
- const mixedColumns = ['id', 'name', sql.raw('UPPER(email) as email_upper')]
136
- const mixedQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users`
137
- // Returns: { text: 'SELECT "id", "name", UPPER(email) as email_upper FROM users', values: [] }
138
-
139
- // Mixed arrays with parameterized fragments
140
- const status = 'premium'
141
- const dynamicColumns = [
142
- 'id',
143
- 'name',
144
- sql`CASE WHEN status = ${status} THEN 'Premium' ELSE 'Regular' END as user_type`,
145
- ]
146
- const dynamicQuery = sql`SELECT ${sql.ident(dynamicColumns)} FROM users`
147
- // Returns: { text: 'SELECT "id", "name", CASE WHEN status = ? THEN \'Premium\' ELSE \'Regular\' END as user_type FROM users', values: ['premium'] }
148
-
149
- // In INSERT statements
150
- const insertColumns = ['name', 'email', 'age']
151
- const insertQuery = sql`
152
- INSERT INTO users (${sql.ident(insertColumns)})
153
- VALUES (${name}, ${email}, ${age})
154
- `
155
- // Returns: { text: 'INSERT INTO users ("name", "email", "age") VALUES (?, ?, ?)', values: [name, email, age] }
156
- ```
157
-
158
- **Security:** Only accepts valid ANSI identifiers (simple: `name`, qualified: `table.column`) for string elements. SQL fragments are passed through as-is.
159
-
160
- ### `sql.in(array: readonly unknown[])`
161
-
162
- Creates parameterized IN clauses from arrays.
163
-
164
- ```typescript
165
- const ids = [1, 2, 3]
166
- const query = sql`SELECT * FROM users WHERE id IN ${sql.in(ids)}`
167
- // Returns: { text: "SELECT * FROM users WHERE id IN (?,?,?)", values: [1, 2, 3] }
168
- ```
169
-
170
- **Features:**
171
-
172
- - Rejects empty arrays (would create invalid SQL)
173
- - Warns for arrays with >1000 items (performance consideration)
174
-
175
- ### `sql.raw(rawSql: string)`
176
-
177
- Embeds raw SQL without parameterization. **โš ๏ธ Use with extreme caution!**
178
-
179
- ```typescript
180
- const query = sql`SELECT * FROM users WHERE created_at > ${sql.raw('datetime("now", "-1 day")')}`
181
- // Returns: { text: 'SELECT * FROM users WHERE created_at > datetime("now", "-1 day")', values: [] }
182
- ```
183
-
184
- **โš ๏ธ Warning:** Never use `sql.raw()` with user input. Only use with trusted, static SQL fragments.
185
-
186
- ### `sql.join(fragments: SqlFragment[], separator?: string)`
187
-
188
- Joins multiple SQL fragments with a separator.
189
-
190
- ```typescript
191
- const conditions = [
192
- sql`name = ${'John'}`,
193
- sql`age = ${30}`,
194
- sql`active = ${true}`,
195
- ]
196
-
197
- const query = sql`SELECT * FROM users WHERE ${sql.join(conditions, ' AND ')}`
198
- // Returns: { text: "SELECT * FROM users WHERE name = ? AND age = ? AND active = ?", values: ['John', 30, true] }
199
- ```
200
-
201
- ## ๐Ÿ” JSON Filter Language
202
-
203
- Build complex WHERE clauses using MongoDB-style JSON filters. Perfect for APIs and dynamic queries.
204
-
205
- ### `compileFilter(filter: JsonFilter): FilterResult`
206
-
207
- Compiles a JSON filter object into a parameterized SQL WHERE clause.
208
-
209
- ```typescript
210
- import { compileFilter } from '@truto/sqlite-builder'
211
-
212
- const filter = {
213
- status: 'ACTIVE',
214
- age: { gte: 18, lt: 65 },
215
- }
216
-
217
- const result = compileFilter(filter)
218
- // Returns: { text: '(("status" = ? AND "age" >= ? AND "age" < ?))', values: ['ACTIVE', 18, 65] }
219
-
220
- // Use with the main sql template
221
- const query = sql`
222
- SELECT * FROM users
223
- WHERE ${sql.raw(result.text)}
224
- `
225
- ```
226
-
227
- ### Supported Operators
228
-
229
- | Operator Family | JSON Form | SQL Fragment | Description |
230
- | ------------------------- | ------------------------------------- | ---------------------------------------- | ----------------------------------- |
231
- | **Equality** | `"field": value` | `"field" = ?` | Direct value comparison |
232
- | **Inequality** | `"field": { "ne": value }` | `"field" <> ?` | Not equal comparison |
233
- | **Comparison** | `"field": { "gt": value }` | `"field" > ?` | Greater than, gte, lt, lte |
234
- | **Set Membership** | `"field": { "in": [1, 2, 3] }` | `"field" IN (?,?,?)` | Value in array |
235
- | **Negative Set** | `"field": { "nin": [1, 2] }` | `"field" NOT IN (?,?)` | Value not in array |
236
- | **NULL Checks** | `"field": { "exists": false }` | `"field" IS NULL` | Check for NULL/NOT NULL |
237
- | **LIKE Patterns** | `"field": { "like": "john%" }` | `"field" LIKE ?` | Pattern matching |
238
- | **Case-insensitive LIKE** | `"field": { "ilike": "%DOE%" }` | `"field" LIKE ? COLLATE NOCASE` | Case-insensitive patterns |
239
- | **Regular Expressions** | `"field": { "regex": "^[A-Z]+" }` | `"field" REGEXP ?` | Regex patterns (requires extension) |
240
- | **Logical AND** | `"and": [filter1, filter2]` | `(filter1 AND filter2)` | All conditions must match |
241
- | **Logical OR** | `"or": [filter1, filter2]` | `(filter1 OR filter2)` | Any condition must match |
242
- | **JSON Path** | `"profile.email": "test@example.com"` | `json_extract("profile", '$.email') = ?` | Query JSON column fields |
243
- | **Alias Blocks** | `"$alias": { "field": value }` | `alias."field" = ?` | Table/alias qualified fields |
244
-
245
- ### Filter Examples
246
-
247
- #### Basic Operations
248
-
249
- ```typescript
250
- // Equality and comparison
251
- const filter1 = {
252
- status: 'ACTIVE',
253
- age: { gte: 18, lt: 65 },
254
- score: { gt: 80, lte: 100 },
255
- }
256
- // SQL: (("status" = ? AND "age" >= ? AND "age" < ? AND "score" > ? AND "score" <= ?))
257
-
258
- // Set membership
259
- const filter2 = {
260
- role: { in: ['ADMIN', 'EDITOR'] },
261
- department: { nin: ['ARCHIVED', 'DELETED'] },
262
- }
263
- // SQL: (("role" IN (?,?) AND "department" NOT IN (?,?)))
264
-
265
- // NULL checks
266
- const filter3 = {
267
- email: { exists: true }, // IS NOT NULL
268
- deleted_at: { exists: false }, // IS NULL
269
- }
270
- // SQL: (("email" IS NOT NULL AND "deleted_at" IS NULL))
271
- ```
272
-
273
- #### Pattern Matching
274
-
275
- ```typescript
276
- // LIKE patterns
277
- const filter4 = {
278
- username: { like: 'john%' }, // Starts with 'john'
279
- email: { ilike: '%@EXAMPLE.COM%' }, // Case-insensitive contains
280
- }
281
- // SQL: (("username" LIKE ? AND "email" LIKE ? COLLATE NOCASE))
282
-
283
- // Regular expressions (requires REGEXP extension)
284
- const filter5 = {
285
- email: { regex: '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$' },
286
- phone: { regex: '^\\+1[0-9]{10}$' },
287
- }
288
- // SQL: (("email" REGEXP ? AND "phone" REGEXP ?))
289
- ```
290
-
291
- #### Logical Operators
292
-
293
- ```typescript
294
- // AND operator (explicit)
295
- const filter6 = {
296
- and: [
297
- { status: 'ACTIVE' },
298
- { age: { gte: 18 } },
299
- { country: { in: ['US', 'CA', 'GB'] } },
300
- ],
301
- }
302
- // SQL: ((("status" = ?) AND ("age" >= ?) AND ("country" IN (?,?,?))))
303
-
304
- // OR operator
305
- const filter7 = {
306
- or: [
307
- { age: { lt: 18 } }, // Minors
308
- { age: { gte: 65 } }, // Seniors
309
- ],
310
- }
311
- // SQL: ((("age" < ?) OR ("age" >= ?)))
312
-
313
- // Mixed AND/OR logic
314
- const filter8 = {
315
- status: 'ACTIVE', // Implicit AND
316
- or: [{ tags: { ilike: '%urgent%' } }, { priority: { gte: 8 } }],
317
- }
318
- // SQL: (((("tags" LIKE ? COLLATE NOCASE) OR ("priority" >= ?)) AND ("status" = ?)))
319
- ```
320
-
321
- #### JSON Path Querying
322
-
323
- For SQLite JSON columns, use dot notation to query nested fields:
324
-
325
- ```typescript
326
- // Query JSON fields
327
- const filter9 = {
328
- 'profile.email': { regex: '.*@example\\.org$' },
329
- 'profile.age': { gte: 21 },
330
- 'settings.theme': { in: ['dark', 'light'] },
331
- 'metadata.tags': { exists: true },
332
- }
333
- // SQL: ((json_extract("profile", '$.email') REGEXP ? AND
334
- // json_extract("profile", '$.age') >= ? AND
335
- // json_extract("settings", '$.theme') IN (?,?) AND
336
- // json_extract("metadata", '$.tags') IS NOT NULL))
337
-
338
- // Complex nested JSON query
339
- const filter10 = {
340
- and: [
341
- { 'user.profile.email': { regex: '.*@company\\.com$' } },
342
- { or: [{ 'user.role': 'ADMIN' }, { 'user.permissions.canEdit': true }] },
343
- ],
344
- }
345
- ```
346
-
347
- #### Qualified Filters for JOINs
348
-
349
- For complex queries involving multiple tables or aliases, use **alias blocks** with keys starting with `$`:
350
-
351
- ```typescript
352
- // Basic alias usage
353
- const joinFilter = {
354
- // Main table fields (no prefix)
355
- status: 'ACTIVE',
356
- age: { gte: 18, lt: 65 },
357
-
358
- // Table alias 't2'
359
- $t2: {
360
- column_in_table_2: { gte: 100 },
361
- 'stats.avg': { lt: 10 }, // JSON path in aliased table
362
- },
363
-
364
- // Table alias 'orders'
365
- $orders: {
366
- amount: { gt: 500 },
367
- status: { in: ['completed', 'shipped'] },
368
- },
369
- }
370
-
371
- const result = compileFilter(joinFilter)
372
- // SQL: ((("status" = ?) AND ("age" >= ? AND "age" < ?) AND
373
- // ((t2."column_in_table_2" >= ?) AND (json_extract(t2."stats", '$.avg') < ?)) AND
374
- // ((orders."amount" > ?) AND (orders."status" IN (?,?)))))
375
- ```
376
-
377
- **Alias Block Rules:**
378
-
379
- - **Alias names**: Must be valid SQL identifiers (`[A-Za-z_][A-Za-z0-9_]*`)
380
- - **Prefixing**: All fields in alias blocks get prefixed with `alias.`
381
- - **JSON paths**: Work seamlessly with aliases: `json_extract(alias."column", '$.path')`
382
- - **Combination**: Alias blocks are AND-combined with root fields and each other
383
- - **Order**: Regular fields processed first, then alias blocks
384
-
385
- ```typescript
386
- // Complex alias example with logical operators
387
- const complexJoinFilter = {
388
- // Primary table conditions
389
- user_status: 'ACTIVE',
390
-
391
- // User profile table
392
- $profile: {
393
- verified: true,
394
- 'preferences.notifications': { ne: false },
395
- or: [{ subscription_type: 'premium' }, { credits: { gte: 100 } }],
396
- },
397
-
398
- // Orders table with complex conditions
399
- $orders: {
400
- and: [
401
- { created_at: { gte: '2024-01-01' } },
402
- { or: [{ total_amount: { gt: 1000 } }, { item_count: { gte: 5 } }] },
403
- ],
404
- },
405
- }
406
-
407
- // Use in JOIN queries
408
- const whereClause = compileFilter(complexJoinFilter)
409
- const query = sql`
410
- SELECT u.id, u.name, p.subscription_type, o.total_amount
411
- FROM users u
412
- JOIN profiles p ON u.id = p.user_id
413
- JOIN orders o ON u.id = o.user_id
414
- WHERE ${sql.raw(whereClause.text)}
415
- `
416
- ```
417
-
418
- **Security & Validation:**
419
-
420
- ```typescript
421
- // โœ… Valid alias identifiers
422
- $users: { name: 'John' } // Simple identifier
423
- $user_profiles: { age: 25 } // Underscore allowed
424
- $_temp: { status: 'active' } // Starting underscore allowed
425
-
426
- // โŒ Invalid alias identifiers (will throw SyntaxError)
427
- $123invalid: { ... } // Cannot start with number
428
- $'invalid-alias': { ... } // Hyphens not allowed
429
- $'table.alias': { ... } // Dots not allowed in alias name
430
- ```
431
-
432
- **Integration with Complex Queries:**
433
-
434
- ```typescript
435
- // Real-world JOIN example
436
- const userOrderFilter = {
437
- // Users table
438
- active: true,
439
- email: { exists: true },
440
-
441
- // User profiles
442
- $profiles: {
443
- 'settings.email_notifications': true,
444
- verified_at: { exists: true },
445
- },
446
-
447
- // Recent orders
448
- $recent_orders: {
449
- created_at: { gte: '2024-01-01' },
450
- status: { in: ['completed', 'shipped'] },
451
- total: { gt: 50 },
452
- },
453
- }
454
-
455
- const whereClause = compileFilter(userOrderFilter)
456
-
457
- const complexQuery = sql`
458
- SELECT
459
- u.id,
460
- u.name,
461
- u.email,
462
- p.verified_at,
463
- COUNT(ro.id) as recent_order_count,
464
- SUM(ro.total) as recent_order_total
465
- FROM users u
466
- JOIN profiles p ON u.id = p.user_id
467
- JOIN orders ro ON u.id = ro.user_id
468
- WHERE ${sql.raw(whereClause.text)}
469
- GROUP BY u.id, u.name, u.email, p.verified_at
470
- HAVING recent_order_count > 0
471
- ORDER BY recent_order_total DESC
472
- `
473
-
474
- const results = db.prepare(complexQuery.text).all(...complexQuery.values)
475
- ```
476
-
477
- #### Kitchen Sink Examples
478
-
479
- Real-world complex filters:
480
-
481
- ```typescript
482
- // Active users in specific regions, either minors/seniors or VIP
483
- const complexFilter = {
484
- and: [
485
- { status: 'ACTIVE' },
486
- { or: [{ age: { lt: 18 } }, { age: { gte: 65 } }, { membership: 'VIP' }] },
487
- { country: { in: ['US', 'CA', 'GB'] } },
488
- { email: { exists: true } },
489
- { 'profile.verified': true },
490
- ],
491
- }
492
-
493
- // Content filtering with multiple criteria
494
- const contentFilter = {
495
- name: { like: 'Project%' },
496
- category: { nin: ['ARCHIVED', 'DELETED', 'SPAM'] },
497
- created_at: { exists: true },
498
- or: [
499
- { tags: { ilike: '%important%' } },
500
- { priority: { gte: 8 } },
501
- { 'metadata.featured': true },
502
- ],
503
- }
504
- ```
505
-
506
- ### Integration with SQL Template
507
-
508
- ```typescript
509
- import { sql, compileFilter } from '@truto/sqlite-builder'
510
-
511
- // Build the WHERE clause
512
- const filter = {
513
- status: 'ACTIVE',
514
- age: { gte: 18 },
515
- role: { in: ['USER', 'ADMIN'] },
516
- }
517
-
518
- const whereClause = compileFilter(filter)
519
-
520
- // Use in complete query
521
- const query = sql`
522
- SELECT id, name, email, created_at
523
- FROM users
524
- WHERE ${sql.raw(whereClause.text)}
525
- ORDER BY created_at DESC
526
- LIMIT ${limit}
527
- `
528
-
529
- // Execute with driver
530
- const results = db.prepare(query.text).all(...query.values)
531
- ```
532
-
533
- ### Security & Validation
534
-
535
- The JSON filter compiler includes comprehensive security measures:
536
-
537
- - **Operator validation**: Only known operators are allowed
538
- - **Identifier safety**: Field names are validated using the same rules as `sql.ident()`
539
- - **Array limits**: IN/NIN arrays limited to 999 items (SQLite limitation)
540
- - **DoS protection**: Nesting depth โ‰ค 10, total operators โ‰ค 100
541
- - **Type validation**: Strict type checking for all operator values
542
- - **SQL injection prevention**: All values are parameterized
543
-
544
- ```typescript
545
- // โŒ These will throw errors
546
- compileFilter({ age: { unknown: 18 } }) // Unknown operator
547
- compileFilter({ 'user; DROP TABLE': 'value' }) // Invalid identifier
548
- compileFilter({ role: { in: [] } }) // Empty array
549
- compileFilter({ role: { in: new Array(1000).fill('x') } }) // Too large array
550
-
551
- // โœ… These are safe and valid
552
- compileFilter({ age: { gte: 18, lte: 65 } }) // Multiple operators
553
- compileFilter({ 'profile.email': { exists: true } }) // JSON path
554
- compileFilter({ or: [{ x: 1 }, { y: 2 }] }) // Logical operators
555
- ```
556
-
557
- ### REGEXP Extension
558
-
559
- To use the `regex` operator, you need to load a REGEXP extension in SQLite:
560
-
561
- ```typescript
562
- // With better-sqlite3
563
- import sqlite3 from 'better-sqlite3'
564
-
565
- const db = new sqlite3('database.db')
566
-
567
- // Load REGEXP extension (varies by implementation)
568
- // This is implementation-specific - check your SQLite setup
569
- db.loadExtension('regexp') // Example - actual method may vary
570
-
571
- // Now regex filters work
572
- const filter = {
573
- email: { regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$' },
574
- }
575
- ```
576
-
577
- ## ๐Ÿ›ก๏ธ Security Model
578
-
579
- ### What's Protected
580
-
581
- - **SQL Injection**: All interpolated values are parameterized
582
- - **Stacked Queries**: Queries containing `;` followed by additional SQL are rejected
583
- - **Identifier Safety**: `sql.ident()` validates against ANSI identifier rules
584
- - **Length Limits**: Queries exceeding 100KB are rejected
585
- - **Filter Security**: JSON filters validate operators, identifiers, and enforce limits
586
-
587
- ### What's Your Responsibility
588
-
589
- - **Never use `sql.raw()` with user input**
590
- - **Validate identifiers before using `sql.ident()`** (though it has built-in validation)
591
- - **Use `sql.in()` instead of string concatenation** for arrays
592
- - **Keep your SQLite driver updated**
593
- - **Load REGEXP extension safely** if using regex filters
594
-
595
- ### Supported Value Types
596
-
597
- ```typescript
598
- // โœ… Safe types (automatically parameterized)
599
- const query = sql`
600
- INSERT INTO users (name, age, active, created_at, data, deleted_at)
601
- VALUES (
602
- ${'John'}, // string
603
- ${30}, // number
604
- ${true}, // boolean
605
- ${new Date()}, // Date โ†’ 'YYYY-MM-DD HH:MM:SS'
606
- ${null}, // null
607
- ${undefined} // undefined โ†’ null
608
- )
609
- `
610
-
611
- // โŒ Unsafe types (will throw TypeError)
612
- sql`SELECT * FROM users WHERE data = ${Buffer.from('test')}` // Use sql.raw() for buffers
613
- sql`SELECT * FROM users WHERE id = ${Symbol('test')}` // Unsupported type
614
- ```
615
-
616
- ## ๐Ÿ“‹ Examples
617
-
618
- ### Basic CRUD Operations
619
-
620
- ```typescript
621
- import { sql } from '@truto/sqlite-builder'
622
-
623
- // CREATE with array identifiers
624
- const insertColumns = ['name', 'email', 'age']
625
- const insertUser = sql`
626
- INSERT INTO users (${sql.ident(insertColumns)})
627
- VALUES (${name}, ${email}, ${age})
628
- `
629
-
630
- // READ with specific columns
631
- const selectColumns = ['id', 'name', 'email', 'created_at']
632
- const getUser = sql`
633
- SELECT ${sql.ident(selectColumns)} FROM users
634
- WHERE id = ${userId}
635
- `
636
-
637
- // UPDATE
638
- const updateUser = sql`
639
- UPDATE users
640
- SET name = ${newName}, updated_at = ${new Date()}
641
- WHERE id = ${userId}
642
- `
643
-
644
- // DELETE
645
- const deleteUser = sql`
646
- DELETE FROM users
647
- WHERE id = ${userId}
648
- `
649
- ```
650
-
651
- ### Dynamic Queries
652
-
653
- ```typescript
654
- // Dynamic WHERE conditions
655
- const filters = []
656
- if (name) filters.push(sql`name = ${name}`)
657
- if (minAge) filters.push(sql`age >= ${minAge}`)
658
- if (isActive !== undefined) filters.push(sql`active = ${isActive}`)
659
-
660
- const whereClause =
661
- filters.length > 0 ? sql.join(filters, ' AND ') : sql.raw('1=1')
662
-
663
- const query = sql`
664
- SELECT * FROM users
665
- WHERE ${whereClause}
666
- ORDER BY created_at DESC
667
- `
668
-
669
- // Dynamic column selection (simplified with array support)
670
- const columns = ['id', 'name', 'email']
671
- const selectQuery = sql`SELECT ${sql.ident(columns)} FROM users`
672
-
673
- // Alternative approach for more complex column expressions
674
- const complexColumns = [
675
- sql.ident('id'),
676
- sql.ident('name'),
677
- sql.raw('UPPER(email) as email_upper'),
678
- ]
679
- const complexQuery = sql`SELECT ${sql.join(complexColumns)} FROM users`
680
- ```
681
-
682
- ### Dynamic Queries with JSON Filters
683
-
684
- ```typescript
685
- import { sql, compileFilter } from '@truto/sqlite-builder'
686
-
687
- // API endpoint that accepts JSON filter
688
- app.get('/api/users', (req, res) => {
689
- // User sends filter as JSON
690
- const filter = req.body.filter || {}
691
-
692
- // Safely compile to SQL
693
- const whereClause = compileFilter(filter)
694
-
695
- const query = sql`
696
- SELECT id, name, email, created_at
697
- FROM users
698
- WHERE ${sql.raw(whereClause.text)}
699
- ORDER BY created_at DESC
700
- LIMIT ${req.query.limit || 20}
701
- `
702
-
703
- const users = db.prepare(query.text).all(...query.values)
704
- res.json(users)
705
- })
706
-
707
- // Example API calls:
708
- // POST /api/users { "filter": { "status": "ACTIVE", "age": { "gte": 18 } } }
709
- // POST /api/users { "filter": { "or": [{ "role": "ADMIN" }, { "verified": true }] } }
710
- ```
711
-
712
- ### Array Identifiers & Qualified Identifiers
713
-
714
- The `sql.ident()` function supports simple identifiers, qualified identifiers (table.column), arrays, and mixed arrays with SQL fragments:
715
-
716
- ```typescript
717
- // โœ… Simple identifiers
718
- const table = 'users'
719
- const column = 'name'
720
- const simpleQuery = sql`SELECT ${sql.ident(column)} FROM ${sql.ident(table)}`
721
- // Result: SELECT "name" FROM "users"
722
-
723
- // โœ… Qualified identifiers (table.column)
724
- const qualifiedQuery = sql`SELECT ${sql.ident('u.name')}, ${sql.ident('p.title')} FROM users u JOIN posts p ON u.id = p.user_id`
725
- // Result: SELECT "u"."name", "p"."title" FROM users u JOIN posts p ON u.id = p.user_id
726
-
727
- // โœ… Pure identifier arrays (clean and concise)
728
- const columns = ['id', 'name', 'email', 'created_at']
729
- const arrayQuery = sql`SELECT ${sql.ident(columns)} FROM users`
730
- // Result: SELECT "id", "name", "email", "created_at" FROM users
731
-
732
- // โœ… Mixed qualified and simple identifiers in arrays
733
- const mixedColumns = ['u.id', 'name', 'u.email', 'p.title', 'created_at']
734
- const mixedQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users u LEFT JOIN posts p ON u.id = p.user_id`
735
- // Result: SELECT "u"."id", "name", "u"."email", "p"."title", "created_at" FROM users u LEFT JOIN posts p ON u.id = p.user_id
736
-
737
- // โœ… NEW: Mixed arrays with identifiers and SQL fragments
738
- const mixedColumns = [
739
- 'id',
740
- 'name',
741
- sql.raw('UPPER(email) as email_upper'),
742
- sql.raw('COUNT(*) as total'),
743
- ]
744
- const mixedQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users GROUP BY id, name, email`
745
- // Result: SELECT "id", "name", UPPER(email) as email_upper, COUNT(*) as total FROM users GROUP BY id, name, email
746
-
747
- // โœ… Mixed arrays with parameterized fragments
748
- const status = 'premium'
749
- const dynamicColumns = [
750
- 'id',
751
- 'name',
752
- sql`CASE WHEN status = ${status} THEN 'Premium User' ELSE 'Regular User' END as user_type`,
753
- sql.raw('created_at'),
754
- ]
755
- const parameterizedQuery = sql`SELECT ${sql.ident(dynamicColumns)} FROM users WHERE active = ${true}`
756
- // Combines identifiers, raw SQL, and parameterized values seamlessly
757
-
758
- // โœ… Works great for INSERT statements
759
- const insertData = { name: 'John', email: 'john@example.com', age: 30 }
760
- const insertColumns = Object.keys(insertData)
761
- const insertValues = Object.values(insertData)
762
- const insertQuery = sql`
763
- INSERT INTO users (${sql.ident(insertColumns)})
764
- VALUES (${insertValues[0]}, ${insertValues[1]}, ${insertValues[2]})
765
- `
766
-
767
- // โœ… Dynamic column selection
768
- const userFields = ['name', 'email']
769
- const includeTimestamps = true
770
- if (includeTimestamps) {
771
- userFields.push('created_at', 'updated_at')
772
- }
773
- const dynamicQuery = sql`SELECT ${sql.ident(userFields)} FROM users`
774
-
775
- // ๐Ÿ†š OLD: Manual joining approach (still works, but much more verbose)
776
- const oldWay = sql`SELECT ${sql.join([
777
- sql.ident('id'),
778
- sql.ident('name'),
779
- sql.raw('UPPER(email) as email_upper'),
780
- ])} FROM users`
781
- ```
782
-
783
- ### Complex Joins
784
-
785
- ```typescript
786
- const getUsersWithPosts = sql`
787
- SELECT
788
- ${sql.ident('u')}.id,
789
- ${sql.ident('u')}.name,
790
- COUNT(${sql.ident('p')}.id) as post_count
791
- FROM ${sql.ident('users')} u
792
- LEFT JOIN ${sql.ident('posts')} p ON u.id = p.user_id
793
- WHERE u.created_at > ${startDate}
794
- AND u.status IN ${sql.in(['active', 'premium'])}
795
- GROUP BY u.id
796
- HAVING post_count > ${minPosts}
797
- ORDER BY post_count DESC
798
- LIMIT ${limit}
799
- `
800
-
801
- // For simpler cases, you can use array identifiers directly
802
- const getUsers = sql`
803
- SELECT ${sql.ident(['id', 'name', 'email', 'created_at'])}
804
- FROM ${sql.ident('users')}
805
- WHERE status = ${'active'}
806
- `
807
- ```
808
-
809
- ### Transactions
810
-
811
- ```typescript
812
- // Works great with better-sqlite3 transactions
813
- const insertUsers = db.transaction((users) => {
814
- const stmt = db.prepare(
815
- sql`
816
- INSERT INTO users (name, email)
817
- VALUES (?, ?)
818
- `.text,
819
- )
820
-
821
- for (const user of users) {
822
- const { values } = sql`${user.name}, ${user.email}`
823
- stmt.run(...values)
824
- }
825
- })
826
-
827
- insertUsers([
828
- { name: 'Alice', email: 'alice@example.com' },
829
- { name: 'Bob', email: 'bob@example.com' },
830
- ])
831
- ```
832
-
833
- ## ๐Ÿงช Testing
834
-
835
- ```bash
836
- # Run tests
837
- bun run test
838
-
839
- # Run tests in watch mode
840
- bun run dev
841
-
842
- # Run tests with coverage
843
- bun run test:coverage
844
-
845
- # Run tests with UI
846
- bun run test:ui
847
- ```
848
-
849
- ## ๐Ÿค Contributing
850
-
851
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
852
-
853
- ### Development Setup
854
-
855
- ```bash
856
- git clone https://github.com/truto/truto-sqlite-builder.git
857
- cd truto-sqlite-builder
858
- bun install
859
- bun run dev # Start tests in watch mode
860
- ```
861
-
862
- ### Release Process
863
-
864
- We use [changesets](https://github.com/changesets/changesets) for version management:
865
-
866
- ```bash
867
- # Add a changeset
868
- bunx changeset
869
-
870
- # Release
871
- bunx changeset version
872
- bun run build
873
- git commit -am "Release"
874
- git push --follow-tags
875
- ```
876
-
877
- ## ๐Ÿ”’ Security Policy
878
-
879
- If you discover a security vulnerability, please email eng@truto.one or create an issue.
880
-
881
- ## ๐Ÿ“„ License
882
-
883
- MIT ยฉ [Truto](https://github.com/trutohq)
884
-
885
- ## ๐Ÿ’ก Inspiration
886
-
887
- This library was inspired by:
888
-
889
- - [sql-template-strings](https://github.com/felixfbecker/sql-template-strings)
890
- - [slonik](https://github.com/gajus/slonik)
891
- - [Postgres.js](https://github.com/porsager/postgres)
892
-
893
- Built with โค๏ธ for the SQLite community.