@truto/sqlite-builder 2.0.2-canary.0 → 2.0.2-canary.11
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
|
+
|
|
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
|
+
`
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### Supported Operators
|
|
11
|
+
|
|
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 |
|
|
27
|
+
|
|
28
|
+
### Filter Examples
|
|
29
|
+
|
|
30
|
+
#### Basic Operations
|
|
31
|
+
|
|
32
|
+
```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 (?,?)))
|
|
47
|
+
|
|
48
|
+
// NULL checks
|
|
49
|
+
const filter3 = {
|
|
50
|
+
email: { exists: true }, // IS NOT NULL
|
|
51
|
+
deleted_at: { exists: false }, // IS NULL
|
|
52
|
+
}
|
|
53
|
+
// SQL: (("email" IS NOT NULL AND "deleted_at" IS NULL))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Pattern Matching
|
|
57
|
+
|
|
58
|
+
```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))
|
|
65
|
+
|
|
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 ?))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Logical Operators
|
|
75
|
+
|
|
76
|
+
```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" = ?)))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### JSON Path Querying
|
|
105
|
+
|
|
106
|
+
For SQLite JSON columns, use dot notation to query nested fields:
|
|
107
|
+
|
|
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
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Qualified Filters for JOINs
|
|
131
|
+
|
|
132
|
+
For complex queries involving multiple tables or aliases, use **alias blocks** with keys starting with `$`:
|
|
133
|
+
|
|
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
|
+
}
|
|
153
|
+
|
|
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 (?,?)))))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Alias Block Rules:**
|
|
161
|
+
|
|
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
|
|
167
|
+
|
|
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
|
+
}
|
|
189
|
+
|
|
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
|
+
`
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Security & Validation:**
|
|
201
|
+
|
|
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
|
+
```
|
|
213
|
+
|
|
214
|
+
**Integration with Complex Queries:**
|
|
215
|
+
|
|
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,
|
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.11","description":"debug canary","license":"MIT","main":"index.js"}
|