@remix-run/data-table 0.3.0 → 0.5.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/README.md +217 -78
- package/dist/cli.d.ts +76 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +78 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/lib/column.d.ts +1 -1
- package/dist/lib/column.d.ts.map +1 -1
- package/dist/lib/database/execution-context.d.ts +5 -5
- package/dist/lib/database/execution-context.d.ts.map +1 -1
- package/dist/lib/database/execution-context.js +1 -0
- package/dist/lib/database/helpers.js +6 -6
- package/dist/lib/database/query-execution.d.ts.map +1 -1
- package/dist/lib/database/query-execution.js +17 -17
- package/dist/lib/database/relations.js +5 -5
- package/dist/lib/database/write-lifecycle.d.ts +2 -2
- package/dist/lib/database/write-lifecycle.d.ts.map +1 -1
- package/dist/lib/database/write-lifecycle.js +5 -5
- package/dist/lib/database.d.ts +74 -59
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +176 -83
- package/dist/lib/{adapter.d.ts → driver.d.ts} +46 -48
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/errors.d.ts +2 -2
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +4 -4
- package/dist/lib/migrations/journal-store.d.ts +6 -6
- package/dist/lib/migrations/journal-store.d.ts.map +1 -1
- package/dist/lib/migrations/journal-store.js +20 -11
- package/dist/lib/migrations/runner.d.ts +12 -19
- package/dist/lib/migrations/runner.d.ts.map +1 -1
- package/dist/lib/migrations/runner.js +152 -130
- package/dist/lib/migrations-node.d.ts +19 -1
- package/dist/lib/migrations-node.d.ts.map +1 -1
- package/dist/lib/migrations-node.js +22 -1
- package/dist/lib/migrations.d.ts +55 -20
- package/dist/lib/migrations.d.ts.map +1 -1
- package/dist/lib/operators.d.ts +8 -7
- package/dist/lib/operators.d.ts.map +1 -1
- package/dist/lib/operators.js +15 -11
- package/dist/lib/query.d.ts +1 -1
- package/dist/lib/query.d.ts.map +1 -1
- package/dist/lib/query.js +4 -4
- package/dist/lib/sql-helpers.d.ts +1 -1
- package/dist/lib/sql-helpers.d.ts.map +1 -1
- package/dist/lib/sql.d.ts +1 -1
- package/dist/lib/sql.js +1 -1
- package/dist/lib/table.d.ts +1 -1
- package/dist/lib/table.d.ts.map +1 -1
- package/dist/lib/table.js +5 -5
- package/dist/migrations/node.d.ts +1 -1
- package/dist/migrations/node.d.ts.map +1 -1
- package/dist/migrations/node.js +1 -1
- package/dist/migrations.d.ts +1 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -3
- package/dist/operators.js +1 -1
- package/dist/sql-helpers.js +1 -1
- package/package.json +13 -9
- package/src/cli.ts +179 -0
- package/src/index.ts +19 -20
- package/src/lib/column.ts +1 -1
- package/src/lib/database/execution-context.ts +10 -6
- package/src/lib/database/helpers.ts +1 -1
- package/src/lib/database/query-execution.ts +15 -11
- package/src/lib/database/write-lifecycle.ts +4 -4
- package/src/lib/database.ts +223 -96
- package/src/lib/{adapter.ts → driver.ts} +48 -48
- package/src/lib/errors.ts +4 -4
- package/src/lib/migrations/journal-store.ts +21 -11
- package/src/lib/migrations/runner.ts +201 -148
- package/src/lib/migrations-node.ts +23 -1
- package/src/lib/migrations.ts +58 -19
- package/src/lib/operators.ts +24 -11
- package/src/lib/query.ts +1 -1
- package/src/lib/sql-helpers.ts +1 -1
- package/src/lib/sql.ts +1 -1
- package/src/lib/table.ts +1 -1
- package/src/migrations/node.ts +1 -1
- package/src/migrations.ts +3 -4
- package/dist/lib/adapter.d.ts.map +0 -1
- /package/dist/lib/{adapter.js → driver.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DatabaseDriver, TransactionToken } from '../driver.ts'
|
|
2
2
|
import { rawSql } from '../sql.ts'
|
|
3
3
|
import type { MigrationDescriptor, MigrationJournalRow } from '../migrations.ts'
|
|
4
4
|
|
|
@@ -18,10 +18,10 @@ function bytesToHex(bytes: Uint8Array): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function ensureMigrationJournal(
|
|
21
|
-
|
|
21
|
+
driver: DatabaseDriver,
|
|
22
22
|
tableName: string,
|
|
23
23
|
): Promise<void> {
|
|
24
|
-
await
|
|
24
|
+
await driver.executeScript(
|
|
25
25
|
'create table if not exists ' +
|
|
26
26
|
tableName +
|
|
27
27
|
' (' +
|
|
@@ -35,11 +35,11 @@ export async function ensureMigrationJournal(
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export async function hasMigrationJournal(
|
|
38
|
-
|
|
38
|
+
driver: DatabaseDriver,
|
|
39
39
|
tableName: string,
|
|
40
40
|
): Promise<boolean> {
|
|
41
41
|
try {
|
|
42
|
-
await
|
|
42
|
+
await driver.execute({
|
|
43
43
|
operation: {
|
|
44
44
|
kind: 'raw',
|
|
45
45
|
sql: rawSql('select 1 from ' + tableName + ' limit 1'),
|
|
@@ -48,15 +48,25 @@ export async function hasMigrationJournal(
|
|
|
48
48
|
|
|
49
49
|
return true
|
|
50
50
|
} catch {
|
|
51
|
+
// The probe fails both when the journal table is missing and when the
|
|
52
|
+
// database itself is unreachable. Only the former means "no journal yet";
|
|
53
|
+
// rethrow connection/auth failures via a table-independent query.
|
|
54
|
+
await driver.execute({
|
|
55
|
+
operation: {
|
|
56
|
+
kind: 'raw',
|
|
57
|
+
sql: rawSql('select 1'),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
51
61
|
return false
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
export async function loadJournalRows(
|
|
56
|
-
|
|
66
|
+
driver: DatabaseDriver,
|
|
57
67
|
tableName: string,
|
|
58
68
|
): Promise<MigrationJournalRow[]> {
|
|
59
|
-
let result = await
|
|
69
|
+
let result = await driver.execute({
|
|
60
70
|
operation: {
|
|
61
71
|
kind: 'raw',
|
|
62
72
|
sql: rawSql(
|
|
@@ -77,7 +87,7 @@ export async function loadJournalRows(
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export async function insertJournalRow(
|
|
80
|
-
|
|
90
|
+
driver: DatabaseDriver,
|
|
81
91
|
tableName: string,
|
|
82
92
|
row: {
|
|
83
93
|
id: string
|
|
@@ -87,7 +97,7 @@ export async function insertJournalRow(
|
|
|
87
97
|
},
|
|
88
98
|
transaction?: TransactionToken,
|
|
89
99
|
): Promise<void> {
|
|
90
|
-
await
|
|
100
|
+
await driver.execute({
|
|
91
101
|
operation: {
|
|
92
102
|
kind: 'raw',
|
|
93
103
|
sql: rawSql('insert into ' + tableName + ' (id, name, checksum, batch) values (?, ?, ?, ?)', [
|
|
@@ -102,12 +112,12 @@ export async function insertJournalRow(
|
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
export async function deleteJournalRow(
|
|
105
|
-
|
|
115
|
+
driver: DatabaseDriver,
|
|
106
116
|
tableName: string,
|
|
107
117
|
id: string,
|
|
108
118
|
transaction?: TransactionToken,
|
|
109
119
|
): Promise<void> {
|
|
110
|
-
await
|
|
120
|
+
await driver.execute({
|
|
111
121
|
operation: {
|
|
112
122
|
kind: 'raw',
|
|
113
123
|
sql: rawSql('delete from ' + tableName + ' where id = ?', [id]),
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DatabaseDriver, TransactionToken } from '../driver.ts'
|
|
2
2
|
import type {
|
|
3
|
-
|
|
3
|
+
MigrationOperationOptions,
|
|
4
4
|
MigrateResult,
|
|
5
5
|
MigrationDescriptor,
|
|
6
6
|
MigrationDirection,
|
|
7
7
|
MigrationJournalRow,
|
|
8
8
|
MigrationRegistry,
|
|
9
|
-
MigrationRunner,
|
|
10
|
-
MigrationRunnerOptions,
|
|
11
9
|
MigrationStatus,
|
|
12
10
|
MigrationStatusEntry,
|
|
13
11
|
MigrationTransactionMode,
|
|
14
12
|
} from '../migrations.ts'
|
|
15
13
|
|
|
14
|
+
interface MigrationRunnerOptions {
|
|
15
|
+
journalTable?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MigrationRunner {
|
|
19
|
+
up(options?: MigrationOperationOptions): Promise<MigrateResult>
|
|
20
|
+
down(options?: MigrationOperationOptions): Promise<MigrateResult>
|
|
21
|
+
status(): Promise<MigrationStatusEntry[]>
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
import { parseTransactionDirective } from './directive.ts'
|
|
17
25
|
import {
|
|
18
26
|
deleteJournalRow,
|
|
@@ -25,12 +33,14 @@ import {
|
|
|
25
33
|
} from './journal-store.ts'
|
|
26
34
|
import { resolveMigrations } from './registry.ts'
|
|
27
35
|
|
|
36
|
+
type MigrationRunnerContext = DatabaseDriver
|
|
37
|
+
|
|
28
38
|
type RunMigrationsInput = {
|
|
29
|
-
|
|
39
|
+
driver: MigrationRunnerContext
|
|
30
40
|
migrations: MigrationDescriptor[]
|
|
31
41
|
journalTable: string
|
|
32
42
|
direction: MigrationDirection
|
|
33
|
-
options:
|
|
43
|
+
options: MigrationOperationOptions
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
function assertStepOption(step: number | undefined): void {
|
|
@@ -43,27 +53,46 @@ function assertStepOption(step: number | undefined): void {
|
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
function
|
|
56
|
+
function assertMigrationOperationOptions(options: MigrationOperationOptions): void {
|
|
47
57
|
if (options.to !== undefined && options.step !== undefined) {
|
|
48
58
|
throw new Error('Cannot combine "to" and "step" migration options in the same run')
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
function resolveTargetOption(
|
|
63
|
+
migrations: MigrationDescriptor[],
|
|
64
|
+
to: string | undefined,
|
|
65
|
+
): string | undefined {
|
|
66
|
+
if (to === undefined) {
|
|
67
|
+
return undefined
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
|
|
70
|
+
// Accept either a bare migration id or the full `id_name` directory form and
|
|
71
|
+
// normalize to the bare id so range filtering compares ids consistently.
|
|
72
|
+
let matches = migrations.filter(
|
|
73
|
+
(migration) => migration.id === to || migration.id + '_' + migration.name === to,
|
|
74
|
+
)
|
|
58
75
|
|
|
59
|
-
if (
|
|
76
|
+
if (matches.length === 0) {
|
|
60
77
|
throw new Error('Unknown migration target: ' + to)
|
|
61
78
|
}
|
|
79
|
+
|
|
80
|
+
if (matches.length > 1) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'Ambiguous migration target "' +
|
|
83
|
+
to +
|
|
84
|
+
'". Matches: ' +
|
|
85
|
+
matches.map((migration) => migration.id + '_' + migration.name).join(', '),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return matches[0].id
|
|
62
90
|
}
|
|
63
91
|
|
|
64
|
-
async function
|
|
92
|
+
async function assertMigrationIntegrity(
|
|
65
93
|
migrations: MigrationDescriptor[],
|
|
66
94
|
journal: MigrationJournalRow[],
|
|
95
|
+
direction: MigrationDirection,
|
|
67
96
|
): Promise<void> {
|
|
68
97
|
let migrationMap = new Map(migrations.map((migration) => [migration.id, migration]))
|
|
69
98
|
|
|
@@ -71,7 +100,15 @@ async function assertNoMigrationDrift(
|
|
|
71
100
|
let migration = migrationMap.get(row.id)
|
|
72
101
|
|
|
73
102
|
if (!migration) {
|
|
74
|
-
|
|
103
|
+
// Rolling back must stay possible when journal rows have no matching
|
|
104
|
+
// migration files, so only forward runs hard-error on orphaned entries.
|
|
105
|
+
if (direction === 'down') {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(
|
|
110
|
+
'Applied migration "' + row.id + '_' + row.name + '" is missing from current migrations',
|
|
111
|
+
)
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
let expected = await computeChecksum(migration)
|
|
@@ -105,193 +142,193 @@ function resolveTransactionMode(migration: MigrationDescriptor): MigrationTransa
|
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
async function runMigrations(input: RunMigrationsInput): Promise<MigrateResult> {
|
|
108
|
-
|
|
145
|
+
if (input.driver.capabilities.migrationLock && !input.driver.withMigrationLock) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Database driver reports migration lock support but does not implement withMigrationLock()',
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (input.driver.withMigrationLock) {
|
|
152
|
+
return input.driver.withMigrationLock(input.journalTable, (driver) =>
|
|
153
|
+
runMigrationsUnlocked({ ...input, driver }),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return runMigrationsUnlocked(input)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runMigrationsUnlocked(input: RunMigrationsInput): Promise<MigrateResult> {
|
|
161
|
+
let driver = input.driver
|
|
109
162
|
let migrations = input.migrations
|
|
110
163
|
let journalTable = input.journalTable
|
|
111
164
|
let dryRun = Boolean(input.options.dryRun)
|
|
112
|
-
let target = input.options.to
|
|
113
165
|
let step = input.options.step
|
|
114
166
|
|
|
115
|
-
|
|
167
|
+
assertMigrationOperationOptions(input.options)
|
|
116
168
|
assertStepOption(step)
|
|
117
|
-
|
|
169
|
+
|
|
170
|
+
let target = resolveTargetOption(migrations, input.options.to)
|
|
118
171
|
|
|
119
172
|
let sql: string[] = []
|
|
120
173
|
|
|
121
|
-
|
|
174
|
+
let journal: MigrationJournalRow[] = []
|
|
122
175
|
|
|
123
|
-
|
|
124
|
-
let
|
|
176
|
+
if (dryRun) {
|
|
177
|
+
let canReadJournal = await hasMigrationJournal(driver, journalTable)
|
|
125
178
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
179
|
+
if (canReadJournal) {
|
|
180
|
+
journal = await loadJournalRows(driver, journalTable)
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
await ensureMigrationJournal(driver, journalTable)
|
|
184
|
+
journal = await loadJournalRows(driver, journalTable)
|
|
185
|
+
}
|
|
128
186
|
|
|
129
|
-
|
|
130
|
-
|
|
187
|
+
let appliedMap = new Map(journal.map((row) => [row.id, row]))
|
|
188
|
+
await assertMigrationIntegrity(migrations, journal, input.direction)
|
|
189
|
+
let toRun: MigrationDescriptor[] = []
|
|
190
|
+
|
|
191
|
+
if (input.direction === 'up') {
|
|
192
|
+
for (let migration of migrations) {
|
|
193
|
+
if (!appliedMap.has(migration.id)) {
|
|
194
|
+
toRun.push(migration)
|
|
131
195
|
}
|
|
132
|
-
} else {
|
|
133
|
-
await ensureMigrationJournal(adapter, journalTable)
|
|
134
|
-
journal = await loadJournalRows(adapter, journalTable)
|
|
135
196
|
}
|
|
136
197
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
if (target) {
|
|
199
|
+
toRun = toRun.filter((migration) => migration.id <= target)
|
|
200
|
+
}
|
|
140
201
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
202
|
+
if (step !== undefined) {
|
|
203
|
+
toRun = toRun.slice(0, step)
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
toRun = migrations.filter((migration) => appliedMap.has(migration.id)).reverse()
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
208
|
+
if (target) {
|
|
209
|
+
toRun = toRun.filter((migration) => migration.id >= target)
|
|
210
|
+
}
|
|
151
211
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} else {
|
|
156
|
-
toRun = migrations.filter((migration) => appliedMap.has(migration.id)).reverse()
|
|
212
|
+
if (step !== undefined) {
|
|
213
|
+
toRun = toRun.slice(0, step)
|
|
214
|
+
}
|
|
157
215
|
|
|
158
|
-
|
|
159
|
-
|
|
216
|
+
for (let migration of toRun) {
|
|
217
|
+
if (migration.down === undefined) {
|
|
218
|
+
throw new Error('Migration "' + migration.id + '" has no down script')
|
|
160
219
|
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
161
222
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
223
|
+
let applied: MigrationStatusEntry[] = []
|
|
224
|
+
let reverted: MigrationStatusEntry[] = []
|
|
225
|
+
let batch = getBatch(journal)
|
|
165
226
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
227
|
+
for (let migration of toRun) {
|
|
228
|
+
let script = (input.direction === 'up' ? migration.up : migration.down) as string
|
|
229
|
+
let mode = resolveTransactionMode(migration)
|
|
230
|
+
|
|
231
|
+
if (mode === 'required' && !driver.capabilities.transactionalDdl) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'Migration "' +
|
|
234
|
+
migration.id +
|
|
235
|
+
'" requires transactional DDL, but the database does not support it',
|
|
236
|
+
)
|
|
171
237
|
}
|
|
172
238
|
|
|
173
|
-
let
|
|
174
|
-
let
|
|
175
|
-
let batch = getBatch(journal)
|
|
239
|
+
let shouldUseTransaction = !dryRun && mode !== 'none' && driver.capabilities.transactionalDdl
|
|
240
|
+
let token: TransactionToken | undefined
|
|
176
241
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (mode === 'required' && !adapter.capabilities.transactionalDdl) {
|
|
182
|
-
throw new Error(
|
|
183
|
-
'Migration "' +
|
|
184
|
-
migration.id +
|
|
185
|
-
'" requires transactional DDL, but adapter does not support it',
|
|
186
|
-
)
|
|
187
|
-
}
|
|
242
|
+
if (shouldUseTransaction) {
|
|
243
|
+
token = await driver.beginTransaction()
|
|
244
|
+
}
|
|
188
245
|
|
|
189
|
-
|
|
190
|
-
let token: TransactionToken | undefined
|
|
246
|
+
sql.push(script)
|
|
191
247
|
|
|
192
|
-
|
|
193
|
-
|
|
248
|
+
try {
|
|
249
|
+
if (!dryRun) {
|
|
250
|
+
if (script.trim().length > 0) {
|
|
251
|
+
await driver.executeScript(script, token)
|
|
252
|
+
}
|
|
194
253
|
}
|
|
195
254
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
255
|
+
if (input.direction === 'up') {
|
|
199
256
|
if (!dryRun) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
257
|
+
await insertJournalRow(
|
|
258
|
+
driver,
|
|
259
|
+
journalTable,
|
|
260
|
+
{
|
|
261
|
+
id: migration.id,
|
|
262
|
+
name: migration.name,
|
|
263
|
+
checksum: await computeChecksum(migration),
|
|
264
|
+
batch,
|
|
265
|
+
},
|
|
266
|
+
token,
|
|
267
|
+
)
|
|
203
268
|
}
|
|
204
269
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
checksum: await computeChecksum(migration),
|
|
214
|
-
batch,
|
|
215
|
-
},
|
|
216
|
-
token,
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
applied.push({
|
|
221
|
-
id: migration.id,
|
|
222
|
-
name: migration.name,
|
|
223
|
-
status: 'applied',
|
|
224
|
-
})
|
|
225
|
-
} else {
|
|
226
|
-
if (!dryRun) {
|
|
227
|
-
await deleteJournalRow(adapter, journalTable, migration.id, token)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
reverted.push({
|
|
231
|
-
id: migration.id,
|
|
232
|
-
name: migration.name,
|
|
233
|
-
status: 'pending',
|
|
234
|
-
})
|
|
270
|
+
applied.push({
|
|
271
|
+
id: migration.id,
|
|
272
|
+
name: migration.name,
|
|
273
|
+
status: 'applied',
|
|
274
|
+
})
|
|
275
|
+
} else {
|
|
276
|
+
if (!dryRun) {
|
|
277
|
+
await deleteJournalRow(driver, journalTable, migration.id, token)
|
|
235
278
|
}
|
|
236
279
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
280
|
+
reverted.push({
|
|
281
|
+
id: migration.id,
|
|
282
|
+
name: migration.name,
|
|
283
|
+
status: 'pending',
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (token) {
|
|
288
|
+
try {
|
|
289
|
+
await driver.rollbackTransaction(token)
|
|
290
|
+
} catch (rollbackError) {
|
|
291
|
+
throw new AggregateError([error, rollbackError], 'Migration and rollback both failed', {
|
|
292
|
+
cause: error,
|
|
293
|
+
})
|
|
243
294
|
}
|
|
244
|
-
|
|
245
|
-
throw error
|
|
246
295
|
}
|
|
296
|
+
|
|
297
|
+
throw error
|
|
247
298
|
}
|
|
248
299
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
reverted,
|
|
252
|
-
sql,
|
|
300
|
+
if (token) {
|
|
301
|
+
await driver.commitTransaction(token)
|
|
253
302
|
}
|
|
254
|
-
}
|
|
255
|
-
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
applied,
|
|
307
|
+
reverted,
|
|
308
|
+
sql,
|
|
256
309
|
}
|
|
257
310
|
}
|
|
258
311
|
|
|
259
|
-
/**
|
|
260
|
-
* Creates a migration runner for applying/reverting SQL migrations against an adapter.
|
|
261
|
-
* @param adapter Database adapter used to execute migration scripts.
|
|
262
|
-
* @param migrations Migration descriptors or registry.
|
|
263
|
-
* @param options Optional runner configuration.
|
|
264
|
-
* @returns A migration runner instance.
|
|
265
|
-
* @example
|
|
266
|
-
* ```ts
|
|
267
|
-
* import { createMigrationRunner } from 'remix/data-table/migrations'
|
|
268
|
-
*
|
|
269
|
-
* let runner = createMigrationRunner(adapter, migrations, {
|
|
270
|
-
* journalTable: 'app_migrations',
|
|
271
|
-
* })
|
|
272
|
-
* await runner.up()
|
|
273
|
-
* ```
|
|
274
|
-
*/
|
|
275
312
|
export function createMigrationRunner(
|
|
276
|
-
|
|
313
|
+
driver: DatabaseDriver,
|
|
277
314
|
migrations: MigrationDescriptor[] | MigrationRegistry,
|
|
278
315
|
options: MigrationRunnerOptions = {},
|
|
279
316
|
): MigrationRunner {
|
|
280
317
|
let journalTable = options.journalTable ?? 'data_table_migrations'
|
|
281
318
|
|
|
282
319
|
return {
|
|
283
|
-
async up(runOptions:
|
|
320
|
+
async up(runOptions: MigrationOperationOptions = {}): Promise<MigrateResult> {
|
|
284
321
|
return runMigrations({
|
|
285
|
-
|
|
322
|
+
driver,
|
|
286
323
|
migrations: resolveMigrations(migrations),
|
|
287
324
|
journalTable,
|
|
288
325
|
direction: 'up',
|
|
289
326
|
options: runOptions,
|
|
290
327
|
})
|
|
291
328
|
},
|
|
292
|
-
async down(runOptions:
|
|
329
|
+
async down(runOptions: MigrationOperationOptions = {}): Promise<MigrateResult> {
|
|
293
330
|
return runMigrations({
|
|
294
|
-
|
|
331
|
+
driver,
|
|
295
332
|
migrations: resolveMigrations(migrations),
|
|
296
333
|
journalTable,
|
|
297
334
|
direction: 'down',
|
|
@@ -299,11 +336,12 @@ export function createMigrationRunner(
|
|
|
299
336
|
})
|
|
300
337
|
},
|
|
301
338
|
async status(): Promise<MigrationStatusEntry[]> {
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
339
|
+
let journal = (await hasMigrationJournal(driver, journalTable))
|
|
340
|
+
? await loadJournalRows(driver, journalTable)
|
|
341
|
+
: []
|
|
305
342
|
let journalMap = new Map(journal.map((row) => [row.id, row]))
|
|
306
343
|
let sortedMigrations = resolveMigrations(migrations)
|
|
344
|
+
let migrationIds = new Set(sortedMigrations.map((migration) => migration.id))
|
|
307
345
|
|
|
308
346
|
let statuses: MigrationStatusEntry[] = []
|
|
309
347
|
|
|
@@ -334,7 +372,22 @@ export function createMigrationRunner(
|
|
|
334
372
|
})
|
|
335
373
|
}
|
|
336
374
|
|
|
337
|
-
|
|
375
|
+
for (let journalRow of journal) {
|
|
376
|
+
if (migrationIds.has(journalRow.id)) {
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
statuses.push({
|
|
381
|
+
id: journalRow.id,
|
|
382
|
+
name: journalRow.name,
|
|
383
|
+
status: 'missing',
|
|
384
|
+
appliedAt: journalRow.appliedAt,
|
|
385
|
+
batch: journalRow.batch,
|
|
386
|
+
checksum: journalRow.checksum,
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return statuses.sort((left, right) => left.id.localeCompare(right.id))
|
|
338
391
|
},
|
|
339
392
|
}
|
|
340
393
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import type { MigrationDescriptor } from './migrations.ts'
|
|
4
|
+
import type { MigrationDescriptor, Seed } from './migrations.ts'
|
|
5
5
|
import { parseMigrationDirectoryName } from './migrations/directory-name.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -81,6 +81,28 @@ export async function loadMigrations(directory: string): Promise<MigrationDescri
|
|
|
81
81
|
return migrations
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Loads a SQL seed file on Node.js.
|
|
86
|
+
*
|
|
87
|
+
* The file may contain multiple SQL statements. Seeds that must be safe to
|
|
88
|
+
* run against an already-seeded database should use idempotent statements
|
|
89
|
+
* (for example, `insert or ignore` on SQLite).
|
|
90
|
+
*
|
|
91
|
+
* @param filename Absolute or relative path to a SQL seed file.
|
|
92
|
+
* @returns A seed function that executes the file's SQL script.
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { loadSeed } from 'remix/data-table/migrations/node'
|
|
96
|
+
*
|
|
97
|
+
* let seed = await loadSeed('./app/data/seed.sql')
|
|
98
|
+
* await db.reset({ migrations, seed })
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export async function loadSeed(filename: string): Promise<Seed> {
|
|
102
|
+
let sql = await fs.readFile(filename, 'utf8')
|
|
103
|
+
return (db) => db.executeScript(sql)
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
function isNodeFileNotFoundError(error: unknown): boolean {
|
|
85
107
|
return (
|
|
86
108
|
typeof error === 'object' &&
|