-
Notifications
You must be signed in to change notification settings - Fork 94
/
Copy pathmigrate.ts
executable file
·137 lines (123 loc) · 4.21 KB
/
migrate.ts
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import * as fs from 'fs'
import * as path from 'path'
import { Database } from '../Database'
import { IMigrate } from '../interfaces'
import MigrationFile = IMigrate.MigrationFile
import MigrationParams = IMigrate.MigrationParams
import MigrationData = IMigrate.MigrationData
export async function readMigrations (migrationPath?: string) {
const migrationsPath = migrationPath || path.join(process.cwd(), 'migrations')
const location = path.resolve(migrationsPath)
// Get the list of migration files, for example:
// { id: 1, name: 'initial', filename: '001-initial.sql' }
// { id: 2, name: 'feature', filename: '002-feature.sql' }
const migrationFiles = await new Promise<MigrationFile[]>(
(resolve, reject) => {
fs.readdir(location, (err, files) => {
if (err) {
return reject(err)
}
resolve(
files
.map(x => x.match(/^(\d+).(.*?)\.sql$/))
.filter(x => x !== null)
.map(x => ({ id: Number(x[1]), name: x[2], filename: x[0] }))
.sort((a, b) => Math.sign(a.id - b.id))
)
})
}
)
if (!migrationFiles.length) {
throw new Error(`No migration files found in '${location}'.`)
}
// Get the list of migrations, for example:
// { id: 1, name: 'initial', filename: '001-initial.sql', up: ..., down: ... }
// { id: 2, name: 'feature', filename: '002-feature.sql', up: ..., down: ... }
return Promise.all(
migrationFiles.map(
migration =>
new Promise<MigrationData>((resolve, reject) => {
const filename = path.join(location, migration.filename)
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) {
return reject(err)
}
const [up, down] = data.split(/^--\s+?down\b/im)
const migrationData = migration as Partial<MigrationData>
migrationData.up = up.replace(/^-- .*?$/gm, '').trim() // Remove comments
migrationData.down = down ? down.trim() : '' // and trim whitespaces
resolve(migrationData as MigrationData)
})
})
)
)
}
/**
* Migrates database schema to the latest version
*/
export async function migrate (db: Database, config: MigrationParams = {}) {
config.force = config.force || false
config.table = config.table || 'migrations'
const { force, table } = config
const migrations = config.migrations
? config.migrations
: await readMigrations(config.migrationsPath)
// Create a database table for migrations meta data if it doesn't exist
await db.run(`CREATE TABLE IF NOT EXISTS "${table}" (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
up TEXT NOT NULL,
down TEXT NOT NULL
)`)
// Get the list of already applied migrations
let dbMigrations = await db.all(
`SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`
)
// Undo migrations that exist only in the database but not in files,
// also undo the last migration if the `force` option is enabled.
const lastMigration = migrations[migrations.length - 1]
for (const migration of dbMigrations
.slice()
.sort((a, b) => Math.sign(b.id - a.id))) {
if (
!migrations.some(x => x.id === migration.id) ||
(force && migration.id === lastMigration.id)
) {
await db.run('BEGIN')
try {
await db.exec(migration.down)
await db.run(`DELETE FROM "${table}" WHERE id = ?`, migration.id)
await db.run('COMMIT')
dbMigrations = dbMigrations.filter(x => x.id !== migration.id)
} catch (err) {
await db.run('ROLLBACK')
throw err
}
} else {
break
}
}
// Apply pending migrations
const lastMigrationId = dbMigrations.length
? dbMigrations[dbMigrations.length - 1].id
: 0
for (const migration of migrations) {
if (migration.id > lastMigrationId) {
await db.run('BEGIN')
try {
await db.exec(migration.up)
await db.run(
`INSERT INTO "${table}" (id, name, up, down) VALUES (?, ?, ?, ?)`,
migration.id,
migration.name,
migration.up,
migration.down
)
await db.run('COMMIT')
} catch (err) {
await db.run('ROLLBACK')
throw err
}
}
}
}