@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.
@@ -7,6 +7,16 @@ import type {
7
7
  PreparedStatement,
8
8
  ODBLiteConfig,
9
9
  } from '../types'
10
+ import {
11
+ convertTemplateToQuery,
12
+ sql as sqlHelper,
13
+ empty,
14
+ raw,
15
+ fragment,
16
+ join,
17
+ set,
18
+ where
19
+ } from '../sql-template'
10
20
 
11
21
  /**
12
22
  * ODB-Lite adapter for DatabaseManager
@@ -15,7 +25,7 @@ import type {
15
25
  export class ODBLiteAdapter implements DatabaseAdapter {
16
26
  readonly type = 'odblite' as const
17
27
  private config: ODBLiteConfig
18
- private serviceClient: ServiceClient
28
+ public serviceClient: ServiceClient
19
29
 
20
30
  constructor(config: ODBLiteConfig) {
21
31
  this.config = config
@@ -27,19 +37,24 @@ export class ODBLiteAdapter implements DatabaseAdapter {
27
37
 
28
38
  async connect(config: any): Promise<Connection> {
29
39
  const databaseName = config.databaseName || 'default'
40
+ const databaseHash = config.databaseHash // Optional: connect directly by hash
30
41
 
31
- // Ensure database exists
32
- const dbInfo = await this.serviceClient.getDatabaseByName(databaseName)
33
-
34
- if (!dbInfo) {
35
- // Create database - pass nodeId from config if provided, otherwise server selects
36
- await this.serviceClient.createDatabase(databaseName, this.config.nodeId)
42
+ // If hash is provided, skip database lookup and connect directly
43
+ if (databaseHash) {
44
+ const client = new ODBLiteClient({
45
+ baseUrl: this.config.serviceUrl,
46
+ apiKey: this.config.apiKey,
47
+ databaseId: databaseHash,
48
+ })
49
+ return new ODBLiteConnection(client, this.serviceClient, databaseName, databaseHash)
37
50
  }
38
51
 
39
- // Get fresh database info
52
+ // Otherwise, query by name
53
+ // Check if database exists
40
54
  const db = await this.serviceClient.getDatabaseByName(databaseName)
55
+
41
56
  if (!db) {
42
- throw new Error(`Database ${databaseName} not found after creation`)
57
+ throw new Error(`Database ${databaseName} not found. Please create it first using the admin API.`)
43
58
  }
44
59
 
45
60
  // Create ODBLiteClient for this database
@@ -87,6 +102,9 @@ class ODBLiteConnection implements Connection {
87
102
  public databaseName: string
88
103
  public databaseHash: string
89
104
 
105
+ // Dual-purpose sql method (postgres.js-compatible)
106
+ public sql: any
107
+
90
108
  constructor(
91
109
  client: ODBLiteClient,
92
110
  serviceClient: ServiceClient,
@@ -97,22 +115,48 @@ class ODBLiteConnection implements Connection {
97
115
  this.serviceClient = serviceClient
98
116
  this.databaseName = databaseName
99
117
  this.databaseHash = databaseHash
100
- }
101
118
 
102
- /**
103
- * Template tag query (postgres.js-like)
104
- */
105
- async sql<T = any>(
106
- strings: TemplateStringsArray,
107
- ...values: any[]
108
- ): Promise<QueryResult<T>> {
109
- // ODBLiteClient.sql is a function that returns a Promise
110
- const result = await this.client.sql(strings, ...values)
119
+ // Create dual-purpose sql method (postgres.js lazy evaluation pattern)
120
+ // Handles both template tag calls AND helper function calls
121
+ const self = this
122
+ this.sql = function(stringsOrValue: any, ...values: any[]): any {
123
+ // Check if called as template tag (has 'raw' property)
124
+ if (stringsOrValue && typeof stringsOrValue === 'object' && 'raw' in stringsOrValue) {
125
+ // Template literal call: sql`SELECT...`
126
+ // Return a thenable SqlFragment (lazy evaluation):
127
+ // - Can be embedded in other queries as SqlFragment
128
+ // - Can be awaited to execute and return rows
129
+ const query = convertTemplateToQuery(stringsOrValue, values)
111
130
 
112
- return {
113
- rows: result.rows as T[],
114
- rowsAffected: result.rowsAffected || 0,
131
+ // Create thenable fragment
132
+ const thenableFragment: any = {
133
+ _isSqlFragment: true,
134
+ sql: query.sql,
135
+ args: query.args
136
+ }
137
+
138
+ // Make it awaitable by adding then method (using bracket notation to avoid linter error)
139
+ thenableFragment['then'] = function(onFulfilled: any, onRejected: any) {
140
+ return self.execute(query)
141
+ .then(result => result.rows)
142
+ .then(onFulfilled, onRejected)
143
+ }
144
+
145
+ return thenableFragment
146
+ } else {
147
+ // Regular function call: sql(['col1', 'col2']) or sql(obj, 'key')
148
+ // Return SqlFragment for use in template literals
149
+ return sqlHelper(stringsOrValue, ...values)
150
+ }
115
151
  }
152
+
153
+ // Attach helper methods as properties (postgres.js style)
154
+ this.sql.empty = empty
155
+ this.sql.raw = raw
156
+ this.sql.fragment = fragment
157
+ this.sql.join = join
158
+ this.sql.set = set
159
+ this.sql.where = where
116
160
  }
117
161
 
118
162
  /**
@@ -129,22 +173,34 @@ class ODBLiteConnection implements Connection {
129
173
  try {
130
174
  // Handle object format { sql, args }
131
175
  if (typeof sql === 'object') {
176
+ // Debug logging for SQL errors
177
+ if (process.env.DEBUG_SQL) {
178
+ console.log('[ODBLite] Executing SQL:', sql.sql)
179
+ console.log('[ODBLite] With args:', sql.args)
180
+ }
132
181
  const result = await this.client.sql.execute(sql.sql, sql.args || [])
133
182
  return {
134
183
  rows: result.rows,
135
184
  rowsAffected: result.rowsAffected || 0,
185
+ lastInsertRowid: (result as any).lastInsertRowid,
136
186
  }
137
187
  }
138
188
 
139
189
  // Handle string format
190
+ if (process.env.DEBUG_SQL) {
191
+ console.log('[ODBLite] Executing SQL:', sql)
192
+ console.log('[ODBLite] With params:', params)
193
+ }
140
194
  const result = await this.client.sql.execute(sql, params)
141
195
 
142
196
  return {
143
197
  rows: result.rows,
144
198
  rowsAffected: result.rowsAffected || 0,
199
+ lastInsertRowid: (result as any).lastInsertRowid,
145
200
  }
146
201
  } catch (error: any) {
147
- throw new Error(`SQL execution failed: ${error.message}`)
202
+ // Re-throw the original error to let the application handle it
203
+ throw error
148
204
  }
149
205
  }
150
206
 
@@ -207,4 +263,12 @@ class ODBLiteConnection implements Connection {
207
263
  // ODBLiteClient connections are stateless HTTP requests
208
264
  // No explicit close needed
209
265
  }
266
+
267
+ /**
268
+ * Create ORM instance for this connection
269
+ */
270
+ createORM(): any {
271
+ const { createORM } = require('../../orm/index.ts')
272
+ return createORM(this)
273
+ }
210
274
  }
