-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmigration.ts
More file actions
95 lines (83 loc) · 2.99 KB
/
migration.ts
File metadata and controls
95 lines (83 loc) · 2.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* Rely on sqlite PRAGMA user_version to apply migrations
* @module
*/
import { db, sql } from './mod.ts'
const getVersionFromFile = (filename: string) => {
const match = filename.match(/^([0-9]+)-/)
if (!match) {
throw Error(
`Invalid migration filename: "${filename}". It must start with a number like "001-".`,
)
}
return Number(match[1])
}
/**
* Runs database migrations from a specified directory.
* Migrations are applied in order based on the version number in their filename.
* It uses the `user_version` pragma in SQLite to keep track of the current database version.
*
* @param migrationsPath - The path to the directory containing the migration files.
*
* @example
* ```ts
* // migrations/001-initial.ts
* export function run(sql) {
* sql`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);`.run();
* }
*
* // main.ts
* import { runMigrations } from '@01edu/migration';
*
* await runMigrations('./migrations');
* ```
*/
export const runMigrations = async (migrationsPath: string) => {
console.log('🚀 Starting migration process...')
const currentVersion = db.sql`PRAGMA user_version`?.[0]?.user_version || 0
console.log(`🔎 Current database version: ${currentVersion}`)
const pendingMigrations = Array.from(Deno.readDirSync(migrationsPath))
.filter((entry) => entry.isFile && /\.(js|ts)$/.test(entry.name))
.map((entry) => ({
name: entry.name,
version: getVersionFromFile(entry.name),
}))
.sort((a, b) => a.version - b.version)
.filter((migration) => migration.version > currentVersion)
const lastVersion = pendingMigrations.at(-1)?.version || 0
if (pendingMigrations.length === 0 || lastVersion <= currentVersion) {
console.log('✅ Your database is already up-to-date.')
return
}
if (!currentVersion) {
db.exec(`PRAGMA user_version = ${lastVersion}`)
console.log('✅ Clean database, no need to migrate')
return
}
console.log(`⏳ Applying ${pendingMigrations.length} new migration(s)...`)
for (const migration of pendingMigrations) {
try {
console.log(
`▶️ Applying version: ${migration.version}: ${migration.name}`,
)
const moduleUrl = `file://${migrationsPath}/${migration.name}`
// Dynamic import is intentional - migration files are provided by consumers at runtime
// @ts-ignore: JSR cannot analyze runtime-determined imports
const { run } = await import(moduleUrl)
if (typeof run !== 'function') {
throw Error(`Migration file does not export a 'run' function.`)
}
db.exec('BEGIN IMMEDIATE')
await run(sql)
db.exec('COMMIT')
db.exec(`PRAGMA user_version = ${migration.version}`)
console.log(`✅ Successfully applied version ${migration.version}`)
} catch (err) {
console.error(err)
console.error(`❌ Failed to apply migration ${migration.name}:`, err)
db.exec('ROLLBACK')
Deno.exit(1)
}
}
console.log('\n✨ Migration process completed successfully!')
}