@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
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatabaseManagerConfig,
|
|
3
|
+
BackendType,
|
|
4
|
+
Connection,
|
|
5
|
+
DatabaseAdapter,
|
|
6
|
+
MigrationConfig,
|
|
7
|
+
QueryResult,
|
|
8
|
+
} from './types'
|
|
9
|
+
import { BunSQLiteAdapter } from './adapters/bun-sqlite'
|
|
10
|
+
import { LibSQLAdapter } from './adapters/libsql'
|
|
11
|
+
import { ODBLiteAdapter } from './adapters/odblite'
|
|
12
|
+
import { parseSQL } from './sql-parser'
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Database factory for creating and managing multiple databases
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Creates multiple databases from single manager instance
|
|
20
|
+
* - Supports libsql, bun:sqlite, and ODB-Lite backends
|
|
21
|
+
* - Connection pooling with LRU eviction
|
|
22
|
+
* - Automatic schema migrations
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const dbManager = new DatabaseManager({
|
|
27
|
+
* backend: 'libsql',
|
|
28
|
+
* databasePath: './data'
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* // Create databases
|
|
32
|
+
* await dbManager.createDatabase('identity', { schemaContent })
|
|
33
|
+
* await dbManager.createDatabase('analytics', { schemaContent })
|
|
34
|
+
*
|
|
35
|
+
* // Get connections
|
|
36
|
+
* const identityDB = await dbManager.getConnection('identity')
|
|
37
|
+
* const analyticsDB = await dbManager.getConnection('analytics')
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class DatabaseManager {
|
|
41
|
+
private config: DatabaseManagerConfig
|
|
42
|
+
private adapter: DatabaseAdapter
|
|
43
|
+
private connections: Map<string, Connection> = new Map()
|
|
44
|
+
|
|
45
|
+
// Connection pool management (LRU)
|
|
46
|
+
private connectionTimestamps: Map<string, number> = new Map()
|
|
47
|
+
private maxConnections: number
|
|
48
|
+
|
|
49
|
+
constructor(config: DatabaseManagerConfig) {
|
|
50
|
+
this.config = config
|
|
51
|
+
this.maxConnections = config.connectionPoolSize || 10
|
|
52
|
+
|
|
53
|
+
// Create appropriate adapter based on backend
|
|
54
|
+
switch (config.backend) {
|
|
55
|
+
case 'bun-sqlite':
|
|
56
|
+
this.adapter = new BunSQLiteAdapter({ databasePath: config.databasePath, create: true })
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
case 'libsql':
|
|
60
|
+
this.adapter = new LibSQLAdapter({
|
|
61
|
+
url: `file:${config.databasePath}`,
|
|
62
|
+
...config.libsql
|
|
63
|
+
})
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
case 'odblite':
|
|
67
|
+
if (!config.odblite) {
|
|
68
|
+
throw new Error('odblite config required for odblite backend')
|
|
69
|
+
}
|
|
70
|
+
this.adapter = new ODBLiteAdapter(config.odblite)
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unknown backend type: ${config.backend}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new database
|
|
80
|
+
* @param name - Database name (becomes filename or ODB-Lite database name)
|
|
81
|
+
* @param options - Optional creation options
|
|
82
|
+
*/
|
|
83
|
+
async createDatabase(
|
|
84
|
+
name: string,
|
|
85
|
+
options?: { schemaContent?: string }
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
console.log(`📦 Creating database: ${name}`)
|
|
88
|
+
|
|
89
|
+
// Build database-specific config
|
|
90
|
+
const dbConfig = this.getDatabaseConfig(name)
|
|
91
|
+
|
|
92
|
+
// Create connection
|
|
93
|
+
const conn = await this.adapter.connect(dbConfig)
|
|
94
|
+
|
|
95
|
+
// Run migrations if schema provided
|
|
96
|
+
if (options?.schemaContent) {
|
|
97
|
+
await this.runMigrations(conn, { schemaContent: options.schemaContent })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Store connection
|
|
101
|
+
this.connections.set(name, conn)
|
|
102
|
+
this.connectionTimestamps.set(name, Date.now())
|
|
103
|
+
|
|
104
|
+
// Evict LRU if pool is full
|
|
105
|
+
if (this.connections.size > this.maxConnections) {
|
|
106
|
+
await this.evictLRU()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`✅ Database created: ${name}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a database exists
|
|
114
|
+
* @param name - Database name
|
|
115
|
+
* @returns true if database exists, false otherwise
|
|
116
|
+
*
|
|
117
|
+
* Note: For local backends (libsql, bun-sqlite), checks file existence.
|
|
118
|
+
* For ODB-Lite, checks if database is in connection cache.
|
|
119
|
+
*/
|
|
120
|
+
async databaseExists(name: string): Promise<boolean> {
|
|
121
|
+
// Check if already in connection cache
|
|
122
|
+
if (this.connections.has(name)) {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// For local file-based backends, check if file exists
|
|
127
|
+
switch (this.config.backend) {
|
|
128
|
+
case 'bun-sqlite': {
|
|
129
|
+
const dbPath = `${this.config.databasePath}/${name}.db`
|
|
130
|
+
return fs.existsSync(dbPath)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'libsql': {
|
|
134
|
+
const dbPath = `${this.config.databasePath}/${name}.db`
|
|
135
|
+
return fs.existsSync(dbPath)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'odblite': {
|
|
139
|
+
// For ODB-Lite, only return true if in cache
|
|
140
|
+
// (no way to check remote database existence without connecting)
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get connection to a database
|
|
151
|
+
* @param name - Database name
|
|
152
|
+
*
|
|
153
|
+
* If database exists on disk but not in cache, it will be connected automatically.
|
|
154
|
+
* If database doesn't exist at all, an error will be thrown.
|
|
155
|
+
*/
|
|
156
|
+
async getConnection(name: string): Promise<Connection> {
|
|
157
|
+
// Check if connection exists in cache
|
|
158
|
+
let conn = this.connections.get(name)
|
|
159
|
+
|
|
160
|
+
if (conn) {
|
|
161
|
+
this.connectionTimestamps.set(name, Date.now())
|
|
162
|
+
return conn
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if database exists on disk
|
|
166
|
+
const exists = await this.databaseExists(name)
|
|
167
|
+
if (exists) {
|
|
168
|
+
// Connect to existing database
|
|
169
|
+
const dbConfig = this.getDatabaseConfig(name)
|
|
170
|
+
conn = await this.adapter.connect(dbConfig)
|
|
171
|
+
|
|
172
|
+
// Store connection
|
|
173
|
+
this.connections.set(name, conn)
|
|
174
|
+
this.connectionTimestamps.set(name, Date.now())
|
|
175
|
+
|
|
176
|
+
// Evict LRU if pool is full
|
|
177
|
+
if (this.connections.size > this.maxConnections) {
|
|
178
|
+
await this.evictLRU()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return conn
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// If database doesn't exist, throw error
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Database "${name}" not found. Call createDatabase("${name}", { schemaContent }) first.`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete a database
|
|
192
|
+
* @param name - Database name
|
|
193
|
+
*/
|
|
194
|
+
async deleteDatabase(name: string): Promise<void> {
|
|
195
|
+
console.log(`🗑️ Deleting database: ${name}`)
|
|
196
|
+
|
|
197
|
+
// Close connection if exists
|
|
198
|
+
const conn = this.connections.get(name)
|
|
199
|
+
if (conn) {
|
|
200
|
+
await conn.close()
|
|
201
|
+
this.connections.delete(name)
|
|
202
|
+
this.connectionTimestamps.delete(name)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Backend-specific deletion
|
|
206
|
+
await this.adapter.disconnect(name)
|
|
207
|
+
|
|
208
|
+
console.log(`✅ Database deleted: ${name}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Execute SQL content on a database connection
|
|
213
|
+
* Useful for running migrations or initial schema
|
|
214
|
+
* @param name - Database name
|
|
215
|
+
* @param sqlContent - SQL content to execute
|
|
216
|
+
*/
|
|
217
|
+
async executeSQLFile(name: string, sqlContent: string): Promise<void> {
|
|
218
|
+
const conn = await this.getConnection(name)
|
|
219
|
+
await this.runMigrations(conn, { schemaContent: sqlContent })
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* List all managed databases
|
|
224
|
+
*/
|
|
225
|
+
async listDatabases(): Promise<string[]> {
|
|
226
|
+
return Array.from(this.connections.keys())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Close all connections
|
|
231
|
+
*/
|
|
232
|
+
async close(): Promise<void> {
|
|
233
|
+
console.log(`🔌 Closing all database connections...`)
|
|
234
|
+
|
|
235
|
+
for (const [name, conn] of this.connections) {
|
|
236
|
+
await conn.close()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.connections.clear()
|
|
240
|
+
this.connectionTimestamps.clear()
|
|
241
|
+
|
|
242
|
+
console.log(`✅ All connections closed`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Run migrations on a connection
|
|
247
|
+
*/
|
|
248
|
+
private async runMigrations(conn: Connection, config: MigrationConfig): Promise<void> {
|
|
249
|
+
// Parse SQL statements
|
|
250
|
+
const { pragmaStatements, regularStatements } = parseSQL(config.schemaContent)
|
|
251
|
+
|
|
252
|
+
// Execute PRAGMA statements first
|
|
253
|
+
for (const pragma of pragmaStatements) {
|
|
254
|
+
try {
|
|
255
|
+
await conn.execute(pragma)
|
|
256
|
+
} catch (error: any) {
|
|
257
|
+
// Ignore PRAGMA errors (they might not be supported on all backends)
|
|
258
|
+
console.log(`⚠️ PRAGMA note: ${error.message}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Execute regular statements
|
|
263
|
+
for (const statement of regularStatements) {
|
|
264
|
+
try {
|
|
265
|
+
await conn.execute(statement)
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
// Ignore "already exists" errors for idempotency
|
|
268
|
+
if (
|
|
269
|
+
error.message?.includes('already exists') ||
|
|
270
|
+
error.message?.includes('no such column') ||
|
|
271
|
+
error.message?.includes('no such table')
|
|
272
|
+
) {
|
|
273
|
+
console.log(` ⚠️ Skipping: ${error.message}`)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
console.error(`❌ Migration failed: ${statement.substring(0, 100)}...`)
|
|
277
|
+
throw error
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`✅ Migrations completed (${regularStatements.length} statements)`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get database-specific configuration
|
|
286
|
+
*/
|
|
287
|
+
private getDatabaseConfig(name: string): any {
|
|
288
|
+
switch (this.config.backend) {
|
|
289
|
+
case 'bun-sqlite':
|
|
290
|
+
return {
|
|
291
|
+
databasePath: `${this.config.databasePath}/${name}.db`,
|
|
292
|
+
create: true
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case 'libsql':
|
|
296
|
+
return {
|
|
297
|
+
url: `file:${this.config.databasePath}/${name}.db`,
|
|
298
|
+
...this.config.libsql
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case 'odblite':
|
|
302
|
+
return {
|
|
303
|
+
databaseName: `${this.config.databasePath}_${name}`,
|
|
304
|
+
...this.config.odblite
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unknown backend: ${this.config.backend}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Evict least recently used connection
|
|
314
|
+
*/
|
|
315
|
+
private async evictLRU(): Promise<void> {
|
|
316
|
+
let oldestKey: string | null = null
|
|
317
|
+
let oldestTime = Infinity
|
|
318
|
+
|
|
319
|
+
for (const [key, timestamp] of this.connectionTimestamps) {
|
|
320
|
+
if (timestamp < oldestTime) {
|
|
321
|
+
oldestTime = timestamp
|
|
322
|
+
oldestKey = key
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (oldestKey) {
|
|
327
|
+
const conn = this.connections.get(oldestKey)
|
|
328
|
+
if (conn) {
|
|
329
|
+
await conn.close()
|
|
330
|
+
}
|
|
331
|
+
this.connections.delete(oldestKey)
|
|
332
|
+
this.connectionTimestamps.delete(oldestKey)
|
|
333
|
+
console.log(`♻️ Evicted LRU connection: ${oldestKey}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export interface ParsedStatements {
|
|
2
|
+
pragmaStatements: string[]
|
|
3
|
+
regularStatements: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SQLParserOptions {
|
|
7
|
+
separatePragma?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse SQL file into statements, separating PRAGMA from regular SQL
|
|
12
|
+
* Handles comments, quotes, and semicolons properly
|
|
13
|
+
*/
|
|
14
|
+
export function parseSQL(sqlContent: string, options: SQLParserOptions = {}): ParsedStatements {
|
|
15
|
+
const separatePragma = options.separatePragma ?? true
|
|
16
|
+
|
|
17
|
+
// Split by semicolons (node-sql-parser doesn't handle multiple statements well)
|
|
18
|
+
const statements = splitStatements(sqlContent)
|
|
19
|
+
|
|
20
|
+
if (!separatePragma) {
|
|
21
|
+
return {
|
|
22
|
+
pragmaStatements: [],
|
|
23
|
+
regularStatements: statements,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pragmaStatements: string[] = []
|
|
28
|
+
const regularStatements: string[] = []
|
|
29
|
+
|
|
30
|
+
for (const statement of statements) {
|
|
31
|
+
const trimmed = statement.trim().toUpperCase()
|
|
32
|
+
if (trimmed.startsWith('PRAGMA')) {
|
|
33
|
+
pragmaStatements.push(statement)
|
|
34
|
+
} else {
|
|
35
|
+
regularStatements.push(statement)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { pragmaStatements, regularStatements }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Split SQL content into individual statements
|
|
44
|
+
* Handles comments, quotes, and semicolons properly
|
|
45
|
+
*/
|
|
46
|
+
function splitStatements(sqlContent: string): string[] {
|
|
47
|
+
const statements: string[] = []
|
|
48
|
+
let currentStatement = ''
|
|
49
|
+
let inQuote = false
|
|
50
|
+
let quoteChar = ''
|
|
51
|
+
|
|
52
|
+
const lines = sqlContent.split('\n')
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
let processedLine = ''
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < line.length; i++) {
|
|
58
|
+
const char = line[i]
|
|
59
|
+
const prevChar = i > 0 ? line[i - 1] : ''
|
|
60
|
+
const nextChar = i < line.length - 1 ? line[i + 1] : ''
|
|
61
|
+
|
|
62
|
+
// Handle single-line comments
|
|
63
|
+
if (!inQuote && char === '-' && nextChar === '-') {
|
|
64
|
+
break // Skip rest of line
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Track quotes
|
|
68
|
+
if ((char === "'" || char === '"') && prevChar !== '\\') {
|
|
69
|
+
if (!inQuote) {
|
|
70
|
+
inQuote = true
|
|
71
|
+
quoteChar = char
|
|
72
|
+
} else if (char === quoteChar) {
|
|
73
|
+
inQuote = false
|
|
74
|
+
quoteChar = ''
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
processedLine += char
|
|
79
|
+
|
|
80
|
+
// Split on semicolon when not in quotes
|
|
81
|
+
if (char === ';' && !inQuote) {
|
|
82
|
+
currentStatement += processedLine
|
|
83
|
+
const stmt = currentStatement.trim()
|
|
84
|
+
if (stmt) {
|
|
85
|
+
statements.push(stmt)
|
|
86
|
+
}
|
|
87
|
+
currentStatement = ''
|
|
88
|
+
processedLine = ''
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (processedLine.trim()) {
|
|
93
|
+
currentStatement += `${processedLine}\n`
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle remaining statement
|
|
98
|
+
const finalStmt = currentStatement.trim()
|
|
99
|
+
if (finalStmt) {
|
|
100
|
+
statements.push(finalStmt)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return statements
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Simple split for systems that don't need PRAGMA separation
|
|
108
|
+
*/
|
|
109
|
+
export function splitSQLStatements(sqlContent: string): string[] {
|
|
110
|
+
const { pragmaStatements, regularStatements } = parseSQL(sqlContent)
|
|
111
|
+
return [...pragmaStatements, ...regularStatements]
|
|
112
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Database as BunDatabase } from 'bun:sqlite'
|
|
2
|
+
import type { Client as LibSQLClient } from '@libsql/client'
|
|
3
|
+
import type { ServiceClient } from '../service/service-client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tenancy modes supported by DatabaseManager
|
|
7
|
+
*/
|
|
8
|
+
export type TenancyMode =
|
|
9
|
+
| 'single-db' // Single database for entire system (identity, operation)
|
|
10
|
+
| 'multi-tenant' // One database per tenant, no metastore (pipeline)
|
|
11
|
+
| 'metastore' // Metastore + per-tenant databases (wallet)
|
|
12
|
+
| 'dual-mode' // Switchable local/remote (tracking)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Backend database types
|
|
16
|
+
*/
|
|
17
|
+
export type BackendType = 'bun-sqlite' | 'libsql' | 'odblite'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Query result from any backend
|
|
21
|
+
*/
|
|
22
|
+
export interface QueryResult<T = any> {
|
|
23
|
+
rows: T[]
|
|
24
|
+
rowsAffected?: number
|
|
25
|
+
lastInsertRowid?: number | bigint
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Unified database connection interface
|
|
30
|
+
* All backends implement this through adapters
|
|
31
|
+
*/
|
|
32
|
+
export interface Connection {
|
|
33
|
+
// Template tag queries (postgres.js-like)
|
|
34
|
+
query<T = any>(strings: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>>
|
|
35
|
+
|
|
36
|
+
// Standard query methods (supports both formats for LibSQL compatibility)
|
|
37
|
+
execute(sql: string | { sql: string; args?: any[] }, params?: any[]): Promise<QueryResult>
|
|
38
|
+
prepare(sql: string): PreparedStatement
|
|
39
|
+
|
|
40
|
+
// Transaction support
|
|
41
|
+
transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T>
|
|
42
|
+
|
|
43
|
+
// Connection management
|
|
44
|
+
close(): Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prepared statement interface
|
|
49
|
+
*/
|
|
50
|
+
export interface PreparedStatement {
|
|
51
|
+
execute(params?: any[]): Promise<QueryResult>
|
|
52
|
+
all(params?: any[]): Promise<any[]>
|
|
53
|
+
get(params?: any[]): Promise<any | null>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Database backend adapter interface
|
|
58
|
+
*/
|
|
59
|
+
export interface DatabaseAdapter {
|
|
60
|
+
readonly type: BackendType
|
|
61
|
+
|
|
62
|
+
// Connection management
|
|
63
|
+
connect(config: any): Promise<Connection>
|
|
64
|
+
disconnect(tenantId?: string): Promise<void>
|
|
65
|
+
|
|
66
|
+
// Health check
|
|
67
|
+
isHealthy(): Promise<boolean>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Migration configuration
|
|
72
|
+
*/
|
|
73
|
+
export interface MigrationConfig {
|
|
74
|
+
schemaContent: string // SQL schema content (not file path)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* DatabaseManager configuration
|
|
79
|
+
* Simplified: Manager defines WHERE databases go, createDatabase defines WHAT
|
|
80
|
+
*/
|
|
81
|
+
export interface DatabaseManagerConfig {
|
|
82
|
+
// Backend type
|
|
83
|
+
backend: BackendType
|
|
84
|
+
|
|
85
|
+
// Database path (where databases are stored)
|
|
86
|
+
databasePath: string // e.g., './data', '/var/lib/databases', 'tenants' (for ODB-Lite prefix)
|
|
87
|
+
|
|
88
|
+
// Backend-specific options (optional)
|
|
89
|
+
libsql?: {
|
|
90
|
+
encryptionKey?: string
|
|
91
|
+
authToken?: string // For Turso remote databases
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
odblite?: {
|
|
95
|
+
serviceUrl: string
|
|
96
|
+
apiKey: string
|
|
97
|
+
nodeId?: string // Optional: specific node to create databases on (server selects if not provided)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Connection pooling
|
|
101
|
+
connectionPoolSize?: number // Max connections per database (default: 10)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Bun SQLite adapter config
|
|
106
|
+
*/
|
|
107
|
+
export interface BunSQLiteConfig {
|
|
108
|
+
databasePath: string
|
|
109
|
+
readonly?: boolean
|
|
110
|
+
create?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* LibSQL adapter config
|
|
115
|
+
*/
|
|
116
|
+
export interface LibSQLConfig {
|
|
117
|
+
url: string
|
|
118
|
+
authToken?: string
|
|
119
|
+
encryptionKey?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ODB-Lite adapter config
|
|
124
|
+
*/
|
|
125
|
+
export interface ODBLiteConfig {
|
|
126
|
+
serviceUrl: string
|
|
127
|
+
apiKey: string
|
|
128
|
+
nodeId?: string // Optional: specific node to create databases on (server selects if not provided)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Tenant database info
|
|
133
|
+
*/
|
|
134
|
+
export interface TenantInfo {
|
|
135
|
+
tenantId: string
|
|
136
|
+
databaseHash?: string // For ODB-Lite
|
|
137
|
+
databasePath?: string // For file-based
|
|
138
|
+
createdAt: Date
|
|
139
|
+
metadata?: Record<string, any>
|
|
140
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,16 @@ export { SQLParser, sql, fragment } from './core/sql-parser.ts';
|
|
|
10
10
|
// Service management exports (high-level tenant database management)
|
|
11
11
|
export { ServiceClient } from './service/service-client.ts';
|
|
12
12
|
|
|
13
|
+
// Database Manager exports (unified multi-backend database abstraction)
|
|
14
|
+
export {
|
|
15
|
+
DatabaseManager,
|
|
16
|
+
parseSQL,
|
|
17
|
+
splitSQLStatements,
|
|
18
|
+
BunSQLiteAdapter,
|
|
19
|
+
LibSQLAdapter,
|
|
20
|
+
ODBLiteAdapter,
|
|
21
|
+
} from './database/index.ts';
|
|
22
|
+
|
|
13
23
|
// Export all types
|
|
14
24
|
export type {
|
|
15
25
|
ODBLiteConfig,
|
|
@@ -30,6 +40,24 @@ export type {
|
|
|
30
40
|
ODBLiteNode
|
|
31
41
|
} from './service/service-client.ts';
|
|
32
42
|
|
|
43
|
+
// Export database manager types
|
|
44
|
+
export type {
|
|
45
|
+
TenancyMode,
|
|
46
|
+
BackendType,
|
|
47
|
+
QueryResult as DatabaseQueryResult,
|
|
48
|
+
Connection,
|
|
49
|
+
DatabaseAdapter,
|
|
50
|
+
PreparedStatement as DatabasePreparedStatement,
|
|
51
|
+
DatabaseManagerConfig,
|
|
52
|
+
MigrationConfig,
|
|
53
|
+
BunSQLiteConfig,
|
|
54
|
+
LibSQLConfig,
|
|
55
|
+
ODBLiteConfig as DatabaseODBLiteConfig,
|
|
56
|
+
TenantInfo,
|
|
57
|
+
ParsedStatements,
|
|
58
|
+
SQLParserOptions,
|
|
59
|
+
} from './database/index.ts';
|
|
60
|
+
|
|
33
61
|
// Export error classes
|
|
34
62
|
export {
|
|
35
63
|
ODBLiteError,
|
|
@@ -97,8 +97,7 @@ export class ServiceClient {
|
|
|
97
97
|
try {
|
|
98
98
|
// Check if database already exists
|
|
99
99
|
console.log(`🔍 Checking if database exists: ${cacheKey}`);
|
|
100
|
-
const
|
|
101
|
-
const existing = databases.find(db => db.name === cacheKey);
|
|
100
|
+
const existing = await this.getDatabaseByName(cacheKey);
|
|
102
101
|
|
|
103
102
|
if (existing) {
|
|
104
103
|
console.log(`✅ Database already exists: ${cacheKey} (${existing.hash})`);
|
|
@@ -159,17 +158,22 @@ export class ServiceClient {
|
|
|
159
158
|
* Create a new database
|
|
160
159
|
*
|
|
161
160
|
* @param name - Database name (should be unique)
|
|
162
|
-
* @param nodeId - ID of the node to host the database
|
|
161
|
+
* @param nodeId - ID of the node to host the database (optional - server will select if null)
|
|
163
162
|
* @returns Created database object with hash
|
|
164
163
|
*/
|
|
165
|
-
async createDatabase(name: string, nodeId
|
|
164
|
+
async createDatabase(name: string, nodeId?: string | null): Promise<ODBLiteDatabase> {
|
|
165
|
+
const body: any = { name }
|
|
166
|
+
if (nodeId) {
|
|
167
|
+
body.nodeId = nodeId
|
|
168
|
+
}
|
|
169
|
+
|
|
166
170
|
const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
|
|
167
171
|
method: 'POST',
|
|
168
172
|
headers: {
|
|
169
173
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
170
174
|
'Content-Type': 'application/json',
|
|
171
175
|
},
|
|
172
|
-
body: JSON.stringify(
|
|
176
|
+
body: JSON.stringify(body),
|
|
173
177
|
});
|
|
174
178
|
|
|
175
179
|
const result = await response.json();
|
|
@@ -201,6 +205,31 @@ export class ServiceClient {
|
|
|
201
205
|
return result.database;
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Get database details by name
|
|
210
|
+
*
|
|
211
|
+
* @param name - Database name
|
|
212
|
+
* @returns Database object or null if not found
|
|
213
|
+
*/
|
|
214
|
+
async getDatabaseByName(name: string): Promise<ODBLiteDatabase | null> {
|
|
215
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases/by-name/${name}`, {
|
|
216
|
+
headers: {
|
|
217
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (response.status === 404) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = await response.json();
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
throw new Error(result.error || 'Failed to get database');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result.database;
|
|
231
|
+
}
|
|
232
|
+
|
|
204
233
|
/**
|
|
205
234
|
* Delete a database
|
|
206
235
|
*
|