@truto/sqlite-builder 2.0.2-canary.11 → 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 +187 -187
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
1
|
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// Dynamic column selection (simplified with array support)
|
|
3
|
+
const columns = ['id', 'name', 'email']
|
|
4
|
+
const selectQuery = sql`SELECT ${sql.ident(columns)} FROM users`
|
|
5
|
+
|
|
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`
|
|
8
13
|
```
|
|
9
14
|
|
|
10
|
-
###
|
|
15
|
+
### Dynamic Queries with JSON Filters
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
```typescript
|
|
18
|
+
import { sql, compileFilter } from '@truto/sqlite-builder'
|
|
19
|
+
|
|
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 }] } }
|
|
42
|
+
```
|
|
27
43
|
|
|
28
|
-
###
|
|
44
|
+
### Array Identifiers & Qualified Identifiers
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
The `sql.ident()` function supports simple identifiers, qualified identifiers (table.column), arrays, and mixed arrays with SQL fragments:
|
|
31
47
|
|
|
32
48
|
```typescript
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
//
|
|
49
|
+
// ✅ Simple identifiers
|
|
50
|
+
const table = 'users'
|
|
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
|
|
78
|
+
|
|
79
|
+
// ✅ Mixed arrays with parameterized fragments
|
|
80
|
+
const status = 'premium'
|
|
81
|
+
const dynamicColumns = [
|
|
82
|
+
'id',
|
|
83
|
+
'name',
|
|
84
|
+
sql`CASE WHEN status = ${status} THEN 'Premium User' ELSE 'Regular User' END as user_type`,
|
|
85
|
+
sql.raw('created_at'),
|
|
86
|
+
]
|
|
87
|
+
const parameterizedQuery = sql`SELECT ${sql.ident(dynamicColumns)} FROM users WHERE active = ${true}`
|
|
88
|
+
// Combines identifiers, raw SQL, and parameterized values seamlessly
|
|
89
|
+
|
|
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)
|
|
94
|
+
const insertQuery = sql`
|
|
95
|
+
INSERT INTO users (${sql.ident(insertColumns)})
|
|
96
|
+
VALUES (${insertValues[0]}, ${insertValues[1]}, ${insertValues[2]})
|
|
97
|
+
`
|
|
47
98
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
// ✅ Dynamic column selection
|
|
100
|
+
const userFields = ['name', 'email']
|
|
101
|
+
const includeTimestamps = true
|
|
102
|
+
if (includeTimestamps) {
|
|
103
|
+
userFields.push('created_at', 'updated_at')
|
|
52
104
|
}
|
|
53
|
-
|
|
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`
|
|
54
113
|
```
|
|
55
114
|
|
|
56
|
-
|
|
115
|
+
### Complex Joins
|
|
57
116
|
|
|
58
117
|
```typescript
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
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
|
+
`
|
|
65
132
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
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
|
+
`
|
|
72
139
|
```
|
|
73
140
|
|
|
74
|
-
|
|
141
|
+
### Transactions
|
|
75
142
|
|
|
76
143
|
```typescript
|
|
77
|
-
//
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
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" = ?)))
|
|
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
|
+
])
|
|
102
163
|
```
|
|
103
164
|
|
|
104
|
-
|
|
165
|
+
## 🧪 Testing
|
|
105
166
|
|
|
106
|
-
|
|
167
|
+
```bash
|
|
168
|
+
# Run tests
|
|
169
|
+
bun run test
|
|
107
170
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
}
|
|
171
|
+
# Run tests in watch mode
|
|
172
|
+
bun run dev
|
|
173
|
+
|
|
174
|
+
# Run tests with coverage
|
|
175
|
+
bun run test:coverage
|
|
176
|
+
|
|
177
|
+
# Run tests with UI
|
|
178
|
+
bun run test:ui
|
|
128
179
|
```
|
|
129
180
|
|
|
130
|
-
|
|
181
|
+
## 🤝 Contributing
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
133
184
|
|
|
134
|
-
|
|
135
|
-
// Basic alias usage
|
|
136
|
-
const joinFilter = {
|
|
137
|
-
// Main table fields (no prefix)
|
|
138
|
-
status: 'ACTIVE',
|
|
139
|
-
age: { gte: 18, lt: 65 },
|
|
140
|
-
|
|
141
|
-
// Table alias 't2'
|
|
142
|
-
$t2: {
|
|
143
|
-
column_in_table_2: { gte: 100 },
|
|
144
|
-
'stats.avg': { lt: 10 }, // JSON path in aliased table
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
// Table alias 'orders'
|
|
148
|
-
$orders: {
|
|
149
|
-
amount: { gt: 500 },
|
|
150
|
-
status: { in: ['completed', 'shipped'] },
|
|
151
|
-
},
|
|
152
|
-
}
|
|
185
|
+
### Development Setup
|
|
153
186
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
192
|
```
|
|
159
193
|
|
|
160
|
-
|
|
194
|
+
### Release Process
|
|
161
195
|
|
|
162
|
-
|
|
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
|
|
196
|
+
We use [changesets](https://github.com/changesets/changesets) for version management:
|
|
167
197
|
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Primary table conditions
|
|
172
|
-
user_status: 'ACTIVE',
|
|
173
|
-
|
|
174
|
-
// User profile table
|
|
175
|
-
$profile: {
|
|
176
|
-
verified: true,
|
|
177
|
-
'preferences.notifications': { ne: false },
|
|
178
|
-
or: [{ subscription_type: 'premium' }, { credits: { gte: 100 } }],
|
|
179
|
-
},
|
|
180
|
-
|
|
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
|
-
}
|
|
198
|
+
```bash
|
|
199
|
+
# Add a changeset
|
|
200
|
+
bunx changeset
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
JOIN orders o ON u.id = o.user_id
|
|
196
|
-
WHERE ${compileFilter(complexJoinFilter)}
|
|
197
|
-
`
|
|
202
|
+
# Release
|
|
203
|
+
bunx changeset version
|
|
204
|
+
bun run build
|
|
205
|
+
git commit -am "Release"
|
|
206
|
+
git push --follow-tags
|
|
198
207
|
```
|
|
199
208
|
|
|
200
|
-
|
|
209
|
+
## 🔒 Security Policy
|
|
201
210
|
|
|
202
|
-
|
|
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
|
-
```
|
|
211
|
+
If you discover a security vulnerability, please email eng@truto.one or create an issue.
|
|
213
212
|
|
|
214
|
-
|
|
213
|
+
## 📄 License
|
|
215
214
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
215
|
+
MIT © [Truto](https://github.com/trutohq)
|
|
216
|
+
|
|
217
|
+
## 💡 Inspiration
|
|
218
|
+
|
|
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)
|
|
224
|
+
|
|
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"}
|