@truto/sqlite-builder 2.0.2-canary.10 → 2.0.2-canary.11

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.
Files changed (2) hide show
  1. package/README.md +179 -179
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,225 +1,225 @@
1
- # 🏗ïļ truto-sqlite-builder
2
1
 
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
2
+ // Interpolate it directly into a sql template — no sql.raw() needed. The
3
+ // filter's placeholders and values are collected automatically and stay aligned.
4
+ const query = sql`
5
+ SELECT * FROM users
6
+ WHERE ${result}
7
+ `
28
8
  ```
29
9
 
30
- ```bash
31
- npm install @truto/sqlite-builder
32
- ```
10
+ ### Supported Operators
33
11
 
34
- ```bash
35
- yarn add @truto/sqlite-builder
36
- ```
12
+ | Operator Family | JSON Form | SQL Fragment | Description |
13
+ | ------------------------- | ------------------------------------- | ---------------------------------------- | ----------------------------------- |
14
+ | **Equality** | `"field": value` | `"field" = ?` | Direct value comparison |
15
+ | **Inequality** | `"field": { "ne": value }` | `"field" <> ?` | Not equal comparison |
16
+ | **Comparison** | `"field": { "gt": value }` | `"field" > ?` | Greater than, gte, lt, lte |
17
+ | **Set Membership** | `"field": { "in": [1, 2, 3] }` | `"field" IN (?,?,?)` | Value in array |
18
+ | **Negative Set** | `"field": { "nin": [1, 2] }` | `"field" NOT IN (?,?)` | Value not in array |
19
+ | **NULL Checks** | `"field": { "exists": false }` | `"field" IS NULL` | Check for NULL/NOT NULL |
20
+ | **LIKE Patterns** | `"field": { "like": "john%" }` | `"field" LIKE ?` | Pattern matching |
21
+ | **Case-insensitive LIKE** | `"field": { "ilike": "%DOE%" }` | `"field" LIKE ? COLLATE NOCASE` | Case-insensitive patterns |
22
+ | **Regular Expressions** | `"field": { "regex": "^[A-Z]+" }` | `"field" REGEXP ?` | Regex patterns (requires extension) |
23
+ | **Logical AND** | `"and": [filter1, filter2]` | `(filter1 AND filter2)` | All conditions must match |
24
+ | **Logical OR** | `"or": [filter1, filter2]` | `(filter1 OR filter2)` | Any condition must match |
25
+ | **JSON Path** | `"profile.email": "test@example.com"` | `json_extract("profile", '$.email') = ?` | Query JSON column fields |
26
+ | **Alias Blocks** | `"$alias": { "field": value }` | `alias."field" = ?` | Table/alias qualified fields |
37
27
 
38
- ```bash
39
- pnpm add @truto/sqlite-builder
40
- ```
28
+ ### Filter Examples
41
29
 
42
- ## 🚀 Quick Start
30
+ #### Basic Operations
43
31
 
44
32
  ```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%' },
33
+ // Equality and comparison
34
+ const filter1 = {
35
+ status: 'ACTIVE',
58
36
  age: { gte: 18, lt: 65 },
59
- or: [{ email: { regex: '.*@example.com$' } }, { phone: { exists: false } }],
37
+ score: { gt: 80, lte: 100 },
60
38
  }
39
+ // SQL: (("status" = ? AND "age" >= ? AND "age" < ? AND "score" > ? AND "score" <= ?))
61
40
 
