@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,425 @@
1
+ import type { Transaction, QueryResult, TransactionCallback, SQLFragment } from '../types.ts';
2
+ import type { HTTPClient } from './http-client.ts';
3
+ import { SQLParser } from './sql-parser.ts';
4
+ import { QueryError } from '../types.ts';
5
+
6
+ /**
7
+ * Create a transaction function similar to the main sql function
8
+ */
9
+ export function createTransaction(httpClient: HTTPClient): Transaction {
10
+ let isCommitted = false;
11
+ let isRolledBack = false;
12
+ const queries: Array<{ sql: string; params: any[] }> = [];
13
+
14
+ const checkTransactionState = (): void => {
15
+ if (isCommitted) {
16
+ throw new QueryError('Transaction has already been committed');
17
+ }
18
+ if (isRolledBack) {
19
+ throw new QueryError('Transaction has been rolled back');
20
+ }
21
+ };
22
+
23
+ const isReadQuery = (sql: string): boolean => {
24
+ const trimmed = sql.trim().toUpperCase();
25
+ return trimmed.startsWith('SELECT') ||
26
+ trimmed.startsWith('WITH') ||
27
+ trimmed.startsWith('EXPLAIN') ||
28
+ trimmed.startsWith('PRAGMA');
29
+ };
30
+
31
+ // Create the callable transaction function with context awareness
32
+ const txFunction = <T = any>(sql: TemplateStringsArray | any, ...values: any[]): Promise<QueryResult<T>> | SQLFragment => {
33
+ checkTransactionState();
34
+
35
+ // Handle template string queries (returns Promise)
36
+ if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values.length >= 0))) {
37
+ const parsed = SQLParser.parse(sql, values);
38
+
39
+ // For read queries, execute immediately
40
+ if (isReadQuery(parsed.sql)) {
41
+ return httpClient.query<T>(parsed.sql, parsed.params);
42
+ }
43
+
44
+ // For write queries, queue them for batch execution
45
+ queries.push(parsed);
46
+
47
+ // Return a placeholder result for queued queries
48
+ return Promise.resolve({
49
+ rows: [],
50
+ rowsAffected: 0,
51
+ executionTime: 0
52
+ });
53
+ }
54
+
55
+ // Handle direct object/array inputs (returns SQLFragment for composing)
56
+ const parsed = SQLParser.parse(sql, values);
57
+ return {
58
+ text: parsed.sql,
59
+ values: parsed.params
60
+ };
61
+ };
62
+
63
+ // Attach utility methods to the transaction function
64
+ txFunction.raw = (text: string): string => SQLParser.raw(text);
65
+ txFunction.identifier = (name: string): string => SQLParser.identifier(name);
66
+
67
+ txFunction.commit = async (): Promise<void> => {
68
+ checkTransactionState();
69
+
70
+ try {
71
+ // Execute BEGIN
72
+ await httpClient.query('BEGIN');
73
+
74
+ // Execute all queued queries
75
+ for (const query of queries) {
76
+ await httpClient.query(query.sql, query.params);
77
+ }
78
+
79
+ // Commit the transaction
80
+ await httpClient.query('COMMIT');
81
+
82
+ isCommitted = true;
83
+ } catch (error) {
84
+ // Rollback on any error
85
+ try {
86
+ await httpClient.query('ROLLBACK');
87
+ } catch (rollbackError) {
88
+ // Ignore rollback errors
89
+ }
90
+
91
+ isRolledBack = true;
92
+ throw new QueryError(
93
+ `Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
94
+ undefined,
95
+ undefined,
96
+ error instanceof Error ? error : undefined
97
+ );
98
+ }
99
+ };
100
+
101
+ txFunction.rollback = async (): Promise<void> => {
102
+ checkTransactionState();
103
+
104
+ try {
105
+ // If we have any queries, we need to actually rollback
106
+ if (queries.length > 0) {
107
+ await httpClient.query('ROLLBACK');
108
+ }
109
+ } finally {
110
+ isRolledBack = true;
111
+ }
112
+ };
113
+
114
+ txFunction.savepoint = async <T = any>(callback: (sql: Transaction) => Promise<T>): Promise<T> => {
115
+ checkTransactionState();
116
+
117
+ // For batched transactions, create a sub-transaction that shares the same logic
118
+ // but manages its own query queue for savepoint isolation
119
+ let savepointCommitted = false;
120
+ const savepointQueries: Array<{ sql: string; params: any[] }> = [];
121
+
122
+ // Create a savepoint transaction that collects queries locally
123
+ const savepointTx = <U = any>(sql: TemplateStringsArray | any, ...values: any[]): Promise<QueryResult<U>> | SQLFragment => {
124
+ if (isCommitted || isRolledBack) {
125
+ throw new QueryError('Transaction is no longer active');
126
+ }
127
+
128
+ // Handle template string queries (returns Promise)
129
+ if (Array.isArray(sql) && 'raw' in sql && typeof sql[0] === 'string') {
130
+ const parsed = SQLParser.parse(sql, values);
131
+
132
+ // For read queries, execute immediately
133
+ if (isReadQuery(parsed.sql)) {
134
+ return httpClient.query<U>(parsed.sql, parsed.params);
135
+ }
136
+
137
+ // For write queries, queue them in the savepoint queue
138
+ savepointQueries.push(parsed);
139
+
140
+ // Return a placeholder result for queued queries
141
+ return Promise.resolve({
142
+ rows: [],
143
+ rowsAffected: 0,
144
+ executionTime: 0
145
+ });
146
+ }
147
+
148
+ // Handle direct object/array inputs (returns SQLFragment for composing)
149
+ const parsed = SQLParser.parse(sql, values);
150
+ return {
151
+ text: parsed.sql,
152
+ values: parsed.params
153
+ };
154
+ };
155
+
156
+ // Attach utility methods to the savepoint transaction
157
+ (savepointTx as any).raw = (text: string): string => SQLParser.raw(text);
158
+ (savepointTx as any).identifier = (name: string): string => SQLParser.identifier(name);
159
+ (savepointTx as any).commit = async (): Promise<void> => {
160
+ savepointCommitted = true;
161
+ };
162
+ (savepointTx as any).rollback = async (): Promise<void> => {
163
+ // Rollback just clears the savepoint queries
164
+ savepointQueries.length = 0;
165
+ };
166
+
167
+ try {
168
+ // Execute the callback with the savepoint transaction
169
+ const result = await callback(savepointTx as Transaction);
170
+
171
+ // If successful, commit the savepoint by adding queries to main transaction
172
+ if (savepointCommitted || savepointQueries.length > 0) {
173
+ queries.push(...savepointQueries);
174
+ }
175
+
176
+ return result;
177
+ } catch (error) {
178
+ // On error, we don't add the savepoint queries to our queue
179
+ // This simulates a rollback to savepoint
180
+ throw error;
181
+ }
182
+ };
183
+
184
+ return txFunction as Transaction;
185
+ }
186
+
187
+ /**
188
+ * Transaction implementation for ODBLite
189
+ * Note: SQLite transactions are simulated at the client level since ODBLite
190
+ * operates on individual queries. This provides a familiar API but doesn't
191
+ * provide true ACID guarantees across multiple HTTP requests.
192
+ */
193
+ export class ODBLiteTransaction {
194
+ private httpClient: HTTPClient;
195
+ private isCommitted = false;
196
+ private isRolledBack = false;
197
+ private queries: Array<{ sql: string; params: any[] }> = [];
198
+
199
+ constructor(httpClient: HTTPClient) {
200
+ this.httpClient = httpClient;
201
+ }
202
+
203
+ /**
204
+ * Execute a query within the transaction
205
+ * For SQLite, we'll queue queries and execute them in batch on commit
206
+ */
207
+ async sql<T = any>(sql: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>> {
208
+ this.checkTransactionState();
209
+
210
+ const parsed = SQLParser.parse(sql, values);
211
+
212
+ // For read queries, execute immediately
213
+ if (this.isReadQuery(parsed.sql)) {
214
+ return await this.httpClient.query<T>(parsed.sql, parsed.params);
215
+ }
216
+
217
+ // For write queries, queue them for batch execution
218
+ this.queries.push(parsed);
219
+
220
+ // Return a placeholder result for queued queries
221
+ return {
222
+ rows: [],
223
+ rowsAffected: 0,
224
+ executionTime: 0
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Commit the transaction by executing all queued queries
230
+ */
231
+ async commit(): Promise<void> {
232
+ this.checkTransactionState();
233
+
234
+ try {
235
+ // Execute BEGIN
236
+ await this.httpClient.query('BEGIN');
237
+
238
+ // Execute all queued queries
239
+ for (const query of this.queries) {
240
+ await this.httpClient.query(query.sql, query.params);
241
+ }
242
+
243
+ // Commit the transaction
244
+ await this.httpClient.query('COMMIT');
245
+
246
+ this.isCommitted = true;
247
+ } catch (error) {
248
+ // Rollback on any error
249
+ try {
250
+ await this.httpClient.query('ROLLBACK');
251
+ } catch (rollbackError) {
252
+ // Ignore rollback errors
253
+ }
254
+
255
+ this.isRolledBack = true;
256
+ throw new QueryError(
257
+ `Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
258
+ undefined,
259
+ undefined,
260
+ error instanceof Error ? error : undefined
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Rollback the transaction
267
+ */
268
+ async rollback(): Promise<void> {
269
+ this.checkTransactionState();
270
+
271
+ try {
272
+ // If we have any queries, we need to actually rollback
273
+ if (this.queries.length > 0) {
274
+ await this.httpClient.query('ROLLBACK');
275
+ }
276
+ } finally {
277
+ this.isRolledBack = true;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Check if transaction is still active
283
+ */
284
+ private checkTransactionState(): void {
285
+ if (this.isCommitted) {
286
+ throw new QueryError('Transaction has already been committed');
287
+ }
288
+ if (this.isRolledBack) {
289
+ throw new QueryError('Transaction has been rolled back');
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Determine if a query is a read operation
295
+ */
296
+ private isReadQuery(sql: string): boolean {
297
+ const trimmed = sql.trim().toUpperCase();
298
+ return trimmed.startsWith('SELECT') ||
299
+ trimmed.startsWith('WITH') ||
300
+ trimmed.startsWith('EXPLAIN') ||
301
+ trimmed.startsWith('PRAGMA');
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Create a simple transaction function that executes immediately
307
+ * This is more suitable for HTTP-based databases where true transactions
308
+ * across multiple requests are not practical
309
+ */
310
+ export function createSimpleTransaction(httpClient: HTTPClient): Transaction {
311
+ let isActive = true;
312
+
313
+ // Create the callable transaction function with context awareness
314
+ const txFunction = <T = any>(sql: TemplateStringsArray | any, ...values: any[]): Promise<QueryResult<T>> | SQLFragment => {
315
+ if (!isActive) {
316
+ throw new QueryError('Transaction is no longer active');
317
+ }
318
+
319
+ // Handle template string queries (returns Promise)
320
+ if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values.length >= 0))) {
321
+ const parsed = SQLParser.parse(sql, values);
322
+ return httpClient.query<T>(parsed.sql, parsed.params);
323
+ }
324
+
325
+ // Handle direct object/array inputs (returns SQLFragment for composing)
326
+ const parsed = SQLParser.parse(sql, values);
327
+ return {
328
+ text: parsed.sql,
329
+ values: parsed.params
330
+ };
331
+ };
332
+
333
+ // Attach utility methods to the transaction function
334
+ txFunction.raw = (text: string): string => SQLParser.raw(text);
335
+ txFunction.identifier = (name: string): string => SQLParser.identifier(name);
336
+
337
+ // Add execute method for compatibility
338
+ txFunction.execute = async <T = any>(sql: string | { sql: string; args?: any[] }, args?: any[]): Promise<QueryResult<T>> => {
339
+ if (!isActive) {
340
+ throw new QueryError('Transaction is no longer active');
341
+ }
342
+
343
+ if (typeof sql === 'string') {
344
+ return await httpClient.query<T>(sql, args || []);
345
+ } else {
346
+ return await httpClient.query<T>(sql.sql, sql.args || []);
347
+ }
348
+ };
349
+
350
+ // Add query method for compatibility
351
+ txFunction.query = async <T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> => {
352
+ if (!isActive) {
353
+ throw new QueryError('Transaction is no longer active');
354
+ }
355
+ return await httpClient.query<T>(sql, params);
356
+ };
357
+
358
+ txFunction.commit = async (): Promise<void> => {
359
+ isActive = false;
360
+ // No-op for simple transactions
361
+ };
362
+
363
+ txFunction.rollback = async (): Promise<void> => {
364
+ isActive = false;
365
+ // No-op for simple transactions - individual queries are atomic
366
+ };
367
+
368
+ txFunction.savepoint = async <T = any>(callback: (sql: Transaction) => Promise<T>): Promise<T> => {
369
+ if (!isActive) {
370
+ throw new QueryError('Transaction is no longer active');
371
+ }
372
+
373
+ // Create a nested transaction for the savepoint
374
+ const savepointTx = createSimpleTransaction(httpClient);
375
+
376
+ try {
377
+ // Execute the callback with the savepoint transaction
378
+ const result = await callback(savepointTx);
379
+
380
+ // Commit the savepoint (no-op for simple transactions)
381
+ await savepointTx.commit();
382
+
383
+ return result;
384
+ } catch (error) {
385
+ // Rollback the savepoint on error
386
+ await savepointTx.rollback();
387
+ throw error;
388
+ }
389
+ };
390
+
391
+ return txFunction as Transaction;
392
+ }
393
+
394
+ /**
395
+ * Simple transaction implementation that executes immediately
396
+ * This is more suitable for HTTP-based databases where true transactions
397
+ * across multiple requests are not practical
398
+ */
399
+ export class SimpleTransaction {
400
+ private httpClient: HTTPClient;
401
+ private isActive = true;
402
+
403
+ constructor(httpClient: HTTPClient) {
404
+ this.httpClient = httpClient;
405
+ }
406
+
407
+ async sql<T = any>(sql: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>> {
408
+ if (!this.isActive) {
409
+ throw new QueryError('Transaction is no longer active');
410
+ }
411
+
412
+ const parsed = SQLParser.parse(sql, values);
413
+ return await this.httpClient.query<T>(parsed.sql, parsed.params);
414
+ }
415
+
416
+ async commit(): Promise<void> {
417
+ this.isActive = false;
418
+ // No-op for simple transactions
419
+ }
420
+
421
+ async rollback(): Promise<void> {
422
+ this.isActive = false;
423
+ // No-op for simple transactions - individual queries are atomic
424
+ }
425
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ // Main entry point for ODB Client
2
+ import { ODBLiteClient } from './core/client.ts';
3
+
4
+ // Core query client exports (postgres.js-like interface)
5
+ export { ODBLiteClient, odblite } from './core/client.ts';
6
+ export { ODBLiteTransaction, SimpleTransaction } from './core/transaction.ts';
7
+ export { HTTPClient } from './core/http-client.ts';
8
+ export { SQLParser, sql, fragment } from './core/sql-parser.ts';
9
+
10
+ // Service management exports (high-level tenant database management)
11
+ export { ServiceClient } from './service/service-client.ts';
12
+
13
+ // Export all types
14
+ export type {
15
+ ODBLiteConfig,
16
+ ODBLiteConnection,
17
+ QueryResult,
18
+ Transaction,
19
+ SQLFragment,
20
+ PreparedQuery,
21
+ QueryParameter,
22
+ PrimitiveType,
23
+ Row
24
+ } from './types.ts';
25
+
26
+ // Export service client types
27
+ export type {
28
+ ServiceClientConfig,
29
+ ODBLiteDatabase,
30
+ ODBLiteNode
31
+ } from './service/service-client.ts';
32
+
33
+ // Export error classes
34
+ export {
35
+ ODBLiteError,
36
+ ConnectionError,
37
+ QueryError
38
+ } from './types.ts';
39
+
40
+ // Export static utility functions for easy access
41
+ export const {
42
+ raw,
43
+ identifier,
44
+ where,
45
+ insertValues,
46
+ updateSet,
47
+ join
48
+ } = ODBLiteClient;
49
+
50
+ // Default export for convenient usage
51
+ export { odblite as default } from './core/client.ts';