@buenojs/bueno 0.8.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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,1142 @@
1
+ /**
2
+ * Database Layer
3
+ *
4
+ * Unified interface over Bun.SQL supporting PostgreSQL, MySQL, and SQLite.
5
+ * Uses Bun 1.3+ native SQL client with tagged template literals.
6
+ */
7
+
8
+ // ============= Types =============
9
+
10
+ export type DatabaseDriver = "postgresql" | "mysql" | "sqlite";
11
+
12
+ /**
13
+ * Database metrics for observability
14
+ */
15
+ export interface DatabaseMetrics {
16
+ queries: number; // SELECT operations
17
+ inserts: number;
18
+ updates: number;
19
+ deletes: number;
20
+ errors: number;
21
+ avgLatency: number; // in milliseconds
22
+ totalLatency: number;
23
+ slowQueries: number; // queries exceeding slowQueryThreshold
24
+ totalOperations: number;
25
+ }
26
+
27
+ export interface DatabaseConfig {
28
+ url: string;
29
+ driver?: DatabaseDriver;
30
+ pool?: {
31
+ max?: number;
32
+ idleTimeout?: number;
33
+ maxLifetime?: number;
34
+ connectionTimeout?: number;
35
+ };
36
+ tls?:
37
+ | boolean
38
+ | {
39
+ rejectUnauthorized?: boolean;
40
+ ca?: string;
41
+ key?: string;
42
+ cert?: string;
43
+ };
44
+ bigint?: boolean;
45
+ prepare?: boolean;
46
+ enableMetrics?: boolean; // Enable metrics collection (default: true)
47
+ slowQueryThreshold?: number; // Threshold in ms to flag slow queries (default: 100)
48
+ }
49
+
50
+ export interface QueryResult {
51
+ rows: unknown[];
52
+ rowCount: number;
53
+ insertId?: number | string;
54
+ }
55
+
56
+ export interface Transaction {
57
+ query<T>(strings: TemplateStringsArray, ...params: unknown[]): Promise<T[]>;
58
+ queryOne<T>(
59
+ strings: TemplateStringsArray,
60
+ ...params: unknown[]
61
+ ): Promise<T | null>;
62
+ execute(
63
+ strings: TemplateStringsArray,
64
+ ...params: unknown[]
65
+ ): Promise<QueryResult>;
66
+ }
67
+
68
+ /**
69
+ * Query event types for event emission
70
+ */
71
+ export type QueryEventType = "query:start" | "query:end" | "query:error";
72
+
73
+ /**
74
+ * Query event data
75
+ */
76
+ export interface QueryEvent {
77
+ type: QueryEventType;
78
+ sql?: string;
79
+ params?: unknown[];
80
+ latency?: number;
81
+ error?: Error;
82
+ operationType?: "query" | "insert" | "update" | "delete" | "other";
83
+ }
84
+
85
+ /**
86
+ * Query event listener
87
+ */
88
+ export type QueryEventListener = (event: QueryEvent) => void;
89
+
90
+ // ============= Driver Detection =============
91
+
92
+ /**
93
+ * Detect database driver from connection string
94
+ */
95
+ export function detectDriver(url: string): DatabaseDriver {
96
+ if (url.startsWith("mysql://") || url.startsWith("mysql2://")) {
97
+ return "mysql";
98
+ }
99
+ if (
100
+ url.startsWith("sqlite://") ||
101
+ url.startsWith("file://") ||
102
+ url.startsWith("file:") ||
103
+ url === ":memory:" ||
104
+ url.endsWith(".db") ||
105
+ url.endsWith(".sqlite") ||
106
+ url.endsWith(".sqlite3")
107
+ ) {
108
+ return "sqlite";
109
+ }
110
+ // PostgreSQL is the default
111
+ return "postgresql";
112
+ }
113
+
114
+ // ============= SQL Fragment Builder =============
115
+
116
+ /**
117
+ * Build SQL fragment for inserts/updates
118
+ */
119
+ function buildInsertFragment(data: Record<string, unknown>): {
120
+ columns: string;
121
+ values: string;
122
+ params: unknown[];
123
+ } {
124
+ const keys = Object.keys(data);
125
+ const params: unknown[] = [];
126
+ const placeholders: string[] = [];
127
+
128
+ for (const key of keys) {
129
+ params.push(data[key]);
130
+ placeholders.push("?");
131
+ }
132
+
133
+ return {
134
+ columns: `(${keys.join(", ")})`,
135
+ values: `(${placeholders.join(", ")})`,
136
+ params,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Build SET clause for updates
142
+ */
143
+ function buildSetFragment(data: Record<string, unknown>): {
144
+ clause: string;
145
+ params: unknown[];
146
+ } {
147
+ const keys = Object.keys(data);
148
+ const params: unknown[] = [];
149
+ const sets: string[] = [];
150
+
151
+ for (const key of keys) {
152
+ params.push(data[key]);
153
+ sets.push(`${key} = ?`);
154
+ }
155
+
156
+ return {
157
+ clause: sets.join(", "),
158
+ params,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Detect operation type from SQL string
164
+ */
165
+ function detectOperationType(sql: string): "query" | "insert" | "update" | "delete" | "other" {
166
+ const normalizedSql = sql.trim().toUpperCase();
167
+ if (normalizedSql.startsWith("SELECT")) return "query";
168
+ if (normalizedSql.startsWith("INSERT")) return "insert";
169
+ if (normalizedSql.startsWith("UPDATE")) return "update";
170
+ if (normalizedSql.startsWith("DELETE")) return "delete";
171
+ return "other";
172
+ }
173
+
174
+ // ============= Database Class =============
175
+
176
+ export class Database {
177
+ private config: DatabaseConfig;
178
+ private driver: DatabaseDriver;
179
+ private sql: unknown = null;
180
+ private _isConnected = false;
181
+
182
+ // Metrics tracking
183
+ private enableMetrics: boolean;
184
+ private slowQueryThreshold: number;
185
+ private metrics: DatabaseMetrics = {
186
+ queries: 0,
187
+ inserts: 0,
188
+ updates: 0,
189
+ deletes: 0,
190
+ errors: 0,
191
+ avgLatency: 0,
192
+ totalLatency: 0,
193
+ slowQueries: 0,
194
+ totalOperations: 0,
195
+ };
196
+
197
+ // Event listeners
198
+ private eventListeners: Map<QueryEventType, Set<QueryEventListener>> = new Map();
199
+
200
+ constructor(config: DatabaseConfig | string) {
201
+ this.config = typeof config === "string" ? { url: config } : config;
202
+ this.driver = this.config.driver ?? detectDriver(this.config.url);
203
+ this.enableMetrics = this.config.enableMetrics ?? true;
204
+ this.slowQueryThreshold = this.config.slowQueryThreshold ?? 100;
205
+ }
206
+
207
+ /**
208
+ * Get current timestamp in milliseconds
209
+ */
210
+ private getTimestamp(): number {
211
+ // Use Bun.nanoseconds() if available, otherwise performance.now()
212
+ try {
213
+ return Bun.nanoseconds() / 1_000_000;
214
+ } catch {
215
+ return performance.now();
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get current metrics snapshot
221
+ */
222
+ getMetrics(): DatabaseMetrics {
223
+ return { ...this.metrics };
224
+ }
225
+
226
+ /**
227
+ * Reset metrics counters
228
+ */
229
+ resetMetrics(): void {
230
+ this.metrics = {
231
+ queries: 0,
232
+ inserts: 0,
233
+ updates: 0,
234
+ deletes: 0,
235
+ errors: 0,
236
+ avgLatency: 0,
237
+ totalLatency: 0,
238
+ slowQueries: 0,
239
+ totalOperations: 0,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Update metrics counters
245
+ */
246
+ private updateMetrics(
247
+ operationType: "query" | "insert" | "update" | "delete" | "other",
248
+ latency: number,
249
+ error?: boolean,
250
+ ): void {
251
+ if (!this.enableMetrics) return;
252
+
253
+ this.metrics.totalOperations++;
254
+ this.metrics.totalLatency += latency;
255
+ this.metrics.avgLatency = this.metrics.totalLatency / this.metrics.totalOperations;
256
+
257
+ if (latency > this.slowQueryThreshold) {
258
+ this.metrics.slowQueries++;
259
+ }
260
+
261
+ if (error) {
262
+ this.metrics.errors++;
263
+ }
264
+
265
+ switch (operationType) {
266
+ case "query":
267
+ this.metrics.queries++;
268
+ break;
269
+ case "insert":
270
+ this.metrics.inserts++;
271
+ break;
272
+ case "update":
273
+ this.metrics.updates++;
274
+ break;
275
+ case "delete":
276
+ this.metrics.deletes++;
277
+ break;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Emit a query event to all listeners
283
+ */
284
+ private emitEvent(event: QueryEvent): void {
285
+ const listeners = this.eventListeners.get(event.type);
286
+ if (listeners) {
287
+ for (const listener of listeners) {
288
+ try {
289
+ listener(event);
290
+ } catch (e) {
291
+ // Don't let listener errors affect query execution
292
+ console.error("Database event listener error:", e);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Subscribe to query events
300
+ */
301
+ on(eventType: QueryEventType, listener: QueryEventListener): void {
302
+ if (!this.eventListeners.has(eventType)) {
303
+ this.eventListeners.set(eventType, new Set());
304
+ }
305
+ this.eventListeners.get(eventType)?.add(listener);
306
+ }
307
+
308
+ /**
309
+ * Unsubscribe from query events
310
+ */
311
+ off(eventType: QueryEventType, listener: QueryEventListener): void {
312
+ this.eventListeners.get(eventType)?.delete(listener);
313
+ }
314
+
315
+ /**
316
+ * Remove all event listeners
317
+ */
318
+ removeAllListeners(eventType?: QueryEventType): void {
319
+ if (eventType) {
320
+ this.eventListeners.delete(eventType);
321
+ } else {
322
+ this.eventListeners.clear();
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Get the driver type
328
+ */
329
+ getDriver(): DatabaseDriver {
330
+ return this.driver;
331
+ }
332
+
333
+ /**
334
+ * Connect to the database using Bun.SQL
335
+ */
336
+ async connect(): Promise<void> {
337
+ if (this._isConnected) return;
338
+
339
+ try {
340
+ // Import Bun's native SQL
341
+ const { SQL } = await import("bun");
342
+
343
+ const options: Record<string, unknown> = {};
344
+
345
+ // Set adapter explicitly if needed
346
+ if (this.driver === "sqlite") {
347
+ options.adapter = "sqlite";
348
+ // Handle file paths
349
+ if (
350
+ !this.config.url.startsWith("sqlite://") &&
351
+ !this.config.url.startsWith("file:") &&
352
+ this.config.url !== ":memory:"
353
+ ) {
354
+ options.filename = this.config.url;
355
+ }
356
+ }
357
+
358
+ // Pool configuration
359
+ if (this.config.pool) {
360
+ if (this.config.pool.max) options.max = this.config.pool.max;
361
+ if (this.config.pool.idleTimeout)
362
+ options.idleTimeout = this.config.pool.idleTimeout;
363
+ if (this.config.pool.maxLifetime)
364
+ options.maxLifetime = this.config.pool.maxLifetime;
365
+ if (this.config.pool.connectionTimeout)
366
+ options.connectionTimeout = this.config.pool.connectionTimeout;
367
+ }
368
+
369
+ // TLS configuration
370
+ if (this.config.tls !== undefined) {
371
+ options.tls = this.config.tls;
372
+ }
373
+
374
+ // BigInt support
375
+ if (this.config.bigint !== undefined) {
376
+ options.bigint = this.config.bigint;
377
+ }
378
+
379
+ // Prepared statements
380
+ if (this.config.prepare !== undefined) {
381
+ options.prepare = this.config.prepare;
382
+ }
383
+
384
+ // Create connection
385
+ if (
386
+ Object.keys(options).length > 0 &&
387
+ !this.config.url.startsWith("sqlite://")
388
+ ) {
389
+ this.sql = new SQL(this.config.url, options);
390
+ } else {
391
+ this.sql = new SQL(this.config.url);
392
+ }
393
+
394
+ this._isConnected = true;
395
+ } catch (error) {
396
+ throw new Error(
397
+ `Failed to connect to database: ${error instanceof Error ? error.message : String(error)}`,
398
+ );
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Check if connected
404
+ */
405
+ get isConnected(): boolean {
406
+ return this._isConnected;
407
+ }
408
+
409
+ /**
410
+ * Get the underlying Bun.SQL instance
411
+ */
412
+ getSql(): unknown {
413
+ return this.sql;
414
+ }
415
+
416
+ /**
417
+ * Execute a raw SQL query using tagged template literal
418
+ */
419
+ async query<T = unknown>(
420
+ strings: TemplateStringsArray,
421
+ ...values: unknown[]
422
+ ): Promise<T[]> {
423
+ this.ensureConnection();
424
+
425
+ const sql = strings.join("?");
426
+ const operationType = detectOperationType(sql);
427
+ const startTime = this.getTimestamp();
428
+
429
+ // Emit query:start event
430
+ this.emitEvent({
431
+ type: "query:start",
432
+ sql,
433
+ params: values,
434
+ operationType,
435
+ });
436
+
437
+ try {
438
+ const sqlFn = this.sql as (
439
+ strings: TemplateStringsArray,
440
+ ...values: unknown[]
441
+ ) => Promise<T[]>;
442
+
443
+ const results = await sqlFn(strings, ...values);
444
+ const latency = this.getTimestamp() - startTime;
445
+
446
+ // Update metrics
447
+ this.updateMetrics(operationType, latency, false);
448
+
449
+ // Emit query:end event
450
+ this.emitEvent({
451
+ type: "query:end",
452
+ sql,
453
+ params: values,
454
+ latency,
455
+ operationType,
456
+ });
457
+
458
+ return results;
459
+ } catch (error) {
460
+ const latency = this.getTimestamp() - startTime;
461
+
462
+ // Update metrics with error
463
+ this.updateMetrics(operationType, latency, true);
464
+
465
+ // Emit query:error event
466
+ this.emitEvent({
467
+ type: "query:error",
468
+ sql,
469
+ params: values,
470
+ latency,
471
+ error: error instanceof Error ? error : new Error(String(error)),
472
+ operationType,
473
+ });
474
+
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Execute a query and return a single row
481
+ */
482
+ async queryOne<T = unknown>(
483
+ strings: TemplateStringsArray,
484
+ ...values: unknown[]
485
+ ): Promise<T | null> {
486
+ const results = await this.query<T>(strings, ...values);
487
+ return results.length > 0 ? results[0] : null;
488
+ }
489
+
490
+ /**
491
+ * Execute a query that doesn't return rows
492
+ */
493
+ async execute(
494
+ strings: TemplateStringsArray,
495
+ ...values: unknown[]
496
+ ): Promise<QueryResult> {
497
+ this.ensureConnection();
498
+
499
+ const sql = strings.join("?");
500
+ const operationType = detectOperationType(sql);
501
+ const startTime = this.getTimestamp();
502
+
503
+ // Emit query:start event
504
+ this.emitEvent({
505
+ type: "query:start",
506
+ sql,
507
+ params: values,
508
+ operationType,
509
+ });
510
+
511
+ try {
512
+ const sqlFn = this.sql as (
513
+ strings: TemplateStringsArray,
514
+ ...values: unknown[]
515
+ ) => Promise<unknown[]>;
516
+
517
+ // For INSERT with RETURNING
518
+ const results = await sqlFn(strings, ...values);
519
+ const latency = this.getTimestamp() - startTime;
520
+
521
+ // Update metrics
522
+ this.updateMetrics(operationType, latency, false);
523
+
524
+ // Emit query:end event
525
+ this.emitEvent({
526
+ type: "query:end",
527
+ sql,
528
+ params: values,
529
+ latency,
530
+ operationType,
531
+ });
532
+
533
+ return {
534
+ rows: results,
535
+ rowCount: results.length,
536
+ };
537
+ } catch (error) {
538
+ const latency = this.getTimestamp() - startTime;
539
+
540
+ // Update metrics with error
541
+ this.updateMetrics(operationType, latency, true);
542
+
543
+ // Emit query:error event
544
+ this.emitEvent({
545
+ type: "query:error",
546
+ sql,
547
+ params: values,
548
+ latency,
549
+ error: error instanceof Error ? error : new Error(String(error)),
550
+ operationType,
551
+ });
552
+
553
+ throw error;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Execute raw SQL string (unsafe)
559
+ */
560
+ async raw<T = unknown>(
561
+ sqlString: string,
562
+ params: unknown[] = [],
563
+ ): Promise<T[]> {
564
+ this.ensureConnection();
565
+
566
+ const operationType = detectOperationType(sqlString);
567
+ const startTime = this.getTimestamp();
568
+
569
+ // Emit query:start event
570
+ this.emitEvent({
571
+ type: "query:start",
572
+ sql: sqlString,
573
+ params,
574
+ operationType,
575
+ });
576
+
577
+ try {
578
+ const sql = this.sql as {
579
+ unsafe: (query: string, params?: unknown[]) => Promise<T[]>;
580
+ };
581
+
582
+ let results: T[];
583
+
584
+ if (sql.unsafe) {
585
+ // For SQLite, convert $1, $2 to ? placeholders
586
+ if (this.driver === "sqlite") {
587
+ let query = sqlString;
588
+ let i = 1;
589
+ while (query.includes(`$${i}`)) {
590
+ query = query.replace(`$${i}`, "?");
591
+ i++;
592
+ }
593
+ results = await sql.unsafe(query, params);
594
+ } else {
595
+ results = await sql.unsafe(sqlString, params);
596
+ }
597
+ } else {
598
+ throw new Error("Raw SQL not supported");
599
+ }
600
+
601
+ const latency = this.getTimestamp() - startTime;
602
+
603
+ // Update metrics
604
+ this.updateMetrics(operationType, latency, false);
605
+
606
+ // Emit query:end event
607
+ this.emitEvent({
608
+ type: "query:end",
609
+ sql: sqlString,
610
+ params,
611
+ latency,
612
+ operationType,
613
+ });
614
+
615
+ return results;
616
+ } catch (error) {
617
+ const latency = this.getTimestamp() - startTime;
618
+
619
+ // Update metrics with error
620
+ this.updateMetrics(operationType, latency, true);
621
+
622
+ // Emit query:error event
623
+ this.emitEvent({
624
+ type: "query:error",
625
+ sql: sqlString,
626
+ params,
627
+ latency,
628
+ error: error instanceof Error ? error : new Error(String(error)),
629
+ operationType,
630
+ });
631
+
632
+ throw error;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Execute a transaction
638
+ */
639
+ async transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T> {
640
+ this.ensureConnection();
641
+
642
+ const sql = this.sql as {
643
+ begin: <R>(fn: (tx: unknown) => Promise<R>) => Promise<R>;
644
+ };
645
+
646
+ return sql.begin(async (tx) => {
647
+ const txWrapper: Transaction = {
648
+ query: async <T>(
649
+ strings: TemplateStringsArray,
650
+ ...values: unknown[]
651
+ ): Promise<T[]> => {
652
+ const t = tx as (
653
+ strings: TemplateStringsArray,
654
+ ...values: unknown[]
655
+ ) => Promise<T[]>;
656
+ return t(strings, ...values);
657
+ },
658
+ queryOne: async <T>(
659
+ strings: TemplateStringsArray,
660
+ ...values: unknown[]
661
+ ): Promise<T | null> => {
662
+ const results = await txWrapper.query<T>(strings, ...values);
663
+ return results.length > 0 ? results[0] : null;
664
+ },
665
+ execute: async (
666
+ strings: TemplateStringsArray,
667
+ ...values: unknown[]
668
+ ): Promise<QueryResult> => {
669
+ const t = tx as (
670
+ strings: TemplateStringsArray,
671
+ ...values: unknown[]
672
+ ) => Promise<unknown[]>;
673
+ const results = await t(strings, ...values);
674
+ return { rows: results, rowCount: results.length };
675
+ },
676
+ };
677
+
678
+ return callback(txWrapper);
679
+ });
680
+ }
681
+
682
+ /**
683
+ * Begin a distributed transaction (2PC)
684
+ */
685
+ async beginDistributed<T>(
686
+ name: string,
687
+ callback: (tx: Transaction) => Promise<T>,
688
+ ): Promise<T> {
689
+ this.ensureConnection();
690
+
691
+ const sql = this.sql as {
692
+ beginDistributed: <R>(
693
+ name: string,
694
+ fn: (tx: unknown) => Promise<R>,
695
+ ) => Promise<R>;
696
+ };
697
+
698
+ if (!sql.beginDistributed) {
699
+ throw new Error(
700
+ "Distributed transactions not supported for this database",
701
+ );
702
+ }
703
+
704
+ return sql.beginDistributed(name, async (tx) => {
705
+ const txWrapper: Transaction = {
706
+ query: async <T>(
707
+ strings: TemplateStringsArray,
708
+ ...values: unknown[]
709
+ ): Promise<T[]> => {
710
+ const t = tx as (
711
+ strings: TemplateStringsArray,
712
+ ...values: unknown[]
713
+ ) => Promise<T[]>;
714
+ return t(strings, ...values);
715
+ },
716
+ queryOne: async <T>(
717
+ strings: TemplateStringsArray,
718
+ ...values: unknown[]
719
+ ): Promise<T | null> => {
720
+ const results = await txWrapper.query<T>(strings, ...values);
721
+ return results.length > 0 ? results[0] : null;
722
+ },
723
+ execute: async (
724
+ strings: TemplateStringsArray,
725
+ ...values: unknown[]
726
+ ): Promise<QueryResult> => {
727
+ const t = tx as (
728
+ strings: TemplateStringsArray,
729
+ ...values: unknown[]
730
+ ) => Promise<unknown[]>;
731
+ const results = await t(strings, ...values);
732
+ return { rows: results, rowCount: results.length };
733
+ },
734
+ };
735
+
736
+ return callback(txWrapper);
737
+ });
738
+ }
739
+
740
+ /**
741
+ * Commit a distributed transaction
742
+ */
743
+ async commitDistributed(name: string): Promise<void> {
744
+ const sql = this.sql as {
745
+ commitDistributed: (name: string) => Promise<void>;
746
+ };
747
+
748
+ if (sql.commitDistributed) {
749
+ await sql.commitDistributed(name);
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Rollback a distributed transaction
755
+ */
756
+ async rollbackDistributed(name: string): Promise<void> {
757
+ const sql = this.sql as {
758
+ rollbackDistributed: (name: string) => Promise<void>;
759
+ };
760
+
761
+ if (sql.rollbackDistributed) {
762
+ await sql.rollbackDistributed(name);
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Reserve a connection from the pool
768
+ */
769
+ async reserve(): Promise<ReservedConnection> {
770
+ this.ensureConnection();
771
+
772
+ const sql = this.sql as {
773
+ reserve: () => Promise<unknown>;
774
+ };
775
+
776
+ if (!sql.reserve) {
777
+ throw new Error("Connection reservation not supported");
778
+ }
779
+
780
+ const reserved = await sql.reserve();
781
+ return new ReservedConnection(reserved);
782
+ }
783
+
784
+ /**
785
+ * Close the connection
786
+ */
787
+ async close(options?: { timeout?: number }): Promise<void> {
788
+ if (!this._isConnected) return;
789
+
790
+ const sql = this.sql as {
791
+ close: (options?: { timeout?: number }) => Promise<void>;
792
+ };
793
+
794
+ if (sql.close) {
795
+ await sql.close(options);
796
+ }
797
+
798
+ this.sql = null;
799
+ this._isConnected = false;
800
+ }
801
+
802
+ /**
803
+ * Get values format
804
+ */
805
+ async values(
806
+ strings: TemplateStringsArray,
807
+ ...values: unknown[]
808
+ ): Promise<unknown[][]> {
809
+ this.ensureConnection();
810
+
811
+ const sql = this.sql as (
812
+ strings: TemplateStringsArray,
813
+ ...values: unknown[]
814
+ ) => { values: () => Promise<unknown[][]> };
815
+
816
+ return sql(strings, ...values).values();
817
+ }
818
+
819
+ /**
820
+ * Get raw format (Buffer arrays)
821
+ */
822
+ async rawFormat(
823
+ strings: TemplateStringsArray,
824
+ ...values: unknown[]
825
+ ): Promise<Buffer[][]> {
826
+ this.ensureConnection();
827
+
828
+ const sql = this.sql as (
829
+ strings: TemplateStringsArray,
830
+ ...values: unknown[]
831
+ ) => { raw: () => Promise<Buffer[][]> };
832
+
833
+ return sql(strings, ...values).raw();
834
+ }
835
+
836
+ /**
837
+ * Execute a simple query (multiple statements allowed)
838
+ */
839
+ async simple(
840
+ strings: TemplateStringsArray,
841
+ ...values: unknown[]
842
+ ): Promise<unknown[]> {
843
+ this.ensureConnection();
844
+
845
+ const sql = this.sql as (
846
+ strings: TemplateStringsArray,
847
+ ...values: unknown[]
848
+ ) => { simple: () => Promise<unknown[]> };
849
+
850
+ return sql(strings, ...values).simple();
851
+ }
852
+
853
+ /**
854
+ * Execute SQL from a file
855
+ */
856
+ async file(path: string, params: unknown[] = []): Promise<unknown[]> {
857
+ this.ensureConnection();
858
+
859
+ const sql = this.sql as {
860
+ file: (path: string, params?: unknown[]) => Promise<unknown[]>;
861
+ };
862
+
863
+ if (sql.file) {
864
+ return sql.file(path, params);
865
+ }
866
+
867
+ throw new Error("File execution not supported");
868
+ }
869
+
870
+ /**
871
+ * Ensure connection is established
872
+ */
873
+ private ensureConnection(): void {
874
+ if (!this._isConnected || !this.sql) {
875
+ throw new Error("Database not connected. Call connect() first.");
876
+ }
877
+ }
878
+ }
879
+
880
+ // ============= Reserved Connection =============
881
+
882
+ export class ReservedConnection {
883
+ private connection: unknown;
884
+
885
+ constructor(connection: unknown) {
886
+ this.connection = connection;
887
+ }
888
+
889
+ async query<T>(
890
+ strings: TemplateStringsArray,
891
+ ...values: unknown[]
892
+ ): Promise<T[]> {
893
+ const conn = this.connection as (
894
+ strings: TemplateStringsArray,
895
+ ...values: unknown[]
896
+ ) => Promise<T[]>;
897
+ return conn(strings, ...values);
898
+ }
899
+
900
+ async queryOne<T>(
901
+ strings: TemplateStringsArray,
902
+ ...values: unknown[]
903
+ ): Promise<T | null> {
904
+ const results = await this.query<T>(strings, ...values);
905
+ return results.length > 0 ? results[0] : null;
906
+ }
907
+
908
+ release(): void {
909
+ const conn = this.connection as { release: () => void };
910
+ if (conn.release) {
911
+ conn.release();
912
+ }
913
+ }
914
+
915
+ [Symbol.dispose](): void {
916
+ this.release();
917
+ }
918
+ }
919
+
920
+ // ============= Connection Factory =============
921
+
922
+ /**
923
+ * Create a database connection
924
+ */
925
+ export async function createConnection(
926
+ config: DatabaseConfig | string,
927
+ ): Promise<Database> {
928
+ const db = new Database(config);
929
+ await db.connect();
930
+ return db;
931
+ }
932
+
933
+ // ============= Query Builder =============
934
+
935
+ /**
936
+ * Simple query builder for common operations
937
+ */
938
+ export class QueryBuilder<T = unknown> {
939
+ private db: Database;
940
+ private tableName: string;
941
+
942
+ constructor(db: Database, tableName: string) {
943
+ this.db = db;
944
+ this.tableName = tableName;
945
+ }
946
+
947
+ /**
948
+ * Select all rows
949
+ */
950
+ async all(): Promise<T[]> {
951
+ return this.db.raw<T>(`SELECT * FROM ${this.tableName}`);
952
+ }
953
+
954
+ /**
955
+ * Find by ID
956
+ */
957
+ async findById(id: number | string): Promise<T | null> {
958
+ const results = await this.db.raw<T>(
959
+ `SELECT * FROM ${this.tableName} WHERE id = $1`,
960
+ [id],
961
+ );
962
+ return results.length > 0 ? results[0] : null;
963
+ }
964
+
965
+ /**
966
+ * Find by field
967
+ */
968
+ async findBy(field: string, value: unknown): Promise<T[]> {
969
+ // Note: Field name needs to be safely inserted
970
+ const sql = `SELECT * FROM ${this.tableName} WHERE ${field} = $1`;
971
+ return this.db.raw<T>(sql, [value]);
972
+ }
973
+
974
+ /**
975
+ * Find one by field
976
+ */
977
+ async findOneBy(field: string, value: unknown): Promise<T | null> {
978
+ const results = await this.findBy(field, value);
979
+ return results.length > 0 ? results[0] : null;
980
+ }
981
+
982
+ /**
983
+ * Insert a row
984
+ */
985
+ async insert(data: Partial<T>): Promise<T> {
986
+ const keys = Object.keys(data);
987
+ const values = Object.values(data);
988
+
989
+ const columns = keys.join(", ");
990
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
991
+
992
+ const result = await this.db.raw<T>(
993
+ `INSERT INTO ${this.tableName} (${columns}) VALUES (${placeholders}) RETURNING *`,
994
+ values,
995
+ );
996
+
997
+ return result[0];
998
+ }
999
+
1000
+ /**
1001
+ * Bulk insert
1002
+ */
1003
+ async insertMany(items: Partial<T>[]): Promise<T[]> {
1004
+ if (items.length === 0) return [];
1005
+
1006
+ const results: T[] = [];
1007
+
1008
+ for (const item of items) {
1009
+ const result = await this.insert(item);
1010
+ results.push(result);
1011
+ }
1012
+
1013
+ return results;
1014
+ }
1015
+
1016
+ /**
1017
+ * Update by ID
1018
+ */
1019
+ async updateById(id: number | string, data: Partial<T>): Promise<T | null> {
1020
+ const keys = Object.keys(data);
1021
+ const values = Object.values(data);
1022
+
1023
+ const setClause = keys.map((k, i) => `${k} = $${i + 1}`).join(", ");
1024
+
1025
+ const result = await this.db.raw<T>(
1026
+ `UPDATE ${this.tableName} SET ${setClause} WHERE id = $${keys.length + 1} RETURNING *`,
1027
+ [...values, id],
1028
+ );
1029
+
1030
+ return result.length > 0 ? result[0] : null;
1031
+ }
1032
+
1033
+ /**
1034
+ * Delete by ID
1035
+ */
1036
+ async deleteById(id: number | string): Promise<boolean> {
1037
+ const result = await this.db.raw(
1038
+ `DELETE FROM ${this.tableName} WHERE id = $1 RETURNING id`,
1039
+ [id],
1040
+ );
1041
+ return result.length > 0;
1042
+ }
1043
+
1044
+ /**
1045
+ * Count rows
1046
+ */
1047
+ async count(where?: string, params: unknown[] = []): Promise<number> {
1048
+ const sql = where
1049
+ ? `SELECT COUNT(*) as count FROM ${this.tableName} WHERE ${where}`
1050
+ : `SELECT COUNT(*) as count FROM ${this.tableName}`;
1051
+
1052
+ const result = await this.db.raw<{ count: number | string }>(sql, params);
1053
+ return Number(result[0]?.count ?? 0);
1054
+ }
1055
+
1056
+ /**
1057
+ * Check if exists
1058
+ */
1059
+ async exists(where: string, params: unknown[] = []): Promise<boolean> {
1060
+ const count = await this.count(where, params);
1061
+ return count > 0;
1062
+ }
1063
+
1064
+ /**
1065
+ * Paginate results
1066
+ */
1067
+ async paginate(
1068
+ page: number,
1069
+ limit: number,
1070
+ where?: string,
1071
+ params: unknown[] = [],
1072
+ ): Promise<{
1073
+ data: T[];
1074
+ total: number;
1075
+ page: number;
1076
+ limit: number;
1077
+ totalPages: number;
1078
+ }> {
1079
+ const offset = (page - 1) * limit;
1080
+
1081
+ const whereClause = where ? `WHERE ${where}` : "";
1082
+
1083
+ const [data, countResult] = await Promise.all([
1084
+ this.db.raw<T>(
1085
+ `SELECT * FROM ${this.tableName} ${whereClause} LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
1086
+ [...params, limit, offset],
1087
+ ),
1088
+ this.db.raw<{ count: number | string }>(
1089
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
1090
+ params,
1091
+ ),
1092
+ ]);
1093
+
1094
+ const total = Number(countResult[0]?.count ?? 0);
1095
+
1096
+ return {
1097
+ data,
1098
+ total,
1099
+ page,
1100
+ limit,
1101
+ totalPages: Math.ceil(total / limit),
1102
+ };
1103
+ }
1104
+ }
1105
+
1106
+ /**
1107
+ * Create a query builder for a table
1108
+ */
1109
+ export function table<T = unknown>(
1110
+ db: Database,
1111
+ tableName: string,
1112
+ ): QueryBuilder<T> {
1113
+ return new QueryBuilder<T>(db, tableName);
1114
+ }
1115
+
1116
+ // ============= SQL Helpers =============
1117
+
1118
+ /**
1119
+ * Create a SQL fragment for safe table/column names
1120
+ */
1121
+ export function sqlFragment(name: string): string {
1122
+ // Escape identifiers
1123
+ return name.replace(/"/g, '""');
1124
+ }
1125
+
1126
+ /**
1127
+ * Build an IN clause
1128
+ */
1129
+ export function buildInClause(values: unknown[]): {
1130
+ placeholder: string;
1131
+ params: unknown[];
1132
+ } {
1133
+ const placeholders = values.map(() => "?").join(", ");
1134
+ return {
1135
+ placeholder: `(${placeholders})`,
1136
+ params: values,
1137
+ };
1138
+ }
1139
+
1140
+ // Re-export schema and migrations
1141
+ export * from "./schema";
1142
+ export * from "./migrations";