@truto/sqlite-builder 2.0.2-canary.0 â 2.0.2-canary.10
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 +225 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# ðïļ truto-sqlite-builder
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40truto%2Fsqlite-builder)
|
|
4
|
+
[](https://github.com/trutohq/truto-sqlite-builder/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
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
|
+
```
|
|
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
|
+
// 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
|
+
},
|
|
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] }
|
|
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, 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.
|
|
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:** `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.
|
|
185
|
+
|
|
186
|
+
### `sql.join(fragments: SqlFragment[], separator?: string | SqlFragment)`
|
|
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
|
+
**Fragments** must be library-minted fragments (forged `{ text, values }` objects are rejected).
|
|
202
|
+
|
|
203
|
+
**Separators** support any structural connector safely:
|
|
204
|
+
|
|
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.
|
|
207
|
+
|
|
208
|
+
## ð JSON Filter Language
|
|
209
|
+
|
|
210
|
+
Build complex WHERE clauses using MongoDB-style JSON filters. Perfect for APIs and dynamic queries.
|
|
211
|
+
|
|
212
|
+
### `compileFilter(filter: JsonFilter): FilterResult`
|
|
213
|
+
|
|
214
|
+
Compiles a JSON filter object into a parameterized SQL WHERE clause.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { compileFilter } from '@truto/sqlite-builder'
|
|
218
|
+
|
|
219
|
+
const filter = {
|
|
220
|
+
status: 'ACTIVE',
|
|
221
|
+
age: { gte: 18, lt: 65 },
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = compileFilter(filter)
|
|
225
|
+
// Returns a branded SQL fragment: { text: '(("status" = ?) AND ("age" >= ?) AND ("age" < ?))', values: ['ACTIVE', 18, 65] }
|
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.10","description":"debug canary","license":"MIT","main":"index.js"}
|