@onebun/drizzle 0.1.11 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/drizzle",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "description": "Drizzle ORM module for OneBun framework - SQLite and PostgreSQL support",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -47,9 +47,9 @@
47
47
  "dev": "bun run --watch src/index.ts"
48
48
  },
49
49
  "dependencies": {
50
- "@onebun/core": "^0.1.15",
51
- "@onebun/envs": "^0.1.4",
52
- "@onebun/logger": "^0.1.5",
50
+ "@onebun/core": "^0.2.0",
51
+ "@onebun/envs": "^0.2.0",
52
+ "@onebun/logger": "^0.2.0",
53
53
  "drizzle-orm": "^0.44.7",
54
54
  "drizzle-kit": "^0.31.6",
55
55
  "drizzle-arktype": "^0.1.3",
@@ -57,7 +57,7 @@
57
57
  "effect": "^3.13.10"
58
58
  },
59
59
  "devDependencies": {
60
- "bun-types": "^1.2.20",
60
+ "bun-types": "^1.3.8",
61
61
  "testcontainers": "^11.7.1"
62
62
  },
63
63
  "engines": {
@@ -1,3 +1,7 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
1
5
  import { SQL } from 'bun';
2
6
  import { Database } from 'bun:sqlite';
3
7
  import { drizzle as drizzlePostgres } from 'drizzle-orm/bun-sql';
@@ -22,7 +26,11 @@ import type {
22
26
  SQLiteUpdateBuilder,
23
27
  } from 'drizzle-orm/sqlite-core';
24
28
 
25
- import { BaseService, Service } from '@onebun/core';
29
+ import {
30
+ BaseService,
31
+ Service,
32
+ type OnModuleInit,
33
+ } from '@onebun/core';
26
34
  import {
27
35
  Env,
28
36
  EnvLoader,
@@ -211,7 +219,7 @@ interface BufferedLogEntry {
211
219
  * ```
212
220
  */
213
221
  @Service()
214
- export class DrizzleService extends BaseService {
222
+ export class DrizzleService extends BaseService implements OnModuleInit {
215
223
  private db: DatabaseInstance | null = null;
216
224
  private dbType: DatabaseTypeLiteral | null = null;
217
225
  private connectionOptions: DatabaseConnectionOptions | null = null;
@@ -298,10 +306,10 @@ export class DrizzleService extends BaseService {
298
306
  }
299
307
 
300
308
  /**
301
- * Async initialization hook - called by the framework after initializeService()
309
+ * Module initialization hook - called by the framework after initializeService()
302
310
  * This ensures the database is fully ready before client code runs
303
311
  */
304
- override async onAsyncInit(): Promise<void> {
312
+ async onModuleInit(): Promise<void> {
305
313
  // Flush any buffered logs now that logger is available
306
314
  this.flushLogBuffer();
307
315
 
@@ -553,6 +561,92 @@ export class DrizzleService extends BaseService {
553
561
  return this.connectionOptions;
554
562
  }
555
563
 
564
+ /**
565
+ * Read migration journal and compute hashes for each migration file
566
+ * Returns a map of hash -> migration filename
567
+ */
568
+ private readMigrationJournal(migrationsFolder: string): Map<string, string> {
569
+ const hashToFilename = new Map<string, string>();
570
+ const journalPath = path.join(migrationsFolder, 'meta', '_journal.json');
571
+
572
+ if (!fs.existsSync(journalPath)) {
573
+ // No journal file - return empty map
574
+ return hashToFilename;
575
+ }
576
+
577
+ try {
578
+ const journalContent = fs.readFileSync(journalPath, 'utf-8');
579
+ const journal = JSON.parse(journalContent) as {
580
+ entries: Array<{ idx: number; when: number; tag: string; breakpoints: boolean }>;
581
+ };
582
+
583
+ for (const entry of journal.entries) {
584
+ const migrationPath = path.join(migrationsFolder, `${entry.tag}.sql`);
585
+
586
+ if (fs.existsSync(migrationPath)) {
587
+ const sqlContent = fs.readFileSync(migrationPath, 'utf-8');
588
+ const hash = crypto.createHash('sha256').update(sqlContent).digest('hex');
589
+ hashToFilename.set(hash, entry.tag);
590
+ }
591
+ }
592
+ } catch {
593
+ // If journal parsing fails, return empty map
594
+ this.safeLog('warn', 'Failed to read migration journal', { journalPath });
595
+ }
596
+
597
+ return hashToFilename;
598
+ }
599
+
600
+ /**
601
+ * Get set of applied migration hashes from __drizzle_migrations table
602
+ */
603
+ private getAppliedMigrationHashes(): Set<string> {
604
+ const hashes = new Set<string>();
605
+
606
+ try {
607
+ if (this.connectionOptions?.type === DatabaseType.SQLITE && this.sqliteClient) {
608
+ // Check if table exists
609
+ const tableExists = this.sqliteClient.query(`
610
+ SELECT name FROM sqlite_master
611
+ WHERE type='table' AND name='__drizzle_migrations'
612
+ `).all();
613
+
614
+ if (tableExists.length > 0) {
615
+ const migrations = this.sqliteClient.query(`
616
+ SELECT hash FROM __drizzle_migrations
617
+ `).all() as Array<{ hash: string }>;
618
+
619
+ for (const m of migrations) {
620
+ hashes.add(m.hash);
621
+ }
622
+ }
623
+ } else if (this.connectionOptions?.type === DatabaseType.POSTGRESQL && this.postgresClient) {
624
+ // Check if table exists using Bun.SQL template literal syntax
625
+ // Cast through unknown as Bun.SQL types don't fully reflect runtime behavior
626
+ const tableExistsResult = this.postgresClient`
627
+ SELECT EXISTS (
628
+ SELECT FROM information_schema.tables
629
+ WHERE table_name = '__drizzle_migrations'
630
+ ) as exists
631
+ ` as unknown as Array<{ exists: boolean }>;
632
+
633
+ if (tableExistsResult.length > 0 && tableExistsResult[0]?.exists) {
634
+ const migrationsResult = this.postgresClient`
635
+ SELECT hash FROM __drizzle_migrations
636
+ ` as unknown as Array<{ hash: string }>;
637
+
638
+ for (const m of migrationsResult) {
639
+ hashes.add(m.hash);
640
+ }
641
+ }
642
+ }
643
+ } catch {
644
+ // If query fails, return empty set (table might not exist yet)
645
+ }
646
+
647
+ return hashes;
648
+ }
649
+
556
650
  /**
557
651
  * Run migrations
558
652
  *
@@ -560,6 +654,8 @@ export class DrizzleService extends BaseService {
560
654
  * and prevents double application of migrations. This method uses drizzle's built-in
561
655
  * migration system which ensures idempotency.
562
656
  *
657
+ * Logs the names of each migration file that was applied during this run.
658
+ *
563
659
  * @param options - Migration options
564
660
  * @param skipWait - Internal flag to skip waitForInit (used by autoInitialize to avoid deadlock)
565
661
  * @throws Error if database is not initialized
@@ -576,6 +672,12 @@ export class DrizzleService extends BaseService {
576
672
 
577
673
  const migrationsFolder = options?.migrationsFolder ?? './drizzle';
578
674
 
675
+ // Read migration journal to get hash -> filename mapping
676
+ const hashToFilename = this.readMigrationJournal(migrationsFolder);
677
+
678
+ // Get already applied migrations before running
679
+ const appliedBefore = this.getAppliedMigrationHashes();
680
+
579
681
  if (this.connectionOptions.type === DatabaseType.SQLITE) {
580
682
  if (!this.db) {
581
683
  throw new Error('Database not initialized');
@@ -583,7 +685,6 @@ export class DrizzleService extends BaseService {
583
685
  await migrate(this.db as BunSQLiteDatabase<Record<string, SQLiteTable>>, {
584
686
  migrationsFolder,
585
687
  });
586
- this.safeLog('info', 'SQLite migrations applied', { migrationsFolder });
587
688
  } else if (this.connectionOptions.type === DatabaseType.POSTGRESQL) {
588
689
  if (!this.db) {
589
690
  throw new Error('Database not initialized');
@@ -591,8 +692,35 @@ export class DrizzleService extends BaseService {
591
692
  await migratePostgres(this.db as BunSQLDatabase<Record<string, PgTable>>, {
592
693
  migrationsFolder,
593
694
  });
594
- this.safeLog('info', 'PostgreSQL migrations applied', { migrationsFolder });
595
695
  }
696
+
697
+ // Get applied migrations after running
698
+ const appliedAfter = this.getAppliedMigrationHashes();
699
+
700
+ // Find newly applied migrations
701
+ const newlyApplied: string[] = [];
702
+ for (const hash of appliedAfter) {
703
+ if (!appliedBefore.has(hash)) {
704
+ const filename = hashToFilename.get(hash);
705
+ if (filename) {
706
+ newlyApplied.push(filename);
707
+ }
708
+ }
709
+ }
710
+
711
+ // Log each applied migration
712
+ for (const filename of newlyApplied) {
713
+ this.safeLog('info', `Applied migration: ${filename}`);
714
+ }
715
+
716
+ // Log summary
717
+ const dbTypeName = this.connectionOptions.type === DatabaseType.SQLITE ? 'SQLite' : 'PostgreSQL';
718
+
719
+ this.safeLog('info', `${dbTypeName} migrations applied`, {
720
+ migrationsFolder,
721
+ newMigrations: newlyApplied.length,
722
+ appliedFiles: newlyApplied,
723
+ });
596
724
  }
597
725
 
598
726
  /**