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