@truto/sqlite-builder 2.0.2-canary.10 โ 2.0.2-canary.13
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 +173 -173
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
# ๐๏ธ truto-sqlite-builder
|
|
2
1
|
|
|
3
|
-
|
|
4
|
-
[
|
|
5
|
-
|
|
6
|
-
[](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
|
-
```
|
|
2
|
+
// Dynamic column selection (simplified with array support)
|
|
3
|
+
const columns = ['id', 'name', 'email']
|
|
4
|
+
const selectQuery = sql`SELECT ${sql.ident(columns)} FROM users`
|
|
37
5
|
|
|
38
|
-
|
|
39
|
-
|
|
6
|
+
// Alternative approach for more complex column expressions
|
|
7
|
+
const complexColumns = [
|
|
8
|
+
sql.ident('id'),
|
|
9
|
+
sql.ident('name'),
|
|
10
|
+
sql.raw('UPPER(email) as email_upper'),
|
|
11
|
+
]
|
|
12
|
+
const complexQuery = sql`SELECT ${sql.join(complexColumns)} FROM users`
|
|
40
13
|
```
|
|
41
14
|
|
|
42
|
-
|
|
15
|
+
### Dynamic Queries with JSON Filters
|
|
43
16
|
|
|
44
17
|
```typescript
|
|
45
|
-
import sqlite3 from 'better-sqlite3'
|
|
46
18
|
import { sql, compileFilter } from '@truto/sqlite-builder'
|
|
47
19
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
},
|
|
83
|
-
}
|
|
84
|
-
|
|
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
|
-
`
|
|
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] }
|
|
20
|
+
// API endpoint that accepts JSON filter
|
|
21
|
+
app.get('/api/users', (req, res) => {
|
|
22
|
+
// User sends filter as JSON
|
|
23
|
+
const filter = req.body.filter || {}
|
|
24
|
+
|
|
25
|
+
// Safely compile to SQL and interpolate directly. Filter values and the
|
|
26
|
+
// LIMIT value are collected in order into query.values.
|
|
27
|
+
const query = sql`
|
|
28
|
+
SELECT id, name, email, created_at
|
|
29
|
+
FROM users
|
|
30
|
+
WHERE ${compileFilter(filter)}
|
|
31
|
+
ORDER BY created_at DESC
|
|
32
|
+
LIMIT ${req.query.limit || 20}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
const users = db.prepare(query.text).all(...query.values)
|
|
36
|
+
res.json(users)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Example API calls:
|
|
40
|
+
// POST /api/users { "filter": { "status": "ACTIVE", "age": { "gte": 18 } } }
|
|
41
|
+
// POST /api/users { "filter": { "or": [{ "role": "ADMIN" }, { "verified": true }] } }
|
|
103
42
|
```
|
|
104
43
|
|
|
105
|
-
|
|
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)[])`
|
|
44
|
+
### Array Identifiers & Qualified Identifiers
|
|
111
45
|
|
|
112
|
-
|
|
46
|
+
The `sql.ident()` function supports simple identifiers, qualified identifiers (table.column), arrays, and mixed arrays with SQL fragments:
|
|
113
47
|
|
|
114
48
|
```typescript
|
|
115
|
-
//
|
|
49
|
+
// โ
Simple identifiers
|
|
116
50
|
const table = 'users'
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
51
|
+
const column = 'name'
|
|
52
|
+
const simpleQuery = sql`SELECT ${sql.ident(column)} FROM ${sql.ident(table)}`
|
|
53
|
+
// Result: SELECT "name" FROM "users"
|
|
54
|
+
|
|
55
|
+
// โ
Qualified identifiers (table.column)
|
|
56
|
+
const qualifiedQuery = sql`SELECT ${sql.ident('u.name')}, ${sql.ident('p.title')} FROM users u JOIN posts p ON u.id = p.user_id`
|
|
57
|
+
// Result: SELECT "u"."name", "p"."title" FROM users u JOIN posts p ON u.id = p.user_id
|
|
58
|
+
|
|
59
|
+
// โ
Pure identifier arrays (clean and concise)
|
|
60
|
+
const columns = ['id', 'name', 'email', 'created_at']
|
|
61
|
+
const arrayQuery = sql`SELECT ${sql.ident(columns)} FROM users`
|
|
62
|
+
// Result: SELECT "id", "name", "email", "created_at" FROM users
|
|
63
|
+
|
|
64
|
+
// โ
Mixed qualified and simple identifiers in arrays
|
|
65
|
+
const mixedColumns = ['u.id', 'name', 'u.email', 'p.title', 'created_at']
|
|
66
|
+
const mixedQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users u LEFT JOIN posts p ON u.id = p.user_id`
|
|
67
|
+
// Result: SELECT "u"."id", "name", "u"."email", "p"."title", "created_at" FROM users u LEFT JOIN posts p ON u.id = p.user_id
|
|
68
|
+
|
|
69
|
+
// โ
NEW: Mixed arrays with identifiers and SQL fragments
|
|
70
|
+
const mixedColumns = [
|
|
71
|
+
'id',
|
|
72
|
+
'name',
|
|
73
|
+
sql.raw('UPPER(email) as email_upper'),
|
|
74
|
+
sql.raw('COUNT(*) as total'),
|
|
75
|
+
]
|
|
76
|
+
const mixedQuery = sql`SELECT ${sql.ident(mixedColumns)} FROM users GROUP BY id, name, email`
|
|
77
|
+
// Result: SELECT "id", "name", UPPER(email) as email_upper, COUNT(*) as total FROM users GROUP BY id, name, email
|
|
138
78
|
|
|
139
|
-
// Mixed arrays with parameterized fragments
|
|
79
|
+
// โ
Mixed arrays with parameterized fragments
|
|
140
80
|
const status = 'premium'
|
|
141
81
|
const dynamicColumns = [
|
|
142
82
|
'id',
|
|
143
83
|
'name',
|
|
144
|
-
sql`CASE WHEN status = ${status} THEN 'Premium' ELSE 'Regular' END as user_type`,
|
|
84
|
+
sql`CASE WHEN status = ${status} THEN 'Premium User' ELSE 'Regular User' END as user_type`,
|
|
85
|
+
sql.raw('created_at'),
|
|
145
86
|
]
|
|
146
|
-
const
|
|
147
|
-
//
|
|
87
|
+
const parameterizedQuery = sql`SELECT ${sql.ident(dynamicColumns)} FROM users WHERE active = ${true}`
|
|
88
|
+
// Combines identifiers, raw SQL, and parameterized values seamlessly
|
|
148
89
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
90
|
+
// โ
Works great for INSERT statements
|
|
91
|
+
const insertData = { name: 'John', email: 'john@example.com', age: 30 }
|
|
92
|
+
const insertColumns = Object.keys(insertData)
|
|
93
|
+
const insertValues = Object.values(insertData)
|
|
151
94
|
const insertQuery = sql`
|
|
152
95
|
INSERT INTO users (${sql.ident(insertColumns)})
|
|
153
|
-
VALUES (${
|
|
96
|
+
VALUES (${insertValues[0]}, ${insertValues[1]}, ${insertValues[2]})
|
|
154
97
|
`
|
|
155
|
-
|
|
98
|
+
|
|
99
|
+
// โ
Dynamic column selection
|
|
100
|
+
const userFields = ['name', 'email']
|
|
101
|
+
const includeTimestamps = true
|
|
102
|
+
if (includeTimestamps) {
|
|
103
|
+
userFields.push('created_at', 'updated_at')
|
|
104
|
+
}
|
|
105
|
+
const dynamicQuery = sql`SELECT ${sql.ident(userFields)} FROM users`
|
|
106
|
+
|
|
107
|
+
// ๐ OLD: Manual joining approach (still works, but much more verbose)
|
|
108
|
+
const oldWay = sql`SELECT ${sql.join([
|
|
109
|
+
sql.ident('id'),
|
|
110
|
+
sql.ident('name'),
|
|
111
|
+
sql.raw('UPPER(email) as email_upper'),
|
|
112
|
+
])} FROM users`
|
|
156
113
|
```
|
|
157
114
|
|
|
158
|
-
|
|
115
|
+
### Complex Joins
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const getUsersWithPosts = sql`
|
|
119
|
+
SELECT
|
|
120
|
+
${sql.ident('u')}.id,
|
|
121
|
+
${sql.ident('u')}.name,
|
|
122
|
+
COUNT(${sql.ident('p')}.id) as post_count
|
|
123
|
+
FROM ${sql.ident('users')} u
|
|
124
|
+
LEFT JOIN ${sql.ident('posts')} p ON u.id = p.user_id
|
|
125
|
+
WHERE u.created_at > ${startDate}
|
|
126
|
+
AND u.status IN ${sql.in(['active', 'premium'])}
|
|
127
|
+
GROUP BY u.id
|
|
128
|
+
HAVING post_count > ${minPosts}
|
|
129
|
+
ORDER BY post_count DESC
|
|
130
|
+
LIMIT ${limit}
|
|
131
|
+
`
|
|
159
132
|
|
|
160
|
-
|
|
133
|
+
// For simpler cases, you can use array identifiers directly
|
|
134
|
+
const getUsers = sql`
|
|
135
|
+
SELECT ${sql.ident(['id', 'name', 'email', 'created_at'])}
|
|
136
|
+
FROM ${sql.ident('users')}
|
|
137
|
+
WHERE status = ${'active'}
|
|
138
|
+
`
|
|
139
|
+
```
|
|
161
140
|
|
|
162
|
-
|
|
141
|
+
### Transactions
|
|
163
142
|
|
|
164
143
|
```typescript
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
144
|
+
// Works great with better-sqlite3 transactions
|
|
145
|
+
const insertUsers = db.transaction((users) => {
|
|
146
|
+
const stmt = db.prepare(
|
|
147
|
+
sql`
|
|
148
|
+
INSERT INTO users (name, email)
|
|
149
|
+
VALUES (?, ?)
|
|
150
|
+
`.text,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
for (const user of users) {
|
|
154
|
+
const { values } = sql`${user.name}, ${user.email}`
|
|
155
|
+
stmt.run(...values)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
insertUsers([
|
|
160
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
161
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
162
|
+
])
|
|
168
163
|
```
|
|
169
164
|
|
|
170
|
-
|
|
165
|
+
## ๐งช Testing
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
```bash
|
|
168
|
+
# Run tests
|
|
169
|
+
bun run test
|
|
174
170
|
|
|
175
|
-
|
|
171
|
+
# Run tests in watch mode
|
|
172
|
+
bun run dev
|
|
176
173
|
|
|
177
|
-
|
|
174
|
+
# Run tests with coverage
|
|
175
|
+
bun run test:coverage
|
|
178
176
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Returns: { text: 'SELECT * FROM users WHERE created_at > datetime("now", "-1 day")', values: [] }
|
|
177
|
+
# Run tests with UI
|
|
178
|
+
bun run test:ui
|
|
182
179
|
```
|
|
183
180
|
|
|
184
|
-
|
|
181
|
+
## ๐ค Contributing
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
187
184
|
|
|
188
|
-
|
|
185
|
+
### Development Setup
|
|
189
186
|
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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] }
|
|
187
|
+
```bash
|
|
188
|
+
git clone https://github.com/truto/truto-sqlite-builder.git
|
|
189
|
+
cd truto-sqlite-builder
|
|
190
|
+
bun install
|
|
191
|
+
bun run dev # Start tests in watch mode
|
|
199
192
|
```
|
|
200
193
|
|
|
201
|
-
|
|
194
|
+
### Release Process
|
|
202
195
|
|
|
203
|
-
|
|
196
|
+
We use [changesets](https://github.com/changesets/changesets) for version management:
|
|
204
197
|
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
```bash
|
|
199
|
+
# Add a changeset
|
|
200
|
+
bunx changeset
|
|
201
|
+
|
|
202
|
+
# Release
|
|
203
|
+
bunx changeset version
|
|
204
|
+
bun run build
|
|
205
|
+
git commit -am "Release"
|
|
206
|
+
git push --follow-tags
|
|
207
|
+
```
|
|
207
208
|
|
|
208
|
-
##
|
|
209
|
+
## ๐ Security Policy
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
If you discover a security vulnerability, please email eng@truto.one or create an issue.
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
## ๐ License
|
|
213
214
|
|
|
214
|
-
|
|
215
|
+
MIT ยฉ [Truto](https://github.com/trutohq)
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
import { compileFilter } from '@truto/sqlite-builder'
|
|
217
|
+
## ๐ก Inspiration
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
This library was inspired by:
|
|
220
|
+
|
|
221
|
+
- [sql-template-strings](https://github.com/felixfbecker/sql-template-strings)
|
|
222
|
+
- [slonik](https://github.com/gajus/slonik)
|
|
223
|
+
- [Postgres.js](https://github.com/porsager/postgres)
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
// Returns a branded SQL fragment: { text: '(("status" = ?) AND ("age" >= ?) AND ("age" < ?))', values: ['ACTIVE', 18, 65] }
|
|
225
|
+
Built with โค๏ธ for the SQLite community.
|
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.13","description":"debug canary","license":"MIT","main":"index.js"}
|