@onebun/drizzle 0.1.8 → 0.1.10

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.8",
3
+ "version": "0.1.10",
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.13",
50
+ "@onebun/core": "^0.1.15",
51
51
  "@onebun/envs": "^0.1.4",
52
52
  "@onebun/logger": "^0.1.5",
53
53
  "drizzle-orm": "^0.44.7",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Universal Query Builders
3
+ *
4
+ * These builders provide type-safe database operations that infer
5
+ * the correct database type (SQLite or PostgreSQL) from table schemas.
6
+ */
7
+
8
+ export { UniversalSelectBuilder, UniversalSelectDistinctBuilder } from './select-builder';
9
+ export { UniversalTransactionClient } from './transaction-client';
@@ -0,0 +1,109 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // Drizzle ORM uses complex conditional types that require `any` for proper type inference
3
+
4
+ import type { DatabaseInstance } from '../types';
5
+ import type { PgTable } from 'drizzle-orm/pg-core';
6
+ import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core';
7
+ import type { SQLiteTable, SQLiteColumn } from 'drizzle-orm/sqlite-core';
8
+
9
+ /**
10
+ * SQLite select query result type with proper typing for table columns
11
+ */
12
+ type SQLiteSelectQueryResult<TTable extends SQLiteTable<any>> = SQLiteSelectBase<
13
+ TTable['_']['name'],
14
+ 'sync',
15
+ void,
16
+ TTable['_']['columns'],
17
+ 'single',
18
+ Record<TTable['_']['name'], 'not-null'>,
19
+ false,
20
+ never,
21
+ TTable['$inferSelect'][],
22
+ Record<keyof TTable['_']['columns'], SQLiteColumn>
23
+ >;
24
+
25
+ /**
26
+ * Universal Select Builder that works with any table type
27
+ *
28
+ * This builder allows using DrizzleService without generic type parameter.
29
+ * The result type is determined by the actual database operation at runtime,
30
+ * but the select result array element type is inferred from the table schema.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // DrizzleService without generic
35
+ * const db = new DrizzleService();
36
+ *
37
+ * // Type of result elements is inferred from table schema
38
+ * const users = await db.select().from(usersTable);
39
+ * // users has type: UserRow[]
40
+ * ```
41
+ */
42
+ export class UniversalSelectBuilder<TFields extends Record<string, unknown> | undefined = undefined> {
43
+ constructor(
44
+ private db: DatabaseInstance,
45
+ private fields?: TFields,
46
+ ) {}
47
+
48
+ /**
49
+ * Select from a SQLite table
50
+ * Returns a chainable query builder that resolves to table row type
51
+ */
52
+ from<TTable extends SQLiteTable<any>>(
53
+ table: TTable,
54
+ ): SQLiteSelectQueryResult<TTable>;
55
+
56
+ /**
57
+ * Select from a PostgreSQL table
58
+ * Returns a chainable query builder that resolves to table row type
59
+ */
60
+ from<TTable extends PgTable<any>>(
61
+ table: TTable,
62
+ ): Promise<TTable['$inferSelect'][]> & {
63
+ where: (condition: any) => Promise<TTable['$inferSelect'][]> & { get: () => Promise<TTable['$inferSelect'] | undefined> };
64
+ };
65
+
66
+ /**
67
+ * Implementation - runtime call to appropriate database
68
+ */
69
+ from(table: SQLiteTable<any> | PgTable<any>): any {
70
+ const selectBuilder = this.fields
71
+ ? (this.db as any).select(this.fields)
72
+ : (this.db as any).select();
73
+
74
+ return selectBuilder.from(table);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Universal SelectDistinct Builder
80
+ * Same as UniversalSelectBuilder but for DISTINCT queries
81
+ */
82
+ export class UniversalSelectDistinctBuilder<TFields extends Record<string, unknown> | undefined = undefined> {
83
+ constructor(
84
+ private db: DatabaseInstance,
85
+ private fields?: TFields,
86
+ ) {}
87
+
88
+ /**
89
+ * Select distinct from a SQLite table
90
+ */
91
+ from<TTable extends SQLiteTable<any>>(
92
+ table: TTable,
93
+ ): SQLiteSelectQueryResult<TTable>;
94
+
95
+ /**
96
+ * Select distinct from a PostgreSQL table
97
+ */
98
+ from<TTable extends PgTable<any>>(
99
+ table: TTable,
100
+ ): Promise<TTable['$inferSelect'][]>;
101
+
102
+ from(table: SQLiteTable<any> | PgTable<any>): any {
103
+ const selectBuilder = this.fields
104
+ ? (this.db as any).selectDistinct(this.fields)
105
+ : (this.db as any).selectDistinct();
106
+
107
+ return selectBuilder.from(table);
108
+ }
109
+ }
@@ -0,0 +1,116 @@
1
+ /* eslint-disable
2
+ @typescript-eslint/no-explicit-any,
3
+ @typescript-eslint/explicit-module-boundary-types */
4
+ // Drizzle ORM uses complex conditional types that require `any` for proper type inference
5
+ // Method overloads define return types, so explicit return type on implementation is not needed
6
+
7
+ import type { DatabaseInstance } from '../types';
8
+ import type { BunSQLDatabase } from 'drizzle-orm/bun-sql';
9
+ import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
10
+ import type { PgTable } from 'drizzle-orm/pg-core';
11
+ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
12
+
13
+ import { UniversalSelectBuilder, UniversalSelectDistinctBuilder } from './select-builder';
14
+
15
+ // Type helpers for insert/update/delete return types
16
+ type SQLiteDb = BunSQLiteDatabase<Record<string, SQLiteTable>>;
17
+ type PgDb = BunSQLDatabase<Record<string, PgTable>>;
18
+
19
+ /**
20
+ * Universal Transaction Client
21
+ *
22
+ * Wraps the transaction object to provide methods with overloads
23
+ * that infer the correct type from table schemas.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * await db.transaction(async (tx) => {
28
+ * // tx has the same API as DrizzleService
29
+ * const users = await tx.select().from(usersTable);
30
+ * await tx.insert(usersTable).values({ name: 'John' });
31
+ * });
32
+ * ```
33
+ */
34
+ export class UniversalTransactionClient {
35
+ constructor(private tx: DatabaseInstance) {}
36
+
37
+ /**
38
+ * Create a SELECT query
39
+ */
40
+ select(): UniversalSelectBuilder;
41
+ select<TFields extends Record<string, unknown>>(fields: TFields): UniversalSelectBuilder<TFields>;
42
+ select<TFields extends Record<string, unknown>>(fields?: TFields) {
43
+ return new UniversalSelectBuilder(this.tx, fields);
44
+ }
45
+
46
+ /**
47
+ * Create a SELECT DISTINCT query
48
+ */
49
+ selectDistinct(): UniversalSelectDistinctBuilder;
50
+ selectDistinct<TFields extends Record<string, unknown>>(fields: TFields): UniversalSelectDistinctBuilder<TFields>;
51
+ selectDistinct<TFields extends Record<string, unknown>>(fields?: TFields) {
52
+ return new UniversalSelectDistinctBuilder(this.tx, fields);
53
+ }
54
+
55
+ /**
56
+ * Create an INSERT query for SQLite table
57
+ */
58
+ insert<TTable extends SQLiteTable<any>>(
59
+ table: TTable,
60
+ ): ReturnType<SQLiteDb['insert']>;
61
+
62
+ /**
63
+ * Create an INSERT query for PostgreSQL table
64
+ */
65
+ insert<TTable extends PgTable<any>>(
66
+ table: TTable,
67
+ ): ReturnType<PgDb['insert']>;
68
+
69
+ insert(table: SQLiteTable<any> | PgTable<any>) {
70
+ return (this.tx as any).insert(table);
71
+ }
72
+
73
+ /**
74
+ * Create an UPDATE query for SQLite table
75
+ */
76
+ update<TTable extends SQLiteTable<any>>(
77
+ table: TTable,
78
+ ): ReturnType<SQLiteDb['update']>;
79
+
80
+ /**
81
+ * Create an UPDATE query for PostgreSQL table
82
+ */
83
+ update<TTable extends PgTable<any>>(
84
+ table: TTable,
85
+ ): ReturnType<PgDb['update']>;
86
+
87
+ update(table: SQLiteTable<any> | PgTable<any>) {
88
+ return (this.tx as any).update(table);
89
+ }
90
+
91
+ /**
92
+ * Create a DELETE query for SQLite table
93
+ */
94
+ delete<TTable extends SQLiteTable<any>>(
95
+ table: TTable,
96
+ ): ReturnType<SQLiteDb['delete']>;
97
+
98
+ /**
99
+ * Create a DELETE query for PostgreSQL table
100
+ */
101
+ delete<TTable extends PgTable<any>>(
102
+ table: TTable,
103
+ ): ReturnType<PgDb['delete']>;
104
+
105
+ delete(table: SQLiteTable<any> | PgTable<any>) {
106
+ return (this.tx as any).delete(table);
107
+ }
108
+
109
+ /**
110
+ * Get the raw transaction object for advanced usage
111
+ * Note: Returns union type - use type guards for specific database type
112
+ */
113
+ getRawTransaction(): DatabaseInstance {
114
+ return this.tx;
115
+ }
116
+ }
@@ -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)
@@ -18,10 +18,14 @@ import {
18
18
  EnvParser,
19
19
  } from '@onebun/envs';
20
20
 
21
+ import {
22
+ UniversalSelectBuilder,
23
+ UniversalSelectDistinctBuilder,
24
+ UniversalTransactionClient,
25
+ } from './builders';
21
26
  import {
22
27
  type DatabaseConnectionOptions,
23
28
  type DatabaseInstance,
24
- type DatabaseInstanceForType,
25
29
  DatabaseType,
26
30
  type DatabaseTypeLiteral,
27
31
  type DrizzleModuleOptions,
@@ -29,6 +33,10 @@ import {
29
33
  type PostgreSQLConnectionOptions,
30
34
  } from './types';
31
35
 
36
+ // Type helpers for insert/update/delete return types
37
+ type SQLiteDb = BunSQLiteDatabase<Record<string, SQLiteTable>>;
38
+ type PgDb = BunSQLDatabase<Record<string, PgTable>>;
39
+
32
40
  /**
33
41
  * Default environment variable prefix
34
42
  */
@@ -158,68 +166,140 @@ async function loadFromEnv(prefix: string = DEFAULT_ENV_PREFIX): Promise<Databas
158
166
  };
159
167
  }
