@ftschopp/dynatable-migrations 1.1.0 → 1.2.1
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/CHANGELOG.md +14 -0
- package/README.md +98 -134
- package/USAGE.md +195 -60
- package/dist/cli.js +7 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/down.d.ts +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +5 -20
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -17
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts +1 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +5 -20
- package/dist/commands/up.js.map +1 -1
- package/dist/core/client.d.ts +7 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +25 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/loader.d.ts +8 -0
- package/dist/core/loader.d.ts.map +1 -1
- package/dist/core/loader.js +76 -43
- package/dist/core/loader.js.map +1 -1
- package/dist/core/runner.d.ts +6 -2
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +124 -67
- package/dist/core/runner.js.map +1 -1
- package/dist/core/tracker.d.ts +20 -1
- package/dist/core/tracker.d.ts.map +1 -1
- package/dist/core/tracker.js +273 -83
- package/dist/core/tracker.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -3
- package/src/cli.ts +8 -3
- package/src/commands/down.ts +6 -22
- package/src/commands/status.ts +2 -19
- package/src/commands/up.ts +9 -22
- package/src/core/client.ts +24 -0
- package/src/core/loader.ts +86 -46
- package/src/core/runner.ts +149 -78
- package/src/core/tracker.ts +305 -94
- package/src/index.ts +2 -1
- package/src/types/index.ts +13 -1
package/src/core/tracker.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
PutCommand,
|
|
4
|
+
GetCommand,
|
|
5
|
+
QueryCommand,
|
|
6
|
+
UpdateCommand,
|
|
7
|
+
DeleteCommand,
|
|
8
|
+
TransactWriteCommand,
|
|
9
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
3
10
|
import { MigrationTracker, MigrationRecord, SchemaChange, MigrationConfig } from '../types';
|
|
4
11
|
|
|
12
|
+
const LOCK_TTL_SECONDS = 300; // 5 minutes
|
|
13
|
+
|
|
5
14
|
export class DynamoDBMigrationTracker implements MigrationTracker {
|
|
6
15
|
private client: DynamoDBDocumentClient;
|
|
7
16
|
private tableName: string;
|
|
8
17
|
private trackingPrefix: string;
|
|
9
18
|
private gsi1Name: string;
|
|
19
|
+
private lockId: string | null = null;
|
|
10
20
|
|
|
11
21
|
constructor(client: DynamoDBDocumentClient, config: MigrationConfig) {
|
|
12
22
|
this.client = client;
|
|
@@ -36,18 +46,103 @@ export class DynamoDBMigrationTracker implements MigrationTracker {
|
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Acquire a distributed lock to prevent concurrent migrations
|
|
51
|
+
*/
|
|
52
|
+
async acquireLock(): Promise<boolean> {
|
|
53
|
+
const lockKey = {
|
|
54
|
+
PK: `${this.trackingPrefix}#LOCK`,
|
|
55
|
+
SK: `${this.trackingPrefix}#LOCK`,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const expiresAt = now + LOCK_TTL_SECONDS * 1000;
|
|
60
|
+
this.lockId = `lock-${now}-${Math.random().toString(36).substring(7)}`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await this.client.send(
|
|
64
|
+
new PutCommand({
|
|
65
|
+
TableName: this.tableName,
|
|
66
|
+
Item: {
|
|
67
|
+
...lockKey,
|
|
68
|
+
lockId: this.lockId,
|
|
69
|
+
acquiredAt: new Date().toISOString(),
|
|
70
|
+
expiresAt,
|
|
71
|
+
},
|
|
72
|
+
// Only succeed if lock doesn't exist or has expired
|
|
73
|
+
ConditionExpression: 'attribute_not_exists(PK) OR expiresAt < :now',
|
|
74
|
+
ExpressionAttributeValues: {
|
|
75
|
+
':now': now,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
if (error.name === 'ConditionalCheckFailedException') {
|
|
82
|
+
this.lockId = null;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Release the distributed lock
|
|
91
|
+
*/
|
|
92
|
+
async releaseLock(): Promise<void> {
|
|
93
|
+
if (!this.lockId) return;
|
|
94
|
+
|
|
95
|
+
const lockKey = {
|
|
96
|
+
PK: `${this.trackingPrefix}#LOCK`,
|
|
97
|
+
SK: `${this.trackingPrefix}#LOCK`,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await this.client.send(
|
|
102
|
+
new DeleteCommand({
|
|
103
|
+
TableName: this.tableName,
|
|
104
|
+
Key: lockKey,
|
|
105
|
+
// Only delete if we own the lock
|
|
106
|
+
ConditionExpression: 'lockId = :lockId',
|
|
107
|
+
ExpressionAttributeValues: {
|
|
108
|
+
':lockId': this.lockId,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
// Lock may have expired or been taken by someone else
|
|
114
|
+
if (error.name !== 'ConditionalCheckFailedException') {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
this.lockId = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all applied migrations with pagination support
|
|
124
|
+
*/
|
|
39
125
|
async getAppliedMigrations(): Promise<MigrationRecord[]> {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
126
|
+
const items: MigrationRecord[] = [];
|
|
127
|
+
let lastKey: Record<string, any> | undefined;
|
|
128
|
+
|
|
129
|
+
do {
|
|
130
|
+
const result = await this.client.send(
|
|
131
|
+
new QueryCommand({
|
|
132
|
+
TableName: this.tableName,
|
|
133
|
+
KeyConditionExpression: 'PK = :pk',
|
|
134
|
+
ExpressionAttributeValues: {
|
|
135
|
+
':pk': this.trackingPrefix,
|
|
136
|
+
},
|
|
137
|
+
ExclusiveStartKey: lastKey,
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
items.push(...((result.Items || []) as MigrationRecord[]));
|
|
142
|
+
lastKey = result.LastEvaluatedKey;
|
|
143
|
+
} while (lastKey);
|
|
49
144
|
|
|
50
|
-
return
|
|
145
|
+
return items;
|
|
51
146
|
}
|
|
52
147
|
|
|
53
148
|
async getCurrentVersion(): Promise<string | null> {
|
|
@@ -64,70 +159,138 @@ export class DynamoDBMigrationTracker implements MigrationTracker {
|
|
|
64
159
|
return result.Item?.currentVersion || null;
|
|
65
160
|
}
|
|
66
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Mark migration as applied using TransactWrite for atomicity
|
|
164
|
+
* Handles both new migrations and re-applying rolled back migrations
|
|
165
|
+
*/
|
|
67
166
|
async markAsApplied(
|
|
68
167
|
version: string,
|
|
69
168
|
name: string,
|
|
70
169
|
schemaDefinition?: Record<string, any>,
|
|
71
|
-
schemaChanges?: SchemaChange[]
|
|
170
|
+
schemaChanges?: SchemaChange[],
|
|
171
|
+
checksum?: string
|
|
72
172
|
): Promise<void> {
|
|
73
173
|
const now = new Date().toISOString();
|
|
74
174
|
|
|
75
|
-
//
|
|
76
|
-
await this.
|
|
77
|
-
new PutCommand({
|
|
78
|
-
TableName: this.tableName,
|
|
79
|
-
Item: {
|
|
80
|
-
PK: this.trackingPrefix,
|
|
81
|
-
SK: version,
|
|
82
|
-
version,
|
|
83
|
-
name,
|
|
84
|
-
timestamp: now,
|
|
85
|
-
appliedAt: now,
|
|
86
|
-
status: 'applied',
|
|
87
|
-
schemaDefinition,
|
|
88
|
-
schemaChanges,
|
|
89
|
-
} as MigrationRecord,
|
|
90
|
-
})
|
|
91
|
-
);
|
|
175
|
+
// Check if migration record already exists (e.g., rolled back or failed)
|
|
176
|
+
const existing = await this.getMigration(version);
|
|
92
177
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
178
|
+
if (existing) {
|
|
179
|
+
// Update existing record (re-applying a rolled back or failed migration)
|
|
180
|
+
// Build dynamic update expression - DynamoDB doesn't accept undefined values
|
|
181
|
+
const setParts = ['#status = :status', 'appliedAt = :appliedAt', '#name = :name'];
|
|
182
|
+
const expressionValues: Record<string, any> = {
|
|
183
|
+
':status': 'applied',
|
|
184
|
+
':appliedAt': now,
|
|
185
|
+
':name': name,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (schemaDefinition !== undefined) {
|
|
189
|
+
setParts.push('schemaDefinition = :schemaDef');
|
|
190
|
+
expressionValues[':schemaDef'] = schemaDefinition;
|
|
191
|
+
}
|
|
192
|
+
if (schemaChanges !== undefined) {
|
|
193
|
+
setParts.push('schemaChanges = :schemaChanges');
|
|
194
|
+
expressionValues[':schemaChanges'] = schemaChanges;
|
|
195
|
+
}
|
|
196
|
+
if (checksum !== undefined) {
|
|
197
|
+
setParts.push('checksum = :checksum');
|
|
198
|
+
expressionValues[':checksum'] = checksum;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.client.send(
|
|
202
|
+
new TransactWriteCommand({
|
|
203
|
+
TransactItems: [
|
|
204
|
+
{
|
|
205
|
+
Update: {
|
|
206
|
+
TableName: this.tableName,
|
|
207
|
+
Key: {
|
|
208
|
+
PK: this.trackingPrefix,
|
|
209
|
+
SK: version,
|
|
210
|
+
},
|
|
211
|
+
UpdateExpression: `SET ${setParts.join(', ')} REMOVE #error, failedAt, rolledBackAt`,
|
|
212
|
+
ExpressionAttributeNames: {
|
|
213
|
+
'#status': 'status',
|
|
214
|
+
'#name': 'name',
|
|
215
|
+
'#error': 'error',
|
|
216
|
+
},
|
|
217
|
+
ExpressionAttributeValues: expressionValues,
|
|
218
|
+
// Only allow re-applying if not currently applied
|
|
219
|
+
ConditionExpression: '#status <> :status',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
Update: {
|
|
224
|
+
TableName: this.tableName,
|
|
225
|
+
Key: {
|
|
226
|
+
PK: `${this.trackingPrefix}#CURRENT`,
|
|
227
|
+
SK: `${this.trackingPrefix}#CURRENT`,
|
|
228
|
+
},
|
|
229
|
+
UpdateExpression:
|
|
230
|
+
'SET currentVersion = :version, updatedAt = :updatedAt, GSI1SK = :gsi1sk',
|
|
231
|
+
ExpressionAttributeValues: {
|
|
232
|
+
':version': version,
|
|
233
|
+
':updatedAt': now,
|
|
234
|
+
':gsi1sk': version,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
// Create new migration record
|
|
243
|
+
await this.client.send(
|
|
244
|
+
new TransactWriteCommand({
|
|
245
|
+
TransactItems: [
|
|
246
|
+
{
|
|
247
|
+
Put: {
|
|
248
|
+
TableName: this.tableName,
|
|
249
|
+
Item: {
|
|
250
|
+
PK: this.trackingPrefix,
|
|
251
|
+
SK: version,
|
|
252
|
+
version,
|
|
253
|
+
name,
|
|
254
|
+
timestamp: now,
|
|
255
|
+
appliedAt: now,
|
|
256
|
+
status: 'applied',
|
|
257
|
+
schemaDefinition,
|
|
258
|
+
schemaChanges,
|
|
259
|
+
checksum,
|
|
260
|
+
} as MigrationRecord,
|
|
261
|
+
// Ensure migration wasn't already applied
|
|
262
|
+
ConditionExpression: 'attribute_not_exists(PK)',
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
Update: {
|
|
267
|
+
TableName: this.tableName,
|
|
268
|
+
Key: {
|
|
269
|
+
PK: `${this.trackingPrefix}#CURRENT`,
|
|
270
|
+
SK: `${this.trackingPrefix}#CURRENT`,
|
|
271
|
+
},
|
|
272
|
+
UpdateExpression:
|
|
273
|
+
'SET currentVersion = :version, updatedAt = :updatedAt, GSI1SK = :gsi1sk',
|
|
274
|
+
ExpressionAttributeValues: {
|
|
275
|
+
':version': version,
|
|
276
|
+
':updatedAt': now,
|
|
277
|
+
':gsi1sk': version,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
}
|
|
109
285
|
}
|
|
110
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Mark migration as rolled back using TransactWrite for atomicity
|
|
289
|
+
*/
|
|
111
290
|
async markAsRolledBack(version: string): Promise<void> {
|
|
112
|
-
|
|
113
|
-
new UpdateCommand({
|
|
114
|
-
TableName: this.tableName,
|
|
115
|
-
Key: {
|
|
116
|
-
PK: this.trackingPrefix,
|
|
117
|
-
SK: version,
|
|
118
|
-
},
|
|
119
|
-
UpdateExpression: 'SET #status = :status, rolledBackAt = :timestamp',
|
|
120
|
-
ExpressionAttributeNames: {
|
|
121
|
-
'#status': 'status',
|
|
122
|
-
},
|
|
123
|
-
ExpressionAttributeValues: {
|
|
124
|
-
':status': 'rolled_back',
|
|
125
|
-
':timestamp': new Date().toISOString(),
|
|
126
|
-
},
|
|
127
|
-
})
|
|
128
|
-
);
|
|
291
|
+
const now = new Date().toISOString();
|
|
129
292
|
|
|
130
|
-
// Find previous version
|
|
293
|
+
// Find previous version
|
|
131
294
|
const migrations = await this.getAppliedMigrations();
|
|
132
295
|
const appliedMigrations = migrations
|
|
133
296
|
.filter((m) => m.status === 'applied' && m.version !== version)
|
|
@@ -135,43 +298,91 @@ export class DynamoDBMigrationTracker implements MigrationTracker {
|
|
|
135
298
|
|
|
136
299
|
const previousVersion = appliedMigrations[0]?.version || 'v0000';
|
|
137
300
|
|
|
301
|
+
// Use TransactWrite to atomically update record and pointer
|
|
138
302
|
await this.client.send(
|
|
139
|
-
new
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
303
|
+
new TransactWriteCommand({
|
|
304
|
+
TransactItems: [
|
|
305
|
+
{
|
|
306
|
+
Update: {
|
|
307
|
+
TableName: this.tableName,
|
|
308
|
+
Key: {
|
|
309
|
+
PK: this.trackingPrefix,
|
|
310
|
+
SK: version,
|
|
311
|
+
},
|
|
312
|
+
UpdateExpression: 'SET #status = :status, rolledBackAt = :timestamp',
|
|
313
|
+
ExpressionAttributeNames: {
|
|
314
|
+
'#status': 'status',
|
|
315
|
+
},
|
|
316
|
+
ExpressionAttributeValues: {
|
|
317
|
+
':status': 'rolled_back',
|
|
318
|
+
':timestamp': now,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
Update: {
|
|
324
|
+
TableName: this.tableName,
|
|
325
|
+
Key: {
|
|
326
|
+
PK: `${this.trackingPrefix}#CURRENT`,
|
|
327
|
+
SK: `${this.trackingPrefix}#CURRENT`,
|
|
328
|
+
},
|
|
329
|
+
UpdateExpression:
|
|
330
|
+
'SET currentVersion = :version, updatedAt = :updatedAt, GSI1SK = :gsi1sk',
|
|
331
|
+
ExpressionAttributeValues: {
|
|
332
|
+
':version': previousVersion,
|
|
333
|
+
':updatedAt': now,
|
|
334
|
+
':gsi1sk': previousVersion,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
],
|
|
151
339
|
})
|
|
152
340
|
);
|
|
153
341
|
}
|
|
154
342
|
|
|
155
343
|
async markAsFailed(version: string, error: string): Promise<void> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
344
|
+
// Check if the migration record exists first
|
|
345
|
+
const existing = await this.getMigration(version);
|
|
346
|
+
|
|
347
|
+
if (existing) {
|
|
348
|
+
// Update existing record
|
|
349
|
+
await this.client.send(
|
|
350
|
+
new UpdateCommand({
|
|
351
|
+
TableName: this.tableName,
|
|
352
|
+
Key: {
|
|
353
|
+
PK: this.trackingPrefix,
|
|
354
|
+
SK: version,
|
|
355
|
+
},
|
|
356
|
+
UpdateExpression: 'SET #status = :status, #error = :error, failedAt = :timestamp',
|
|
357
|
+
ExpressionAttributeNames: {
|
|
358
|
+
'#status': 'status',
|
|
359
|
+
'#error': 'error',
|
|
360
|
+
},
|
|
361
|
+
ExpressionAttributeValues: {
|
|
362
|
+
':status': 'failed',
|
|
363
|
+
':error': error,
|
|
364
|
+
':timestamp': new Date().toISOString(),
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
// Create new record for failed migration
|
|
370
|
+
await this.client.send(
|
|
371
|
+
new PutCommand({
|
|
372
|
+
TableName: this.tableName,
|
|
373
|
+
Item: {
|
|
374
|
+
PK: this.trackingPrefix,
|
|
375
|
+
SK: version,
|
|
376
|
+
version,
|
|
377
|
+
name: 'unknown',
|
|
378
|
+
status: 'failed',
|
|
379
|
+
error,
|
|
380
|
+
failedAt: new Date().toISOString(),
|
|
381
|
+
timestamp: new Date().toISOString(),
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
}
|
|
175
386
|
}
|
|
176
387
|
|
|
177
388
|
async recordSchemaChange(change: SchemaChange): Promise<void> {
|
package/src/index.ts
CHANGED
|
@@ -9,8 +9,9 @@ export * from './types';
|
|
|
9
9
|
// Core
|
|
10
10
|
export { DynamoDBMigrationTracker } from './core/tracker';
|
|
11
11
|
export { MigrationLoader } from './core/loader';
|
|
12
|
-
export { MigrationRunner } from './core/runner';
|
|
12
|
+
export { MigrationRunner, type RunOptions } from './core/runner';
|
|
13
13
|
export { ConfigLoader, loadConfig } from './core/config';
|
|
14
|
+
export { createDynamoDBClient } from './core/client';
|
|
14
15
|
|
|
15
16
|
// Template
|
|
16
17
|
export { generateMigrationTemplate } from './templates/migration';
|
package/src/types/index.ts
CHANGED
|
@@ -138,6 +138,16 @@ export interface MigrationTracker {
|
|
|
138
138
|
*/
|
|
139
139
|
getCurrentVersion(): Promise<string | null>;
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Acquire a distributed lock
|
|
143
|
+
*/
|
|
144
|
+
acquireLock(): Promise<boolean>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Release the distributed lock
|
|
148
|
+
*/
|
|
149
|
+
releaseLock(): Promise<void>;
|
|
150
|
+
|
|
141
151
|
/**
|
|
142
152
|
* Mark migration as applied
|
|
143
153
|
*/
|
|
@@ -145,7 +155,8 @@ export interface MigrationTracker {
|
|
|
145
155
|
version: string,
|
|
146
156
|
name: string,
|
|
147
157
|
schemaDefinition?: Record<string, any>,
|
|
148
|
-
schemaChanges?: SchemaChange[]
|
|
158
|
+
schemaChanges?: SchemaChange[],
|
|
159
|
+
checksum?: string
|
|
149
160
|
): Promise<void>;
|
|
150
161
|
|
|
151
162
|
/**
|
|
@@ -182,6 +193,7 @@ export interface MigrationFile {
|
|
|
182
193
|
name: string;
|
|
183
194
|
filePath: string;
|
|
184
195
|
migration: Migration;
|
|
196
|
+
checksum?: string;
|
|
185
197
|
}
|
|
186
198
|
|
|
187
199
|
/**
|