@onebun/drizzle 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/drizzle",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Drizzle ORM module for OneBun framework - SQLite and PostgreSQL support",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -47,7 +47,7 @@
47
47
  "dev": "bun run --watch src/index.ts"
48
48
  },
49
49
  "dependencies": {
50
- "@onebun/core": "^0.1.11",
50
+ "@onebun/core": "^0.1.14",
51
51
  "@onebun/envs": "^0.1.4",
52
52
  "@onebun/logger": "^0.1.5",
53
53
  "drizzle-orm": "^0.44.7",
@@ -30,7 +30,7 @@ const DRIZZLE_MODULE_OPTIONS = Symbol('DRIZZLE_MODULE_OPTIONS');
30
30
  * - DB_URL: Database connection URL (default: ':memory:' for SQLite)
31
31
  * - DB_SCHEMA_PATH: Path to schema files (optional)
32
32
  * - DB_MIGRATIONS_FOLDER: Path to migrations folder (default: './drizzle')
33
- * - DB_AUTO_MIGRATE: Whether to run migrations on startup (default: false)
33
+ * - DB_AUTO_MIGRATE: Whether to run migrations on startup (default: true)
34
34
  * - DB_LOG_QUERIES: Whether to log SQL queries (default: false)
35
35
  *
36
36
  * @example Basic usage with environment variables (global by default)
@@ -158,6 +158,16 @@ async function loadFromEnv(prefix: string = DEFAULT_ENV_PREFIX): Promise<Databas
158
158
  };
159
159
  }
160
160
 
161
+ /**
162
+ * Buffered log entry for pre-logger initialization logging
163
+ */
164
+ interface BufferedLogEntry {
165
+ level: 'debug' | 'info' | 'warn' | 'error';
166
+ message: string;
167
+ meta?: object;
168
+ timestamp: number;
169
+ }
170
+
161
171
  /**
162
172
  * Drizzle service for database operations
163
173
  * Generic type parameter TDbType specifies the database type (SQLITE or POSTGRESQL)
@@ -182,44 +192,94 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
182
192
  private initPromise: Promise<void> | null = null;
183
193
  private sqliteClient: Database | null = null;
184
194
  private postgresClient: SQL | null = null;
195
+ private logBuffer: BufferedLogEntry[] = [];
196
+ private exitHandlerRegistered = false;
185
197
 
186
198
  constructor() {
187
199
  super();
188
- // Only start auto-initialization if there's configuration to use
189
- // Check synchronously to avoid unnecessary async work
190
- if (this.shouldAutoInitialize()) {
191
- this.initPromise = this.autoInitialize();
200
+ // Register exit handler to flush buffered logs on crash
201
+ this.registerExitHandler();
202
+ }
203
+
204
+ /**
205
+ * Register process exit handler to flush buffered logs to console.error on crash
206
+ */
207
+ private registerExitHandler(): void {
208
+ if (this.exitHandlerRegistered) {
209
+ return;
192
210
  }
211
+ this.exitHandlerRegistered = true;
212
+
213
+ const flushToConsole = () => {
214
+ if (this.logBuffer.length > 0) {
215
+ // eslint-disable-next-line no-console
216
+ console.error('[DrizzleService] Buffered logs (app crashed before logger init):');
217
+ for (const entry of this.logBuffer) {
218
+ const timestamp = new Date(entry.timestamp).toISOString();
219
+ // eslint-disable-next-line no-console
220
+ console.error(` [${timestamp}] [${entry.level.toUpperCase()}] ${entry.message}`, entry.meta ?? '');
221
+ }
222
+ }
223
+ };
224
+
225
+ // Register handlers for various exit scenarios
226
+ process.on('exit', flushToConsole);
227
+ process.on('uncaughtException', (err) => {
228
+ flushToConsole();
229
+ // eslint-disable-next-line no-console
230
+ console.error('[DrizzleService] Uncaught exception:', err);
231
+ });
232
+ process.on('unhandledRejection', (reason) => {
233
+ flushToConsole();
234
+ // eslint-disable-next-line no-console
235
+ console.error('[DrizzleService] Unhandled rejection:', reason);
236
+ });
193
237
  }
194
238
 
195
239
  /**
196
- * Check synchronously if auto-initialization should be attempted
197
- * This avoids starting async work when there's no configuration
198
- * Note: Only checks env vars here to avoid slow require() in constructor.
199
- * Module options are checked in autoInitialize() which is async.
240
+ * Safe logging that buffers logs before logger is available
241
+ * When logger becomes available, buffered logs are flushed
242
+ * If app crashes before logger init, logs are output via console.error
200
243
  */
