@pineliner/odb-client 1.0.0 → 1.0.2
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/database/adapters/bun-sqlite.d.ts +14 -0
- package/dist/database/adapters/bun-sqlite.d.ts.map +1 -0
- package/dist/database/adapters/libsql.d.ts +14 -0
- package/dist/database/adapters/libsql.d.ts.map +1 -0
- package/dist/database/adapters/odblite.d.ts +15 -0
- package/dist/database/adapters/odblite.d.ts.map +1 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/manager.d.ts +92 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/sql-parser.d.ts +17 -0
- package/dist/database/sql-parser.d.ts.map +1 -0
- package/dist/database/types.d.ts +106 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/index.cjs +740 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +714 -8
- package/dist/service/service-client.d.ts +9 -2
- package/dist/service/service-client.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/database/adapters/bun-sqlite.ts +170 -0
- package/src/database/adapters/libsql.ts +156 -0
- package/src/database/adapters/odblite.ts +181 -0
- package/src/database/index.ts +32 -0
- package/src/database/manager.ts +336 -0
- package/src/database/sql-parser.ts +112 -0
- package/src/database/types.ts +140 -0
- package/src/index.ts +28 -0
- package/src/service/service-client.ts +34 -5
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as __WEBPACK_EXTERNAL_MODULE_bun_sqlite__ from "bun:sqlite";
|
|
2
|
+
import * as __WEBPACK_EXTERNAL_MODULE__libsql_client__ from "@libsql/client";
|
|
3
|
+
import * as __WEBPACK_EXTERNAL_MODULE_node_fs__ from "node:fs";
|
|
1
4
|
// Core types for the ODBLite client
|
|
2
5
|
// Error types
|
|
3
6
|
class ODBLiteError extends Error {
|
|
@@ -814,8 +817,7 @@ ODBLiteClient.join;
|
|
|
814
817
|
try {
|
|
815
818
|
// Check if database already exists
|
|
816
819
|
console.log(`🔍 Checking if database exists: ${cacheKey}`);
|
|
817
|
-
const
|
|
818
|
-
const existing = databases.find((db)=>db.name === cacheKey);
|
|
820
|
+
const existing = await this.getDatabaseByName(cacheKey);
|
|
819
821
|
if (existing) {
|
|
820
822
|
console.log(`✅ Database already exists: ${cacheKey} (${existing.hash})`);
|
|
821
823
|
this.databaseCache.set(cacheKey, existing.hash);
|
|
@@ -859,19 +861,20 @@ ODBLiteClient.join;
|
|
|
859
861
|
* Create a new database
|
|
860
862
|
*
|
|
861
863
|
* @param name - Database name (should be unique)
|
|
862
|
-
* @param nodeId - ID of the node to host the database
|
|
864
|
+
* @param nodeId - ID of the node to host the database (optional - server will select if null)
|
|
863
865
|
* @returns Created database object with hash
|
|
864
866
|
*/ async createDatabase(name, nodeId) {
|
|
867
|
+
const body = {
|
|
868
|
+
name
|
|
869
|
+
};
|
|
870
|
+
if (nodeId) body.nodeId = nodeId;
|
|
865
871
|
const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
|
|
866
872
|
method: 'POST',
|
|
867
873
|
headers: {
|
|
868
874
|
Authorization: `Bearer ${this.apiKey}`,
|
|
869
875
|
'Content-Type': 'application/json'
|
|
870
876
|
},
|
|
871
|
-
body: JSON.stringify(
|
|
872
|
-
name,
|
|
873
|
-
nodeId
|
|
874
|
-
})
|
|
877
|
+
body: JSON.stringify(body)
|
|
875
878
|
});
|
|
876
879
|
const result = await response.json();
|
|
877
880
|
if (!result.success) throw new Error(result.error || 'Failed to create database');
|
|
@@ -893,6 +896,22 @@ ODBLiteClient.join;
|
|
|
893
896
|
return result.database;
|
|
894
897
|
}
|
|
895
898
|
/**
|
|
899
|
+
* Get database details by name
|
|
900
|
+
*
|
|
901
|
+
* @param name - Database name
|
|
902
|
+
* @returns Database object or null if not found
|
|
903
|
+
*/ async getDatabaseByName(name) {
|
|
904
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases/by-name/${name}`, {
|
|
905
|
+
headers: {
|
|
906
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
if (404 === response.status) return null;
|
|
910
|
+
const result = await response.json();
|
|
911
|
+
if (!result.success) throw new Error(result.error || 'Failed to get database');
|
|
912
|
+
return result.database;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
896
915
|
* Delete a database
|
|
897
916
|
*
|
|
898
917
|
* @param hash - Database hash to delete
|
|
@@ -983,10 +1002,697 @@ ODBLiteClient.join;
|
|
|
983
1002
|
this.databaseCache.set(cacheKey, hash);
|
|
984
1003
|
}
|
|
985
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Bun SQLite adapter for DatabaseManager
|
|
1007
|
+
* Wraps bun:sqlite with Connection interface
|
|
1008
|
+
*/ class BunSQLiteAdapter {
|
|
1009
|
+
type = 'bun-sqlite';
|
|
1010
|
+
config;
|
|
1011
|
+
constructor(config){
|
|
1012
|
+
this.config = config;
|
|
1013
|
+
}
|
|
1014
|
+
async connect(config) {
|
|
1015
|
+
const db = new __WEBPACK_EXTERNAL_MODULE_bun_sqlite__.Database(config.databasePath, {
|
|
1016
|
+
readonly: config.readonly,
|
|
1017
|
+
create: config.create ?? true
|
|
1018
|
+
});
|
|
1019
|
+
return new BunSQLiteConnection(db);
|
|
1020
|
+
}
|
|
1021
|
+
async disconnect(tenantId) {
|
|
1022
|
+
// bun:sqlite doesn't require explicit disconnection
|
|
1023
|
+
// File handles are cleaned up by GC
|
|
1024
|
+
}
|
|
1025
|
+
async isHealthy() {
|
|
1026
|
+
try {
|
|
1027
|
+
const db = new __WEBPACK_EXTERNAL_MODULE_bun_sqlite__.Database(this.config.databasePath, {
|
|
1028
|
+
readonly: true,
|
|
1029
|
+
create: false
|
|
1030
|
+
});
|
|
1031
|
+
db.query('SELECT 1').get();
|
|
1032
|
+
db.close();
|
|
1033
|
+
return true;
|
|
1034
|
+
} catch {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Connection implementation for Bun SQLite
|
|
1041
|
+
*/ class BunSQLiteConnection {
|
|
1042
|
+
db;
|
|
1043
|
+
inTransaction = false;
|
|
1044
|
+
constructor(db){
|
|
1045
|
+
this.db = db;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Template tag query (postgres.js-like)
|
|
1049
|
+
*/ async query(strings, ...values) {
|
|
1050
|
+
// Build SQL from template
|
|
1051
|
+
const sql = strings.reduce((acc, str, i)=>acc + str + (i < values.length ? '?' : ''), '');
|
|
1052
|
+
return this.execute(sql, values);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Execute SQL with parameters
|
|
1056
|
+
*/ async execute(sql, params = []) {
|
|
1057
|
+
try {
|
|
1058
|
+
const stmt = this.db.query(sql);
|
|
1059
|
+
const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
|
|
1060
|
+
if (isSelect) {
|
|
1061
|
+
const rows = stmt.all(...params);
|
|
1062
|
+
return {
|
|
1063
|
+
rows: rows,
|
|
1064
|
+
rowsAffected: 0
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
{
|
|
1068
|
+
const result = stmt.run(...params);
|
|
1069
|
+
return {
|
|
1070
|
+
rows: [],
|
|
1071
|
+
rowsAffected: result.changes || 0,
|
|
1072
|
+
lastInsertRowid: result.lastInsertRowid
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
throw new Error(`SQL execution failed: ${error.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Prepare statement for repeated execution
|
|
1081
|
+
*/ prepare(sql) {
|
|
1082
|
+
const stmt = this.db.query(sql);
|
|
1083
|
+
return {
|
|
1084
|
+
execute: async (params = [])=>{
|
|
1085
|
+
const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
|
|
1086
|
+
if (isSelect) {
|
|
1087
|
+
const rows = stmt.all(...params);
|
|
1088
|
+
return {
|
|
1089
|
+
rows: rows,
|
|
1090
|
+
rowsAffected: 0
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
{
|
|
1094
|
+
const result = stmt.run(...params);
|
|
1095
|
+
return {
|
|
1096
|
+
rows: [],
|
|
1097
|
+
rowsAffected: result.changes || 0,
|
|
1098
|
+
lastInsertRowid: result.lastInsertRowid
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
all: async (params = [])=>stmt.all(...params),
|
|
1103
|
+
get: async (params = [])=>stmt.get(...params)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Execute function in transaction
|
|
1108
|
+
*/ async transaction(fn) {
|
|
1109
|
+
if (this.inTransaction) // Nested transaction - just execute the function
|
|
1110
|
+
return fn(this);
|
|
1111
|
+
this.inTransaction = true;
|
|
1112
|
+
try {
|
|
1113
|
+
await this.execute('BEGIN');
|
|
1114
|
+
const result = await fn(this);
|
|
1115
|
+
await this.execute('COMMIT');
|
|
1116
|
+
return result;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
await this.execute('ROLLBACK');
|
|
1119
|
+
throw error;
|
|
1120
|
+
} finally{
|
|
1121
|
+
this.inTransaction = false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Close connection
|
|
1126
|
+
*/ async close() {
|
|
1127
|
+
this.db.close();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* LibSQL adapter for DatabaseManager
|
|
1132
|
+
* Wraps @libsql/client with Connection interface
|
|
1133
|
+
*/ class LibSQLAdapter {
|
|
1134
|
+
type = 'libsql';
|
|
1135
|
+
config;
|
|
1136
|
+
constructor(config){
|
|
1137
|
+
this.config = config;
|
|
1138
|
+
}
|
|
1139
|
+
async connect(config) {
|
|
1140
|
+
const client = (0, __WEBPACK_EXTERNAL_MODULE__libsql_client__.createClient)({
|
|
1141
|
+
url: config.url,
|
|
1142
|
+
authToken: config.authToken,
|
|
1143
|
+
encryptionKey: config.encryptionKey
|
|
1144
|
+
});
|
|
1145
|
+
return new LibSQLConnection(client);
|
|
1146
|
+
}
|
|
1147
|
+
async disconnect(tenantId) {
|
|
1148
|
+
// LibSQL clients don't require explicit disconnection
|
|
1149
|
+
// Connections are pooled internally
|
|
1150
|
+
}
|
|
1151
|
+
async isHealthy() {
|
|
1152
|
+
try {
|
|
1153
|
+
const client = (0, __WEBPACK_EXTERNAL_MODULE__libsql_client__.createClient)({
|
|
1154
|
+
url: this.config.url,
|
|
1155
|
+
authToken: this.config.authToken
|
|
1156
|
+
});
|
|
1157
|
+
await client.execute('SELECT 1');
|
|
1158
|
+
return true;
|
|
1159
|
+
} catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Connection implementation for LibSQL
|
|
1166
|
+
*/ class LibSQLConnection {
|
|
1167
|
+
client;
|
|
1168
|
+
txClient;
|
|
1169
|
+
constructor(client){
|
|
1170
|
+
this.client = client;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Template tag query (postgres.js-like)
|
|
1174
|
+
*/ async query(strings, ...values) {
|
|
1175
|
+
// Build SQL from template
|
|
1176
|
+
const sql = strings.reduce((acc, str, i)=>acc + str + (i < values.length ? '?' : ''), '');
|
|
1177
|
+
return this.execute(sql, values);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Execute SQL with parameters
|
|
1181
|
+
* Supports both formats: execute(sql, params) and execute({sql, args})
|
|
1182
|
+
*/ async execute(sql, params = []) {
|
|
1183
|
+
try {
|
|
1184
|
+
const target = this.txClient || this.client;
|
|
1185
|
+
// Support both execute(sql, params) and execute({sql, args}) formats
|
|
1186
|
+
let query;
|
|
1187
|
+
query = 'string' == typeof sql ? {
|
|
1188
|
+
sql,
|
|
1189
|
+
args: params
|
|
1190
|
+
} : {
|
|
1191
|
+
sql: sql.sql,
|
|
1192
|
+
args: sql.args || []
|
|
1193
|
+
};
|
|
1194
|
+
const result = await target.execute(query);
|
|
1195
|
+
return {
|
|
1196
|
+
rows: result.rows,
|
|
1197
|
+
rowsAffected: Number(result.rowsAffected),
|
|
1198
|
+
lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid.toString()) : void 0
|
|
1199
|
+
};
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
throw new Error(`SQL execution failed: ${error.message}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Prepare statement for repeated execution
|
|
1206
|
+
*/ prepare(sql) {
|
|
1207
|
+
return {
|
|
1208
|
+
execute: async (params = [])=>this.execute(sql, params),
|
|
1209
|
+
all: async (params = [])=>{
|
|
1210
|
+
const result = await this.execute(sql, params);
|
|
1211
|
+
return result.rows;
|
|
1212
|
+
},
|
|
1213
|
+
get: async (params = [])=>{
|
|
1214
|
+
const result = await this.execute(sql, params);
|
|
1215
|
+
return result.rows[0] || null;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Execute function in transaction
|
|
1221
|
+
*/ async transaction(fn) {
|
|
1222
|
+
if (this.txClient) // Nested transaction - just execute the function
|
|
1223
|
+
return fn(this);
|
|
1224
|
+
// Use manual BEGIN/COMMIT/ROLLBACK for simplicity
|
|
1225
|
+
try {
|
|
1226
|
+
await this.execute('BEGIN');
|
|
1227
|
+
const result = await fn(this);
|
|
1228
|
+
await this.execute('COMMIT');
|
|
1229
|
+
return result;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
await this.execute('ROLLBACK');
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Close connection
|
|
1237
|
+
*/ async close() {
|
|
1238
|
+
// LibSQL client.close() if available in future versions
|
|
1239
|
+
// For now, connections are managed by the client pool
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* ODB-Lite adapter for DatabaseManager
|
|
1244
|
+
* Wraps ServiceClient and ODBLiteClient with Connection interface
|
|
1245
|
+
*/ class ODBLiteAdapter {
|
|
1246
|
+
type = 'odblite';
|
|
1247
|
+
config;
|
|
1248
|
+
serviceClient;
|
|
1249
|
+
constructor(config){
|
|
1250
|
+
this.config = config;
|
|
1251
|
+
this.serviceClient = new ServiceClient({
|
|
1252
|
+
baseUrl: config.serviceUrl,
|
|
1253
|
+
apiKey: config.apiKey
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async connect(config) {
|
|
1257
|
+
const databaseName = config.databaseName || 'default';
|
|
1258
|
+
// Ensure database exists
|
|
1259
|
+
const dbInfo = await this.serviceClient.getDatabaseByName(databaseName);
|
|
1260
|
+
if (!dbInfo) // Create database - pass nodeId from config if provided, otherwise server selects
|
|
1261
|
+
await this.serviceClient.createDatabase(databaseName, this.config.nodeId);
|
|
1262
|
+
// Get fresh database info
|
|
1263
|
+
const db = await this.serviceClient.getDatabaseByName(databaseName);
|
|
1264
|
+
if (!db) throw new Error(`Database ${databaseName} not found after creation`);
|
|
1265
|
+
// Create ODBLiteClient for this database
|
|
1266
|
+
const client = new ODBLiteClient({
|
|
1267
|
+
baseUrl: this.config.serviceUrl,
|
|
1268
|
+
apiKey: this.config.apiKey,
|
|
1269
|
+
databaseId: db.hash
|
|
1270
|
+
});
|
|
1271
|
+
return new ODBLiteConnection(client, this.serviceClient, databaseName);
|
|
1272
|
+
}
|
|
1273
|
+
async disconnect(tenantId) {
|
|
1274
|
+
if (tenantId) {
|
|
1275
|
+
const prefix = 'pipeline_tenant_' // TODO: make configurable
|
|
1276
|
+
;
|
|
1277
|
+
const databaseName = `${prefix}${tenantId}`;
|
|
1278
|
+
try {
|
|
1279
|
+
await this.serviceClient.deleteDatabase(databaseName);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
console.warn(`Failed to delete database ${databaseName}:`, error.message);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
async isHealthy() {
|
|
1286
|
+
try {
|
|
1287
|
+
await this.serviceClient.listDatabases();
|
|
1288
|
+
return true;
|
|
1289
|
+
} catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Connection implementation for ODB-Lite
|
|
1296
|
+
*/ class ODBLiteConnection {
|
|
1297
|
+
client;
|
|
1298
|
+
serviceClient;
|
|
1299
|
+
databaseName;
|
|
1300
|
+
inTransaction = false;
|
|
1301
|
+
constructor(client, serviceClient, databaseName){
|
|
1302
|
+
this.client = client;
|
|
1303
|
+
this.serviceClient = serviceClient;
|
|
1304
|
+
this.databaseName = databaseName;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Template tag query (postgres.js-like)
|
|
1308
|
+
*/ async query(strings, ...values) {
|
|
1309
|
+
// ODBLiteClient.sql is a function that returns a Promise
|
|
1310
|
+
const result = await this.client.sql(strings, ...values);
|
|
1311
|
+
return {
|
|
1312
|
+
rows: result.rows,
|
|
1313
|
+
rowsAffected: result.rowsAffected || 0
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Execute SQL with parameters
|
|
1318
|
+
*/ async execute(sql, params = []) {
|
|
1319
|
+
try {
|
|
1320
|
+
const result = await this.client.sql.execute(sql, params);
|
|
1321
|
+
return {
|
|
1322
|
+
rows: result.rows,
|
|
1323
|
+
rowsAffected: result.rowsAffected || 0
|
|
1324
|
+
};
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
throw new Error(`SQL execution failed: ${error.message}`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Prepare statement for repeated execution
|
|
1331
|
+
*/ prepare(sql) {
|
|
1332
|
+
return {
|
|
1333
|
+
execute: async (params = [])=>this.execute(sql, params),
|
|
1334
|
+
all: async (params = [])=>{
|
|
1335
|
+
const result = await this.execute(sql, params);
|
|
1336
|
+
return result.rows;
|
|
1337
|
+
},
|
|
1338
|
+
get: async (params = [])=>{
|
|
1339
|
+
const result = await this.execute(sql, params);
|
|
1340
|
+
return result.rows[0] || null;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Execute function in transaction
|
|
1346
|
+
*/ async transaction(fn) {
|
|
1347
|
+
if (this.inTransaction) // Nested transaction - just execute the function
|
|
1348
|
+
return fn(this);
|
|
1349
|
+
this.inTransaction = true;
|
|
1350
|
+
try {
|
|
1351
|
+
await this.execute('BEGIN');
|
|
1352
|
+
const result = await fn(this);
|
|
1353
|
+
await this.execute('COMMIT');
|
|
1354
|
+
return result;
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
await this.execute('ROLLBACK');
|
|
1357
|
+
throw error;
|
|
1358
|
+
} finally{
|
|
1359
|
+
this.inTransaction = false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Close connection
|
|
1364
|
+
*/ async close() {
|
|
1365
|
+
// ODBLiteClient connections are stateless HTTP requests
|
|
1366
|
+
// No explicit close needed
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Parse SQL file into statements, separating PRAGMA from regular SQL
|
|
1371
|
+
* Handles comments, quotes, and semicolons properly
|
|
1372
|
+
*/ function parseSQL(sqlContent, options = {}) {
|
|
1373
|
+
const separatePragma = options.separatePragma ?? true;
|
|
1374
|
+
// Split by semicolons (node-sql-parser doesn't handle multiple statements well)
|
|
1375
|
+
const statements = splitStatements(sqlContent);
|
|
1376
|
+
if (!separatePragma) return {
|
|
1377
|
+
pragmaStatements: [],
|
|
1378
|
+
regularStatements: statements
|
|
1379
|
+
};
|
|
1380
|
+
const pragmaStatements = [];
|
|
1381
|
+
const regularStatements = [];
|
|
1382
|
+
for (const statement of statements){
|
|
1383
|
+
const trimmed = statement.trim().toUpperCase();
|
|
1384
|
+
if (trimmed.startsWith('PRAGMA')) pragmaStatements.push(statement);
|
|
1385
|
+
else regularStatements.push(statement);
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
pragmaStatements,
|
|
1389
|
+
regularStatements
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Split SQL content into individual statements
|
|
1394
|
+
* Handles comments, quotes, and semicolons properly
|
|
1395
|
+
*/ function splitStatements(sqlContent) {
|
|
1396
|
+
const statements = [];
|
|
1397
|
+
let currentStatement = '';
|
|
1398
|
+
let inQuote = false;
|
|
1399
|
+
let quoteChar = '';
|
|
1400
|
+
const lines = sqlContent.split('\n');
|
|
1401
|
+
for (const line of lines){
|
|
1402
|
+
let processedLine = '';
|
|
1403
|
+
for(let i = 0; i < line.length; i++){
|
|
1404
|
+
const char = line[i];
|
|
1405
|
+
const prevChar = i > 0 ? line[i - 1] : '';
|
|
1406
|
+
const nextChar = i < line.length - 1 ? line[i + 1] : '';
|
|
1407
|
+
// Handle single-line comments
|
|
1408
|
+
if (!inQuote && '-' === char && '-' === nextChar) break; // Skip rest of line
|
|
1409
|
+
// Track quotes
|
|
1410
|
+
if (("'" === char || '"' === char) && '\\' !== prevChar) {
|
|
1411
|
+
if (inQuote) {
|
|
1412
|
+
if (char === quoteChar) {
|
|
1413
|
+
inQuote = false;
|
|
1414
|
+
quoteChar = '';
|
|
1415
|
+
}
|
|
1416
|
+
} else {
|
|
1417
|
+
inQuote = true;
|
|
1418
|
+
quoteChar = char;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
processedLine += char;
|
|
1422
|
+
// Split on semicolon when not in quotes
|
|
1423
|
+
if (';' === char && !inQuote) {
|
|
1424
|
+
currentStatement += processedLine;
|
|
1425
|
+
const stmt = currentStatement.trim();
|
|
1426
|
+
if (stmt) statements.push(stmt);
|
|
1427
|
+
currentStatement = '';
|
|
1428
|
+
processedLine = '';
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (processedLine.trim()) currentStatement += `${processedLine}\n`;
|
|
1432
|
+
}
|
|
1433
|
+
// Handle remaining statement
|
|
1434
|
+
const finalStmt = currentStatement.trim();
|
|
1435
|
+
if (finalStmt) statements.push(finalStmt);
|
|
1436
|
+
return statements;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Simple split for systems that don't need PRAGMA separation
|
|
1440
|
+
*/ function splitSQLStatements(sqlContent) {
|
|
1441
|
+
const { pragmaStatements, regularStatements } = parseSQL(sqlContent);
|
|
1442
|
+
return [
|
|
1443
|
+
...pragmaStatements,
|
|
1444
|
+
...regularStatements
|
|
1445
|
+
];
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Database factory for creating and managing multiple databases
|
|
1449
|
+
*
|
|
1450
|
+
* Features:
|
|
1451
|
+
* - Creates multiple databases from single manager instance
|
|
1452
|
+
* - Supports libsql, bun:sqlite, and ODB-Lite backends
|
|
1453
|
+
* - Connection pooling with LRU eviction
|
|
1454
|
+
* - Automatic schema migrations
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* ```ts
|
|
1458
|
+
* const dbManager = new DatabaseManager({
|
|
1459
|
+
* backend: 'libsql',
|
|
1460
|
+
* databasePath: './data'
|
|
1461
|
+
* })
|
|
1462
|
+
*
|
|
1463
|
+
* // Create databases
|
|
1464
|
+
* await dbManager.createDatabase('identity', { schemaContent })
|
|
1465
|
+
* await dbManager.createDatabase('analytics', { schemaContent })
|
|
1466
|
+
*
|
|
1467
|
+
* // Get connections
|
|
1468
|
+
* const identityDB = await dbManager.getConnection('identity')
|
|
1469
|
+
* const analyticsDB = await dbManager.getConnection('analytics')
|
|
1470
|
+
* ```
|
|
1471
|
+
*/ class DatabaseManager {
|
|
1472
|
+
config;
|
|
1473
|
+
adapter;
|
|
1474
|
+
connections = new Map();
|
|
1475
|
+
// Connection pool management (LRU)
|
|
1476
|
+
connectionTimestamps = new Map();
|
|
1477
|
+
maxConnections;
|
|
1478
|
+
constructor(config){
|
|
1479
|
+
this.config = config;
|
|
1480
|
+
this.maxConnections = config.connectionPoolSize || 10;
|
|
1481
|
+
// Create appropriate adapter based on backend
|
|
1482
|
+
switch(config.backend){
|
|
1483
|
+
case 'bun-sqlite':
|
|
1484
|
+
this.adapter = new BunSQLiteAdapter({
|
|
1485
|
+
databasePath: config.databasePath,
|
|
1486
|
+
create: true
|
|
1487
|
+
});
|
|
1488
|
+
break;
|
|
1489
|
+
case 'libsql':
|
|
1490
|
+
this.adapter = new LibSQLAdapter({
|
|
1491
|
+
url: `file:${config.databasePath}`,
|
|
1492
|
+
...config.libsql
|
|
1493
|
+
});
|
|
1494
|
+
break;
|
|
1495
|
+
case 'odblite':
|
|
1496
|
+
if (!config.odblite) throw new Error('odblite config required for odblite backend');
|
|
1497
|
+
this.adapter = new ODBLiteAdapter(config.odblite);
|
|
1498
|
+
break;
|
|
1499
|
+
default:
|
|
1500
|
+
throw new Error(`Unknown backend type: ${config.backend}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Create a new database
|
|
1505
|
+
* @param name - Database name (becomes filename or ODB-Lite database name)
|
|
1506
|
+
* @param options - Optional creation options
|
|
1507
|
+
*/ async createDatabase(name, options) {
|
|
1508
|
+
console.log(`📦 Creating database: ${name}`);
|
|
1509
|
+
// Build database-specific config
|
|
1510
|
+
const dbConfig = this.getDatabaseConfig(name);
|
|
1511
|
+
// Create connection
|
|
1512
|
+
const conn = await this.adapter.connect(dbConfig);
|
|
1513
|
+
// Run migrations if schema provided
|
|
1514
|
+
if (options?.schemaContent) await this.runMigrations(conn, {
|
|
1515
|
+
schemaContent: options.schemaContent
|
|
1516
|
+
});
|
|
1517
|
+
// Store connection
|
|
1518
|
+
this.connections.set(name, conn);
|
|
1519
|
+
this.connectionTimestamps.set(name, Date.now());
|
|
1520
|
+
// Evict LRU if pool is full
|
|
1521
|
+
if (this.connections.size > this.maxConnections) await this.evictLRU();
|
|
1522
|
+
console.log(`✅ Database created: ${name}`);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Check if a database exists
|
|
1526
|
+
* @param name - Database name
|
|
1527
|
+
* @returns true if database exists, false otherwise
|
|
1528
|
+
*
|
|
1529
|
+
* Note: For local backends (libsql, bun-sqlite), checks file existence.
|
|
1530
|
+
* For ODB-Lite, checks if database is in connection cache.
|
|
1531
|
+
*/ async databaseExists(name) {
|
|
1532
|
+
// Check if already in connection cache
|
|
1533
|
+
if (this.connections.has(name)) return true;
|
|
1534
|
+
// For local file-based backends, check if file exists
|
|
1535
|
+
switch(this.config.backend){
|
|
1536
|
+
case 'bun-sqlite':
|
|
1537
|
+
{
|
|
1538
|
+
const dbPath = `${this.config.databasePath}/${name}.db`;
|
|
1539
|
+
return __WEBPACK_EXTERNAL_MODULE_node_fs__["default"].existsSync(dbPath);
|
|
1540
|
+
}
|
|
1541
|
+
case 'libsql':
|
|
1542
|
+
{
|
|
1543
|
+
const dbPath = `${this.config.databasePath}/${name}.db`;
|
|
1544
|
+
return __WEBPACK_EXTERNAL_MODULE_node_fs__["default"].existsSync(dbPath);
|
|
1545
|
+
}
|
|
1546
|
+
case 'odblite':
|
|
1547
|
+
// For ODB-Lite, only return true if in cache
|
|
1548
|
+
// (no way to check remote database existence without connecting)
|
|
1549
|
+
return false;
|
|
1550
|
+
default:
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Get connection to a database
|
|
1556
|
+
* @param name - Database name
|
|
1557
|
+
*
|
|
1558
|
+
* If database exists on disk but not in cache, it will be connected automatically.
|
|
1559
|
+
* If database doesn't exist at all, an error will be thrown.
|
|
1560
|
+
*/ async getConnection(name) {
|
|
1561
|
+
// Check if connection exists in cache
|
|
1562
|
+
let conn = this.connections.get(name);
|
|
1563
|
+
if (conn) {
|
|
1564
|
+
this.connectionTimestamps.set(name, Date.now());
|
|
1565
|
+
return conn;
|
|
1566
|
+
}
|
|
1567
|
+
// Check if database exists on disk
|
|
1568
|
+
const exists = await this.databaseExists(name);
|
|
1569
|
+
if (exists) {
|
|
1570
|
+
// Connect to existing database
|
|
1571
|
+
const dbConfig = this.getDatabaseConfig(name);
|
|
1572
|
+
conn = await this.adapter.connect(dbConfig);
|
|
1573
|
+
// Store connection
|
|
1574
|
+
this.connections.set(name, conn);
|
|
1575
|
+
this.connectionTimestamps.set(name, Date.now());
|
|
1576
|
+
// Evict LRU if pool is full
|
|
1577
|
+
if (this.connections.size > this.maxConnections) await this.evictLRU();
|
|
1578
|
+
return conn;
|
|
1579
|
+
}
|
|
1580
|
+
// If database doesn't exist, throw error
|
|
1581
|
+
throw new Error(`Database "${name}" not found. Call createDatabase("${name}", { schemaContent }) first.`);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Delete a database
|
|
1585
|
+
* @param name - Database name
|
|
1586
|
+
*/ async deleteDatabase(name) {
|
|
1587
|
+
console.log(`🗑️ Deleting database: ${name}`);
|
|
1588
|
+
// Close connection if exists
|
|
1589
|
+
const conn = this.connections.get(name);
|
|
1590
|
+
if (conn) {
|
|
1591
|
+
await conn.close();
|
|
1592
|
+
this.connections.delete(name);
|
|
1593
|
+
this.connectionTimestamps.delete(name);
|
|
1594
|
+
}
|
|
1595
|
+
// Backend-specific deletion
|
|
1596
|
+
await this.adapter.disconnect(name);
|
|
1597
|
+
console.log(`✅ Database deleted: ${name}`);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Execute SQL content on a database connection
|
|
1601
|
+
* Useful for running migrations or initial schema
|
|
1602
|
+
* @param name - Database name
|
|
1603
|
+
* @param sqlContent - SQL content to execute
|
|
1604
|
+
*/ async executeSQLFile(name, sqlContent) {
|
|
1605
|
+
const conn = await this.getConnection(name);
|
|
1606
|
+
await this.runMigrations(conn, {
|
|
1607
|
+
schemaContent: sqlContent
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* List all managed databases
|
|
1612
|
+
*/ async listDatabases() {
|
|
1613
|
+
return Array.from(this.connections.keys());
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Close all connections
|
|
1617
|
+
*/ async close() {
|
|
1618
|
+
console.log(`🔌 Closing all database connections...`);
|
|
1619
|
+
for (const [name, conn] of this.connections)await conn.close();
|
|
1620
|
+
this.connections.clear();
|
|
1621
|
+
this.connectionTimestamps.clear();
|
|
1622
|
+
console.log(`✅ All connections closed`);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Run migrations on a connection
|
|
1626
|
+
*/ async runMigrations(conn, config) {
|
|
1627
|
+
// Parse SQL statements
|
|
1628
|
+
const { pragmaStatements, regularStatements } = parseSQL(config.schemaContent);
|
|
1629
|
+
// Execute PRAGMA statements first
|
|
1630
|
+
for (const pragma of pragmaStatements)try {
|
|
1631
|
+
await conn.execute(pragma);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
// Ignore PRAGMA errors (they might not be supported on all backends)
|
|
1634
|
+
console.log(`⚠️ PRAGMA note: ${error.message}`);
|
|
1635
|
+
}
|
|
1636
|
+
// Execute regular statements
|
|
1637
|
+
for (const statement of regularStatements)try {
|
|
1638
|
+
await conn.execute(statement);
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
// Ignore "already exists" errors for idempotency
|
|
1641
|
+
if (error.message?.includes('already exists') || error.message?.includes('no such column') || error.message?.includes('no such table')) {
|
|
1642
|
+
console.log(` ⚠️ Skipping: ${error.message}`);
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
console.error(`❌ Migration failed: ${statement.substring(0, 100)}...`);
|
|
1646
|
+
throw error;
|
|
1647
|
+
}
|
|
1648
|
+
console.log(`✅ Migrations completed (${regularStatements.length} statements)`);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Get database-specific configuration
|
|
1652
|
+
*/ getDatabaseConfig(name) {
|
|
1653
|
+
switch(this.config.backend){
|
|
1654
|
+
case 'bun-sqlite':
|
|
1655
|
+
return {
|
|
1656
|
+
databasePath: `${this.config.databasePath}/${name}.db`,
|
|
1657
|
+
create: true
|
|
1658
|
+
};
|
|
1659
|
+
case 'libsql':
|
|
1660
|
+
return {
|
|
1661
|
+
url: `file:${this.config.databasePath}/${name}.db`,
|
|
1662
|
+
...this.config.libsql
|
|
1663
|
+
};
|
|
1664
|
+
case 'odblite':
|
|
1665
|
+
return {
|
|
1666
|
+
databaseName: `${this.config.databasePath}_${name}`,
|
|
1667
|
+
...this.config.odblite
|
|
1668
|
+
};
|
|
1669
|
+
default:
|
|
1670
|
+
throw new Error(`Unknown backend: ${this.config.backend}`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Evict least recently used connection
|
|
1675
|
+
*/ async evictLRU() {
|
|
1676
|
+
let oldestKey = null;
|
|
1677
|
+
let oldestTime = 1 / 0;
|
|
1678
|
+
for (const [key, timestamp] of this.connectionTimestamps)if (timestamp < oldestTime) {
|
|
1679
|
+
oldestTime = timestamp;
|
|
1680
|
+
oldestKey = key;
|
|
1681
|
+
}
|
|
1682
|
+
if (oldestKey) {
|
|
1683
|
+
const conn = this.connections.get(oldestKey);
|
|
1684
|
+
if (conn) await conn.close();
|
|
1685
|
+
this.connections.delete(oldestKey);
|
|
1686
|
+
this.connectionTimestamps.delete(oldestKey);
|
|
1687
|
+
console.log(`♻️ Evicted LRU connection: ${oldestKey}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
986
1691
|
// Main entry point for ODB Client
|
|
987
1692
|
// Core query client exports (postgres.js-like interface)
|
|
988
1693
|
// Service management exports (high-level tenant database management)
|
|
1694
|
+
// Database Manager exports (unified multi-backend database abstraction)
|
|
989
1695
|
// Export error classes
|
|
990
1696
|
// Export static utility functions for easy access
|
|
991
1697
|
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 };
|
|
1698
|
+
export { BunSQLiteAdapter, ConnectionError, DatabaseManager, HTTPClient, LibSQLAdapter, ODBLiteAdapter, 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, parseSQL, src_raw as raw, splitSQLStatements, sql_parser_sql as sql, src_updateSet as updateSet, src_where as where };
|