@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.
@@ -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
+ }