201
- private shouldAutoInitialize(): boolean {
202
- // Check if DB_URL env var is set
203
- // Module options will be checked in autoInitialize() if this returns false
204
- const dbUrl = process.env.DB_URL;
205
- if (dbUrl && dbUrl.trim() !== '') {
206
- return true;
244
+ private safeLog(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: object): void {
245
+ if (this.logger) {
246
+ this.logger[level](message, meta);
247
+ } else {
248
+ // Buffer the log for later
249
+ this.logBuffer.push({
250
+ level,
251
+ message,
252
+ meta,
253
+ timestamp: Date.now(),
254
+ });
207
255
  }
256
+ }
208
257
 
209
- // Also check if DrizzleModule has options set (static check without require)
210
- // This is done by checking if the module was already loaded
211
- try {
212
- // Only check if module is already in cache (don't trigger new require)
213
- const modulePath = require.resolve('./drizzle.module');
214
- const cachedModule = require.cache[modulePath];
215
- if (cachedModule?.exports?.DrizzleModule?.getOptions?.()?.connection) {
216
- return true;
217
- }
218
- } catch {
219
- // Module not resolved or not in cache
258
+ /**
259
+ * Flush buffered logs to the logger (called when logger becomes available)
260
+ */
261
+ private flushLogBuffer(): void {
262
+ if (!this.logger || this.logBuffer.length === 0) {
263
+ return;
264
+ }
265
+
266
+ this.logger.debug(`Flushing ${this.logBuffer.length} buffered log entries`);
267
+ for (const entry of this.logBuffer) {
268
+ this.logger[entry.level](entry.message, entry.meta);
220
269
  }
270
+ this.logBuffer = [];
271
+ }
272
+
273
+ /**
274
+ * Async initialization hook - called by the framework after initializeService()
275
+ * This ensures the database is fully ready before client code runs
276
+ */
277
+ override async onAsyncInit(): Promise<void> {
278
+ // Flush any buffered logs now that logger is available
279
+ this.flushLogBuffer();
221
280
 
222
- return false;
281
+ // Run auto-initialization
282
+ await this.autoInitialize();
223
283
  }
224
284
 
225
285
  /**
@@ -232,16 +292,30 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
232
292
 
233
293
  // If module options are provided, use them
234
294
  if (moduleOptions?.connection) {
235
- this.logger.debug('Auto-initializing database service from module options', {
295
+ this.safeLog('debug', 'Auto-initializing database service from module options', {
236
296
  type: moduleOptions.connection.type,
237
297
  });
238
298
 
239
- await this.initialize(moduleOptions.connection);
299
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
300
+ await this.initialize(moduleOptions.connection, true);
240
301
 
241
- // Auto-migrate if enabled
242
- if (moduleOptions.autoMigrate) {
302
+ // Auto-migrate is enabled by default (unless explicitly set to false)
303
+ const shouldAutoMigrate = moduleOptions.autoMigrate !== false;
304
+ if (shouldAutoMigrate) {
243
305
  const migrationsFolder = moduleOptions.migrationsFolder ?? './drizzle';
244
- await this.runMigrations({ migrationsFolder });
306
+ this.safeLog('debug', 'Running auto-migrations', { migrationsFolder });
307
+ try {
308
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
309
+ await this.runMigrations({ migrationsFolder }, true);
310
+ this.safeLog('debug', 'Auto-migrations completed successfully');
311
+ } catch (migrationError) {
312
+ this.safeLog('warn', 'Auto-migration failed, database initialized without migrations', {
313
+ error: migrationError instanceof Error ? migrationError.message : String(migrationError),
314
+ });
315
+ // Don't rethrow - allow DB to be used even if migrations fail
316
+ }
317
+ } else {
318
+ this.safeLog('debug', 'Auto-migrations disabled via module options');
245
319
  }
246
320
 
247
321
  this.initialized = true;
@@ -255,7 +329,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
255
329
  // Check process.env directly to ensure we only auto-initialize when explicitly configured
256
330
  const dbUrlFromProcess = process.env[`${envPrefix}_URL`];
257
331
  if (!dbUrlFromProcess || dbUrlFromProcess.trim() === '') {
258
- this.logger.debug('Skipping auto-initialization: no database configuration found in process.env');
332
+ this.safeLog('debug', 'Skipping auto-initialization: no database configuration found in process.env');
259
333
  this.initialized = false;
260
334
 
261
335
  return;
@@ -275,22 +349,24 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
275
349
  options: parsePostgreSQLUrl(envConfig.url),
276
350
  };
277
351
 
278
- this.logger.debug(`Auto-initializing database service with type: ${connectionOptions.type}`, {
352
+ this.safeLog('debug', `Auto-initializing database service with type: ${connectionOptions.type}`, {
279
353
  envPrefix,
280
354
  });
281
355
 
282
- await this.initialize(connectionOptions);
356
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
357
+ await this.initialize(connectionOptions, true);
283
358
 
284
- // Auto-migrate if enabled
359
+ // Auto-migrate is enabled by default (env schema default is true)
285
360
  if (envConfig.autoMigrate) {
286
361
  const migrationsFolder = envConfig.migrationsFolder ?? './drizzle';
287
- await this.runMigrations({ migrationsFolder });
362
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
363
+ await this.runMigrations({ migrationsFolder }, true);
288
364
  }
289
365
 
290
366
  this.initialized = true;
291
367
  } catch (error) {
292
368
  // Don't throw error - just log it and allow manual initialization
293
- this.logger.debug('Failed to auto-initialize database from environment', { error });
369
+ this.safeLog('debug', 'Failed to auto-initialize database from environment', { error });
294
370
  this.initialized = false;
295
371
  }
296
372
  }
@@ -320,12 +396,18 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
320
396
 
321
397
  /**
322
398
  * Initialize database connection
399
+ * @param options - Database connection options
400
+ * @param skipWait - Internal flag to skip waitForInit (used by autoInitialize to avoid deadlock)
323
401
  */
324
- async initialize(options: DatabaseConnectionOptions): Promise<void> {
325
- await this.waitForInit();
402
+ async initialize(options: DatabaseConnectionOptions, skipWait = false): Promise<void> {
403
+ // Skip waitForInit when called from autoInitialize to avoid deadlock
404
+ // (autoInitialize is the function that creates initPromise)
405
+ if (!skipWait) {
406
+ await this.waitForInit();
407
+ }
326
408
 
327
409
  if (this.initialized && this.connectionOptions) {
328
- this.logger.warn('Database already initialized, closing existing connection');
410
+ this.safeLog('warn', 'Database already initialized, closing existing connection');
329
411
  await this.close();
330
412
  }
331
413
 
@@ -336,7 +418,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
336
418
  const sqliteOptions = options.options;
337
419
  this.sqliteClient = new Database(sqliteOptions.url, sqliteOptions.options);
338
420
  this.db = drizzleSQLite(this.sqliteClient);
339
- this.logger.info('SQLite database initialized', { url: sqliteOptions.url });
421
+ this.safeLog('info', 'SQLite database initialized', { url: sqliteOptions.url });
340
422
  } else if (options.type === DatabaseType.POSTGRESQL) {
341
423
  const pgOptions = options.options;
342
424
 
@@ -352,7 +434,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
352
434
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
435
  this.postgresClient = (this.db as any).$client as SQL | null;
354
436
 
355
- this.logger.info('PostgreSQL database initialized with Bun.SQL', {
437
+ this.safeLog('info', 'PostgreSQL database initialized with Bun.SQL', {
356
438
  host: pgOptions.host,
357
439
  port: pgOptions.port,
358
440
  database: pgOptions.database,
@@ -448,10 +530,14 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
448
530
  * migration system which ensures idempotency.
449
531
  *
450
532
  * @param options - Migration options
533
+ * @param skipWait - Internal flag to skip waitForInit (used by autoInitialize to avoid deadlock)
451
534
  * @throws Error if database is not initialized
452
535
  */
453
- async runMigrations(options?: MigrationOptions): Promise<void> {
454
- await this.waitForInit();
536
+ async runMigrations(options?: MigrationOptions, skipWait = false): Promise<void> {
537
+ // Skip waitForInit when called from autoInitialize to avoid deadlock
538
+ if (!skipWait) {
539
+ await this.waitForInit();
540
+ }
455
541
 
456
542
  if (!this.db || !this.connectionOptions) {
457
543
  throw new Error('Database not initialized. Call initialize() first.');
@@ -466,7 +552,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
466
552
  await migrate(this.db as BunSQLiteDatabase<Record<string, SQLiteTable>>, {
467
553
  migrationsFolder,
468
554
  });
469
- this.logger.info('SQLite migrations applied', { migrationsFolder });
555
+ this.safeLog('info', 'SQLite migrations applied', { migrationsFolder });
470
556
  } else if (this.connectionOptions.type === DatabaseType.POSTGRESQL) {
471
557
  if (!this.db) {
472
558
  throw new Error('Database not initialized');
@@ -474,7 +560,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
474
560
  await migratePostgres(this.db as BunSQLDatabase<Record<string, PgTable>>, {
475
561
  migrationsFolder,
476
562
  });
477
- this.logger.info('PostgreSQL migrations applied', { migrationsFolder });
563
+ this.safeLog('info', 'PostgreSQL migrations applied', { migrationsFolder });
478
564
  }
479
565
  }
480
566
 
@@ -505,7 +591,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
505
591
  this.dbType = null;
506
592
  this.connectionOptions = null;
507
593
  this.initialized = false;
508
- this.logger.info('Database connection closed');
594
+ this.safeLog('info', 'Database connection closed');
509
595
  }
510
596
 
511
597
  /**
@@ -525,4 +611,135 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
525
611
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
526
612
  return await (this.db as any).transaction(callback);
527
613
  }
614
+
615
+ // ============================================
616
+ // Direct database operation methods (proxies)
617
+ // ============================================
618
+
619
+ /**
620
+ * Create a SELECT query
621
+ *
622
+ * @example
623
+ * ```typescript
624
+ * // Select all columns
625
+ * const users = await this.db.select().from(usersTable);
626
+ *
627
+ * // Select specific columns
628
+ * const names = await this.db.select({ name: usersTable.name }).from(usersTable);
629
+ * ```
630
+ */
631
+ select(): ReturnType<DatabaseInstanceForType<TDbType>['select']>;
632
+ select<TSelection extends Record<string, unknown>>(
633
+ fields: TSelection,
634
+ ): ReturnType<DatabaseInstanceForType<TDbType>['select']>;
635
+ select<TSelection extends Record<string, unknown>>(
636
+ fields?: TSelection,
637
+ ): ReturnType<DatabaseInstanceForType<TDbType>['select']> {
638
+ const db = this.getDatabase();
639
+ if (fields) {
640
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
641
+ return (db as any).select(fields);
642
+ }
643
+
644
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
645
+ return (db as any).select();
646
+ }
647
+
648
+ /**
649
+ * Create a SELECT DISTINCT query
650
+ *
651
+ * @example
652
+ * ```typescript
653
+ * // Select distinct values
654
+ * const uniqueNames = await this.db.selectDistinct({ name: usersTable.name }).from(usersTable);
655
+ * ```
656
+ */
657
+ selectDistinct(): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']>;
658
+ selectDistinct<TSelection extends Record<string, unknown>>(
659
+ fields: TSelection,
660
+ ): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']>;
661
+ selectDistinct<TSelection extends Record<string, unknown>>(
662
+ fields?: TSelection,
663
+ ): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']> {
664
+ const db = this.getDatabase();
665
+ if (fields) {
666
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
667
+ return (db as any).selectDistinct(fields);
668
+ }
669
+
670
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
671
+ return (db as any).selectDistinct();
672
+ }
673
+
674
+ /**
675
+ * Create an INSERT query
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * // Insert a single row
680
+ * await this.db.insert(usersTable).values({ name: 'John', email: 'john@example.com' });
681
+ *
682
+ * // Insert with returning
683
+ * const [newUser] = await this.db.insert(usersTable)
684
+ * .values({ name: 'John', email: 'john@example.com' })
685
+ * .returning();
686
+ * ```
687
+ */
688
+ insert<TTable extends Parameters<DatabaseInstanceForType<TDbType>['insert']>[0]>(
689
+ table: TTable,
690
+ ): ReturnType<DatabaseInstanceForType<TDbType>['insert']> {
691
+ const db = this.getDatabase();
692
+
693
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
694
+ return (db as any).insert(table);
695
+ }
696
+
697
+ /**
698
+ * Create an UPDATE query
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * // Update rows
703
+ * await this.db.update(usersTable)
704
+ * .set({ name: 'Jane' })
705
+ * .where(eq(usersTable.id, 1));
706
+ *
707
+ * // Update with returning
708
+ * const [updated] = await this.db.update(usersTable)
709
+ * .set({ name: 'Jane' })
710
+ * .where(eq(usersTable.id, 1))
711
+ * .returning();
712
+ * ```
713
+ */
714
+ update<TTable extends Parameters<DatabaseInstanceForType<TDbType>['update']>[0]>(
715
+ table: TTable,
716
+ ): ReturnType<DatabaseInstanceForType<TDbType>['update']> {
717
+ const db = this.getDatabase();
718
+
719
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
720
+ return (db as any).update(table);
721
+ }
722
+
723
+ /**
724
+ * Create a DELETE query
725
+ *
726
+ * @example
727
+ * ```typescript
728
+ * // Delete rows
729
+ * await this.db.delete(usersTable).where(eq(usersTable.id, 1));
730
+ *
731
+ * // Delete with returning
732
+ * const [deleted] = await this.db.delete(usersTable)
733
+ * .where(eq(usersTable.id, 1))
734
+ * .returning();
735
+ * ```
736
+ */
737
+ delete<TTable extends Parameters<DatabaseInstanceForType<TDbType>['delete']>[0]>(
738
+ table: TTable,
739
+ ): ReturnType<DatabaseInstanceForType<TDbType>['delete']> {
740
+ const db = this.getDatabase();
741
+
742
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
743
+ return (db as any).delete(table);
744
+ }
528
745
  }
package/src/types.ts CHANGED
@@ -125,7 +125,7 @@ export interface DrizzleModuleOptions {
125
125
 
126
126
  /**
127
127
  * Whether to run migrations automatically on startup
128
- * Default: false
128
+ * Default: true
129
129
  */
130
130
  autoMigrate?: boolean;
131
131