@pineliner/odb-client 1.0.0

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/index.js ADDED
@@ -0,0 +1,992 @@
1
+ // Core types for the ODBLite client
2
+ // Error types
3
+ class ODBLiteError extends Error {
4
+ code;
5
+ query;
6
+ params;
7
+ constructor(message, code, query, params){
8
+ super(message), this.code = code, this.query = query, this.params = params;
9
+ this.name = 'ODBLiteError';
10
+ }
11
+ }
12
+ class ConnectionError extends ODBLiteError {
13
+ originalError;
14
+ constructor(message, originalError){
15
+ super(message, 'CONNECTION_ERROR'), this.originalError = originalError;
16
+ this.name = 'ConnectionError';
17
+ }
18
+ }
19
+ class types_QueryError extends ODBLiteError {
20
+ originalError;
21
+ constructor(message, query, params, originalError){
22
+ super(message, 'QUERY_ERROR', query, params), this.originalError = originalError;
23
+ this.name = 'QueryError';
24
+ }
25
+ }
26
+ /**
27
+ * HTTP client for communicating with ODBLite service
28
+ */ class HTTPClient {
29
+ config;
30
+ constructor(config){
31
+ this.config = {
32
+ timeout: 30000,
33
+ retries: 3,
34
+ ...config
35
+ };
36
+ // Ensure baseUrl doesn't end with slash
37
+ if ('string' == typeof this.config.baseUrl) this.config.baseUrl = this.config.baseUrl.replace(/\/$/, '');
38
+ }
39
+ /**
40
+ * Execute a query against the ODBLite service
41
+ */ async query(sql, params = []) {
42
+ if (!this.config.databaseId) throw new ConnectionError('No database ID configured. Use setDatabase() first.');
43
+ const url = `${this.config.baseUrl}/query/${this.config.databaseId}`;
44
+ const body = {
45
+ sql,
46
+ params
47
+ };
48
+ try {
49
+ const response = await this.makeRequest(url, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Authorization: `Bearer ${this.config.apiKey}`
54
+ },
55
+ body: JSON.stringify(body)
56
+ });
57
+ const data = await response.json();
58
+ if (!data.success) throw new types_QueryError(data.error || 'Query failed', sql, params);
59
+ // Handle nested data structure: { success: true, data: { rows: [...] } }
60
+ let rows = [];
61
+ if (data.data && 'object' == typeof data.data && 'rows' in data.data) rows = data.data.rows;
62
+ else if (Array.isArray(data.data)) rows = data.data;
63
+ else if (Array.isArray(data.rows)) rows = data.rows;
64
+ return {
65
+ rows: rows,
66
+ rowsAffected: data.rowsAffected || data.data?.rowsAffected || 0,
67
+ executionTime: data.executionTime || data.data?.executionTime || 0,
68
+ databaseName: data.databaseName || data.data?.databaseName
69
+ };
70
+ } catch (error) {
71
+ if (error instanceof types_QueryError || error instanceof ConnectionError) throw error;
72
+ throw new types_QueryError(error instanceof Error ? error.message : 'Unknown error occurred', sql, params, error instanceof Error ? error : void 0);
73
+ }
74
+ }
75
+ /**
76
+ * Check database health
77
+ */ async ping() {
78
+ if (!this.config.databaseId) return false;
79
+ try {
80
+ const url = `${this.config.baseUrl}/api/db/${this.config.databaseId}/health`;
81
+ const response = await this.makeRequest(url, {
82
+ method: 'GET',
83
+ headers: {
84
+ Authorization: `Bearer ${this.config.apiKey}`
85
+ }
86
+ });
87
+ const data = await response.json();
88
+ return 'healthy' === data.status;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Get database information
95
+ */ async getDatabaseInfo() {
96
+ if (!this.config.databaseId) throw new ConnectionError('No database ID configured');
97
+ const url = `${this.config.baseUrl}/query/${this.config.databaseId}`;
98
+ const response = await this.makeRequest(url, {
99
+ method: 'GET',
100
+ headers: {
101
+ Authorization: `Bearer ${this.config.apiKey}`
102
+ }
103
+ });
104
+ const data = await response.json();
105
+ if (!data.success) throw new ConnectionError(data.error || 'Failed to get database info');
106
+ return data;
107
+ }
108
+ /**
109
+ * Set the database ID for subsequent queries
110
+ */ setDatabase(databaseId) {
111
+ this.config.databaseId = databaseId;
112
+ }
113
+ /**
114
+ * Make HTTP request with retry logic
115
+ */ async makeRequest(url, options) {
116
+ let lastError;
117
+ for(let attempt = 1; attempt <= (this.config.retries || 3); attempt++)try {
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(()=>controller.abort(), this.config.timeout);
120
+ const response = await fetch(url, {
121
+ ...options,
122
+ signal: controller.signal
123
+ });
124
+ clearTimeout(timeoutId);
125
+ if (!response.ok) {
126
+ const errorText = await response.text();
127
+ let errorData;
128
+ try {
129
+ errorData = JSON.parse(errorText);
130
+ } catch {
131
+ errorData = {
132
+ error: errorText
133
+ };
134
+ }
135
+ throw new ConnectionError(`HTTP ${response.status}: ${errorData.error || response.statusText}`);
136
+ }
137
+ return response;
138
+ } catch (error) {
139
+ lastError = error instanceof Error ? error : new Error('Unknown error');
140
+ // Don't retry on client errors (4xx)
141
+ if (error instanceof ConnectionError && error.message.includes('HTTP 4')) throw error;
142
+ // Wait before retry (exponential backoff)
143
+ if (attempt < (this.config.retries || 3)) {
144
+ const delay = Math.min(1000 * 2 ** (attempt - 1), 10000);
145
+ await new Promise((resolve)=>setTimeout(resolve, delay));
146
+ }
147
+ }
148
+ throw new ConnectionError(`Failed after ${this.config.retries} attempts: ${lastError?.message}`, lastError);
149
+ }
150
+ /**
151
+ * Create a new HTTPClient with updated config
152
+ */ configure(updates) {
153
+ return new HTTPClient({
154
+ ...this.config,
155
+ ...updates
156
+ });
157
+ }
158
+ /**
159
+ * Get current configuration
160
+ */ getConfig() {
161
+ return {
162
+ ...this.config
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * SQL template string parser that converts postgres.js-style template strings
168
+ * into LibSQL-compatible parameterized queries
169
+ */ class sql_parser_SQLParser {
170
+ /**
171
+ * Parse input that can be template strings, objects, or arrays
172
+ * Context-aware like postgres.js
173
+ */ static parse(sql, values) {
174
+ // Handle template string arrays (have the raw property or look like template strings)
175
+ if (Array.isArray(sql) && ('raw' in sql || 'string' == typeof sql[0] && void 0 !== values)) return this.parseTemplateString(sql, values || []);
176
+ // Handle direct object/array inputs (context detection)
177
+ return this.parseContextualInput(sql, values);
178
+ }
179
+ /**
180
+ * Parse contextual input (objects, arrays, etc.)
181
+ * Uses heuristics to detect the intended context
182
+ */ static parseContextualInput(input, values, context) {
183
+ if (Array.isArray(input)) {
184
+ // Check if this is an array of objects (for INSERT VALUES)
185
+ if (input.length > 0 && input[0] && 'object' == typeof input[0] && input[0].constructor === Object) {
186
+ const insertResult = this.insertValues(input);
187
+ return {
188
+ sql: insertResult.text,
189
+ params: insertResult.values
190
+ };
191
+ }
192
+ // Array of primitives - assume it's for IN clause
193
+ const placeholders = input.map(()=>'?').join(', ');
194
+ return {
195
+ sql: `(${placeholders})`,
196
+ params: input.map((v)=>this.convertValue(v))
197
+ };
198
+ }
199
+ if (input && 'object' == typeof input && input.constructor === Object) {
200
+ // Plain object - try to detect context based on structure and usage patterns
201
+ const entries = Object.entries(input).filter(([, value])=>void 0 !== value);
202
+ if (0 === entries.length) return {
203
+ sql: '',
204
+ params: []
205
+ };
206
+ // Analyze the object structure to determine intent
207
+ const hasNullValues = entries.some(([, value])=>null === value);
208
+ const hasArrayValues = entries.some(([, value])=>Array.isArray(value));
209
+ const hasComplexValues = entries.some(([, value])=>null !== value && 'object' == typeof value && !Array.isArray(value));
210
+ // Strong indicators for WHERE clauses:
211
+ // - null values (for IS NULL checks)
212
+ // - array values (for IN clauses)
213
+ // - complex nested objects
214
+ if (hasNullValues || hasArrayValues || hasComplexValues) {
215
+ const whereResult = this.where(input);
216
+ return {
217
+ sql: whereResult.text,
218
+ params: whereResult.values
219
+ };
220
+ }
221
+ // For simple objects with primitive values, we need to guess context
222
+ // This is inherently ambiguous - could be SET or VALUES
223
+ // We'll default to a flexible format that works for both
224
+ // If single object, more likely to be SET clause
225
+ if (entries.length <= 3 && !Array.isArray(input)) // Return SET format by default
226
+ return this.buildSetClause(input);
227
+ // For larger objects or arrays of objects, likely INSERT VALUES
228
+ const insertResult = this.insertValues(input);
229
+ return {
230
+ sql: insertResult.text,
231
+ params: insertResult.values
232
+ };
233
+ }
234
+ // Fallback for other types
235
+ return {
236
+ sql: '?',
237
+ params: [
238
+ this.convertValue(input)
239
+ ]
240
+ };
241
+ }
242
+ /**
243
+ * Build a SET clause for UPDATE statements
244
+ */ static buildSetClause(data) {
245
+ const entries = Object.entries(data).filter(([, value])=>void 0 !== value);
246
+ if (0 === entries.length) return {
247
+ sql: '',
248
+ params: []
249
+ };
250
+ const setClauses = entries.map(([key])=>`${this.escapeIdentifier(key)} = ?`).join(', ');
251
+ const values = entries.map(([, value])=>this.convertValue(value));
252
+ return {
253
+ sql: setClauses,
254
+ params: values
255
+ };
256
+ }
257
+ /**
258
+ * Parse template string SQL into LibSQL format
259
+ */ static parseTemplateString(sql, values) {
260
+ const fragments = [
261
+ ...sql
262
+ ];
263
+ const params = [];
264
+ let query = '';
265
+ for(let i = 0; i < fragments.length; i++){
266
+ query += fragments[i];
267
+ if (i < values.length) {
268
+ const value = values[i];
269
+ // Handle different value types using context detection
270
+ if (Array.isArray(value)) {
271
+ // Handle IN clauses: sql`SELECT * FROM users WHERE id IN ${[1, 2, 3]}`
272
+ const placeholders = value.map(()=>"?").join(', ');
273
+ query += `(${placeholders})`;
274
+ params.push(...value.map((v)=>this.convertValue(v)));
275
+ } else if (value && 'object' == typeof value && value.constructor === Object) {
276
+ // Use context detection for objects
277
+ const contextResult = this.parseContextualInput(value);
278
+ query += contextResult.sql;
279
+ params.push(...contextResult.params);
280
+ } else if ('string' == typeof value && value.startsWith('__RAW__')) // Handle raw SQL: sql`SELECT * FROM ${raw('users')}`
281
+ query += value.slice(7); // Remove __RAW__ prefix
282
+ else {
283
+ // Regular parameter
284
+ query += '?';
285
+ params.push(this.convertValue(value));
286
+ }
287
+ }
288
+ }
289
+ return {
290
+ sql: query.trim(),
291
+ params
292
+ };
293
+ }
294
+ /**
295
+ * Create a raw SQL fragment (not parameterized)
296
+ */ static raw(text) {
297
+ return `__RAW__${text}`;
298
+ }
299
+ /**
300
+ * Create an identifier (table name, column name, etc.)
301
+ */ static identifier(name) {
302
+ return this.escapeIdentifier(name);
303
+ }
304
+ /**
305
+ * Escape SQL identifiers (table names, column names)
306
+ */ static escapeIdentifier(identifier) {
307
+ // SQLite uses double quotes for identifiers
308
+ return `"${identifier.replace(/"/g, '""')}"`;
309
+ }
310
+ /**
311
+ * Convert JavaScript values to LibSQL-compatible values
312
+ */ static convertValue(value) {
313
+ if (null == value) return null;
314
+ if (value instanceof Date) // Convert Date to ISO string for SQLite
315
+ return value.toISOString();
316
+ if (value instanceof Buffer) // Convert Buffer to Uint8Array for LibSQL
317
+ return new Uint8Array(value);
318
+ if ('boolean' == typeof value) // SQLite uses 0/1 for booleans
319
+ return value ? 1 : 0;
320
+ if ('bigint' == typeof value) // Convert BigInt to string to avoid precision loss
321
+ return value.toString();
322
+ if ('object' == typeof value) // Serialize objects as JSON
323
+ return JSON.stringify(value);
324
+ return value;
325
+ }
326
+ /**
327
+ * Create a SQL fragment for building complex queries
328
+ */ static fragment(sql, ...values) {
329
+ const parsed = this.parse(sql, values);
330
+ return {
331
+ text: parsed.sql,
332
+ values: parsed.params
333
+ };
334
+ }
335
+ /**
336
+ * Join multiple SQL fragments
337
+ */ static join(fragments, separator = ' ') {
338
+ const text = fragments.map((f)=>f.text).join(separator);
339
+ const values = fragments.flatMap((f)=>f.values);
340
+ return {
341
+ text,
342
+ values
343
+ };
344
+ }
345
+ /**
346
+ * Helper for building WHERE clauses from objects
347
+ */ static where(conditions) {
348
+ const entries = Object.entries(conditions).filter(([, value])=>void 0 !== value);
349
+ if (0 === entries.length) return {
350
+ text: '',
351
+ values: []
352
+ };
353
+ const clauses = entries.map(([key, value])=>{
354
+ if (null === value) return `${this.escapeIdentifier(key)} IS NULL`;
355
+ if (Array.isArray(value)) {
356
+ const placeholders = value.map(()=>'?').join(', ');
357
+ return `${this.escapeIdentifier(key)} IN (${placeholders})`;
358
+ }
359
+ return `${this.escapeIdentifier(key)} = ?`;
360
+ });
361
+ const values = entries.flatMap(([, value])=>{
362
+ if (null === value) return [];
363
+ if (Array.isArray(value)) return value.map((v)=>this.convertValue(v));
364
+ return [
365
+ this.convertValue(value)
366
+ ];
367
+ });
368
+ return {
369
+ text: clauses.join(' AND '),
370
+ values
371
+ };
372
+ }
373
+ /**
374
+ * Helper for building INSERT VALUES from objects
375
+ */ static insertValues(data) {
376
+ const records = Array.isArray(data) ? data : [
377
+ data
378
+ ];
379
+ if (0 === records.length) throw new Error('No data provided for insert');
380
+ const keys = Object.keys(records[0]);
381
+ const columns = keys.map((key)=>this.escapeIdentifier(key)).join(', ');
382
+ const valueClauses = records.map((record)=>{
383
+ const placeholders = keys.map(()=>'?').join(', ');
384
+ return `(${placeholders})`;
385
+ }).join(', ');
386
+ const values = records.flatMap((record)=>keys.map((key)=>this.convertValue(record[key])));
387
+ return {
388
+ text: `(${columns}) VALUES ${valueClauses}`,
389
+ values
390
+ };
391
+ }
392
+ /**
393
+ * Helper for building UPDATE SET clauses from objects
394
+ */ static updateSet(data) {
395
+ const entries = Object.entries(data).filter(([, value])=>void 0 !== value);
396
+ if (0 === entries.length) throw new Error('No data provided for update');
397
+ const setClauses = entries.map(([key])=>`${this.escapeIdentifier(key)} = ?`).join(', ');
398
+ const values = entries.map(([, value])=>this.convertValue(value));
399
+ return {
400
+ text: setClauses,
401
+ values
402
+ };
403
+ }
404
+ }
405
+ // Export convenience functions
406
+ const sql_parser_sql = sql_parser_SQLParser.parse.bind(sql_parser_SQLParser);
407
+ sql_parser_SQLParser.raw.bind(sql_parser_SQLParser);
408
+ sql_parser_SQLParser.identifier.bind(sql_parser_SQLParser);
409
+ const fragment = sql_parser_SQLParser.fragment.bind(sql_parser_SQLParser);
410
+ sql_parser_SQLParser.join.bind(sql_parser_SQLParser);
411
+ sql_parser_SQLParser.where.bind(sql_parser_SQLParser);
412
+ sql_parser_SQLParser.insertValues.bind(sql_parser_SQLParser);
413
+ sql_parser_SQLParser.updateSet.bind(sql_parser_SQLParser);
414
+ /**
415
+ * Transaction implementation for ODBLite
416
+ * Note: SQLite transactions are simulated at the client level since ODBLite
417
+ * operates on individual queries. This provides a familiar API but doesn't
418
+ * provide true ACID guarantees across multiple HTTP requests.
419
+ */ class ODBLiteTransaction {
420
+ httpClient;
421
+ isCommitted = false;
422
+ isRolledBack = false;
423
+ queries = [];
424
+ constructor(httpClient){
425
+ this.httpClient = httpClient;
426
+ }
427
+ /**
428
+ * Execute a query within the transaction
429
+ * For SQLite, we'll queue queries and execute them in batch on commit
430
+ */ async sql(sql, ...values) {
431
+ this.checkTransactionState();
432
+ const parsed = sql_parser_SQLParser.parse(sql, values);
433
+ // For read queries, execute immediately
434
+ if (this.isReadQuery(parsed.sql)) return await this.httpClient.query(parsed.sql, parsed.params);
435
+ // For write queries, queue them for batch execution
436
+ this.queries.push(parsed);
437
+ // Return a placeholder result for queued queries
438
+ return {
439
+ rows: [],
440
+ rowsAffected: 0,
441
+ executionTime: 0
442
+ };
443
+ }
444
+ /**
445
+ * Commit the transaction by executing all queued queries
446
+ */ async commit() {
447
+ this.checkTransactionState();
448
+ try {
449
+ // Execute BEGIN
450
+ await this.httpClient.query('BEGIN');
451
+ // Execute all queued queries
452
+ for (const query of this.queries)await this.httpClient.query(query.sql, query.params);
453
+ // Commit the transaction
454
+ await this.httpClient.query('COMMIT');
455
+ this.isCommitted = true;
456
+ } catch (error) {
457
+ // Rollback on any error
458
+ try {
459
+ await this.httpClient.query('ROLLBACK');
460
+ } catch (rollbackError) {
461
+ // Ignore rollback errors
462
+ }
463
+ this.isRolledBack = true;
464
+ throw new types_QueryError(`Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`, void 0, void 0, error instanceof Error ? error : void 0);
465
+ }
466
+ }
467
+ /**
468
+ * Rollback the transaction
469
+ */ async rollback() {
470
+ this.checkTransactionState();
471
+ try {
472
+ // If we have any queries, we need to actually rollback
473
+ if (this.queries.length > 0) await this.httpClient.query('ROLLBACK');
474
+ } finally{
475
+ this.isRolledBack = true;
476
+ }
477
+ }
478
+ /**
479
+ * Check if transaction is still active
480
+ */ checkTransactionState() {
481
+ if (this.isCommitted) throw new types_QueryError('Transaction has already been committed');
482
+ if (this.isRolledBack) throw new types_QueryError('Transaction has been rolled back');
483
+ }
484
+ /**
485
+ * Determine if a query is a read operation
486
+ */ isReadQuery(sql) {
487
+ const trimmed = sql.trim().toUpperCase();
488
+ return trimmed.startsWith('SELECT') || trimmed.startsWith('WITH') || trimmed.startsWith('EXPLAIN') || trimmed.startsWith('PRAGMA');
489
+ }
490
+ }
491
+ /**
492
+ * Create a simple transaction function that executes immediately
493
+ * This is more suitable for HTTP-based databases where true transactions
494
+ * across multiple requests are not practical
495
+ */ function createSimpleTransaction(httpClient) {
496
+ let isActive = true;
497
+ // Create the callable transaction function with context awareness
498
+ const txFunction = (sql, ...values)=>{
499
+ if (!isActive) throw new types_QueryError('Transaction is no longer active');
500
+ // Handle template string queries (returns Promise)
501
+ if (Array.isArray(sql) && ('raw' in sql || 'string' == typeof sql[0] && values.length >= 0)) {
502
+ const parsed = sql_parser_SQLParser.parse(sql, values);
503
+ return httpClient.query(parsed.sql, parsed.params);
504
+ }
505
+ // Handle direct object/array inputs (returns SQLFragment for composing)
506
+ const parsed = sql_parser_SQLParser.parse(sql, values);
507
+ return {
508
+ text: parsed.sql,
509
+ values: parsed.params
510
+ };
511
+ };
512
+ // Attach utility methods to the transaction function
513
+ txFunction.raw = (text)=>sql_parser_SQLParser.raw(text);
514
+ txFunction.identifier = (name)=>sql_parser_SQLParser.identifier(name);
515
+ // Add execute method for compatibility
516
+ txFunction.execute = async (sql, args)=>{
517
+ if (!isActive) throw new types_QueryError('Transaction is no longer active');
518
+ if ('string' == typeof sql) return await httpClient.query(sql, args || []);
519
+ return await httpClient.query(sql.sql, sql.args || []);
520
+ };
521
+ // Add query method for compatibility
522
+ txFunction.query = async (sql, params = [])=>{
523
+ if (!isActive) throw new types_QueryError('Transaction is no longer active');
524
+ return await httpClient.query(sql, params);
525
+ };
526
+ txFunction.commit = async ()=>{
527
+ isActive = false;
528
+ // No-op for simple transactions
529
+ };
530
+ txFunction.rollback = async ()=>{
531
+ isActive = false;
532
+ // No-op for simple transactions - individual queries are atomic
533
+ };
534
+ txFunction.savepoint = async (callback)=>{
535
+ if (!isActive) throw new types_QueryError('Transaction is no longer active');
536
+ // Create a nested transaction for the savepoint
537
+ const savepointTx = createSimpleTransaction(httpClient);
538
+ try {
539
+ // Execute the callback with the savepoint transaction
540
+ const result = await callback(savepointTx);
541
+ // Commit the savepoint (no-op for simple transactions)
542
+ await savepointTx.commit();
543
+ return result;
544
+ } catch (error) {
545
+ // Rollback the savepoint on error
546
+ await savepointTx.rollback();
547
+ throw error;
548
+ }
549
+ };
550
+ return txFunction;
551
+ }
552
+ /**
553
+ * Simple transaction implementation that executes immediately
554
+ * This is more suitable for HTTP-based databases where true transactions
555
+ * across multiple requests are not practical
556
+ */ class SimpleTransaction {
557
+ httpClient;
558
+ isActive = true;
559
+ constructor(httpClient){
560
+ this.httpClient = httpClient;
561
+ }
562
+ async sql(sql, ...values) {
563
+ if (!this.isActive) throw new types_QueryError('Transaction is no longer active');
564
+ const parsed = sql_parser_SQLParser.parse(sql, values);
565
+ return await this.httpClient.query(parsed.sql, parsed.params);
566
+ }
567
+ async commit() {
568
+ this.isActive = false;
569
+ // No-op for simple transactions
570
+ }
571
+ async rollback() {
572
+ this.isActive = false;
573
+ // No-op for simple transactions - individual queries are atomic
574
+ }
575
+ }
576
+ /**
577
+ * Main ODBLite client that provides postgres.js-like interface
578
+ */ class ODBLiteClient {
579
+ httpClient;
580
+ config;
581
+ sql;
582
+ constructor(config){
583
+ this.config = config;
584
+ this.httpClient = new HTTPClient(config);
585
+ // Create the callable sql function with attached utility methods
586
+ const sqlFunction = (sql, ...values)=>{
587
+ // Handle template string queries (returns Promise)
588
+ if (Array.isArray(sql) && ('raw' in sql || 'string' == typeof sql[0] && values.length >= 0)) {
589
+ const parsed = sql_parser_SQLParser.parse(sql, values);
590
+ return this.httpClient.query(parsed.sql, parsed.params);
591
+ }
592
+ // Handle direct object/array inputs (returns SQLFragment for composing)
593
+ const parsed = sql_parser_SQLParser.parse(sql, values);
594
+ return {
595
+ text: parsed.sql,
596
+ values: parsed.params
597
+ };
598
+ };
599
+ // Attach minimal utility methods to the function
600
+ sqlFunction.raw = (text)=>sql_parser_SQLParser.raw(text);
601
+ sqlFunction.identifier = (name)=>sql_parser_SQLParser.identifier(name);
602
+ // Attach client methods to the function
603
+ sqlFunction.query = async (sql, params = [])=>await this.httpClient.query(sql, params);
604
+ // libsql-compatible execute method (for backward compatibility)
605
+ sqlFunction.execute = async (sql, args)=>{
606
+ if ('string' == typeof sql) return await this.httpClient.query(sql, args || []);
607
+ return await this.httpClient.query(sql.sql, sql.args || []);
608
+ };
609
+ // Enhanced begin method with callback support
610
+ sqlFunction.begin = async (modeOrCallback, callback)=>{
611
+ // Determine if this is callback-style or traditional
612
+ if ('function' == typeof modeOrCallback) // begin(callback)
613
+ return this.executeTransactionWithCallback(modeOrCallback);
614
+ if ('string' == typeof modeOrCallback && callback) // begin(mode, callback)
615
+ return this.executeTransactionWithCallback(callback, modeOrCallback);
616
+ // begin() - traditional style
617
+ return createSimpleTransaction(this.httpClient);
618
+ };
619
+ sqlFunction.ping = async ()=>await this.httpClient.ping();
620
+ sqlFunction.end = async ()=>{
621
+ // No-op for HTTP-based client
622
+ };
623
+ sqlFunction.setDatabase = (databaseId)=>{
624
+ this.httpClient.setDatabase(databaseId);
625
+ this.config.databaseId = databaseId;
626
+ return sqlFunction;
627
+ };
628
+ sqlFunction.getDatabaseInfo = async ()=>await this.httpClient.getDatabaseInfo();
629
+ sqlFunction.configure = (updates)=>{
630
+ const newConfig = {
631
+ ...this.config,
632
+ ...updates
633
+ };
634
+ return new ODBLiteClient(newConfig).sql;
635
+ };
636
+ this.sql = sqlFunction;
637
+ }
638
+ /**
639
+ * Execute a transaction with callback (postgres.js style)
640
+ */ async executeTransactionWithCallback(callback, mode) {
641
+ const tx = createSimpleTransaction(this.httpClient);
642
+ try {
643
+ // Execute the callback with the transaction
644
+ const result = await callback(tx);
645
+ // Commit the transaction
646
+ await tx.commit();
647
+ return result;
648
+ } catch (error) {
649
+ // Rollback on any error
650
+ await tx.rollback();
651
+ throw error;
652
+ }
653
+ }
654
+ /**
655
+ * Raw query method
656
+ * Usage: client.query('SELECT * FROM users WHERE id = ?', [123])
657
+ */ async query(sql, params = []) {
658
+ return await this.httpClient.query(sql, params);
659
+ }
660
+ /**
661
+ * Begin a transaction
662
+ * Note: Uses simple transaction model suitable for HTTP-based access
663
+ */ async begin() {
664
+ return createSimpleTransaction(this.httpClient);
665
+ }
666
+ /**
667
+ * Health check
668
+ */ async ping() {
669
+ return await this.httpClient.ping();
670
+ }
671
+ /**
672
+ * Close connection (no-op for HTTP client)
673
+ */ async end() {
674
+ // No-op for HTTP-based client
675
+ }
676
+ /**
677
+ * Set the database ID for queries
678
+ */ setDatabase(databaseId) {
679
+ this.httpClient.setDatabase(databaseId);
680
+ this.config.databaseId = databaseId;
681
+ return this;
682
+ }
683
+ /**
684
+ * Get database information
685
+ */ async getDatabaseInfo() {
686
+ return await this.httpClient.getDatabaseInfo();
687
+ }
688
+ /**
689
+ * Create a new client instance with updated configuration
690
+ */ configure(updates) {
691
+ const newConfig = {
692
+ ...this.config,
693
+ ...updates
694
+ };
695
+ return new ODBLiteClient(newConfig);
696
+ }
697
+ /**
698
+ * Create raw SQL that won't be parameterized
699
+ * Usage: sql`SELECT * FROM ${raw('users')}`
700
+ */ static raw(text) {
701
+ return sql_parser_SQLParser.raw(text);
702
+ }
703
+ /**
704
+ * Escape identifier (table/column names)
705
+ * Usage: sql`SELECT * FROM ${identifier('user-table')}`
706
+ */ static identifier(name) {
707
+ return sql_parser_SQLParser.identifier(name);
708
+ }
709
+ /**
710
+ * Build WHERE clause from object
711
+ * Usage: const whereClause = ODBLiteClient.where({ id: 1, name: 'John' });
712
+ */ static where(conditions) {
713
+ return sql_parser_SQLParser.where(conditions);
714
+ }
715
+ /**
716
+ * Build INSERT VALUES from object(s)
717
+ * Usage: const insertClause = ODBLiteClient.insertValues({ name: 'John', age: 30 });
718
+ */ static insertValues(data) {
719
+ return sql_parser_SQLParser.insertValues(data);
720
+ }
721
+ /**
722
+ * Build UPDATE SET clause from object
723
+ * Usage: const setClause = ODBLiteClient.updateSet({ name: 'John', age: 30 });
724
+ */ static updateSet(data) {
725
+ return sql_parser_SQLParser.updateSet(data);
726
+ }
727
+ /**
728
+ * Join SQL fragments
729
+ * Usage: const query = ODBLiteClient.join([baseQuery, whereClause], ' WHERE ');
730
+ */ static join(fragments, separator = ' ') {
731
+ return sql_parser_SQLParser.join(fragments, separator);
732
+ }
733
+ }
734
+ function odblite(configOrBaseUrl, apiKey, databaseId) {
735
+ const client = 'string' == typeof configOrBaseUrl ? new ODBLiteClient({
736
+ baseUrl: configOrBaseUrl,
737
+ apiKey: apiKey,
738
+ databaseId
739
+ }) : new ODBLiteClient(configOrBaseUrl);
740
+ return client.sql;
741
+ }
742
+ // Export static utility functions
743
+ ODBLiteClient.raw;
744
+ ODBLiteClient.identifier;
745
+ ODBLiteClient.where;
746
+ ODBLiteClient.insertValues;
747
+ ODBLiteClient.updateSet;
748
+ ODBLiteClient.join;
749
+ /**
750
+ * Service Client - High-level client for managing tenant databases via ODB-Lite Tenant API
751
+ *
752
+ * This client provides automatic database provisioning and management for multi-tenant applications.
753
+ * It handles:
754
+ * - Automatic database creation on first use
755
+ * - Database hash caching for performance
756
+ * - Tenant API integration with ODB-Lite
757
+ * - Query execution via ODB-Lite's query API
758
+ *
759
+ * @example
760
+ * ```typescript
761
+ * const service = new ServiceClient({
762
+ * baseUrl: 'http://localhost:8671',
763
+ * apiKey: 'odblite_tenant_key'
764
+ * });
765
+ *
766
+ * // Automatically creates database if it doesn't exist
767
+ * const dbHash = await service.ensureDatabaseForTenant('wallet', 'tenant-123');
768
+ *
769
+ * // Execute queries
770
+ * const result = await service.query(dbHash, 'SELECT * FROM wallets', []);
771
+ * ```
772
+ */ /**
773
+ * Service Client for managing tenant databases in ODB-Lite
774
+ *
775
+ * This is a higher-level client that sits on top of the base ODB client.
776
+ * It provides automatic database provisioning and management for services
777
+ * that need per-tenant database isolation.
778
+ */ class ServiceClient {
779
+ apiUrl;
780
+ apiKey;
781
+ databaseCache;
782
+ constructor(config){
783
+ this.apiUrl = config.baseUrl;
784
+ this.apiKey = config.apiKey;
785
+ this.databaseCache = new Map();
786
+ }
787
+ /**
788
+ * Get or create a database for a tenant
789
+ *
790
+ * This is the main method used by services. It will:
791
+ * 1. Check the cache for an existing database hash
792
+ * 2. Query ODB-Lite to see if the database exists
793
+ * 3. Create the database if it doesn't exist
794
+ * 4. Cache and return the database hash
795
+ *
796
+ * @param prefix - Database name prefix (e.g., 'wallet', 'tracking')
797
+ * @param tenantId - Tenant identifier
798
+ * @returns Database hash for querying
799
+ *
800
+ * @example
801
+ * ```typescript
802
+ * const hash = await service.ensureDatabaseForTenant('wallet', 'tenant-123');
803
+ * // Returns hash for database named 'wallet_tenant-123'
804
+ * ```
805
+ */ async ensureDatabaseForTenant(prefix, tenantId) {
806
+ const cacheKey = `${prefix}_${tenantId}`;
807
+ console.log(`📊 Ensuring database for ${cacheKey}`);
808
+ // Check cache first
809
+ const cached = this.databaseCache.get(cacheKey);
810
+ if (cached) {
811
+ console.log(`✅ Found cached database hash: ${cached}`);
812
+ return cached;
813
+ }
814
+ try {
815
+ // Check if database already exists
816
+ console.log(`🔍 Checking if database exists: ${cacheKey}`);
817
+ const databases = await this.listDatabases();
818
+ const existing = databases.find((db)=>db.name === cacheKey);
819
+ if (existing) {
820
+ console.log(`✅ Database already exists: ${cacheKey} (${existing.hash})`);
821
+ this.databaseCache.set(cacheKey, existing.hash);
822
+ return existing.hash;
823
+ }
824
+ // Create new database
825
+ console.log(`🆕 Creating new database: ${cacheKey}`);
826
+ const nodes = await this.listNodes();
827
+ console.log(`📡 Available nodes: ${nodes.length}`);
828
+ if (0 === nodes.length) throw new Error('No available nodes to create database');
829
+ // Use first healthy node
830
+ const node = nodes.find((n)=>'healthy' === n.status) || nodes[0];
831
+ if (!node) throw new Error('No available nodes to create database');
832
+ console.log(`🎯 Using node: ${node.nodeId}`);
833
+ const database = await this.createDatabase(cacheKey, node.nodeId);
834
+ console.log(`✅ Database created successfully: ${database.hash}`);
835
+ this.databaseCache.set(cacheKey, database.hash);
836
+ return database.hash;
837
+ } catch (error) {
838
+ console.error(`❌ Error ensuring database for ${cacheKey}:`, error.message);
839
+ throw error;
840
+ }
841
+ }
842
+ /**
843
+ * List all databases owned by this tenant
844
+ *
845
+ * Queries ODB-Lite's tenant API to get all databases accessible with the current API key.
846
+ *
847
+ * @returns Array of database objects
848
+ */ async listDatabases() {
849
+ const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
850
+ headers: {
851
+ Authorization: `Bearer ${this.apiKey}`
852
+ }
853
+ });
854
+ const result = await response.json();
855
+ if (!result.success) throw new Error(result.error || 'Failed to list databases');
856
+ return result.databases;
857
+ }
858
+ /**
859
+ * Create a new database
860
+ *
861
+ * @param name - Database name (should be unique)
862
+ * @param nodeId - ID of the node to host the database
863
+ * @returns Created database object with hash
864
+ */ async createDatabase(name, nodeId) {
865
+ const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
866
+ method: 'POST',
867
+ headers: {
868
+ Authorization: `Bearer ${this.apiKey}`,
869
+ 'Content-Type': 'application/json'
870
+ },
871
+ body: JSON.stringify({
872
+ name,
873
+ nodeId
874
+ })
875
+ });
876
+ const result = await response.json();
877
+ if (!result.success) throw new Error(result.error || 'Failed to create database');
878
+ return result.database;
879
+ }
880
+ /**
881
+ * Get database details by hash
882
+ *
883
+ * @param hash - Database hash
884
+ * @returns Database object
885
+ */ async getDatabase(hash) {
886
+ const response = await fetch(`${this.apiUrl}/api/tenant/databases/${hash}`, {
887
+ headers: {
888
+ Authorization: `Bearer ${this.apiKey}`
889
+ }
890
+ });
891
+ const result = await response.json();
892
+ if (!result.success) throw new Error(result.error || 'Failed to get database');
893
+ return result.database;
894
+ }
895
+ /**
896
+ * Delete a database
897
+ *
898
+ * @param hash - Database hash to delete
899
+ */ async deleteDatabase(hash) {
900
+ const response = await fetch(`${this.apiUrl}/api/tenant/databases/${hash}`, {
901
+ method: 'DELETE',
902
+ headers: {
903
+ Authorization: `Bearer ${this.apiKey}`
904
+ }
905
+ });
906
+ const result = await response.json();
907
+ if (!result.success) throw new Error(result.error || 'Failed to delete database');
908
+ // Remove from cache
909
+ for (const [key, cachedHash] of this.databaseCache.entries())if (cachedHash === hash) {
910
+ this.databaseCache.delete(key);
911
+ break;
912
+ }
913
+ }
914
+ /**
915
+ * List available nodes
916
+ *
917
+ * @returns Array of node objects
918
+ */ async listNodes() {
919
+ const response = await fetch(`${this.apiUrl}/api/tenant/nodes`, {
920
+ headers: {
921
+ Authorization: `Bearer ${this.apiKey}`
922
+ }
923
+ });
924
+ const result = await response.json();
925
+ if (!result.success) throw new Error(result.error || 'Failed to list nodes');
926
+ return result.nodes;
927
+ }
928
+ /**
929
+ * Execute a query on a specific database
930
+ *
931
+ * This is a low-level query method. For most use cases, you should use
932
+ * the full ODB client (`odblite()` function) instead, which provides
933
+ * template tag support and better ergonomics.
934
+ *
935
+ * @param databaseHash - Hash of the database to query
936
+ * @param sql - SQL query string
937
+ * @param params - Query parameters
938
+ * @returns Query result
939
+ */ async query(databaseHash, sql, params = []) {
940
+ const response = await fetch(`${this.apiUrl}/query/${databaseHash}`, {
941
+ method: 'POST',
942
+ headers: {
943
+ 'Content-Type': 'application/json',
944
+ Authorization: `Bearer ${this.apiKey}`
945
+ },
946
+ body: JSON.stringify({
947
+ sql,
948
+ params
949
+ })
950
+ });
951
+ const result = await response.json();
952
+ if (!result.success) throw new Error(result.error || 'Query failed');
953
+ return result.data;
954
+ }
955
+ /**
956
+ * Clear the database hash cache
957
+ *
958
+ * Useful when database mappings have changed or for testing.
959
+ */ clearCache() {
960
+ this.databaseCache.clear();
961
+ }
962
+ /**
963
+ * Get cached database hash for a specific tenant (if exists)
964
+ *
965
+ * @param prefix - Database name prefix
966
+ * @param tenantId - Tenant identifier
967
+ * @returns Cached hash or undefined
968
+ */ getCachedHash(prefix, tenantId) {
969
+ const cacheKey = `${prefix}_${tenantId}`;
970
+ return this.databaseCache.get(cacheKey);
971
+ }
972
+ /**
973
+ * Pre-cache a database hash
974
+ *
975
+ * Useful when you know the mapping ahead of time and want to avoid
976
+ * the initial lookup.
977
+ *
978
+ * @param prefix - Database name prefix
979
+ * @param tenantId - Tenant identifier
980
+ * @param hash - Database hash
981
+ */ setCachedHash(prefix, tenantId, hash) {
982
+ const cacheKey = `${prefix}_${tenantId}`;
983
+ this.databaseCache.set(cacheKey, hash);
984
+ }
985
+ }
986
+ // Main entry point for ODB Client
987
+ // Core query client exports (postgres.js-like interface)
988
+ // Service management exports (high-level tenant database management)
989
+ // Export error classes
990
+ // Export static utility functions for easy access
991
+ const { raw: src_raw, identifier: src_identifier, where: src_where, insertValues: src_insertValues, updateSet: src_updateSet, join: src_join } = ODBLiteClient;
992
+ export { ConnectionError, HTTPClient, ODBLiteClient, ODBLiteError, ODBLiteTransaction, types_QueryError as QueryError, sql_parser_SQLParser as SQLParser, ServiceClient, SimpleTransaction, odblite as default, fragment, src_identifier as identifier, src_insertValues as insertValues, src_join as join, odblite, src_raw as raw, sql_parser_sql as sql, src_updateSet as updateSet, src_where as where };