@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/commands/status.ts
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
1
|
import { MigrationRunner } from '../core/runner';
|
|
2
|
+
import { createDynamoDBClient } from '../core/client';
|
|
4
3
|
import { MigrationConfig } from '../types';
|
|
5
4
|
|
|
6
5
|
export async function showStatus(config: MigrationConfig): Promise<void> {
|
|
7
|
-
|
|
8
|
-
const ddbClient = new DynamoDBClient({
|
|
9
|
-
region: config.client.region,
|
|
10
|
-
endpoint: config.client.endpoint,
|
|
11
|
-
credentials: config.client.credentials,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const client = DynamoDBDocumentClient.from(ddbClient, {
|
|
15
|
-
marshallOptions: {
|
|
16
|
-
removeUndefinedValues: true,
|
|
17
|
-
convertClassInstanceToMap: true,
|
|
18
|
-
},
|
|
19
|
-
unmarshallOptions: {
|
|
20
|
-
wrapNumbers: false,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
6
|
+
const client = createDynamoDBClient(config);
|
|
24
7
|
const runner = new MigrationRunner(client, config);
|
|
25
8
|
|
|
26
9
|
try {
|
package/src/commands/up.ts
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
1
|
import { MigrationRunner } from '../core/runner';
|
|
2
|
+
import { createDynamoDBClient } from '../core/client';
|
|
4
3
|
import { MigrationConfig } from '../types';
|
|
5
4
|
|
|
6
|
-
export async function runMigrations(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const client = DynamoDBDocumentClient.from(ddbClient, {
|
|
15
|
-
marshallOptions: {
|
|
16
|
-
removeUndefinedValues: true,
|
|
17
|
-
convertClassInstanceToMap: true,
|
|
18
|
-
},
|
|
19
|
-
unmarshallOptions: {
|
|
20
|
-
wrapNumbers: false,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
5
|
+
export async function runMigrations(
|
|
6
|
+
config: MigrationConfig,
|
|
7
|
+
limit?: number,
|
|
8
|
+
dryRun: boolean = false
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const client = createDynamoDBClient(config);
|
|
24
11
|
const runner = new MigrationRunner(client, config);
|
|
25
12
|
|
|
26
13
|
try {
|
|
27
|
-
const executed = await runner.up(limit);
|
|
14
|
+
const executed = await runner.up({ limit, dryRun });
|
|
28
15
|
|
|
29
|
-
if (executed.length > 0) {
|
|
16
|
+
if (!dryRun && executed.length > 0) {
|
|
30
17
|
console.log(`\n🎉 Successfully applied ${executed.length} migration(s)`);
|
|
31
18
|
|
|
32
19
|
const currentVersion = await runner.getCurrentVersion();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { MigrationConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a DynamoDB Document Client from config
|
|
7
|
+
*/
|
|
8
|
+
export function createDynamoDBClient(config: MigrationConfig): DynamoDBDocumentClient {
|
|
9
|
+
const ddbClient = new DynamoDBClient({
|
|
10
|
+
region: config.client.region,
|
|
11
|
+
endpoint: config.client.endpoint,
|
|
12
|
+
credentials: config.client.credentials,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return DynamoDBDocumentClient.from(ddbClient, {
|
|
16
|
+
marshallOptions: {
|
|
17
|
+
removeUndefinedValues: true,
|
|
18
|
+
convertClassInstanceToMap: true,
|
|
19
|
+
},
|
|
20
|
+
unmarshallOptions: {
|
|
21
|
+
wrapNumbers: false,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
package/src/core/loader.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
3
4
|
import { Migration, MigrationFile } from '../types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -12,6 +13,14 @@ export class MigrationLoader {
|
|
|
12
13
|
this.migrationsDir = migrationsDir;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Calculate checksum of a file
|
|
18
|
+
*/
|
|
19
|
+
calculateChecksum(filePath: string): string {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
21
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Load all migration files
|
|
17
26
|
*/
|
|
@@ -53,63 +62,94 @@ export class MigrationLoader {
|
|
|
53
62
|
* Load single migration file
|
|
54
63
|
*/
|
|
55
64
|
private async loadMigration(filePath: string): Promise<MigrationFile | null> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let module: any;
|
|
63
|
-
|
|
64
|
-
// For TypeScript files, use require (ts-node must be registered)
|
|
65
|
-
if (absolutePath.endsWith('.ts')) {
|
|
66
|
-
// Register ts-node if not already registered
|
|
67
|
-
try {
|
|
68
|
-
require('ts-node').register({
|
|
69
|
-
transpileOnly: true,
|
|
70
|
-
skipProject: true, // Don't use project tsconfig
|
|
71
|
-
compilerOptions: {
|
|
72
|
-
module: 'commonjs',
|
|
73
|
-
target: 'ES2020',
|
|
74
|
-
esModuleInterop: true,
|
|
75
|
-
moduleResolution: 'node',
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
} catch {
|
|
79
|
-
// ts-node might already be registered
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Clear require cache to reload file
|
|
83
|
-
delete require.cache[absolutePath];
|
|
65
|
+
// Convert to absolute path
|
|
66
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
67
|
+
? filePath
|
|
68
|
+
: path.resolve(process.cwd(), filePath);
|
|
69
|
+
|
|
70
|
+
let module: any;
|
|
84
71
|
|
|
72
|
+
// For TypeScript files, use require (ts-node must be registered)
|
|
73
|
+
if (absolutePath.endsWith('.ts')) {
|
|
74
|
+
// Register ts-node if not already registered
|
|
75
|
+
this.registerTsNode();
|
|
76
|
+
|
|
77
|
+
// Clear require cache to reload file
|
|
78
|
+
delete require.cache[absolutePath];
|
|
79
|
+
|
|
80
|
+
try {
|
|
85
81
|
module = require(absolutePath);
|
|
86
|
-
}
|
|
87
|
-
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Failed to load TypeScript migration ${filePath}: ${error.message}. ` +
|
|
85
|
+
`Ensure ts-node is installed and the file has valid TypeScript syntax.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// For JavaScript files, use dynamic import
|
|
90
|
+
try {
|
|
88
91
|
const fileUrl = `file://${absolutePath}`;
|
|
89
92
|
module = await import(fileUrl);
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
throw new Error(`Failed to load JavaScript migration ${filePath}: ${error.message}`);
|
|
90
95
|
}
|
|
96
|
+
}
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
const migration: Migration = module.migration || module.default;
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
if (!migration) {
|
|
101
|
+
console.warn(`Warning: No migration export found in ${filePath}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate migration structure
|
|
106
|
+
this.validateMigration(migration, filePath);
|
|
107
|
+
|
|
108
|
+
const fileName = path.basename(filePath);
|
|
109
|
+
const { version, name } = this.parseMigrationFileName(fileName);
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
// Calculate checksum
|
|
112
|
+
const checksum = this.calculateChecksum(absolutePath);
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
return {
|
|
115
|
+
version,
|
|
116
|
+
name,
|
|
117
|
+
filePath,
|
|
118
|
+
migration,
|
|
119
|
+
checksum,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Register ts-node for TypeScript file loading
|
|
125
|
+
*/
|
|
126
|
+
private registerTsNode(): void {
|
|
127
|
+
try {
|
|
128
|
+
// Check if ts-node is already registered
|
|
129
|
+
const tsNodeSymbol = Symbol.for('ts-node.register.instance');
|
|
130
|
+
if ((process as any)[tsNodeSymbol]) {
|
|
131
|
+
return; // Already registered
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
require('ts-node').register({
|
|
135
|
+
transpileOnly: true,
|
|
136
|
+
skipProject: true, // Don't use project tsconfig
|
|
137
|
+
compilerOptions: {
|
|
138
|
+
module: 'commonjs',
|
|
139
|
+
target: 'ES2020',
|
|
140
|
+
esModuleInterop: true,
|
|
141
|
+
moduleResolution: 'node',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
111
144
|
} catch (error: any) {
|
|
112
|
-
|
|
145
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'ts-node is required to load TypeScript migrations. ' +
|
|
148
|
+
'Install it with: npm install ts-node'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// Other errors might mean ts-node is already registered differently
|
|
152
|
+
console.warn(`Warning: Could not register ts-node: ${error.message}`);
|
|
113
153
|
}
|
|
114
154
|
}
|
|
115
155
|
|
package/src/core/runner.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import {
|
|
3
|
+
ScanCommand,
|
|
4
|
+
QueryCommand,
|
|
5
|
+
GetCommand,
|
|
6
|
+
PutCommand,
|
|
7
|
+
UpdateCommand,
|
|
8
|
+
DeleteCommand,
|
|
9
|
+
BatchGetCommand,
|
|
10
|
+
BatchWriteCommand,
|
|
11
|
+
TransactWriteCommand,
|
|
12
|
+
TransactGetCommand,
|
|
13
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
2
14
|
import { MigrationConfig, MigrationContext, MigrationFile, MigrationStatus } from '../types';
|
|
3
15
|
import { DynamoDBMigrationTracker } from './tracker';
|
|
4
16
|
import { MigrationLoader } from './loader';
|
|
5
17
|
|
|
18
|
+
export interface RunOptions {
|
|
19
|
+
limit?: number;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
export class MigrationRunner {
|
|
7
24
|
private client: DynamoDBDocumentClient;
|
|
8
25
|
private config: MigrationConfig;
|
|
@@ -26,105 +43,173 @@ export class MigrationRunner {
|
|
|
26
43
|
/**
|
|
27
44
|
* Run all pending migrations
|
|
28
45
|
*/
|
|
29
|
-
async up(
|
|
46
|
+
async up(options: RunOptions = {}): Promise<MigrationFile[]> {
|
|
47
|
+
const { limit, dryRun = false } = options;
|
|
48
|
+
|
|
30
49
|
await this.initialize();
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
// Acquire lock unless dry run
|
|
52
|
+
if (!dryRun) {
|
|
53
|
+
const lockAcquired = await this.tracker.acquireLock();
|
|
54
|
+
if (!lockAcquired) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Could not acquire migration lock. Another migration may be in progress. ' +
|
|
57
|
+
'If you believe this is an error, wait a few minutes and try again.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const appliedMigrations = await this.tracker.getAppliedMigrations();
|
|
64
|
+
const appliedVersions = appliedMigrations
|
|
65
|
+
.filter((m) => m.status === 'applied')
|
|
66
|
+
.map((m) => m.version);
|
|
67
|
+
|
|
68
|
+
// Check for checksum mismatches on applied migrations
|
|
69
|
+
for (const applied of appliedMigrations.filter((m) => m.status === 'applied')) {
|
|
70
|
+
const file = await this.loader.getMigration(applied.version);
|
|
71
|
+
if (file && applied.checksum && file.checksum !== applied.checksum) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`⚠️ Warning: Migration ${applied.version} has been modified since it was applied. ` +
|
|
74
|
+
`Original checksum: ${applied.checksum}, Current: ${file.checksum}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
let pendingMigrations = await this.loader.getPendingMigrations(appliedVersions);
|
|
38
80
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
// Apply limit if specified
|
|
82
|
+
if (limit) {
|
|
83
|
+
pendingMigrations = pendingMigrations.slice(0, limit);
|
|
84
|
+
}
|
|
43
85
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
86
|
+
if (pendingMigrations.length === 0) {
|
|
87
|
+
console.log('✅ No pending migrations');
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
48
90
|
|
|
49
|
-
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
console.log(`\n🔍 DRY RUN - Would apply ${pendingMigrations.length} migration(s):\n`);
|
|
93
|
+
for (const migrationFile of pendingMigrations) {
|
|
94
|
+
console.log(` ${migrationFile.version}: ${migrationFile.name}`);
|
|
95
|
+
if (migrationFile.migration.description) {
|
|
96
|
+
console.log(` ${migrationFile.migration.description}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
console.log('\nNo changes were made.\n');
|
|
100
|
+
return pendingMigrations;
|
|
101
|
+
}
|
|
50
102
|
|
|
51
|
-
|
|
103
|
+
console.log(`\n📦 Found ${pendingMigrations.length} pending migration(s)\n`);
|
|
52
104
|
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
console.log(`⬆️ Applying ${migrationFile.version}: ${migrationFile.name}`);
|
|
105
|
+
const executed: MigrationFile[] = [];
|
|
56
106
|
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
for (const migrationFile of pendingMigrations) {
|
|
108
|
+
try {
|
|
109
|
+
console.log(`⬆️ Applying ${migrationFile.version}: ${migrationFile.name}`);
|
|
59
110
|
|
|
60
|
-
|
|
61
|
-
migrationFile.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
111
|
+
const context = this.createContext();
|
|
112
|
+
await migrationFile.migration.up(context);
|
|
113
|
+
|
|
114
|
+
await this.tracker.markAsApplied(
|
|
115
|
+
migrationFile.version,
|
|
116
|
+
migrationFile.name,
|
|
117
|
+
migrationFile.migration.schema,
|
|
118
|
+
undefined,
|
|
119
|
+
migrationFile.checksum
|
|
120
|
+
);
|
|
65
121
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
console.log(`✅ Applied ${migrationFile.version}: ${migrationFile.name}\n`);
|
|
123
|
+
executed.push(migrationFile);
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
console.error(`❌ Failed to apply ${migrationFile.version}: ${error.message}\n`);
|
|
70
126
|
|
|
71
|
-
|
|
127
|
+
await this.tracker.markAsFailed(migrationFile.version, error.message);
|
|
72
128
|
|
|
73
|
-
|
|
129
|
+
throw new Error(`Migration ${migrationFile.version} failed: ${error.message}`);
|
|
130
|
+
}
|
|
74
131
|
}
|
|
75
|
-
}
|
|
76
132
|
|
|
77
|
-
|
|
133
|
+
return executed;
|
|
134
|
+
} finally {
|
|
135
|
+
// Always release lock
|
|
136
|
+
if (!dryRun) {
|
|
137
|
+
await this.tracker.releaseLock();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
78
140
|
}
|
|
79
141
|
|
|
80
142
|
/**
|
|
81
143
|
* Rollback last migration
|
|
82
144
|
*/
|
|
83
|
-
async down(steps: number = 1): Promise<MigrationFile[]> {
|
|
145
|
+
async down(steps: number = 1, dryRun: boolean = false): Promise<MigrationFile[]> {
|
|
84
146
|
await this.initialize();
|
|
85
147
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (applied.length === 0) {
|
|
93
|
-
console.log('✅ No migrations to rollback');
|
|
94
|
-
return [];
|
|
148
|
+
// Acquire lock unless dry run
|
|
149
|
+
if (!dryRun) {
|
|
150
|
+
const lockAcquired = await this.tracker.acquireLock();
|
|
151
|
+
if (!lockAcquired) {
|
|
152
|
+
throw new Error('Could not acquire migration lock. Another migration may be in progress.');
|
|
153
|
+
}
|
|
95
154
|
}
|
|
96
155
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
156
|
+
try {
|
|
157
|
+
const appliedMigrations = await this.tracker.getAppliedMigrations();
|
|
158
|
+
const applied = appliedMigrations
|
|
159
|
+
.filter((m) => m.status === 'applied')
|
|
160
|
+
.sort((a, b) => b.version.localeCompare(a.version))
|
|
161
|
+
.slice(0, steps);
|
|
100
162
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
if (applied.length === 0) {
|
|
164
|
+
console.log('✅ No migrations to rollback');
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
104
167
|
|
|
105
|
-
|
|
106
|
-
|
|
168
|
+
if (dryRun) {
|
|
169
|
+
console.log(`\n🔍 DRY RUN - Would rollback ${applied.length} migration(s):\n`);
|
|
170
|
+
for (const record of applied) {
|
|
171
|
+
console.log(` ${record.version}: ${record.name}`);
|
|
107
172
|
}
|
|
173
|
+
console.log('\nNo changes were made.\n');
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
108
176
|
|
|
109
|
-
|
|
177
|
+
console.log(`\n📦 Rolling back ${applied.length} migration(s)\n`);
|
|
110
178
|
|
|
111
|
-
|
|
112
|
-
await migrationFile.migration.down(context);
|
|
179
|
+
const rolledBack: MigrationFile[] = [];
|
|
113
180
|
|
|
114
|
-
|
|
181
|
+
for (const record of applied) {
|
|
182
|
+
try {
|
|
183
|
+
const migrationFile = await this.loader.getMigration(record.version);
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.error(`❌ Failed to rollback ${record.version}: ${error.message}\n`);
|
|
185
|
+
if (!migrationFile) {
|
|
186
|
+
throw new Error(`Migration file not found for version ${record.version}`);
|
|
187
|
+
}
|
|
120
188
|
|
|
121
|
-
|
|
189
|
+
console.log(`⬇️ Rolling back ${migrationFile.version}: ${migrationFile.name}`);
|
|
122
190
|
|
|
123
|
-
|
|
191
|
+
const context = this.createContext();
|
|
192
|
+
await migrationFile.migration.down(context);
|
|
193
|
+
|
|
194
|
+
await this.tracker.markAsRolledBack(migrationFile.version);
|
|
195
|
+
|
|
196
|
+
console.log(`✅ Rolled back ${migrationFile.version}: ${migrationFile.name}\n`);
|
|
197
|
+
rolledBack.push(migrationFile);
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
console.error(`❌ Failed to rollback ${record.version}: ${error.message}\n`);
|
|
200
|
+
|
|
201
|
+
await this.tracker.markAsFailed(record.version, error.message);
|
|
202
|
+
|
|
203
|
+
throw new Error(`Rollback of ${record.version} failed: ${error.message}`);
|
|
204
|
+
}
|
|
124
205
|
}
|
|
125
|
-
}
|
|
126
206
|
|
|
127
|
-
|
|
207
|
+
return rolledBack;
|
|
208
|
+
} finally {
|
|
209
|
+
if (!dryRun) {
|
|
210
|
+
await this.tracker.releaseLock();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
128
213
|
}
|
|
129
214
|
|
|
130
215
|
/**
|
|
@@ -187,20 +272,6 @@ export class MigrationRunner {
|
|
|
187
272
|
* Create migration context
|
|
188
273
|
*/
|
|
189
274
|
private createContext(): MigrationContext {
|
|
190
|
-
// Import DynamoDB commands
|
|
191
|
-
const {
|
|
192
|
-
ScanCommand,
|
|
193
|
-
QueryCommand,
|
|
194
|
-
GetCommand,
|
|
195
|
-
PutCommand,
|
|
196
|
-
UpdateCommand,
|
|
197
|
-
DeleteCommand,
|
|
198
|
-
BatchGetCommand,
|
|
199
|
-
BatchWriteCommand,
|
|
200
|
-
TransactWriteCommand,
|
|
201
|
-
TransactGetCommand,
|
|
202
|
-
} = require('@aws-sdk/lib-dynamodb');
|
|
203
|
-
|
|
204
275
|
return {
|
|
205
276
|
client: this.client,
|
|
206
277
|
tableName: this.config.tableName,
|