@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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Client - High-level client for managing tenant databases via ODB-Lite Tenant API
|
|
3
|
+
*
|
|
4
|
+
* This client provides automatic database provisioning and management for multi-tenant applications.
|
|
5
|
+
* It handles:
|
|
6
|
+
* - Automatic database creation on first use
|
|
7
|
+
* - Database hash caching for performance
|
|
8
|
+
* - Tenant API integration with ODB-Lite
|
|
9
|
+
* - Query execution via ODB-Lite's query API
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const service = new ServiceClient({
|
|
14
|
+
* baseUrl: 'http://localhost:8671',
|
|
15
|
+
* apiKey: 'odblite_tenant_key'
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Automatically creates database if it doesn't exist
|
|
19
|
+
* const dbHash = await service.ensureDatabaseForTenant('wallet', 'tenant-123');
|
|
20
|
+
*
|
|
21
|
+
* // Execute queries
|
|
22
|
+
* const result = await service.query(dbHash, 'SELECT * FROM wallets', []);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface ODBLiteDatabase {
|
|
27
|
+
hash: string;
|
|
28
|
+
name: string;
|
|
29
|
+
tenantId: string;
|
|
30
|
+
nodeId: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ODBLiteNode {
|
|
36
|
+
nodeId: string;
|
|
37
|
+
url: string;
|
|
38
|
+
status: string;
|
|
39
|
+
lastHealthCheck: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ServiceClientConfig {
|
|
43
|
+
/** Base URL of ODB-Lite server (e.g., http://localhost:8671) */
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
/** Tenant API key for authentication */
|
|
46
|
+
apiKey: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Service Client for managing tenant databases in ODB-Lite
|
|
51
|
+
*
|
|
52
|
+
* This is a higher-level client that sits on top of the base ODB client.
|
|
53
|
+
* It provides automatic database provisioning and management for services
|
|
54
|
+
* that need per-tenant database isolation.
|
|
55
|
+
*/
|
|
56
|
+
export class ServiceClient {
|
|
57
|
+
private apiUrl: string;
|
|
58
|
+
private apiKey: string;
|
|
59
|
+
private databaseCache: Map<string, string>; // cacheKey -> databaseHash
|
|
60
|
+
|
|
61
|
+
constructor(config: ServiceClientConfig) {
|
|
62
|
+
this.apiUrl = config.baseUrl;
|
|
63
|
+
this.apiKey = config.apiKey;
|
|
64
|
+
this.databaseCache = new Map();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get or create a database for a tenant
|
|
69
|
+
*
|
|
70
|
+
* This is the main method used by services. It will:
|
|
71
|
+
* 1. Check the cache for an existing database hash
|
|
72
|
+
* 2. Query ODB-Lite to see if the database exists
|
|
73
|
+
* 3. Create the database if it doesn't exist
|
|
74
|
+
* 4. Cache and return the database hash
|
|
75
|
+
*
|
|
76
|
+
* @param prefix - Database name prefix (e.g., 'wallet', 'tracking')
|
|
77
|
+
* @param tenantId - Tenant identifier
|
|
78
|
+
* @returns Database hash for querying
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const hash = await service.ensureDatabaseForTenant('wallet', 'tenant-123');
|
|
83
|
+
* // Returns hash for database named 'wallet_tenant-123'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async ensureDatabaseForTenant(prefix: string, tenantId: string): Promise<string> {
|
|
87
|
+
const cacheKey = `${prefix}_${tenantId}`;
|
|
88
|
+
console.log(`📊 Ensuring database for ${cacheKey}`);
|
|
89
|
+
|
|
90
|
+
// Check cache first
|
|
91
|
+
const cached = this.databaseCache.get(cacheKey);
|
|
92
|
+
if (cached) {
|
|
93
|
+
console.log(`✅ Found cached database hash: ${cached}`);
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Check if database already exists
|
|
99
|
+
console.log(`🔍 Checking if database exists: ${cacheKey}`);
|
|
100
|
+
const databases = await this.listDatabases();
|
|
101
|
+
const existing = databases.find(db => db.name === cacheKey);
|
|
102
|
+
|
|
103
|
+
if (existing) {
|
|
104
|
+
console.log(`✅ Database already exists: ${cacheKey} (${existing.hash})`);
|
|
105
|
+
this.databaseCache.set(cacheKey, existing.hash);
|
|
106
|
+
return existing.hash;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Create new database
|
|
110
|
+
console.log(`🆕 Creating new database: ${cacheKey}`);
|
|
111
|
+
const nodes = await this.listNodes();
|
|
112
|
+
console.log(`📡 Available nodes: ${nodes.length}`);
|
|
113
|
+
|
|
114
|
+
if (nodes.length === 0) {
|
|
115
|
+
throw new Error('No available nodes to create database');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Use first healthy node
|
|
119
|
+
const node = nodes.find(n => n.status === 'healthy') || nodes[0];
|
|
120
|
+
if (!node) {
|
|
121
|
+
throw new Error('No available nodes to create database');
|
|
122
|
+
}
|
|
123
|
+
console.log(`🎯 Using node: ${node.nodeId}`);
|
|
124
|
+
|
|
125
|
+
const database = await this.createDatabase(cacheKey, node.nodeId);
|
|
126
|
+
console.log(`✅ Database created successfully: ${database.hash}`);
|
|
127
|
+
|
|
128
|
+
this.databaseCache.set(cacheKey, database.hash);
|
|
129
|
+
return database.hash;
|
|
130
|
+
} catch (error: any) {
|
|
131
|
+
console.error(`❌ Error ensuring database for ${cacheKey}:`, error.message);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List all databases owned by this tenant
|
|
138
|
+
*
|
|
139
|
+
* Queries ODB-Lite's tenant API to get all databases accessible with the current API key.
|
|
140
|
+
*
|
|
141
|
+
* @returns Array of database objects
|
|
142
|
+
*/
|
|
143
|
+
async listDatabases(): Promise<ODBLiteDatabase[]> {
|
|
144
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
|
|
145
|
+
headers: {
|
|
146
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await response.json();
|
|
151
|
+
if (!result.success) {
|
|
152
|
+
throw new Error(result.error || 'Failed to list databases');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result.databases;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a new database
|
|
160
|
+
*
|
|
161
|
+
* @param name - Database name (should be unique)
|
|
162
|
+
* @param nodeId - ID of the node to host the database
|
|
163
|
+
* @returns Created database object with hash
|
|
164
|
+
*/
|
|
165
|
+
async createDatabase(name: string, nodeId: string): Promise<ODBLiteDatabase> {
|
|
166
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
170
|
+
'Content-Type': 'application/json',
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({ name, nodeId }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await response.json();
|
|
176
|
+
if (!result.success) {
|
|
177
|
+
throw new Error(result.error || 'Failed to create database');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result.database;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get database details by hash
|
|
185
|
+
*
|
|
186
|
+
* @param hash - Database hash
|
|
187
|
+
* @returns Database object
|
|
188
|
+
*/
|
|
189
|
+
async getDatabase(hash: string): Promise<ODBLiteDatabase> {
|
|
190
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases/${hash}`, {
|
|
191
|
+
headers: {
|
|
192
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await response.json();
|
|
197
|
+
if (!result.success) {
|
|
198
|
+
throw new Error(result.error || 'Failed to get database');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result.database;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Delete a database
|
|
206
|
+
*
|
|
207
|
+
* @param hash - Database hash to delete
|
|
208
|
+
*/
|
|
209
|
+
async deleteDatabase(hash: string): Promise<void> {
|
|
210
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/databases/${hash}`, {
|
|
211
|
+
method: 'DELETE',
|
|
212
|
+
headers: {
|
|
213
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const result = await response.json();
|
|
218
|
+
if (!result.success) {
|
|
219
|
+
throw new Error(result.error || 'Failed to delete database');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Remove from cache
|
|
223
|
+
for (const [key, cachedHash] of this.databaseCache.entries()) {
|
|
224
|
+
if (cachedHash === hash) {
|
|
225
|
+
this.databaseCache.delete(key);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* List available nodes
|
|
233
|
+
*
|
|
234
|
+
* @returns Array of node objects
|
|
235
|
+
*/
|
|
236
|
+
async listNodes(): Promise<ODBLiteNode[]> {
|
|
237
|
+
const response = await fetch(`${this.apiUrl}/api/tenant/nodes`, {
|
|
238
|
+
headers: {
|
|
239
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await response.json();
|
|
244
|
+
if (!result.success) {
|
|
245
|
+
throw new Error(result.error || 'Failed to list nodes');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result.nodes;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Execute a query on a specific database
|
|
253
|
+
*
|
|
254
|
+
* This is a low-level query method. For most use cases, you should use
|
|
255
|
+
* the full ODB client (`odblite()` function) instead, which provides
|
|
256
|
+
* template tag support and better ergonomics.
|
|
257
|
+
*
|
|
258
|
+
* @param databaseHash - Hash of the database to query
|
|
259
|
+
* @param sql - SQL query string
|
|
260
|
+
* @param params - Query parameters
|
|
261
|
+
* @returns Query result
|
|
262
|
+
*/
|
|
263
|
+
async query<T = any>(databaseHash: string, sql: string, params: any[] = []): Promise<T> {
|
|
264
|
+
const response = await fetch(`${this.apiUrl}/query/${databaseHash}`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({ sql, params }),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = await response.json();
|
|
274
|
+
if (!result.success) {
|
|
275
|
+
throw new Error(result.error || 'Query failed');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result.data;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Clear the database hash cache
|
|
283
|
+
*
|
|
284
|
+
* Useful when database mappings have changed or for testing.
|
|
285
|
+
*/
|
|
286
|
+
clearCache(): void {
|
|
287
|
+
this.databaseCache.clear();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get cached database hash for a specific tenant (if exists)
|
|
292
|
+
*
|
|
293
|
+
* @param prefix - Database name prefix
|
|
294
|
+
* @param tenantId - Tenant identifier
|
|
295
|
+
* @returns Cached hash or undefined
|
|
296
|
+
*/
|
|
297
|
+
getCachedHash(prefix: string, tenantId: string): string | undefined {
|
|
298
|
+
const cacheKey = `${prefix}_${tenantId}`;
|
|
299
|
+
return this.databaseCache.get(cacheKey);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Pre-cache a database hash
|
|
304
|
+
*
|
|
305
|
+
* Useful when you know the mapping ahead of time and want to avoid
|
|
306
|
+
* the initial lookup.
|
|
307
|
+
*
|
|
308
|
+
* @param prefix - Database name prefix
|
|
309
|
+
* @param tenantId - Tenant identifier
|
|
310
|
+
* @param hash - Database hash
|
|
311
|
+
*/
|
|
312
|
+
setCachedHash(prefix: string, tenantId: string, hash: string): void {
|
|
313
|
+
const cacheKey = `${prefix}_${tenantId}`;
|
|
314
|
+
this.databaseCache.set(cacheKey, hash);
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Core types for the ODBLite client
|
|
2
|
+
export interface ODBLiteConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
databaseId?: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
retries?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface QueryResult<T = any> {
|
|
11
|
+
rows: T[];
|
|
12
|
+
rowsAffected: number;
|
|
13
|
+
executionTime: number;
|
|
14
|
+
databaseName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ODBLiteResponse<T = any> {
|
|
18
|
+
success: boolean;
|
|
19
|
+
data?: T[];
|
|
20
|
+
rows?: T[];
|
|
21
|
+
rowsAffected?: number;
|
|
22
|
+
executionTime?: number;
|
|
23
|
+
dbId?: string;
|
|
24
|
+
databaseName?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Template string SQL types
|
|
29
|
+
export interface SQLFragment {
|
|
30
|
+
text: string;
|
|
31
|
+
values: any[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PreparedQuery {
|
|
35
|
+
sql: string;
|
|
36
|
+
params: any[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Transaction types - uses same callable pattern as main sql function
|
|
40
|
+
export interface Transaction {
|
|
41
|
+
// Primary call signatures - context-aware like main sql function
|
|
42
|
+
<T = any>(sql: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>>;
|
|
43
|
+
<T = any>(input: any): SQLFragment; // For tx(object), tx(array), etc.
|
|
44
|
+
|
|
45
|
+
// Utility methods
|
|
46
|
+
raw(text: string): string;
|
|
47
|
+
identifier(name: string): string;
|
|
48
|
+
|
|
49
|
+
// Additional query methods for compatibility
|
|
50
|
+
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
51
|
+
execute<T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>>;
|
|
52
|
+
|
|
53
|
+
// Savepoint support (postgres.js style)
|
|
54
|
+
savepoint<T = any>(callback: (sql: Transaction) => Promise<T>): Promise<T>;
|
|
55
|
+
|
|
56
|
+
// Transaction control
|
|
57
|
+
rollback(): Promise<void>;
|
|
58
|
+
commit(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Transaction callback function type
|
|
62
|
+
export type TransactionCallback<T = any> = (sql: Transaction) => Promise<T> | T | Promise<T[]> | T[];
|
|
63
|
+
|
|
64
|
+
// Transaction mode type
|
|
65
|
+
export type TransactionMode = 'read write' | 'read only' | string;
|
|
66
|
+
|
|
67
|
+
// Connection interface similar to postgres.js
|
|
68
|
+
export interface ODBLiteConnection {
|
|
69
|
+
// Template string queries (postgres.js style)
|
|
70
|
+
sql<T = any>(sql: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>>;
|
|
71
|
+
|
|
72
|
+
// Raw query method
|
|
73
|
+
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
74
|
+
|
|
75
|
+
// Transaction support
|
|
76
|
+
begin(): Promise<Transaction>;
|
|
77
|
+
|
|
78
|
+
// Health check
|
|
79
|
+
ping(): Promise<boolean>;
|
|
80
|
+
|
|
81
|
+
// Close connection
|
|
82
|
+
end(): Promise<void>;
|
|
83
|
+
|
|
84
|
+
// Configuration
|
|
85
|
+
config: ODBLiteConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Helper types for PostgreSQL-like features
|
|
89
|
+
export type PrimitiveType = string | number | boolean | null | Date | Buffer;
|
|
90
|
+
export type QueryParameter = PrimitiveType | PrimitiveType[];
|
|
91
|
+
|
|
92
|
+
// Row result types
|
|
93
|
+
export interface Row {
|
|
94
|
+
[column: string]: any;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Error types
|
|
98
|
+
export class ODBLiteError extends Error {
|
|
99
|
+
constructor(
|
|
100
|
+
message: string,
|
|
101
|
+
public code?: string,
|
|
102
|
+
public query?: string,
|
|
103
|
+
public params?: any[]
|
|
104
|
+
) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'ODBLiteError';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class ConnectionError extends ODBLiteError {
|
|
111
|
+
constructor(message: string, public originalError?: Error) {
|
|
112
|
+
super(message, 'CONNECTION_ERROR');
|
|
113
|
+
this.name = 'ConnectionError';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class QueryError extends ODBLiteError {
|
|
118
|
+
constructor(
|
|
119
|
+
message: string,
|
|
120
|
+
query?: string,
|
|
121
|
+
params?: any[],
|
|
122
|
+
public originalError?: Error
|
|
123
|
+
) {
|
|
124
|
+
super(message, 'QUERY_ERROR', query, params);
|
|
125
|
+
this.name = 'QueryError';
|
|
126
|
+
}
|
|
127
|
+
}
|