62
- // Interpolate the compiled filter directly: its placeholders and values are
63
- // collected automatically, so query.values is always correctly aligned.
64
- const query = sql`
65
- SELECT * FROM users
66
- WHERE ${compileFilter(filter)}
67
- `
68
-
69
- const results = db.prepare(query.text).all(...query.values)
70
-
71
- // Qualified filters for JOINs with alias blocks
72
- const joinFilter = {
73
- status: 'ACTIVE', // Main table
74
- $profiles: {
75
- // Profile table alias
76
- verified: true,
77
- 'settings.theme': 'dark', // JSON path in profile
78
- },
79
- $orders: {
80
- // Orders table alias
81
- total: { gt: 100 },
82
- },
41
+ // Set membership
42
+ const filter2 = {
43
+ role: { in: ['ADMIN', 'EDITOR'] },
44
+ department: { nin: ['ARCHIVED', 'DELETED'] },
83
45
  }
46
+ // SQL: (("role" IN (?,?) AND "department" NOT IN (?,?)))
84
47
 
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 ${compileFilter(joinFilter)}
91
- `
48
+ // NULL checks
49
+ const filter3 = {
50
+ email: { exists: true }, // IS NOT NULL
51
+ deleted_at: { exists: false }, // IS NULL
52
+ }
53
+ // SQL: (("email" IS NOT NULL AND "deleted_at" IS NULL))
92
54
  ```
93
55
 
94
- ## 📖 API Reference
95
-
96
- ### `sql` Tagged Template
97
-
98
- The main function for building SQL queries.
56
+ #### Pattern Matching
99
57
 
100
58
  ```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
59
+ // LIKE patterns
60
+ const filter4 = {
61
+ username: { like: 'john%' }, // Starts with 'john'
62
+ email: { ilike: '%@EXAMPLE.COM%' }, // Case-insensitive contains
63
+ }
64
+ // SQL: (("username" LIKE ? AND "email" LIKE ? COLLATE NOCASE))
109
65
 
110
- ### `sql.ident(identifier: string | readonly (string | SqlFragment)[])`
66
+ // Regular expressions (requires REGEXP extension)
67
+ const filter5 = {
68
+ email: { regex: '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$' },
69
+ phone: { regex: '^\\+1[0-9]{10}$' },
70
+ }
71
+ // SQL: (("email" REGEXP ? AND "phone" REGEXP ?))
72
+ ```
111
73
 
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.
74
+ #### Logical Operators
113
75
 
114
76
  ```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
- ```
77
+ // AND operator (explicit)
78
+ const filter6 = {
79
+ and: [
80
+ { status: 'ACTIVE' },
81
+ { age: { gte: 18 } },
82
+ { country: { in: ['US', 'CA', 'GB'] } },
83
+ ],
84
+ }
85
+ // SQL: ((("status" = ?) AND ("age" >= ?) AND ("country" IN (?,?,?))))
86
+
87
+ // OR operator
88
+ const filter7 = {
89
+ or: [
90
+ { age: { lt: 18 } }, // Minors
91
+ { age: { gte: 65 } }, // Seniors
92
+ ],
93
+ }
94
+ // SQL: ((("age" < ?) OR ("age" >= ?)))
157
95
 
158
- **Security:** Only accepts valid ANSI identifiers (simple: `name`, qualified: `table.column`) for string elements, each capped at 255 characters. SQL fragments are passed through as-is, but **only fragments minted by this library** (e.g. `sql.raw`, nested `sql\`\``) are accepted — a plain `{ text, values }`object (for example from`JSON.parse`) is rejected, so untrusted data can never masquerade as raw SQL.
96
+ // Mixed AND/OR logic
97
+ const filter8 = {
98
+ status: 'ACTIVE', // Implicit AND
99
+ or: [{ tags: { ilike: '%urgent%' } }, { priority: { gte: 8 } }],
100
+ }
101
+ // SQL: (((("tags" LIKE ? COLLATE NOCASE) OR ("priority" >= ?)) AND ("status" = ?)))
102
+ ```
159
103
 
160
- ### `sql.in(array: readonly unknown[])`
104
+ #### JSON Path Querying
161
105
 
162
- Creates parameterized IN clauses from arrays.
106
+ For SQLite JSON columns, use dot notation to query nested fields:
163
107
 
164
108
  ```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] }
109
+ // Query JSON fields
110
+ const filter9 = {
111
+ 'profile.email': { regex: '.*@example\\.org$' },
112
+ 'profile.age': { gte: 21 },
113
+ 'settings.theme': { in: ['dark', 'light'] },
114
+ 'metadata.tags': { exists: true },
115
+ }
116
+ // SQL: ((json_extract("profile", '$.email') REGEXP ? AND
117
+ // json_extract("profile", '$.age') >= ? AND
118
+ // json_extract("settings", '$.theme') IN (?,?) AND
119
+ // json_extract("metadata", '$.tags') IS NOT NULL))
120
+
121
+ // Complex nested JSON query
122
+ const filter10 = {
123
+ and: [
124
+ { 'user.profile.email': { regex: '.*@company\\.com$' } },
125
+ { or: [{ 'user.role': 'ADMIN' }, { 'user.permissions.canEdit': true }] },
126
+ ],
127
+ }
168
128
  ```
169
129
 
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)`
130
+ #### Qualified Filters for JOINs
176
131
 
177
- Embeds raw SQL without parameterization. **⚠ïļ Use with extreme caution!**
132
+ For complex queries involving multiple tables or aliases, use **alias blocks** with keys starting with `$`:
178
133
 
179
134
  ```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:** `sql.raw()` is the library's single, explicit trust boundary — its argument becomes SQL verbatim. Never pass user input to it. For dynamic WHERE clauses use `compileFilter()`; for identifiers use `sql.ident()`. As a safety net, the `sql` tag verifies that the number of `?` placeholders in the final query exactly matches the number of bound values, so a raw fragment that smuggles a stray `?` (or forgets to carry its value) throws instead of producing a misaligned query.
135
+ // Basic alias usage
136
+ const joinFilter = {
137
+ // Main table fields (no prefix)
138
+ status: 'ACTIVE',
139
+ age: { gte: 18, lt: 65 },
185
140
 
186
- ### `sql.join(fragments: SqlFragment[], separator?: string | SqlFragment)`
141
+ // Table alias 't2'
142
+ $t2: {
143
+ column_in_table_2: { gte: 100 },
144
+ 'stats.avg': { lt: 10 }, // JSON path in aliased table
145
+ },
187
146
 
188
- Joins multiple SQL fragments with a separator.
147
+ // Table alias 'orders'
148
+ $orders: {
149
+ amount: { gt: 500 },
150
+ status: { in: ['completed', 'shipped'] },
151
+ },
152
+ }
189
153
 
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] }
154
+ const result = compileFilter(joinFilter)
155
+ // SQL: ((("status" = ?) AND ("age" >= ? AND "age" < ?) AND
156
+ // ((t2."column_in_table_2" >= ?) AND (json_extract(t2."stats", '$.avg') < ?)) AND
157
+ // ((orders."amount" > ?) AND (orders."status" IN (?,?)))))
199
158
  ```