@@ -5,6 +5,10 @@ export { DatabaseManager } from './manager'
5
5
  export { parseSQL, splitSQLStatements } from './sql-parser'
6
6
  export type { ParsedStatements, SQLParserOptions } from './sql-parser'
7
7
 
8
+ // SQL Template helpers (postgres.js-compatible)
9
+ export { sql, raw, empty, fragment, join, set, where, convertTemplateToQuery } from './sql-template'
10
+ export type { SqlQuery, SqlFragment } from './sql-template'
11
+
8
12
  // Backend adapters
9
13
  export { BunSQLiteAdapter } from './adapters/bun-sqlite'
10
14
  export { LibSQLAdapter } from './adapters/libsql'
@@ -0,0 +1,363 @@
1
+ /**
2
+ * SQL Template Examples - postgres.js Compatible Usage
3
+ *
4
+ * This file demonstrates all supported postgres.js patterns with our sql-template implementation.
5
+ * These examples show the exact usage from postgres.js documentation.
6
+ *
7
+ * NOTE: This file is for documentation only - not included in build
8
+ * @file sql-template.examples.ts
9
+ */
10
+
11
+ // @ts-nocheck - This file is for documentation purposes only
12
+
13
+ import { sql, set, fragment, empty, raw, join, where } from './sql-template'
14
+ import type { Connection } from './types'
15
+
16
+ // Mock connection for type checking
17
+ declare const db: any
18
+
19
+ /**
20
+ * Example 1: Query Parameters (Basic)
21
+ */
22
+ async function example1_queryParameters() {
23
+ const name = 'John'
24
+ const age = 30
25
+
26
+ // Simple parameterized query
27
+ await db`SELECT * FROM users WHERE name = ${name} AND age > ${age}`
28
+ // → SELECT * FROM users WHERE name = ? AND age = ?
29
+ // → args: ["John", 30]
30
+ }
31
+
32
+ /**
33
+ * Example 2: Dynamic Column Selection
34
+ */
35
+ async function example2_dynamicColumns() {
36
+ const columns = ['name', 'age']
37
+
38
+ await db`
39
+ SELECT ${sql(columns)}
40
+ FROM users
41
+ `
42
+ // → SELECT name, age FROM users
43
+ }
44
+
45
+ /**
46
+ * Example 3: Dynamic Insert (Single Object)
47
+ */
48
+ async function example3_dynamicInsert() {
49
+ const user = {
50
+ name: 'Murray',
51
+ age: 68
52
+ }
53
+
54
+ // With explicit columns
55
+ await db`INSERT INTO users ${sql(user, 'name', 'age')}`
56
+ // → INSERT INTO users (name, age) VALUES (?, ?)
57
+ // → args: ["Murray", 68]
58
+
59
+ // With columns array
60
+ const columns = ['name', 'age']
61
+ await db`INSERT INTO users ${sql(user, columns)}`
62
+ // → INSERT INTO users (name, age) VALUES (?, ?)
63
+ }
64
+
65
+ /**
66
+ * Example 4: Bulk Insert (Array of Objects)
67
+ */
68
+ async function example4_bulkInsert() {
69
+ const users = [
70
+ { name: 'Murray', age: 68, garbage: 'ignore' },
71
+ { name: 'Walter', age: 80 }
72
+ ]
73
+
74
+ // With specific columns
75
+ await db`INSERT INTO users ${sql(users, 'name', 'age')}`
76
+ // → INSERT INTO users (name, age) VALUES (?, ?), (?, ?)
77
+ // → args: ["Murray", 68, "Walter", 80]
78
+
79
+ // Using all keys from first object
80
+ await db`INSERT INTO users ${sql(users)}`
81
+ // → INSERT INTO users (name, age, garbage) VALUES (?, ?, ?), (?, ?, ?)
82
+ // → args: ["Murray", 68, "ignore", "Walter", 80, undefined]
83
+ }
84
+
85
+ /**
86
+ * Example 5: Dynamic Column Updates
87
+ */
88
+ async function example5_dynamicUpdate() {
89
+ const user = {
90
+ id: 1,
91
+ name: 'Murray',
92
+ age: 68
93
+ }
94
+
95
+ // Using set() helper with specific columns
96
+ await db`
97
+ UPDATE users
98
+ SET ${set(user, 'name', 'age')}
99
+ WHERE user_id = ${user.id}
100
+ `
101
+ // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
102
+ // → args: ["Murray", 68, 1]
103
+
104
+ // Using set() with object (all keys)
105
+ const updates = { name: user.name, age: user.age }
106
+ await db`
107
+ UPDATE users
108
+ SET ${set(updates)}
109
+ WHERE user_id = ${user.id}
110
+ `
111
+ // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
112
+
113
+ // Columns can also be given with an array
114
+ const columns = ['name', 'age']
115
+ await db`
116
+ UPDATE users
117
+ SET ${set(user, ...columns)}
118
+ WHERE user_id = ${user.id}
119
+ `
120
+ }
121
+
122
+ /**
123
+ * Example 6: Multiple Updates in One Query
124
+ */
125
+ async function example6_multipleUpdates() {
126
+ const users = [
127
+ [1, 'John', 34],
128
+ [2, 'Jane', 27],
129
+ ]
130
+
131
+ await db`
132
+ UPDATE users
133
+ SET name = update_data.name, age = (update_data.age)::int
134
+ FROM (VALUES ${sql(users)}) AS update_data (id, name, age)
135
+ WHERE users.id = (update_data.id)::int
136
+ RETURNING users.id, users.name, users.age
137
+ `
138
+ // → UPDATE users SET name = update_data.name, age = (update_data.age)::int
139
+ // FROM (VALUES (?, ?, ?), (?, ?, ?)) AS update_data (id, name, age)
140
+ // WHERE users.id = (update_data.id)::int
141
+ // RETURNING users.id, users.name, users.age
142
+ // → args: [1, "John", 34, 2, "Jane", 27]
143
+ }
144
+
145
+ /**
146
+ * Example 7: Dynamic WHERE IN Clause
147
+ */
148
+ async function example7_whereIn() {
149
+ // Using sql() wrapper
150
+ const users1 = await db`
151
+ SELECT *
152
+ FROM users
153
+ WHERE age IN ${sql([68, 75, 23])}
154
+ `
155
+ // → SELECT * FROM users WHERE age IN (?, ?, ?)
156
+ // → args: [68, 75, 23]
157
+
158
+ // Using array directly (auto-converted)
159
+ const users2 = await db`
160
+ SELECT *
161
+ FROM users
162
+ WHERE age IN ${[68, 75, 23]}
163
+ `
164
+ // → SELECT * FROM users WHERE age IN (?, ?, ?)
165
+ // → args: [68, 75, 23]
166
+ }
167
+
168
+ /**
169
+ * Example 8: Dynamic Values in SELECT
170
+ */
171
+ async function example8_dynamicValues() {
172
+ const [{ a, b, c }] = await db`
173
+ SELECT *
174
+ FROM (VALUES ${sql(['a', 'b', 'c'])}) AS x(a, b, c)
175
+ `
176
+ // Note: sql(['a', 'b', 'c']) produces "a, b, c" (column list)
177
+ // For VALUES with actual data, use 2D array or direct template
178
+ }
179
+
180
+ /**
181
+ * Example 9: Conditional Fragments
182
+ */
183
+ async function example9_conditionalFragments() {
184
+ const isAdmin = true
185
+ const minAge = 25
186
+
187
+ await db`
188
+ SELECT * FROM users
189
+ WHERE is_active = 1
190
+ ${isAdmin ? fragment`AND role = 'admin'` : empty()}
191
+ ${minAge ? fragment`AND age >= ${minAge}` : empty()}
192
+ `
193
+ // → SELECT * FROM users WHERE is_active = 1 AND role = 'admin' AND age >= ?
194
+ // → args: [25]
195
+ }
196
+
197
+ /**
198
+ * Example 10: Transactions
199
+ */
200
+ async function example10_transactions() {
201
+ const user = { name: 'John', email: 'john@example.com' }
202
+ const userId = 1
203
+ const amount = 100
204
+
205
+ await db.begin(async tx => {
206
+ await tx`INSERT INTO users ${sql(user, 'name', 'email')}`
207
+ await tx`UPDATE accounts SET balance = balance - ${amount} WHERE user_id = ${userId}`
208
+ })
209
+ // Automatically commits on success, rolls back on error
210
+ }
211
+
212
+ /**
213
+ * Example 11: Raw SQL (Dangerous!)
214
+ */
215
+ async function example11_rawSql() {
216
+ const tableName = 'users' // From config, not user input!
217
+ const userId = 1
218
+
219
+ await db`SELECT * FROM ${raw(tableName)} WHERE id = ${userId}`
220
+ // → SELECT * FROM users WHERE id = ?
221
+ // → args: [1]
222
+
223
+ // ❌ NEVER DO THIS with user input:
224
+ // const userTable = req.query.table // Dangerous!
225
+ // await db`SELECT * FROM ${raw(userTable)}` // SQL injection!
226
+ }
227
+
228
+ /**
229
+ * Example 12: Reusable Fragments
230
+ */
231
+ async function example12_reusableFragments() {
232
+ // Static fragment
233
+ const activeUsers = fragment`is_active = 1 AND is_deleted = 0`
234
+
235
+ // Parameterized fragment function
236
+ const olderThan = (age: number) => fragment`age > ${age}`
237
+ const youngerThan = (age: number) => fragment`age < ${age}`
238
+
239
+ await db`
240
+ SELECT * FROM users
241
+ WHERE ${activeUsers}
242
+ AND ${olderThan(25)}
243
+ `
244
+ // → SELECT * FROM users WHERE is_active = 1 AND is_deleted = 0 AND age > ?
245
+ // → args: [25]
246
+
247
+ // Complex reusable conditions
248
+ const validUser = fragment`
249
+ is_active = 1
250
+ AND is_deleted = 0
251
+ AND is_verified = 1
252
+ AND banned_at IS NULL
253
+ `
254
+
255
+ const adminUser = fragment`${validUser} AND role = 'admin'`
256
+
257
+ await db`SELECT * FROM users WHERE ${adminUser}`
258
+ }
259
+
260
+ /**
261
+ * Example 13: Combining Fragments with join()
262
+ */
263
+ async function example13_joiningFragments() {
264
+ const conditions = [
265
+ fragment`age > ${25}`,
266
+ fragment`country = ${'US'}`,
267
+ fragment`is_active = 1`
268
+ ]
269
+
270
+ await db`
271
+ SELECT * FROM users
272
+ WHERE ${join(conditions, ' AND ')}
273
+ `
274
+ // → SELECT * FROM users WHERE age > ? AND country = ? AND is_active = 1
275
+ // → args: [25, "US"]
276
+ }
277
+
278
+ /**
279
+ * Example 14: WHERE Helper
280
+ */
281
+ async function example14_whereHelper() {
282
+ const filters = { is_active: 1, role: 'admin', age: 30 }
283
+
284
+ await db`
285
+ SELECT * FROM users
286
+ WHERE ${where(filters)}
287
+ `
288
+ // → SELECT * FROM users WHERE is_active = ? AND role = ? AND age = ?
289
+ // → args: [1, "admin", 30]
290
+ }
291
+
292
+ /**
293
+ * Example 15: Complex Real-World Example
294
+ */
295
+ async function example15_complexRealWorld() {
296
+ const searchTerm = 'john'
297
+ const filters = {
298
+ is_active: 1,
299
+ is_deleted: 0
300
+ }
301
+ const minAge = 25
302
+ const maxAge = 65
303
+ const roles = ['admin', 'moderator']
304
+ const sortBy = 'created_at'
305
+
306
+ await db`
307
+ SELECT ${sql(['id', 'name', 'email', 'role', 'created_at'])}
308
+ FROM users
309
+ WHERE ${where(filters)}
310
+ ${searchTerm ? fragment`AND (name LIKE ${'%' + searchTerm + '%'} OR email LIKE ${'%' + searchTerm + '%'})` : empty()}
311
+ ${minAge ? fragment`AND age >= ${minAge}` : empty()}
312
+ ${maxAge ? fragment`AND age <= ${maxAge}` : empty()}
313
+ ${roles.length > 0 ? fragment`AND role IN ${roles}` : empty()}
314
+ ORDER BY ${raw(sortBy)} DESC
315
+ LIMIT 100
316
+ `
317
+ // → SELECT id, name, email, role, created_at FROM users
318
+ // WHERE is_active = ? AND is_deleted = ?
319
+ // AND (name LIKE ? OR email LIKE ?)
320
+ // AND age >= ?
321
+ // AND age <= ?
322
+ // AND role IN (?, ?)
323
+ // ORDER BY created_at DESC
324
+ // LIMIT 100
325
+ // → args: [1, 0, "%john%", "%john%", 25, 65, "admin", "moderator"]
326
+ }
327
+
328
+ /**
329
+ * Example 16: Batch Operations
330
+ */
331
+ async function example16_batchOperations() {
332
+ // Batch insert with 2D array
333
+ const userData = [
334
+ [1, 'John', 30],
335
+ [2, 'Jane', 25],
336
+ [3, 'Bob', 35]
337
+ ]
338
+
339
+ await db`
340
+ INSERT INTO users (id, name, age)
341
+ VALUES ${sql(userData)}
342
+ `
343
+ // → INSERT INTO users (id, name, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
344
+ // → args: [1, "John", 30, 2, "Jane", 25, 3, "Bob", 35]
345
+ }
346
+
347
+ /**
348
+ * All examples demonstrate postgres.js-compatible patterns:
349
+ *
350
+ * ✅ Template literals with automatic parameterization
351
+ * ✅ sql(columns[]) for dynamic column selection
352
+ * ✅ sql(object, ...keys) for INSERT statements
353
+ * ✅ sql(objects[], ...keys) for bulk INSERT
354
+ * ✅ sql(array[][]) for VALUES clause
355
+ * ✅ Array values for IN clauses
356
+ * ✅ set(object, ...keys) for UPDATE SET clauses
357
+ * ✅ where(object) for WHERE conditions
358
+ * ✅ fragment`` for reusable query parts
359
+ * ✅ empty() for conditional fragments
360
+ * ✅ raw() for unescaped SQL (use carefully!)
361
+ * ✅ join(fragments[], separator) for combining fragments
362
+ * ✅ Transactions with begin()
363
+ */