@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- 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";
|