@pineliner/odb-client 1.0.6 → 1.0.7
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/dist/core/http-client.d.ts.map +1 -1
- package/dist/database/adapters/bun-sqlite.d.ts.map +1 -1
- package/dist/database/adapters/libsql.d.ts.map +1 -1
- package/dist/database/adapters/odblite.d.ts +2 -1
- package/dist/database/adapters/odblite.d.ts.map +1 -1
- package/dist/database/index.d.ts +2 -0
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/sql-template.d.ts +432 -0
- package/dist/database/sql-template.d.ts.map +1 -0
- package/dist/database/sql-template.examples.d.ts +28 -0
- package/dist/database/sql-template.examples.d.ts.map +1 -0
- package/dist/database/types.d.ts +8 -1
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +1861 -1669
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +779 -653
- package/dist/orm/index.d.ts +228 -0
- package/dist/orm/index.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/http-client.ts +1 -0
- package/src/database/adapters/bun-sqlite.ts +73 -15
- package/src/database/adapters/libsql.ts +73 -15
- package/src/database/adapters/odblite.ts +87 -23
- package/src/database/index.ts +4 -0
- package/src/database/sql-template.examples.ts +363 -0
- package/src/database/sql-template.ts +660 -0
- package/src/database/types.ts +15 -3
- package/src/index.ts +31 -0
- package/src/orm/index.ts +538 -0
- package/src/types.ts +2 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Template Tag Converter - postgres.js Compatible
|
|
3
|
+
*
|
|
4
|
+
* Provides postgres.js-style SQL template literals and helper methods
|
|
5
|
+
* for safe, composable SQL query building.
|
|
6
|
+
*
|
|
7
|
+
* ## Basic Usage
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { getConnection, sql } from '@pineliner/odb-client'
|
|
11
|
+
*
|
|
12
|
+
* const db = await getConnection('tenant-id')
|
|
13
|
+
*
|
|
14
|
+
* // Simple query with parameters
|
|
15
|
+
* const users = await db`SELECT * FROM users WHERE id = ${userId}`
|
|
16
|
+
*
|
|
17
|
+
* // Use sql() helper for dynamic parts
|
|
18
|
+
* const columns = ['name', 'email', 'age']
|
|
19
|
+
* const result = await db`SELECT ${sql(columns)} FROM users`
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## Supported Patterns (postgres.js Compatible)
|
|
23
|
+
*
|
|
24
|
+
* ### 1. Query Parameters (Basic Template Literals)
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // Parameters are automatically escaped
|
|
27
|
+
* await db`SELECT * FROM users WHERE name = ${name} AND age > ${age}`
|
|
28
|
+
* // → SELECT * FROM users WHERE name = ? AND age = ? [name, age]
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* ### 2. Dynamic Column Selection
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const columns = ['name', 'age']
|
|
34
|
+
*
|
|
35
|
+
* await db`
|
|
36
|
+
* SELECT ${sql(columns)}
|
|
37
|
+
* FROM users
|
|
38
|
+
* `
|
|
39
|
+
* // → SELECT name, age FROM users
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* ### 3. Dynamic Inserts (Single Object)
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const user = {
|
|
45
|
+
* name: 'Murray',
|
|
46
|
+
* age: 68
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* await db`INSERT INTO users ${sql(user, 'name', 'age')}`
|
|
50
|
+
* // → INSERT INTO users (name, age) VALUES (?, ?)
|
|
51
|
+
* // ["Murray", 68]
|
|
52
|
+
*
|
|
53
|
+
* // Columns can also be given with an array
|
|
54
|
+
* const columns = ['name', 'age']
|
|
55
|
+
* await db`INSERT INTO users ${sql(user, columns)}`
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* ### 4. Bulk Inserts (Array of Objects)
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const users = [
|
|
61
|
+
* { name: 'Murray', age: 68, garbage: 'ignore' },
|
|
62
|
+
* { name: 'Walter', age: 80 }
|
|
63
|
+
* ]
|
|
64
|
+
*
|
|
65
|
+
* await db`INSERT INTO users ${sql(users, 'name', 'age')}`
|
|
66
|
+
* // → INSERT INTO users (name, age) VALUES (?, ?), (?, ?)
|
|
67
|
+
* // ["Murray", 68, "Walter", 80]
|
|
68
|
+
*
|
|
69
|
+
* // Omit column names to use all object keys
|
|
70
|
+
* await db`INSERT INTO users ${sql(users)}`
|
|
71
|
+
* // → INSERT INTO users (name, age, garbage) VALUES (?, ?, ?), (?, ?, ?)
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* ### 5. Dynamic Column Updates
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const user = {
|
|
77
|
+
* id: 1,
|
|
78
|
+
* name: 'Murray',
|
|
79
|
+
* age: 68
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* await db`
|
|
83
|
+
* UPDATE users
|
|
84
|
+
* SET ${set(user, 'name', 'age')}
|
|
85
|
+
* WHERE user_id = ${user.id}
|
|
86
|
+
* `
|
|
87
|
+
* // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
|
|
88
|
+
* // ["Murray", 68, 1]
|
|
89
|
+
*
|
|
90
|
+
* // Or using set() helper
|
|
91
|
+
* await db`
|
|
92
|
+
* UPDATE users
|
|
93
|
+
* SET ${set({ name: user.name, age: user.age })}
|
|
94
|
+
* WHERE user_id = ${user.id}
|
|
95
|
+
* `
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* ### 6. Multiple Updates in One Query
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const users = [
|
|
101
|
+
* [1, 'John', 34],
|
|
102
|
+
* [2, 'Jane', 27],
|
|
103
|
+
* ]
|
|
104
|
+
*
|
|
105
|
+
* await db`
|
|
106
|
+
* UPDATE users
|
|
107
|
+
* SET name = update_data.name, age = (update_data.age)::int
|
|
108
|
+
* FROM (VALUES ${sql(users)}) AS update_data (id, name, age)
|
|
109
|
+
* WHERE users.id = (update_data.id)::int
|
|
110
|
+
* RETURNING users.id, users.name, users.age
|
|
111
|
+
* `
|
|
112
|
+
* // → UPDATE users SET ... FROM (VALUES (?, ?, ?), (?, ?, ?)) AS update_data ...
|
|
113
|
+
* // [1, "John", 34, 2, "Jane", 27]
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* ### 7. Dynamic WHERE IN Clause
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const users = await db`
|
|
119
|
+
* SELECT *
|
|
120
|
+
* FROM users
|
|
121
|
+
* WHERE age IN ${sql([68, 75, 23])}
|
|
122
|
+
* `
|
|
123
|
+
* // → SELECT * FROM users WHERE age IN (?, ?, ?)
|
|
124
|
+
* // [68, 75, 23]
|
|
125
|
+
*
|
|
126
|
+
* // Or use array directly in template
|
|
127
|
+
* await db`SELECT * FROM users WHERE age IN ${[68, 75, 23]}`
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* ### 8. Dynamic Values in SELECT
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const [{ a, b, c }] = await db`
|
|
133
|
+
* SELECT *
|
|
134
|
+
* FROM (VALUES ${sql(['a', 'b', 'c'])}) AS x(a, b, c)
|
|
135
|
+
* `
|
|
136
|
+
* // Note: For VALUES with string literals, use the array directly or wrap in sql()
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* ### 9. Conditional Fragments
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const isAdmin = true
|
|
142
|
+
* const minAge = 25
|
|
143
|
+
*
|
|
144
|
+
* await db`
|
|
145
|
+
* SELECT * FROM users
|
|
146
|
+
* WHERE is_active = 1
|
|
147
|
+
* ${isAdmin ? fragment`AND role = 'admin'` : empty()}
|
|
148
|
+
* ${minAge ? fragment`AND age >= ${minAge}` : empty()}
|
|
149
|
+
* `
|
|
150
|
+
* // → SELECT * FROM users WHERE is_active = 1 AND role = 'admin' AND age >= ?
|
|
151
|
+
* // [25]
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* ### 10. Transactions
|
|
155
|
+
* ```typescript
|
|
156
|
+
* await db.begin(async tx => {
|
|
157
|
+
* await tx`INSERT INTO users ${sql(user, 'name', 'email')}`
|
|
158
|
+
* await tx`UPDATE accounts SET balance = balance - ${amount} WHERE user_id = ${userId}`
|
|
159
|
+
* })
|
|
160
|
+
* // Automatically commits on success, rolls back on error
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* ### 11. Raw SQL (use with caution)
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const tableName = 'users' // From trusted source only!
|
|
166
|
+
* await db`SELECT * FROM ${raw(tableName)} WHERE id = ${userId}`
|
|
167
|
+
* // → SELECT * FROM users WHERE id = ?
|
|
168
|
+
* // [userId]
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* ### 12. Reusable Fragments
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const activeUsers = fragment`is_active = 1 AND is_deleted = 0`
|
|
174
|
+
* const olderThan = (age: number) => fragment`age > ${age}`
|
|
175
|
+
*
|
|
176
|
+
* await db`
|
|
177
|
+
* SELECT * FROM users
|
|
178
|
+
* WHERE ${activeUsers}
|
|
179
|
+
* AND ${olderThan(25)}
|
|
180
|
+
* `
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* ## API Reference
|
|
184
|
+
*
|
|
185
|
+
* - `sql(columns: string[])` - Dynamic column list for SELECT
|
|
186
|
+
* - `sql(object, ...keys)` - INSERT format from single object
|
|
187
|
+
* - `sql(objects[], ...keys)` - Bulk INSERT from array of objects
|
|
188
|
+
* - `sql(array[][])` - Bulk insert values (2D array)
|
|
189
|
+
* - `fragment` `` ` - Template tag for reusable query fragments
|
|
190
|
+
* - `raw(string)` - Unescaped SQL fragment (dangerous!)
|
|
191
|
+
* - `empty()` - Empty SQL fragment for conditionals
|
|
192
|
+
* - `set(object)` - Generate SET clause for UPDATE
|
|
193
|
+
* - `where(object)` - Generate WHERE clause with AND conditions
|
|
194
|
+
* - `join(fragments[], separator)` - Combine multiple fragments
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
export interface SqlQuery {
|
|
198
|
+
sql: string
|
|
199
|
+
args: any[]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface SqlFragment {
|
|
203
|
+
_isSqlFragment: true
|
|
204
|
+
sql: string
|
|
205
|
+
args: any[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Convert template literal to parameterized query
|
|
210
|
+
*
|
|
211
|
+
* @param strings - Template literal strings
|
|
212
|
+
* @param values - Template literal values
|
|
213
|
+
* @returns Object with sql string and args array
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const query = convertTemplateToQuery(
|
|
218
|
+
* ['SELECT * FROM users WHERE id = ', ' AND name = ', ''],
|
|
219
|
+
* [userId, userName]
|
|
220
|
+
* )
|
|
221
|
+
* // → { sql: "SELECT * FROM users WHERE id = ? AND name = ?", args: [userId, userName] }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export function convertTemplateToQuery(
|
|
225
|
+
strings: TemplateStringsArray,
|
|
226
|
+
values: any[]
|
|
227
|
+
): SqlQuery {
|
|
228
|
+
let sql = ''
|
|
229
|
+
const args: any[] = []
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < strings.length; i++) {
|
|
232
|
+
sql += strings[i]
|
|
233
|
+
|
|
234
|
+
if (i < values.length) {
|
|
235
|
+
const value = values[i]
|
|
236
|
+
|
|
237
|
+
// Handle SQL fragments (from sql`...` or sql())
|
|
238
|
+
if (isSqlFragment(value)) {
|
|
239
|
+
sql += value.sql
|
|
240
|
+
args.push(...value.args)
|
|
241
|
+
}
|
|
242
|
+
// Handle arrays for IN clauses or bulk inserts
|
|
243
|
+
else if (Array.isArray(value)) {
|
|
244
|
+
// Check if it's a 2D array (bulk insert)
|
|
245
|
+
if (value.length > 0 && Array.isArray(value[0])) {
|
|
246
|
+
// Bulk insert: [[1, 'John'], [2, 'Jane']]
|
|
247
|
+
const rowPlaceholders = value.map(row =>
|
|
248
|
+
`(${row.map(() => '?').join(', ')})`
|
|
249
|
+
).join(', ')
|
|
250
|
+
sql += rowPlaceholders
|
|
251
|
+
args.push(...value.flat())
|
|
252
|
+
} else {
|
|
253
|
+
// Simple array for IN clause: [1, 2, 3]
|
|
254
|
+
const placeholders = value.map(() => '?').join(', ')
|
|
255
|
+
sql += `(${placeholders})`
|
|
256
|
+
args.push(...value)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Regular parameterized value
|
|
260
|
+
else {
|
|
261
|
+
sql += '?'
|
|
262
|
+
args.push(value)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { sql, args }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if value is a SQL fragment
|
|
272
|
+
*/
|
|
273
|
+
function isSqlFragment(value: any): value is SqlFragment {
|
|
274
|
+
return value && typeof value === 'object' && value._isSqlFragment === true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* sql() helper - postgres.js compatible
|
|
279
|
+
*
|
|
280
|
+
* Multi-purpose SQL helper that adapts based on arguments.
|
|
281
|
+
*
|
|
282
|
+
* @param value - Array, object, array of objects, or 2D array
|
|
283
|
+
* @param keys - Optional keys (string[] or ...string)
|
|
284
|
+
* @returns SQL fragment
|
|
285
|
+
*
|
|
286
|
+
* @example Dynamic column selection
|
|
287
|
+
* ```typescript
|
|
288
|
+
* sql(['name', 'email', 'age'])
|
|
289
|
+
* // → "name, email, age"
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @example Single object INSERT
|
|
293
|
+
* ```typescript
|
|
294
|
+
* sql({ name: 'John', age: 30 }, 'name', 'age')
|
|
295
|
+
* // → "(name, age) VALUES (?, ?)" with args ["John", 30]
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* @example Single object UPDATE
|
|
299
|
+
* ```typescript
|
|
300
|
+
* sql({ name: 'John', age: 30 }, 'name', 'age')
|
|
301
|
+
* // Context determines if INSERT or UPDATE format
|
|
302
|
+
* // For UPDATE: "name" = ?, "age" = ? with args ["John", 30]
|
|
303
|
+
* // For INSERT: (name, age) VALUES (?, ?) with args ["John", 30]
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* @example Array of objects (bulk insert)
|
|
307
|
+
* ```typescript
|
|
308
|
+
* sql([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }], 'name', 'age')
|
|
309
|
+
* // → "(name, age) VALUES (?, ?), (?, ?)" with args ["John", 30, "Jane", 25]
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example 2D array (bulk insert values)
|
|
313
|
+
* ```typescript
|
|
314
|
+
* sql([[1, 'John'], [2, 'Jane']])
|
|
315
|
+
* // → "(?, ?), (?, ?)" with args [1, "John", 2, "Jane"]
|
|
316
|
+
* ```
|
|
317
|
+
*
|
|
318
|
+
* @example VALUES clause
|
|
319
|
+
* ```typescript
|
|
320
|
+
* sql(['a', 'b', 'c'])
|
|
321
|
+
* // In SELECT context: "a, b, c"
|
|
322
|
+
* // In VALUES context: You'd use the array directly in template
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
export function sql(
|
|
326
|
+
value: any,
|
|
327
|
+
...keys: (string | string[])[]
|
|
328
|
+
): SqlFragment {
|
|
329
|
+
// Normalize keys - accept both sql(obj, 'a', 'b') and sql(obj, ['a', 'b'])
|
|
330
|
+
let columnKeys: string[] = []
|
|
331
|
+
if (keys.length > 0) {
|
|
332
|
+
if (Array.isArray(keys[0])) {
|
|
333
|
+
columnKeys = keys[0]
|
|
334
|
+
} else {
|
|
335
|
+
columnKeys = keys as string[]
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Pattern 0: Single string → raw identifier (column/table name)
|
|
340
|
+
if (typeof value === 'string' && columnKeys.length === 0) {
|
|
341
|
+
return {
|
|
342
|
+
_isSqlFragment: true,
|
|
343
|
+
sql: value,
|
|
344
|
+
args: []
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Pattern 1: Array of strings → column list for SELECT
|
|
349
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string') {
|
|
350
|
+
return {
|
|
351
|
+
_isSqlFragment: true,
|
|
352
|
+
sql: value.join(', '),
|
|
353
|
+
args: []
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Pattern 2: Array of objects with keys → bulk insert
|
|
358
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && !Array.isArray(value[0])) {
|
|
359
|
+
if (columnKeys.length === 0) {
|
|
360
|
+
// Use keys from first object
|
|
361
|
+
columnKeys = Object.keys(value[0])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Extract values in column order for each object
|
|
365
|
+
const allValues: any[] = []
|
|
366
|
+
const rowPlaceholders: string[] = []
|
|
367
|
+
|
|
368
|
+
for (const obj of value) {
|
|
369
|
+
const rowValues = columnKeys.map(key => obj[key])
|
|
370
|
+
allValues.push(...rowValues)
|
|
371
|
+
rowPlaceholders.push(`(${columnKeys.map(() => '?').join(', ')})`)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
_isSqlFragment: true,
|
|
376
|
+
sql: `(${columnKeys.join(', ')}) VALUES ${rowPlaceholders.join(', ')}`,
|
|
377
|
+
args: allValues
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Pattern 3: Single object → INSERT or UPDATE format
|
|
382
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
383
|
+
// If no keys specified, use all keys from the object
|
|
384
|
+
if (columnKeys.length === 0) {
|
|
385
|
+
columnKeys = Object.keys(value)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const values = columnKeys.map(key => value[key])
|
|
389
|
+
const placeholders = columnKeys.map(() => '?').join(', ')
|
|
390
|
+
|
|
391
|
+
// Return INSERT format - context will determine usage
|
|
392
|
+
// For UPDATE, use set() helper instead
|
|
393
|
+
return {
|
|
394
|
+
_isSqlFragment: true,
|
|
395
|
+
sql: `(${columnKeys.join(', ')}) VALUES (${placeholders})`,
|
|
396
|
+
args: values
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Pattern 4: 2D Array → bulk insert values (raw)
|
|
401
|
+
if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
|
|
402
|
+
const rowPlaceholders = value.map(row =>
|
|
403
|
+
`(${row.map(() => '?').join(', ')})`
|
|
404
|
+
).join(', ')
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
_isSqlFragment: true,
|
|
408
|
+
sql: rowPlaceholders,
|
|
409
|
+
args: value.flat()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Pattern 5: Empty fragment (no arguments or empty array)
|
|
414
|
+
return {
|
|
415
|
+
_isSqlFragment: true,
|
|
416
|
+
sql: '',
|
|
417
|
+
args: []
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create an empty SQL fragment
|
|
423
|
+
*
|
|
424
|
+
* Useful for conditional queries where you want to add nothing.
|
|
425
|
+
*
|
|
426
|
+
* @returns Empty SQL fragment
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const isFiltered = false
|
|
431
|
+
* await db`
|
|
432
|
+
* SELECT * FROM users
|
|
433
|
+
* WHERE is_active = 1
|
|
434
|
+
* ${isFiltered ? sql`AND age > 25` : empty()}
|
|
435
|
+
* `
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
export function empty(): SqlFragment {
|
|
439
|
+
return {
|
|
440
|
+
_isSqlFragment: true,
|
|
441
|
+
sql: '',
|
|
442
|
+
args: []
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create a raw SQL fragment (use with extreme caution!)
|
|
448
|
+
*
|
|
449
|
+
* This bypasses parameterization and inserts the string directly into SQL.
|
|
450
|
+
* **WARNING**: Can lead to SQL injection if used with untrusted input!
|
|
451
|
+
*
|
|
452
|
+
* Only use for:
|
|
453
|
+
* - Table/column names from trusted sources
|
|
454
|
+
* - SQL keywords or operators
|
|
455
|
+
* - Pre-validated identifiers
|
|
456
|
+
*
|
|
457
|
+
* @param value - Raw SQL string (must be from trusted source)
|
|
458
|
+
* @returns SQL fragment with no parameters
|
|
459
|
+
*
|
|
460
|
+
* @example Safe usage (trusted table name)
|
|
461
|
+
* ```typescript
|
|
462
|
+
* const tableName = 'users' // From config, not user input!
|
|
463
|
+
* await db`SELECT * FROM ${raw(tableName)} WHERE id = ${userId}`
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @example UNSAFE - Never do this!
|
|
467
|
+
* ```typescript
|
|
468
|
+
* // ❌ DANGEROUS - SQL injection vulnerability!
|
|
469
|
+
* const userInput = req.query.table // From user!
|
|
470
|
+
* await db`SELECT * FROM ${raw(userInput)}` // Don't do this!
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
export function raw(value: string): SqlFragment {
|
|
474
|
+
return {
|
|
475
|
+
_isSqlFragment: true,
|
|
476
|
+
sql: value,
|
|
477
|
+
args: []
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Create a reusable SQL fragment from template literal
|
|
483
|
+
*
|
|
484
|
+
* Useful for building composable query parts that can be reused.
|
|
485
|
+
*
|
|
486
|
+
* @param strings - Template literal strings
|
|
487
|
+
* @param values - Template literal values
|
|
488
|
+
* @returns SQL fragment that can be embedded in other queries
|
|
489
|
+
*
|
|
490
|
+
* @example Basic fragment
|
|
491
|
+
* ```typescript
|
|
492
|
+
* const activeUsers = fragment`is_active = 1 AND is_deleted = 0`
|
|
493
|
+
*
|
|
494
|
+
* await db`SELECT * FROM users WHERE ${activeUsers}`
|
|
495
|
+
* await db`SELECT COUNT(*) FROM users WHERE ${activeUsers}`
|
|
496
|
+
* ```
|
|
497
|
+
*
|
|
498
|
+
* @example Parameterized fragment
|
|
499
|
+
* ```typescript
|
|
500
|
+
* const olderThan = (age: number) => fragment`age > ${age}`
|
|
501
|
+
* const youngerThan = (age: number) => fragment`age < ${age}`
|
|
502
|
+
*
|
|
503
|
+
* await db`
|
|
504
|
+
* SELECT * FROM users
|
|
505
|
+
* WHERE ${olderThan(25)} AND ${youngerThan(65)}
|
|
506
|
+
* `
|
|
507
|
+
* ```
|
|
508
|
+
*
|
|
509
|
+
* @example Complex reusable conditions
|
|
510
|
+
* ```typescript
|
|
511
|
+
* const validUser = fragment`
|
|
512
|
+
* is_active = 1
|
|
513
|
+
* AND is_deleted = 0
|
|
514
|
+
* AND is_verified = 1
|
|
515
|
+
* AND banned_at IS NULL
|
|
516
|
+
* `
|
|
517
|
+
*
|
|
518
|
+
* const adminUser = fragment`${validUser} AND role = 'admin'`
|
|
519
|
+
*
|
|
520
|
+
* await db`SELECT * FROM users WHERE ${adminUser}`
|
|
521
|
+
* ```
|
|
522
|
+
*/
|
|
523
|
+
export function fragment(
|
|
524
|
+
strings: TemplateStringsArray,
|
|
525
|
+
...values: any[]
|
|
526
|
+
): SqlFragment {
|
|
527
|
+
const query = convertTemplateToQuery(strings, values)
|
|
528
|
+
return {
|
|
529
|
+
_isSqlFragment: true,
|
|
530
|
+
...query
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Combine multiple SQL fragments
|
|
536
|
+
*
|
|
537
|
+
* @param fragments - Array of SQL fragments
|
|
538
|
+
* @param separator - Optional separator (default: ', ')
|
|
539
|
+
* @returns Combined SQL fragment
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```typescript
|
|
543
|
+
* const conditions = [
|
|
544
|
+
* sql`age > ${25}`,
|
|
545
|
+
* sql`country = ${'US'}`,
|
|
546
|
+
* sql`is_active = 1`
|
|
547
|
+
* ]
|
|
548
|
+
*
|
|
549
|
+
* await db`
|
|
550
|
+
* SELECT * FROM users
|
|
551
|
+
* WHERE ${join(conditions, ' AND ')}
|
|
552
|
+
* `
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
export function join(fragments: SqlFragment[], separator: string = ', '): SqlFragment {
|
|
556
|
+
if (fragments.length === 0) {
|
|
557
|
+
return empty()
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const sql = fragments.map(f => f.sql).join(separator)
|
|
561
|
+
const args = fragments.flatMap(f => f.args)
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
_isSqlFragment: true,
|
|
565
|
+
sql,
|
|
566
|
+
args
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create SET clause for UPDATE queries
|
|
572
|
+
*
|
|
573
|
+
* @param data - Object with column: value pairs
|
|
574
|
+
* @param keys - Optional array of keys to include (filters the object)
|
|
575
|
+
* @returns SQL fragment for SET clause
|
|
576
|
+
*
|
|
577
|
+
* @example All keys
|
|
578
|
+
* ```typescript
|
|
579
|
+
* const updates = { name: 'John Doe', age: 31, updated_at: new Date() }
|
|
580
|
+
*
|
|
581
|
+
* await db`
|
|
582
|
+
* UPDATE users
|
|
583
|
+
* SET ${set(updates)}
|
|
584
|
+
* WHERE id = ${userId}
|
|
585
|
+
* `
|
|
586
|
+
* // → UPDATE users SET name = ?, age = ?, updated_at = ? WHERE id = ?
|
|
587
|
+
* ```
|
|
588
|
+
*
|
|
589
|
+
* @example Specific keys only
|
|
590
|
+
* ```typescript
|
|
591
|
+
* const user = { id: 1, name: 'Murray', age: 68 }
|
|
592
|
+
*
|
|
593
|
+
* await db`
|
|
594
|
+
* UPDATE users
|
|
595
|
+
* SET ${set(user, 'name', 'age')}
|
|
596
|
+
* WHERE user_id = ${user.id}
|
|
597
|
+
* `
|
|
598
|
+
* // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
export function set(
|
|
602
|
+
data: Record<string, any>,
|
|
603
|
+
...keys: string[]
|
|
604
|
+
): SqlFragment {
|
|
605
|
+
let columns: string[]
|
|
606
|
+
|
|
607
|
+
if (keys.length > 0) {
|
|
608
|
+
// Use only specified keys
|
|
609
|
+
columns = keys
|
|
610
|
+
} else {
|
|
611
|
+
// Use all keys from object
|
|
612
|
+
columns = Object.keys(data)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (columns.length === 0) {
|
|
616
|
+
return empty()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const setClauses = columns.map(key => `${key} = ?`)
|
|
620
|
+
const values = columns.map(key => data[key])
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
_isSqlFragment: true,
|
|
624
|
+
sql: setClauses.join(', '),
|
|
625
|
+
args: values
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Create WHERE clause from object (AND conditions)
|
|
631
|
+
*
|
|
632
|
+
* @param conditions - Object with column: value pairs
|
|
633
|
+
* @returns SQL fragment for WHERE clause
|
|
634
|
+
*
|
|
635
|
+
* @example
|
|
636
|
+
* ```typescript
|
|
637
|
+
* const filters = { is_active: 1, role: 'admin', age: 30 }
|
|
638
|
+
*
|
|
639
|
+
* await db`
|
|
640
|
+
* SELECT * FROM users
|
|
641
|
+
* WHERE ${where(filters)}
|
|
642
|
+
* `
|
|
643
|
+
* // → SELECT * FROM users WHERE is_active = ? AND role = ? AND age = ?
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
export function where(conditions: Record<string, any>): SqlFragment {
|
|
647
|
+
const entries = Object.entries(conditions)
|
|
648
|
+
if (entries.length === 0) {
|
|
649
|
+
return empty()
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const whereClauses = entries.map(([key]) => `${key} = ?`)
|
|
653
|
+
const values = entries.map(([, value]) => value)
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
_isSqlFragment: true,
|
|
657
|
+
sql: whereClauses.join(' AND '),
|
|
658
|
+
args: values
|
|
659
|
+
}
|
|
660
|
+
}
|
package/src/database/types.ts
CHANGED
|
@@ -28,12 +28,21 @@ export interface QueryResult<T = any> {
|
|
|
28
28
|
/**
|
|
29
29
|
* Unified database connection interface
|
|
30
30
|
* All backends implement this through adapters
|
|
31
|
+
*
|
|
32
|
+
* postgres.js-compatible API:
|
|
33
|
+
* - sql`...` returns rows array directly (not QueryResult)
|
|
34
|
+
* - sql() can be called as helper function for fragments
|
|
35
|
+
* - sql.empty(), sql.raw(), etc. are available as helper methods
|
|
31
36
|
*/
|
|
32
37
|
export interface Connection {
|
|
33
|
-
// Template tag queries (postgres.js-
|
|
34
|
-
|
|
38
|
+
// Template tag queries (postgres.js-compatible)
|
|
39
|
+
// Returns rows array directly, not { rows: [...] }
|
|
40
|
+
sql<T = any>(strings: TemplateStringsArray, ...values: any[]): Promise<T[]>
|
|
35
41
|
|
|
36
|
-
//
|
|
42
|
+
// Helper function overload (for fragments like sql(['col1', 'col2']))
|
|
43
|
+
sql(value: any, ...keys: string[]): any
|
|
44
|
+
|
|
45
|
+
// Standard query methods (return QueryResult)
|
|
37
46
|
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>
|
|
38
47
|
execute(sql: string | { sql: string; args?: any[] }, params?: any[]): Promise<QueryResult>
|
|
39
48
|
prepare(sql: string): PreparedStatement
|
|
@@ -45,6 +54,9 @@ export interface Connection {
|
|
|
45
54
|
// Connection management
|
|
46
55
|
close(): Promise<void>
|
|
47
56
|
|
|
57
|
+
// ORM support (optional)
|
|
58
|
+
createORM?(): any
|
|
59
|
+
|
|
48
60
|
// Metadata (optional, ODB-Lite specific)
|
|
49
61
|
databaseHash?: string
|
|
50
62
|
databaseName?: string
|