@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/README.md +316 -0
- package/dist/core/client.d.ts +110 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/http-client.d.ts +37 -0
- package/dist/core/http-client.d.ts.map +1 -0
- package/dist/core/sql-parser.d.ts +70 -0
- package/dist/core/sql-parser.d.ts.map +1 -0
- package/dist/core/transaction.d.ts +60 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/index.cjs +1052 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +992 -0
- package/dist/service/service-client.d.ts +151 -0
- package/dist/service/service-client.d.ts.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/core/client.ts +277 -0
- package/src/core/http-client.ts +200 -0
- package/src/core/sql-parser.ts +330 -0
- package/src/core/transaction.ts +425 -0
- package/src/index.ts +51 -0
- package/src/service/service-client.ts +316 -0
- package/src/types.ts +127 -0
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
|
+
});
|