200
159
 
201
- **Fragments** must be library-minted fragments (forged `{ text, values }` objects are rejected).
160
+ **Alias Block Rules:**
202
161
 
203
- **Separators** support any structural connector safely:
162
+ - **Alias names**: Must be valid SQL identifiers (`[A-Za-z_][A-Za-z0-9_]*`)
163
+ - **Prefixing**: All fields in alias blocks get prefixed with `alias.`
164
+ - **JSON paths**: Work seamlessly with aliases: `json_extract(alias."column", '$.path')`
165
+ - **Combination**: Alias blocks are AND-combined with root fields and each other
166
+ - **Order**: Regular fields processed first, then alias blocks
204
167
 
205
- - A **string** separator (e.g. `', '`, `' AND '`, `' UNION ALL '`) is validated to be a pure connector. Separators containing string/identifier quotes (`'`, `"`, `` ` ``, `[`, `]`), statement terminators (`;`), comment markers (`--`, `/*`, `*/`), `NUL`, backslashes, or unbalanced parentheses are rejected — so even an untrusted separator cannot break out of the expression.
206
- - A **`SqlFragment`** separator (e.g. `sql\` OR weight = ${w} OR \``) lets you parameterize the connector itself; its values are interleaved between fragments automatically.
168
+ ```typescript
169
+ // Complex alias example with logical operators
170
+ const complexJoinFilter = {
171
+ // Primary table conditions
172
+ user_status: 'ACTIVE',
207
173
 
208
- ## 🔍 JSON Filter Language
174
+ // User profile table
175
+ $profile: {
176
+ verified: true,
177
+ 'preferences.notifications': { ne: false },
178
+ or: [{ subscription_type: 'premium' }, { credits: { gte: 100 } }],
179
+ },
209
180
 
210
- Build complex WHERE clauses using MongoDB-style JSON filters. Perfect for APIs and dynamic queries.
181
+ // Orders table with complex conditions
182
+ $orders: {
183
+ and: [
184
+ { created_at: { gte: '2024-01-01' } },
185
+ { or: [{ total_amount: { gt: 1000 } }, { item_count: { gte: 5 } }] },
186
+ ],
187
+ },
188
+ }
211
189
 
212
- ### `compileFilter(filter: JsonFilter): FilterResult`
190
+ // Use in JOIN queries
191
+ const query = sql`
192
+ SELECT u.id, u.name, p.subscription_type, o.total_amount
193
+ FROM users u
194
+ JOIN profiles p ON u.id = p.user_id
195
+ JOIN orders o ON u.id = o.user_id
196
+ WHERE ${compileFilter(complexJoinFilter)}
197
+ `
198
+ ```
213
199
 
214
- Compiles a JSON filter object into a parameterized SQL WHERE clause.
200
+ **Security & Validation:**
215
201
 
216
202
  ```typescript
217
- import { compileFilter } from '@truto/sqlite-builder'
203
+ // ✅ Valid alias identifiers
204
+ $users: { name: 'John' } // Simple identifier
205
+ $user_profiles: { age: 25 } // Underscore allowed
206
+ $_temp: { status: 'active' } // Starting underscore allowed
207
+
208
+ // ❌ Invalid alias identifiers (will throw SyntaxError)
209
+ $123invalid: { ... } // Cannot start with number
210
+ $'invalid-alias': { ... } // Hyphens not allowed
211
+ $'table.alias': { ... } // Dots not allowed in alias name
212
+ ```
218
213
 
219
- const filter = {
220
- status: 'ACTIVE',
221
- age: { gte: 18, lt: 65 },
222
- }
214
+ **Integration with Complex Queries:**
215
+
216
+ ```typescript
217
+ // Real-world JOIN example
218
+ const userOrderFilter = {
219
+ // Users table
220
+ active: true,
221
+ email: { exists: true },
223
222
 
224
- const result = compileFilter(filter)
225
- // Returns a branded SQL fragment: { text: '(("status" = ?) AND ("age" >= ?) AND ("age" < ?))', values: ['ACTIVE', 18, 65] }
223
+ // User profiles
224
+ $profiles: {
225
+ 'settings.email_notifications': true,
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@truto/sqlite-builder","version":"2.0.2-canary.10","description":"debug canary","license":"MIT","main":"index.js"}
1
+ {"name":"@truto/sqlite-builder","version":"2.0.2-canary.11","description":"debug canary","license":"MIT","main":"index.js"}