@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.
Files changed (2) hide show
  1. package/README.md +187 -187
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,225 +1,225 @@
1
1
 
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
- `
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
- ### Supported Operators
15
+ ### Dynamic Queries with JSON Filters
11
16
 
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 |
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
- ### Filter Examples
44
+ ### Array Identifiers & Qualified Identifiers
29
45
 
30
- #### Basic Operations
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
- // Equality and comparison
34
- const filter1 = {
35
- status: 'ACTIVE',
36
- age: { gte: 18, lt: 65 },
37
- score: { gt: 80, lte: 100 },
38
- }
39
- // SQL: (("status" = ? AND "age" >= ? AND "age" < ? AND "score" > ? AND "score" <= ?))
40
-
41
- // Set membership
42
- const filter2 = {
43
- role: { in: ['ADMIN', 'EDITOR'] },
44
- department: { nin: ['ARCHIVED', 'DELETED'] },
45
- }
46
- // SQL: (("role" IN (?,?) AND "department" NOT IN (?,?)))
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
- // NULL checks
49
- const filter3 = {
50
- email: { exists: true }, // IS NOT NULL
51
- deleted_at: { exists: false }, // IS NULL
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
- // SQL: (("email" IS NOT NULL AND "deleted_at" IS NULL))
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
- #### Pattern Matching
115
+ ### Complex Joins
57
116
 
58
117
  ```typescript
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))
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
- // 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 ?))
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
- #### Logical Operators
141
+ ### Transactions
75
142
 
76
143
  ```typescript
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" >= ?)))
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
- #### JSON Path Querying
165
+ ## 🧪 Testing
105
166
 
106
- For SQLite JSON columns, use dot notation to query nested fields:
167
+ ```bash
168
+ # Run tests
169
+ bun run test
107
170
 
108
- ```typescript
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
- }
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
- #### Qualified Filters for JOINs
181
+ ## 🤝 Contributing
131
182
 
132
- For complex queries involving multiple tables or aliases, use **alias blocks** with keys starting with `$`:
183
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
133
184
 
134
- ```typescript
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
- 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 (?,?)))))
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
- **Alias Block Rules:**
194
+ ### Release Process
161
195
 
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
196
+ We use [changesets](https://github.com/changesets/changesets) for version management:
167
197
 
168
- ```typescript
169
- // Complex alias example with logical operators
170
- const complexJoinFilter = {
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
- // 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
- `
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
- **Security & Validation:**
209
+ ## 🔒 Security Policy
201
210
 
202
- ```typescript
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
- **Integration with Complex Queries:**
213
+ ## 📄 License
215
214
 
216
- ```typescript
217
- // Real-world JOIN example
218
- const userOrderFilter = {
219
- // Users table
220
- active: true,
221
- email: { exists: true },
222
-
223
- // User profiles
224
- $profiles: {
225
- 'settings.email_notifications': true,
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.11","description":"debug canary","license":"MIT","main":"index.js"}
1
+ {"name":"@truto/sqlite-builder","version":"2.0.2-canary.13","description":"debug canary","license":"MIT","main":"index.js"}