@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.
- package/README.md +179 -179
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
# ðïļ truto-sqlite-builder
|
|
2
1
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
31
|
-
npm install @truto/sqlite-builder
|
|
32
|
-
```
|
|
10
|
+
### Supported Operators
|
|
33
11
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
pnpm add @truto/sqlite-builder
|
|
40
|
-
```
|
|
28
|
+
### Filter Examples
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
#### Basic Operations
|
|
43
31
|
|
|
44
32
|
```typescript
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
37
|
+
score: { gt: 80, lte: 100 },
|
|
60
38
|
}
|
|
39
|
+
// SQL: (("status" = ? AND "age" >= ? AND "age" < ? AND "score" > ? AND "score" <= ?))
|
|
61
40
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
### `sql` Tagged Template
|
|
97
|
-
|
|
98
|
-
The main function for building SQL queries.
|
|
56
|
+
#### Pattern Matching
|
|
99
57
|
|
|
100
58
|
```typescript
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
#### Logical Operators
|
|
113
75
|
|
|
114
76
|
```typescript
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
#### JSON Path Querying
|
|
161
105
|
|
|
162
|
-
|
|
106
|
+
For SQLite JSON columns, use dot notation to query nested fields:
|
|
163
107
|
|
|
164
108
|
```typescript
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
+
For complex queries involving multiple tables or aliases, use **alias blocks** with keys starting with `$`:
|
|
178
133
|
|
|
179
134
|
```typescript
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
// Table alias 'orders'
|
|
148
|
+
$orders: {
|
|
149
|
+
amount: { gt: 500 },
|
|
150
|
+
status: { in: ['completed', 'shipped'] },
|
|
151
|
+
},
|
|
152
|
+
}
|
|
189
153
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
**
|
|
160
|
+
**Alias Block Rules:**
|
|
202
161
|
|
|
203
|
-
**
|
|
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
|
-
|
|
206
|
-
|
|
168
|
+
```typescript
|
|
169
|
+
// Complex alias example with logical operators
|
|
170
|
+
const complexJoinFilter = {
|
|
171
|
+
// Primary table conditions
|
|
172
|
+
user_status: 'ACTIVE',
|
|
207
173
|
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
**Security & Validation:**
|
|
215
201
|
|
|
216
202
|
```typescript
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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.
|
|
1
|
+
{"name":"@truto/sqlite-builder","version":"2.0.2-canary.11","description":"debug canary","license":"MIT","main":"index.js"}
|