@ftschopp/dynatable-migrations 1.0.0 → 1.2.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/CHANGELOG.md +14 -0
- package/README.md +102 -134
- package/USAGE.md +199 -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 +88 -46
- package/src/core/runner.ts +151 -78
- package/src/core/tracker.ts +306 -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,96 @@ 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(
|
|
95
|
+
`Failed to load JavaScript migration ${filePath}: ${error.message}`
|
|
96
|
+
);
|
|
90
97
|
}
|
|
98
|
+
}
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
const migration: Migration = module.migration || module.default;
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
if (!migration) {
|
|
103
|
+
console.warn(`Warning: No migration export found in ${filePath}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate migration structure
|
|
108
|
+
this.validateMigration(migration, filePath);
|
|
109
|
+
|
|
110
|
+
const fileName = path.basename(filePath);
|
|
111
|
+
const { version, name } = this.parseMigrationFileName(fileName);
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
// Calculate checksum
|
|
114
|
+
const checksum = this.calculateChecksum(absolutePath);
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
return {
|
|
117
|
+
version,
|
|
118
|
+
name,
|
|
119
|
+
filePath,
|
|
120
|
+
migration,
|
|
121
|
+
checksum,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Register ts-node for TypeScript file loading
|
|
127
|
+
*/
|
|
128
|
+
private registerTsNode(): void {
|
|
129
|
+
try {
|
|
130
|
+
// Check if ts-node is already registered
|
|
131
|
+
const tsNodeSymbol = Symbol.for('ts-node.register.instance');
|
|
132
|
+
if ((process as any)[tsNodeSymbol]) {
|
|
133
|
+
return; // Already registered
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
require('ts-node').register({
|
|
137
|
+
transpileOnly: true,
|
|
138
|
+
skipProject: true, // Don't use project tsconfig
|
|
139
|
+
compilerOptions: {
|
|
140
|
+
module: 'commonjs',
|
|
141
|
+
target: 'ES2020',
|
|
142
|
+
esModuleInterop: true,
|
|
143
|
+
moduleResolution: 'node',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
111
146
|
} catch (error: any) {
|
|
112
|
-
|
|
147
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'ts-node is required to load TypeScript migrations. ' +
|
|
150
|
+
'Install it with: npm install ts-node'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// Other errors might mean ts-node is already registered differently
|
|
154
|
+
console.warn(`Warning: Could not register ts-node: ${error.message}`);
|
|
113
155
|
}
|
|
114
156
|
}
|
|
115
157
|
|
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,175 @@ 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
|
+
}
|
|
36
61
|
|
|
37
|
-
|
|
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
|
+
}
|
|
38
78
|
|
|
39
|
-
|
|
40
|
-
if (limit) {
|
|
41
|
-
pendingMigrations = pendingMigrations.slice(0, limit);
|
|
42
|
-
}
|
|
79
|
+
let pendingMigrations = await this.loader.getPendingMigrations(appliedVersions);
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
// Apply limit if specified
|
|
82
|
+
if (limit) {
|
|
83
|
+
pendingMigrations = pendingMigrations.slice(0, limit);
|
|
84
|
+
}
|
|
48
85
|
|
|
49
|
-
|
|
86
|
+
if (pendingMigrations.length === 0) {
|
|
87
|
+
console.log('✅ No pending migrations');
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
50
90
|
|
|
51
|
-
|
|
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
|
+
}
|
|
52
102
|
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
console.log(`⬆️ Applying ${migrationFile.version}: ${migrationFile.name}`);
|
|
103
|
+
console.log(`\n📦 Found ${pendingMigrations.length} pending migration(s)\n`);
|
|
56
104
|
|
|
57
|
-
|
|
58
|
-
await migrationFile.migration.up(context);
|
|
105
|
+
const executed: MigrationFile[] = [];
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
migrationFile.name
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
for (const migrationFile of pendingMigrations) {
|
|
108
|
+
try {
|
|
109
|
+
console.log(`⬆️ Applying ${migrationFile.version}: ${migrationFile.name}`);
|
|
110
|
+
|
|
111
|
+
const context = this.createContext();
|
|
112
|
+
await migrationFile.migration.up(context);
|
|
65
113
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
114
|
+
await this.tracker.markAsApplied(
|
|
115
|
+
migrationFile.version,
|
|
116
|
+
migrationFile.name,
|
|
117
|
+
migrationFile.migration.schema,
|
|
118
|
+
undefined,
|
|
119
|
+
migrationFile.checksum
|
|
120
|
+
);
|
|
70
121
|
|
|
71
|
-
|
|
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`);
|
|
72
126
|
|
|
73
|
-
|
|
127
|
+
await this.tracker.markAsFailed(migrationFile.version, error.message);
|
|
128
|
+
|
|
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
|
-
|
|
93
|
-
|
|
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(
|
|
153
|
+
'Could not acquire migration lock. Another migration may be in progress.'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
95
156
|
}
|
|
96
157
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
try {
|
|
159
|
+
const appliedMigrations = await this.tracker.getAppliedMigrations();
|
|
160
|
+
const applied = appliedMigrations
|
|
161
|
+
.filter((m) => m.status === 'applied')
|
|
162
|
+
.sort((a, b) => b.version.localeCompare(a.version))
|
|
163
|
+
.slice(0, steps);
|
|
100
164
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
if (applied.length === 0) {
|
|
166
|
+
console.log('✅ No migrations to rollback');
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
104
169
|
|
|
105
|
-
|
|
106
|
-
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
console.log(`\n🔍 DRY RUN - Would rollback ${applied.length} migration(s):\n`);
|
|
172
|
+
for (const record of applied) {
|
|
173
|
+
console.log(` ${record.version}: ${record.name}`);
|
|
107
174
|
}
|
|
175
|
+
console.log('\nNo changes were made.\n');
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
108
178
|
|
|
109
|
-
|
|
179
|
+
console.log(`\n📦 Rolling back ${applied.length} migration(s)\n`);
|
|
110
180
|
|
|
111
|
-
|
|
112
|
-
await migrationFile.migration.down(context);
|
|
181
|
+
const rolledBack: MigrationFile[] = [];
|
|
113
182
|
|
|
114
|
-
|
|
183
|
+
for (const record of applied) {
|
|
184
|
+
try {
|
|
185
|
+
const migrationFile = await this.loader.getMigration(record.version);
|
|
115
186
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.error(`❌ Failed to rollback ${record.version}: ${error.message}\n`);
|
|
187
|
+
if (!migrationFile) {
|
|
188
|
+
throw new Error(`Migration file not found for version ${record.version}`);
|
|
189
|
+
}
|
|
120
190
|
|
|
121
|
-
|
|
191
|
+
console.log(`⬇️ Rolling back ${migrationFile.version}: ${migrationFile.name}`);
|
|
122
192
|
|
|
123
|
-
|
|
193
|
+
const context = this.createContext();
|
|
194
|
+
await migrationFile.migration.down(context);
|
|
195
|
+
|
|
196
|
+
await this.tracker.markAsRolledBack(migrationFile.version);
|
|
197
|
+
|
|
198
|
+
console.log(`✅ Rolled back ${migrationFile.version}: ${migrationFile.name}\n`);
|
|
199
|
+
rolledBack.push(migrationFile);
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
console.error(`❌ Failed to rollback ${record.version}: ${error.message}\n`);
|
|
202
|
+
|
|
203
|
+
await this.tracker.markAsFailed(record.version, error.message);
|
|
204
|
+
|
|
205
|
+
throw new Error(`Rollback of ${record.version} failed: ${error.message}`);
|
|
206
|
+
}
|
|
124
207
|
}
|
|
125
|
-
}
|
|
126
208
|
|
|
127
|
-
|
|
209
|
+
return rolledBack;
|
|
210
|
+
} finally {
|
|
211
|
+
if (!dryRun) {
|
|
212
|
+
await this.tracker.releaseLock();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
128
215
|
}
|
|
129
216
|
|
|
130
217
|
/**
|
|
@@ -187,20 +274,6 @@ export class MigrationRunner {
|
|
|
187
274
|
* Create migration context
|
|
188
275
|
*/
|
|
189
276
|
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
277
|
return {
|
|
205
278
|
client: this.client,
|
|
206
279
|
tableName: this.config.tableName,
|