160
168
 
169
+ /**
170
+ * Buffered log entry for pre-logger initialization logging
171
+ */
172
+ interface BufferedLogEntry {
173
+ level: 'debug' | 'info' | 'warn' | 'error';
174
+ message: string;
175
+ meta?: object;
176
+ timestamp: number;
177
+ }
178
+
161
179
  /**
162
180
  * Drizzle service for database operations
163
- * Generic type parameter TDbType specifies the database type (SQLITE or POSTGRESQL)
181
+ *
182
+ * The service automatically infers database types from table schemas.
183
+ * No generic parameter is required - just use select(), insert(), update(), delete()
184
+ * with your table schemas and TypeScript will infer the correct types.
164
185
  *
165
186
  * @example
166
187
  * ```typescript
167
- * // For SQLite
168
- * const drizzleService = new DrizzleService<DatabaseType.SQLITE>(...);
169
- * const db = drizzleService.getDatabase(); // Type: BunSQLiteDatabase
170
- *
171
- * // For PostgreSQL
172
- * const drizzleService = new DrizzleService<DatabaseType.POSTGRESQL>(...);
173
- * const db = drizzleService.getDatabase(); // Type: BunSQLDatabase
188
+ * // Define tables with proper types
189
+ * const users = sqliteTable('users', { ... }); // SQLite table
190
+ * const orders = pgTable('orders', { ... }); // PostgreSQL table
191
+ *
192
+ * // Use DrizzleService without generic parameter
193
+ * @Service()
194
+ * class UserService extends BaseService {
195
+ * constructor(private db: DrizzleService) {
196
+ * super();
197
+ * }
198
+ *
199
+ * async findAll() {
200
+ * // TypeScript infers SQLite types from `users` table
201
+ * return this.db.select().from(users);
202
+ * }
203
+ * }
174
204
  * ```
175
205
  */
