@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.
Files changed (2) hide show
  1. package/README.md +173 -173
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,225 +1,225 @@
1
- # ๐Ÿ—๏ธ truto-sqlite-builder
2
1
 
3
- [![npm version](https://badge.fury.io/js/%40truto%2Fsqlite-builder.svg)](https://badge.fury.io/js/%40truto%2Fsqlite-builder)
4
- [![CI](https://github.com/trutohq/truto-sqlite-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/trutohq/truto-sqlite-builder/actions/workflows/ci.yml)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ```bash
39
- pnpm add @truto/sqlite-builder
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
- ## ๐Ÿš€ Quick Start
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
- 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] }
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
- **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)[])`
44
+ ### Array Identifiers & Qualified Identifiers
111
45
 
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.
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
- // Single identifier
49
+ // โœ… Simple identifiers
116
50
  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: [] }
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 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'] }
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
- // In INSERT statements
150
- const insertColumns = ['name', 'email', 'age']
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 (${name}, ${email}, ${age})
96
+ VALUES (${insertValues[0]}, ${insertValues[1]}, ${insertValues[2]})
154
97
  `
155
- // Returns: { text: 'INSERT INTO users ("name", "email", "age") VALUES (?, ?, ?)', values: [name, email, age] }
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
- **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.
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
- ### `sql.in(array: readonly unknown[])`
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
- Creates parameterized IN clauses from arrays.
141
+ ### Transactions
163
142
 
164
143
  ```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] }
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
- **Features:**
165
+ ## ๐Ÿงช Testing
171
166
 
172
- - Rejects empty arrays (would create invalid SQL)
173
- - Warns for arrays with >1000 items (performance consideration)
167
+ ```bash
168
+ # Run tests
169
+ bun run test
174
170
 
175
- ### `sql.raw(rawSql: string)`
171
+ # Run tests in watch mode
172
+ bun run dev
176
173
 
177
- Embeds raw SQL without parameterization. **โš ๏ธ Use with extreme caution!**
174
+ # Run tests with coverage
175
+ bun run test:coverage
178
176
 
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: [] }
177
+ # Run tests with UI
178
+ bun run test:ui
182
179
  ```
183
180
 
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.
181
+ ## ๐Ÿค Contributing
185
182
 
186
- ### `sql.join(fragments: SqlFragment[], separator?: string | SqlFragment)`
183
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
187
184
 
188
- Joins multiple SQL fragments with a separator.
185
+ ### Development Setup
189
186
 
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] }
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
- **Fragments** must be library-minted fragments (forged `{ text, values }` objects are rejected).
194
+ ### Release Process
202
195
 
203
- **Separators** support any structural connector safely:
196
+ We use [changesets](https://github.com/changesets/changesets) for version management:
204
197
 
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.
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
- ## ๐Ÿ” JSON Filter Language
209
+ ## ๐Ÿ”’ Security Policy
209
210
 
210
- Build complex WHERE clauses using MongoDB-style JSON filters. Perfect for APIs and dynamic queries.
211
+ If you discover a security vulnerability, please email eng@truto.one or create an issue.
211
212
 
212
- ### `compileFilter(filter: JsonFilter): FilterResult`
213
+ ## ๐Ÿ“„ License
213
214
 
214
- Compiles a JSON filter object into a parameterized SQL WHERE clause.
215
+ MIT ยฉ [Truto](https://github.com/trutohq)
215
216
 
216
- ```typescript
217
- import { compileFilter } from '@truto/sqlite-builder'
217
+ ## ๐Ÿ’ก Inspiration
218
218
 
219
- const filter = {
220
- status: 'ACTIVE',
221
- age: { gte: 18, lt: 65 },
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
- const result = compileFilter(filter)
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.10","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"}