176
206
  @Service()
177
- export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLiteral> extends BaseService {
207
+ export class DrizzleService extends BaseService {
178
208
  private db: DatabaseInstance | null = null;
179
- private dbType: TDbType | null = null;
209
+ private dbType: DatabaseTypeLiteral | null = null;
180
210
  private connectionOptions: DatabaseConnectionOptions | null = null;
181
211
  private initialized = false;
182
212
  private initPromise: Promise<void> | null = null;
183
213
  private sqliteClient: Database | null = null;
184
214
  private postgresClient: SQL | null = null;
215
+ private logBuffer: BufferedLogEntry[] = [];
216
+ private exitHandlerRegistered = false;
185
217
 
186
218
  constructor() {
187
219
  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();
220
+ // Register exit handler to flush buffered logs on crash
221
+ this.registerExitHandler();
222
+ }
223
+
224
+ /**
225
+ * Register process exit handler to flush buffered logs to console.error on crash
226
+ */
227
+ private registerExitHandler(): void {
228
+ if (this.exitHandlerRegistered) {
229
+ return;
192
230
  }
231
+ this.exitHandlerRegistered = true;
232
+
233
+ const flushToConsole = () => {
234
+ if (this.logBuffer.length > 0) {
235
+ // eslint-disable-next-line no-console
236
+ console.error('[DrizzleService] Buffered logs (app crashed before logger init):');
237
+ for (const entry of this.logBuffer) {
238
+ const timestamp = new Date(entry.timestamp).toISOString();
239
+ // eslint-disable-next-line no-console
240
+ console.error(` [${timestamp}] [${entry.level.toUpperCase()}] ${entry.message}`, entry.meta ?? '');
241
+ }
242
+ }
243
+ };
244
+
245
+ // Register handlers for various exit scenarios
246
+ process.on('exit', flushToConsole);
247
+ process.on('uncaughtException', (err) => {
248
+ flushToConsole();
249
+ // eslint-disable-next-line no-console
250
+ console.error('[DrizzleService] Uncaught exception:', err);
251
+ });
252
+ process.on('unhandledRejection', (reason) => {
253
+ flushToConsole();
254
+ // eslint-disable-next-line no-console
255
+ console.error('[DrizzleService] Unhandled rejection:', reason);
256
+ });
193
257
  }
194
258
 
195
259
  /**
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.
260
+ * Safe logging that buffers logs before logger is available
261
+ * When logger becomes available, buffered logs are flushed
262
+ * If app crashes before logger init, logs are output via console.error
200
263
  */
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;
264
+ private safeLog(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: object): void {
265
+ if (this.logger) {
266
+ this.logger[level](message, meta);
267
+ } else {
268
+ // Buffer the log for later
269
+ this.logBuffer.push({
270
+ level,
271
+ message,
272
+ meta,
273
+ timestamp: Date.now(),
274
+ });
207
275
  }
276
+ }
208
277
 
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
278
+ /**
279
+ * Flush buffered logs to the logger (called when logger becomes available)
280
+ */
281
+ private flushLogBuffer(): void {
282
+ if (!this.logger || this.logBuffer.length === 0) {
283
+ return;
284
+ }
285
+
286
+ this.logger.debug(`Flushing ${this.logBuffer.length} buffered log entries`);
287
+ for (const entry of this.logBuffer) {
288
+ this.logger[entry.level](entry.message, entry.meta);
220
289
  }
290
+ this.logBuffer = [];
291
+ }
292
+
293
+ /**
294
+ * Async initialization hook - called by the framework after initializeService()
295
+ * This ensures the database is fully ready before client code runs
296
+ */
297
+ override async onAsyncInit(): Promise<void> {
298
+ // Flush any buffered logs now that logger is available
299
+ this.flushLogBuffer();
221
300
 
222
- return false;
301
+ // Run auto-initialization
302
+ await this.autoInitialize();
223
303
  }
224
304
 
225
305
  /**
@@ -232,16 +312,30 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
232
312
 
233
313
  // If module options are provided, use them
234
314
  if (moduleOptions?.connection) {
235
- this.logger.debug('Auto-initializing database service from module options', {
315
+ this.safeLog('debug', 'Auto-initializing database service from module options', {
236
316
  type: moduleOptions.connection.type,
237
317
  });
238
318
 
239
- await this.initialize(moduleOptions.connection);
319
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
320
+ await this.initialize(moduleOptions.connection, true);
240
321
 
241
- // Auto-migrate if enabled
242
- if (moduleOptions.autoMigrate) {
322
+ // Auto-migrate is enabled by default (unless explicitly set to false)
323
+ const shouldAutoMigrate = moduleOptions.autoMigrate !== false;
324
+ if (shouldAutoMigrate) {
243
325
  const migrationsFolder = moduleOptions.migrationsFolder ?? './drizzle';
244
- await this.runMigrations({ migrationsFolder });
326
+ this.safeLog('debug', 'Running auto-migrations', { migrationsFolder });
327
+ try {
328
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
329
+ await this.runMigrations({ migrationsFolder }, true);
330
+ this.safeLog('debug', 'Auto-migrations completed successfully');
331
+ } catch (migrationError) {
332
+ this.safeLog('warn', 'Auto-migration failed, database initialized without migrations', {
333
+ error: migrationError instanceof Error ? migrationError.message : String(migrationError),
334
+ });
335
+ // Don't rethrow - allow DB to be used even if migrations fail
336
+ }
337
+ } else {
338
+ this.safeLog('debug', 'Auto-migrations disabled via module options');
245
339
  }
246
340
 
247
341
  this.initialized = true;
@@ -255,7 +349,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
255
349
  // Check process.env directly to ensure we only auto-initialize when explicitly configured
256
350
  const dbUrlFromProcess = process.env[`${envPrefix}_URL`];
257
351
  if (!dbUrlFromProcess || dbUrlFromProcess.trim() === '') {
258
- this.logger.debug('Skipping auto-initialization: no database configuration found in process.env');
352
+ this.safeLog('debug', 'Skipping auto-initialization: no database configuration found in process.env');
259
353
  this.initialized = false;
260
354
 
261
355
  return;
@@ -275,22 +369,24 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
275
369
  options: parsePostgreSQLUrl(envConfig.url),
276
370
  };
277
371
 
278
- this.logger.debug(`Auto-initializing database service with type: ${connectionOptions.type}`, {
372
+ this.safeLog('debug', `Auto-initializing database service with type: ${connectionOptions.type}`, {
279
373
  envPrefix,
280
374
  });
281
375
 
282
- await this.initialize(connectionOptions);
376
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
377
+ await this.initialize(connectionOptions, true);
283
378
 
284
- // Auto-migrate if enabled
379
+ // Auto-migrate is enabled by default (env schema default is true)
285
380
  if (envConfig.autoMigrate) {
286
381
  const migrationsFolder = envConfig.migrationsFolder ?? './drizzle';
287
- await this.runMigrations({ migrationsFolder });
382
+ // Pass skipWait=true to avoid deadlock (we're already inside initPromise)
383
+ await this.runMigrations({ migrationsFolder }, true);
288
384
  }
289
385
 
290
386
  this.initialized = true;
291
387
  } catch (error) {
292
388
  // Don't throw error - just log it and allow manual initialization
293
- this.logger.debug('Failed to auto-initialize database from environment', { error });
389
+ this.safeLog('debug', 'Failed to auto-initialize database from environment', { error });
294
390
  this.initialized = false;
295
391
  }
296
392
  }
@@ -320,23 +416,29 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
320
416
 
321
417
  /**
322
418
  * Initialize database connection
419
+ * @param options - Database connection options
420
+ * @param skipWait - Internal flag to skip waitForInit (used by autoInitialize to avoid deadlock)
323
421
  */
324
- async initialize(options: DatabaseConnectionOptions): Promise<void> {
325
- await this.waitForInit();
422
+ async initialize(options: DatabaseConnectionOptions, skipWait = false): Promise<void> {
423
+ // Skip waitForInit when called from autoInitialize to avoid deadlock
424
+ // (autoInitialize is the function that creates initPromise)
425
+ if (!skipWait) {
426
+ await this.waitForInit();
427
+ }
326
428
 
327
429
  if (this.initialized && this.connectionOptions) {
328
- this.logger.warn('Database already initialized, closing existing connection');
430
+ this.safeLog('warn', 'Database already initialized, closing existing connection');
329
431
  await this.close();
330
432
  }
331
433
 
332
434
  this.connectionOptions = options;
333
- this.dbType = options.type as TDbType;
435
+ this.dbType = options.type;
334
436
 
335
437
  if (options.type === DatabaseType.SQLITE) {
336
438
  const sqliteOptions = options.options;
337
439
  this.sqliteClient = new Database(sqliteOptions.url, sqliteOptions.options);
338
440
  this.db = drizzleSQLite(this.sqliteClient);
339
- this.logger.info('SQLite database initialized', { url: sqliteOptions.url });
441
+ this.safeLog('info', 'SQLite database initialized', { url: sqliteOptions.url });
340
442
  } else if (options.type === DatabaseType.POSTGRESQL) {
341
443
  const pgOptions = options.options;
342
444
 
@@ -352,7 +454,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
352
454
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
455
  this.postgresClient = (this.db as any).$client as SQL | null;
354
456
 
355
- this.logger.info('PostgreSQL database initialized with Bun.SQL', {
457
+ this.safeLog('info', 'PostgreSQL database initialized with Bun.SQL', {
356
458
  host: pgOptions.host,
357
459
  port: pgOptions.port,
358
460
  database: pgOptions.database,
@@ -367,16 +469,20 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
367
469
  }
368
470
 
369
471
  /**
370
- * Get database instance with proper typing based on generic type parameter
371
- * Returns correctly typed database instance based on TDbType
472
+ * Get raw database instance
473
+ *
474
+ * Returns a union type - use isSQLite()/isPostgreSQL() type guards
475
+ * or getSQLiteDatabase()/getPostgreSQLDatabase() for specific types.
476
+ *
477
+ * For most use cases, prefer using select(), insert(), update(), delete()
478
+ * methods which automatically infer types from table schemas.
372
479
  */
373
- getDatabase(): DatabaseInstanceForType<TDbType> {
480
+ getDatabase(): DatabaseInstance {
374
481
  if (!this.db || !this.dbType) {
375
482
  throw new Error('Database not initialized. Call initialize() first.');
376
483
  }
377
484
 
378
- // Type assertion is safe because dbType matches TDbType
379
- return this.db as DatabaseInstanceForType<TDbType>;
485
+ return this.db;
380
486
  }
381
487
 
382
488
  /**
@@ -448,10 +554,14 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
448
554
  * migration system which ensures idempotency.
449
555
  *
450
556
  * @param options - Migration options
557
+ * @param skipWait - Internal flag to skip waitForInit (used by autoInitialize to avoid deadlock)
451
558
  * @throws Error if database is not initialized
452
559
  */
453
- async runMigrations(options?: MigrationOptions): Promise<void> {
454
- await this.waitForInit();
560
+ async runMigrations(options?: MigrationOptions, skipWait = false): Promise<void> {
561
+ // Skip waitForInit when called from autoInitialize to avoid deadlock
562
+ if (!skipWait) {
563
+ await this.waitForInit();
564
+ }
455
565
 
456
566
  if (!this.db || !this.connectionOptions) {
457
567
  throw new Error('Database not initialized. Call initialize() first.');
@@ -466,7 +576,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
466
576
  await migrate(this.db as BunSQLiteDatabase<Record<string, SQLiteTable>>, {
467
577
  migrationsFolder,
468
578
  });
469
- this.logger.info('SQLite migrations applied', { migrationsFolder });
579
+ this.safeLog('info', 'SQLite migrations applied', { migrationsFolder });
470
580
  } else if (this.connectionOptions.type === DatabaseType.POSTGRESQL) {
471
581
  if (!this.db) {
472
582
  throw new Error('Database not initialized');
@@ -474,7 +584,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
474
584
  await migratePostgres(this.db as BunSQLDatabase<Record<string, PgTable>>, {
475
585
  migrationsFolder,
476
586
  });
477
- this.logger.info('PostgreSQL migrations applied', { migrationsFolder });
587
+ this.safeLog('info', 'PostgreSQL migrations applied', { migrationsFolder });
478
588
  }
479
589
  }
480
590
 
@@ -505,14 +615,26 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
505
615
  this.dbType = null;
506
616
  this.connectionOptions = null;
507
617
  this.initialized = false;
508
- this.logger.info('Database connection closed');
618
+ this.safeLog('info', 'Database connection closed');
509
619
  }
510
620
 
511
621
  /**
512
- * Execute a transaction with proper typing based on TDbType
622
+ * Execute a transaction with universal transaction client
623
+ *
624
+ * The transaction client provides the same API as DrizzleService
625
+ * with automatic type inference from table schemas.
626
+ *
627
+ * @example
628
+ * ```typescript
629
+ * await db.transaction(async (tx) => {
630
+ * const users = await tx.select().from(usersTable);
631
+ * await tx.insert(usersTable).values({ name: 'John' });
632
+ * await tx.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.id, 1));
633
+ * });
634
+ * ```
513
635
  */
514
636
  async transaction<R>(
515
- callback: (tx: DatabaseInstanceForType<TDbType>) => Promise<R>,
637
+ callback: (tx: UniversalTransactionClient) => Promise<R>,
516
638
  ): Promise<R> {
517
639
  await this.waitForInit();
518
640
 
@@ -520,43 +642,42 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
520
642
  throw new Error('Database not initialized. Call initialize() first.');
521
643
  }
522
644
 
523
- // Type assertion needed because TypeScript cannot infer methods from conditional types
524
- // Runtime type is correct - this is a TypeScript limitation
525
645
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
526
- return await (this.db as any).transaction(callback);
646
+ return await (this.db as any).transaction(async (rawTx: DatabaseInstance) => {
647
+ const wrappedTx = new UniversalTransactionClient(rawTx);
648
+
649
+ return await callback(wrappedTx);
650
+ });
527
651
  }
528
652
 
529
653
  // ============================================
530
- // Direct database operation methods (proxies)
654
+ // Direct database operation methods
655
+ // These methods use universal builders that infer
656
+ // database type from table schemas
531
657
  // ============================================
532
658
 
533
659
  /**
534
660
  * Create a SELECT query
535
661
  *
662
+ * Returns a builder with from() method that infers the correct
663
+ * database type from the table schema.
664
+ *
536
665
  * @example
537
666
  * ```typescript
538
- * // Select all columns
667
+ * // Select all columns - type inferred from table
539
668
  * const users = await this.db.select().from(usersTable);
540
669
  *
541
670
  * // Select specific columns
542
671
  * const names = await this.db.select({ name: usersTable.name }).from(usersTable);
543
672
  * ```
544
673
  */
545
- select(): ReturnType<DatabaseInstanceForType<TDbType>['select']>;
546
- select<TSelection extends Record<string, unknown>>(
547
- fields: TSelection,
548
- ): ReturnType<DatabaseInstanceForType<TDbType>['select']>;
549
- select<TSelection extends Record<string, unknown>>(
550
- fields?: TSelection,
551
- ): ReturnType<DatabaseInstanceForType<TDbType>['select']> {
674
+ select(): UniversalSelectBuilder;
675
+ select<TFields extends Record<string, unknown>>(fields: TFields): UniversalSelectBuilder<TFields>;
676
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
677
+ select<TFields extends Record<string, unknown>>(fields?: TFields) {
552
678
  const db = this.getDatabase();
553
- if (fields) {
554
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
555
- return (db as any).select(fields);
556
- }
557
679
 
558
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
559
- return (db as any).select();
680
+ return new UniversalSelectBuilder(db, fields);
560
681
  }
561
682
 
562
683
  /**
@@ -568,25 +689,17 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
568
689
  * const uniqueNames = await this.db.selectDistinct({ name: usersTable.name }).from(usersTable);
569
690
  * ```
570
691
  */
571
- selectDistinct(): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']>;
572
- selectDistinct<TSelection extends Record<string, unknown>>(
573
- fields: TSelection,
574
- ): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']>;
575
- selectDistinct<TSelection extends Record<string, unknown>>(
576
- fields?: TSelection,
577
- ): ReturnType<DatabaseInstanceForType<TDbType>['selectDistinct']> {
692
+ selectDistinct(): UniversalSelectDistinctBuilder;
693
+ selectDistinct<TFields extends Record<string, unknown>>(fields: TFields): UniversalSelectDistinctBuilder<TFields>;
694
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
695
+ selectDistinct<TFields extends Record<string, unknown>>(fields?: TFields) {
578
696
  const db = this.getDatabase();
579
- if (fields) {
580
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
581
- return (db as any).selectDistinct(fields);
582
- }
583
697
 
584
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
585
- return (db as any).selectDistinct();
698
+ return new UniversalSelectDistinctBuilder(db, fields);
586
699
  }
587
700
 
588
701
  /**
589
- * Create an INSERT query
702
+ * Create an INSERT query for SQLite table
590
703
  *
591
704
  * @example
592
705
  * ```typescript
@@ -599,9 +712,15 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
599
712
  * .returning();
600
713
  * ```
601
714
  */
602
- insert<TTable extends Parameters<DatabaseInstanceForType<TDbType>['insert']>[0]>(
603
- table: TTable,
604
- ): ReturnType<DatabaseInstanceForType<TDbType>['insert']> {
715
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
+ insert<TTable extends SQLiteTable<any>>(table: TTable): ReturnType<SQLiteDb['insert']>;
717
+ /**
718
+ * Create an INSERT query for PostgreSQL table
719
+ */
720
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
721
+ insert<TTable extends PgTable<any>>(table: TTable): ReturnType<PgDb['insert']>;
722
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
723
+ insert(table: SQLiteTable<any> | PgTable<any>) {
605
724
  const db = this.getDatabase();
606
725
 
607
726
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -609,7 +728,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
609
728
  }
610
729
 
611
730
  /**
612
- * Create an UPDATE query
731
+ * Create an UPDATE query for SQLite table
613
732
  *
614
733
  * @example
615
734
  * ```typescript
@@ -625,9 +744,15 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
625
744
  * .returning();
626
745
  * ```
627
746
  */
628
- update<TTable extends Parameters<DatabaseInstanceForType<TDbType>['update']>[0]>(
629
- table: TTable,
630
- ): ReturnType<DatabaseInstanceForType<TDbType>['update']> {
747
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
748
+ update<TTable extends SQLiteTable<any>>(table: TTable): ReturnType<SQLiteDb['update']>;
749
+ /**
750
+ * Create an UPDATE query for PostgreSQL table
751
+ */
752
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
753
+ update<TTable extends PgTable<any>>(table: TTable): ReturnType<PgDb['update']>;
754
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
755
+ update(table: SQLiteTable<any> | PgTable<any>) {
631
756
  const db = this.getDatabase();
632
757
 
633
758
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -635,7 +760,7 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
635
760
  }
636
761
 
637
762
  /**
638
- * Create a DELETE query
763
+ * Create a DELETE query for SQLite table
639
764
  *
640
765
  * @example
641
766
  * ```typescript
@@ -648,9 +773,15 @@ export class DrizzleService<TDbType extends DatabaseTypeLiteral = DatabaseTypeLi
648
773
  * .returning();
649
774
  * ```
650
775
  */
651
- delete<TTable extends Parameters<DatabaseInstanceForType<TDbType>['delete']>[0]>(
652
- table: TTable,
653
- ): ReturnType<DatabaseInstanceForType<TDbType>['delete']> {
776
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
777
+ delete<TTable extends SQLiteTable<any>>(table: TTable): ReturnType<SQLiteDb['delete']>;
778
+ /**
779
+ * Create a DELETE query for PostgreSQL table
780
+ */
781
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
782
+ delete<TTable extends PgTable<any>>(table: TTable): ReturnType<PgDb['delete']>;
783
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
784
+ delete(table: SQLiteTable<any> | PgTable<any>) {
654
785
  const db = this.getDatabase();
655
786
 
656
787
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -24,7 +24,6 @@
24
24
  */
25
25
  import type { DrizzleService } from './drizzle.service';
26
26
  import type { BaseRepository } from './repository';
27
- import type { InferDbTypeFromTable } from './types';
28
27
  import type { PgTable } from 'drizzle-orm/pg-core';
29
28
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
30
29
 
@@ -32,8 +31,6 @@ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
32
31
  const ENTITY_METADATA = new Map<Function, PgTable<any> | SQLiteTable<any>>();
33
32
 
34
33
  export function Entity<TTable extends PgTable<any> | SQLiteTable<any>>(table: TTable) {
35
- type TDbType = InferDbTypeFromTable<TTable>;
36
-
37
34
  return <T extends new (...args: any[]) => BaseRepository<TTable>>(target: T) => {
38
35
  // Store table schema in metadata
39
36
  ENTITY_METADATA.set(target, table);
@@ -42,8 +39,8 @@ export function Entity<TTable extends PgTable<any> | SQLiteTable<any>>(table: TT
42
39
  // The wrapped class only requires DrizzleService as constructor argument
43
40
  class WrappedRepository extends target {
44
41
  constructor(...args: any[]) {
45
- // First argument should be DrizzleService with correct type (inferred from table)
46
- const drizzleService = args[0] as DrizzleService<TDbType>;
42
+ // First argument should be DrizzleService (no generic parameter needed)
43
+ const drizzleService = args[0] as DrizzleService;
47
44
  // Pass drizzleService and table to parent constructor
48
45
  super(drizzleService, table);
49
46
  }
@@ -57,7 +54,7 @@ export function Entity<TTable extends PgTable<any> | SQLiteTable<any>>(table: TT
57
54
  });
58
55
 
59
56
  // Return wrapped class - TypeScript will infer the correct constructor signature
60
- return WrappedRepository as any as new (drizzleService: DrizzleService<TDbType>) => InstanceType<T>;
57
+ return WrappedRepository as any as new (drizzleService: DrizzleService) => InstanceType<T>;
61
58
  };
62
59
  }
63
60
 
package/src/index.ts CHANGED
@@ -25,6 +25,13 @@ export { DatabaseType } from './types';
25
25
  export { DrizzleModule } from './drizzle.module';
26
26
  export { DrizzleService } from './drizzle.service';
27
27
 
28
+ // Universal builders for type inference
29
+ export {
30
+ UniversalSelectBuilder,
31
+ UniversalSelectDistinctBuilder,
32
+ UniversalTransactionClient,
33
+ } from './builders';
34
+
28
35
  // Repository
29
36
  export { BaseRepository, type QueryBuilder } from './repository';
30
37
 
package/src/repository.ts CHANGED
@@ -4,19 +4,18 @@
4
4
 
5
5
  import { eq, type SQL } from 'drizzle-orm';
6
6
 
7
- import type { IRepository } from './types';
8
- import type { DatabaseTypeLiteral, DatabaseInstanceForType } from './types';
7
+ import type { IRepository, DatabaseInstance } from './types';
9
8
  import type { PgTable } from 'drizzle-orm/pg-core';
10
9
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
11
10
 
12
11
 
12
+ import { UniversalTransactionClient } from './builders';
13
13
  import { DrizzleService } from './drizzle.service';
14
14
  import {
15
15
  getPrimaryKeyColumn,
16
16
  type SelectType,
17
17
  type InsertType,
18
18
  } from './schema-utils';
19
- import { DatabaseType } from './types';
20
19
 
21
20
  /**
22
21
  * Query builder interface for type-safe database operations
@@ -90,55 +89,13 @@ export interface QueryBuilder {
90
89
  * }
91
90
  * ```
92
91
  */
93
- /**
94
- * Infer database type from table schema
95
- */
96
- type InferDbTypeFromTable<TTable> =
97
- TTable extends SQLiteTable<any>
98
- ? DatabaseType.SQLITE
99
- : TTable extends PgTable<any>
100
- ? DatabaseType.POSTGRESQL
101
- : never;
102
-
103
- /**
104
- * Base repository class for Drizzle ORM
105
- *
106
- * Simplified version with single generic parameter - database type is automatically inferred from table schema
107
- *
108
- * @example
109
- * ```typescript
110
- * import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
111
- * import { BaseRepository } from '@onebun/drizzle';
112
- *
113
- * const users = sqliteTable('users', {
114
- * id: integer('id').primaryKey({ autoIncrement: true }),
115
- * name: text('name').notNull(),
116
- * });
117
- *
118
- * export class UserRepository extends BaseRepository<typeof users> {
119
- * constructor(drizzleService: DrizzleService) {
120
- * super(drizzleService, users);
121
- * }
122
- * }
123
- * ```
124
- *
125
- * Advanced version with explicit database type (for edge cases):
126
- * ```typescript
127
- * export class UserRepository extends BaseRepository<DatabaseType.SQLITE, typeof users> {
128
- * constructor(drizzleService: DrizzleService<DatabaseType.SQLITE>) {
129
- * super(drizzleService, users);
130
- * }
131
- * }
132
- * ```
133
- */
134
92
  export class BaseRepository<
135
93
  TTable extends PgTable<any> | SQLiteTable<any>,
136
- TDbType extends DatabaseTypeLiteral = InferDbTypeFromTable<TTable>
137
94
  > implements IRepository<SelectType<TTable>> {
138
95
  /**
139
- * Database instance with proper typing based on TDbType
96
+ * Database instance
140
97
  */
141
- protected readonly db: DatabaseInstanceForType<TDbType>;
98
+ protected readonly db: DatabaseInstance;
142
99
 
143
100
  /**
144
101
  * Table schema instance
@@ -146,20 +103,16 @@ export class BaseRepository<
146
103
  protected readonly table: TTable;
147
104
 
148
105
  /**
149
- * DrizzleService instance (type inferred from table)
106
+ * DrizzleService instance
150
107
  */
151
- protected readonly drizzleService: DrizzleService<TDbType>;
108
+ protected readonly drizzleService: DrizzleService;
152
109
 
153
110
  constructor(
154
- drizzleService: DrizzleService<DatabaseTypeLiteral>,
111
+ drizzleService: DrizzleService,
155
112
  table: TTable,
156
113
  ) {
157
114
  this.table = table;
158
- // Store drizzleService - type is inferred from table schema
159
- // Type assertion is safe because TDbType is inferred from TTable
160
- this.drizzleService = drizzleService as DrizzleService<TDbType>;
161
- // getDatabase() returns correctly typed instance based on TDbType
162
- // TDbType is automatically inferred from TTable
115
+ this.drizzleService = drizzleService;
163
116
  this.db = this.drizzleService.getDatabase();
164
117
  }
165
118
 
@@ -308,9 +261,17 @@ export class BaseRepository<
308
261
 
309
262
  /**
310
263
  * Execute a transaction
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * await userRepository.transaction(async (tx) => {
268
+ * const users = await tx.select().from(usersTable);
269
+ * await tx.insert(usersTable).values({ name: 'John' });
270
+ * });
271
+ * ```
311
272
  */
312
273
  async transaction<R>(
313
- callback: (tx: DatabaseInstanceForType<TDbType>) => Promise<R>,
274
+ callback: (tx: UniversalTransactionClient) => Promise<R>,
314
275
  ): Promise<R> {
315
276
  return await this.drizzleService.transaction(callback);
316
277
  }
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