@ftschopp/dynatable-migrations 1.0.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/.gitkeep +0 -0
- package/CHANGELOG.md +13 -0
- package/README.md +421 -0
- package/USAGE.md +557 -0
- package/bin/dynatable-migrate.js +30 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +123 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/down.d.ts +3 -0
- package/dist/commands/down.d.ts.map +1 -0
- package/dist/commands/down.js +37 -0
- package/dist/commands/down.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +84 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/up.d.ts +3 -0
- package/dist/commands/up.d.ts.map +1 -0
- package/dist/commands/up.js +37 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +160 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/loader.d.ts +47 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +226 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/runner.d.ts +38 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +166 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/tracker.d.ts +19 -0
- package/dist/core/tracker.d.ts.map +1 -0
- package/dist/core/tracker.js +172 -0
- package/dist/core/tracker.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/migration.d.ts +5 -0
- package/dist/templates/migration.d.ts.map +1 -0
- package/dist/templates/migration.js +96 -0
- package/dist/templates/migration.js.map +1 -0
- package/dist/types/index.d.ts +159 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/migrations/.gitkeep +0 -0
- package/package.json +43 -0
- package/src/cli.ts +106 -0
- package/src/commands/create.ts +110 -0
- package/src/commands/down.ts +42 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/status.ts +92 -0
- package/src/commands/up.ts +39 -0
- package/src/core/config.ts +140 -0
- package/src/core/loader.ts +233 -0
- package/src/core/runner.ts +223 -0
- package/src/core/tracker.ts +219 -0
- package/src/index.ts +23 -0
- package/src/templates/migration.ts +92 -0
- package/src/types/index.ts +196 -0
- package/tsconfig.json +19 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loadConfig } from './core/config';
|
|
5
|
+
import { createMigration } from './commands/create';
|
|
6
|
+
import { runMigrations } from './commands/up';
|
|
7
|
+
import { rollbackMigrations } from './commands/down';
|
|
8
|
+
import { showStatus } from './commands/status';
|
|
9
|
+
import { initProject } from './commands/init';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('dynatable-migrate')
|
|
15
|
+
.description('DynamoDB migration tool for single table design')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
|
|
18
|
+
// Init command
|
|
19
|
+
program
|
|
20
|
+
.command('init')
|
|
21
|
+
.description('Initialize migrations in current project')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await initProject();
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
console.error(`Error: ${error.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Create command
|
|
32
|
+
program
|
|
33
|
+
.command('create <name>')
|
|
34
|
+
.description('Create a new migration file')
|
|
35
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
36
|
+
.option('-t, --type <type>', 'Version bump type: major, minor, or patch (default: patch)')
|
|
37
|
+
.option('-e, --explicit <version>', 'Explicit version (e.g., 2.0.0)')
|
|
38
|
+
.action(async (name: string, options) => {
|
|
39
|
+
try {
|
|
40
|
+
let migrationsDir = './migrations';
|
|
41
|
+
|
|
42
|
+
// Try to load config to get migrations directory
|
|
43
|
+
try {
|
|
44
|
+
const config = await loadConfig(options.config);
|
|
45
|
+
migrationsDir = config.migrationsDir || './migrations';
|
|
46
|
+
} catch {
|
|
47
|
+
// If config doesn't exist, use default
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await createMigration(name, migrationsDir, options.type, options.explicit);
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
console.error(`Error: ${error.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Up command
|
|
58
|
+
program
|
|
59
|
+
.command('up')
|
|
60
|
+
.description('Run pending migrations')
|
|
61
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
62
|
+
.option('-l, --limit <number>', 'Limit number of migrations to run')
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
try {
|
|
65
|
+
const config = await loadConfig(options.config);
|
|
66
|
+
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
|
|
67
|
+
await runMigrations(config, limit);
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
console.error(`Error: ${error.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Down command
|
|
75
|
+
program
|
|
76
|
+
.command('down')
|
|
77
|
+
.description('Rollback migrations')
|
|
78
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
79
|
+
.option('-s, --steps <number>', 'Number of migrations to rollback', '1')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
try {
|
|
82
|
+
const config = await loadConfig(options.config);
|
|
83
|
+
const steps = parseInt(options.steps, 10);
|
|
84
|
+
await rollbackMigrations(config, steps);
|
|
85
|
+
} catch (error: any) {
|
|
86
|
+
console.error(`Error: ${error.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Status command
|
|
92
|
+
program
|
|
93
|
+
.command('status')
|
|
94
|
+
.description('Show migration status')
|
|
95
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
96
|
+
.action(async (options) => {
|
|
97
|
+
try {
|
|
98
|
+
const config = await loadConfig(options.config);
|
|
99
|
+
await showStatus(config);
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
console.error(`Error: ${error.message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { MigrationLoader } from '../core/loader';
|
|
4
|
+
import { generateMigrationTemplate } from '../templates/migration';
|
|
5
|
+
|
|
6
|
+
type VersionBumpType = 'major' | 'minor' | 'patch';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get next version based on bump type
|
|
10
|
+
*/
|
|
11
|
+
async function getNextVersion(loader: MigrationLoader, bumpType: VersionBumpType): Promise<string> {
|
|
12
|
+
const migrations = await loader['loadMigrations']();
|
|
13
|
+
|
|
14
|
+
if (migrations.length === 0) {
|
|
15
|
+
return '0.1.0';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const lastMigration = migrations[migrations.length - 1];
|
|
19
|
+
if (!lastMigration) {
|
|
20
|
+
return '0.1.0';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lastVersion = lastMigration.version;
|
|
24
|
+
const parts = lastVersion.split('.');
|
|
25
|
+
|
|
26
|
+
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
|
|
27
|
+
return '0.1.0';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const major = parseInt(parts[0], 10);
|
|
31
|
+
const minor = parseInt(parts[1], 10);
|
|
32
|
+
const patch = parseInt(parts[2], 10);
|
|
33
|
+
|
|
34
|
+
switch (bumpType) {
|
|
35
|
+
case 'major':
|
|
36
|
+
return `${major + 1}.0.0`;
|
|
37
|
+
case 'minor':
|
|
38
|
+
return `${major}.${minor + 1}.0`;
|
|
39
|
+
case 'patch':
|
|
40
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
41
|
+
default:
|
|
42
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createMigration(
|
|
47
|
+
name: string,
|
|
48
|
+
migrationsDir: string = './migrations',
|
|
49
|
+
bumpType?: string,
|
|
50
|
+
explicitVersion?: string
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
// Ensure migrations directory exists
|
|
53
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
54
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
55
|
+
console.log(`š Created migrations directory: ${migrationsDir}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generate version
|
|
59
|
+
const loader = new MigrationLoader(migrationsDir);
|
|
60
|
+
let version: string;
|
|
61
|
+
|
|
62
|
+
if (explicitVersion) {
|
|
63
|
+
// Validate explicit version is semver
|
|
64
|
+
if (!/^\d+\.\d+\.\d+$/.test(explicitVersion)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Invalid version format: ${explicitVersion}. Expected semver format (e.g., 1.0.0)`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
version = explicitVersion;
|
|
70
|
+
} else if (bumpType) {
|
|
71
|
+
// Validate bump type
|
|
72
|
+
const validTypes: VersionBumpType[] = ['major', 'minor', 'patch'];
|
|
73
|
+
if (!validTypes.includes(bumpType as VersionBumpType)) {
|
|
74
|
+
throw new Error(`Invalid bump type: ${bumpType}. Expected: major, minor, or patch`);
|
|
75
|
+
}
|
|
76
|
+
version = await getNextVersion(loader, bumpType as VersionBumpType);
|
|
77
|
+
} else {
|
|
78
|
+
// Default: increment patch
|
|
79
|
+
version = await getNextVersion(loader, 'patch');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sanitize name (replace spaces and special chars with underscores)
|
|
83
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
84
|
+
|
|
85
|
+
// Generate file name
|
|
86
|
+
const fileName = `${version}_${sanitizedName}.ts`;
|
|
87
|
+
const filePath = path.join(migrationsDir, fileName);
|
|
88
|
+
|
|
89
|
+
// Check if file already exists
|
|
90
|
+
if (fs.existsSync(filePath)) {
|
|
91
|
+
throw new Error(`Migration file already exists: ${filePath}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Generate migration content
|
|
95
|
+
const content = generateMigrationTemplate(version, sanitizedName);
|
|
96
|
+
|
|
97
|
+
// Write file
|
|
98
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
99
|
+
|
|
100
|
+
// Determine version type for informational message
|
|
101
|
+
const versionType = bumpType ? bumpType.toUpperCase() : explicitVersion ? 'EXPLICIT' : 'PATCH';
|
|
102
|
+
|
|
103
|
+
console.log(`\nā
Created migration: ${fileName}`);
|
|
104
|
+
console.log(` Path: ${filePath}`);
|
|
105
|
+
console.log(` Version: ${version} (${versionType})\n`);
|
|
106
|
+
console.log(`Next steps:`);
|
|
107
|
+
console.log(` 1. Edit ${fileName}`);
|
|
108
|
+
console.log(` 2. Implement up() and down() functions`);
|
|
109
|
+
console.log(` 3. Run: dynatable-migrate up\n`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { MigrationRunner } from '../core/runner';
|
|
4
|
+
import { MigrationConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
export async function rollbackMigrations(
|
|
7
|
+
config: MigrationConfig,
|
|
8
|
+
steps: number = 1
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
// Create DynamoDB client
|
|
11
|
+
const ddbClient = new DynamoDBClient({
|
|
12
|
+
region: config.client.region,
|
|
13
|
+
endpoint: config.client.endpoint,
|
|
14
|
+
credentials: config.client.credentials,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const client = DynamoDBDocumentClient.from(ddbClient, {
|
|
18
|
+
marshallOptions: {
|
|
19
|
+
removeUndefinedValues: true,
|
|
20
|
+
convertClassInstanceToMap: true,
|
|
21
|
+
},
|
|
22
|
+
unmarshallOptions: {
|
|
23
|
+
wrapNumbers: false,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const runner = new MigrationRunner(client, config);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const rolledBack = await runner.down(steps);
|
|
31
|
+
|
|
32
|
+
if (rolledBack.length > 0) {
|
|
33
|
+
console.log(`\nš Successfully rolled back ${rolledBack.length} migration(s)`);
|
|
34
|
+
|
|
35
|
+
const currentVersion = await runner.getCurrentVersion();
|
|
36
|
+
console.log(`š Current version: ${currentVersion}\n`);
|
|
37
|
+
}
|
|
38
|
+
} catch (error: any) {
|
|
39
|
+
console.error(`\nā Rollback failed: ${error.message}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ConfigLoader } from '../core/config';
|
|
4
|
+
|
|
5
|
+
export async function initProject(): Promise<void> {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
console.log('\nš Initializing DynamoDB migrations\n');
|
|
9
|
+
|
|
10
|
+
// Create migrations directory
|
|
11
|
+
const migrationsDir = path.join(cwd, 'migrations');
|
|
12
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
13
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
14
|
+
console.log(`ā
Created: ${migrationsDir}/`);
|
|
15
|
+
|
|
16
|
+
// Create .gitkeep
|
|
17
|
+
fs.writeFileSync(path.join(migrationsDir, '.gitkeep'), '');
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`ā ļø Directory already exists: ${migrationsDir}/`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create config file
|
|
23
|
+
const configPath = path.join(cwd, 'dynatable.config.js');
|
|
24
|
+
if (!fs.existsSync(configPath)) {
|
|
25
|
+
ConfigLoader.createDefaultConfig(configPath);
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`ā ļø Config already exists: ${configPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('\nā
Initialization complete!\n');
|
|
31
|
+
console.log('Next steps:');
|
|
32
|
+
console.log(' 1. Edit dynatable.config.js with your DynamoDB settings');
|
|
33
|
+
console.log(' 2. Create your first migration:');
|
|
34
|
+
console.log(' dynatable-migrate create add_user_email');
|
|
35
|
+
console.log(' 3. Run migrations:');
|
|
36
|
+
console.log(' dynatable-migrate up\n');
|
|
37
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { MigrationRunner } from '../core/runner';
|
|
4
|
+
import { MigrationConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
export async function showStatus(config: MigrationConfig): Promise<void> {
|
|
7
|
+
// Create DynamoDB client
|
|
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
|
+
|
|
24
|
+
const runner = new MigrationRunner(client, config);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const statuses = await runner.status();
|
|
28
|
+
const currentVersion = await runner.getCurrentVersion();
|
|
29
|
+
|
|
30
|
+
console.log(`\nš Migration Status\n`);
|
|
31
|
+
console.log(`Table: ${config.tableName}`);
|
|
32
|
+
console.log(`Current version: ${currentVersion || 'v0000 (no migrations)'}`);
|
|
33
|
+
console.log(`Migrations directory: ${config.migrationsDir || './migrations'}\n`);
|
|
34
|
+
|
|
35
|
+
if (statuses.length === 0) {
|
|
36
|
+
console.log('No migrations found.\n');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Group by status
|
|
41
|
+
const pending = statuses.filter((s) => s.status === 'pending');
|
|
42
|
+
const applied = statuses.filter((s) => s.status === 'applied');
|
|
43
|
+
const failed = statuses.filter((s) => s.status === 'failed');
|
|
44
|
+
const rolledBack = statuses.filter((s) => s.status === 'rolled_back');
|
|
45
|
+
|
|
46
|
+
// Show applied migrations
|
|
47
|
+
if (applied.length > 0) {
|
|
48
|
+
console.log(`ā
Applied (${applied.length}):`);
|
|
49
|
+
for (const status of applied) {
|
|
50
|
+
const date = status.appliedAt ? new Date(status.appliedAt).toLocaleString() : '';
|
|
51
|
+
console.log(` ${status.version} - ${status.name} (${date})`);
|
|
52
|
+
}
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Show pending migrations
|
|
57
|
+
if (pending.length > 0) {
|
|
58
|
+
console.log(`ā³ Pending (${pending.length}):`);
|
|
59
|
+
for (const status of pending) {
|
|
60
|
+
console.log(` ${status.version} - ${status.name}`);
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Show rolled back migrations
|
|
66
|
+
if (rolledBack.length > 0) {
|
|
67
|
+
console.log(`ā¬
ļø Rolled Back (${rolledBack.length}):`);
|
|
68
|
+
for (const status of rolledBack) {
|
|
69
|
+
console.log(` ${status.version} - ${status.name}`);
|
|
70
|
+
}
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Show failed migrations
|
|
75
|
+
if (failed.length > 0) {
|
|
76
|
+
console.log(`ā Failed (${failed.length}):`);
|
|
77
|
+
for (const status of failed) {
|
|
78
|
+
console.log(` ${status.version} - ${status.name}`);
|
|
79
|
+
if (status.error) {
|
|
80
|
+
console.log(` Error: ${status.error}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Summary
|
|
87
|
+
console.log(`Total: ${statuses.length} migration(s)\n`);
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error(`\nā Failed to get status: ${error.message}\n`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { MigrationRunner } from '../core/runner';
|
|
4
|
+
import { MigrationConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
export async function runMigrations(config: MigrationConfig, limit?: number): Promise<void> {
|
|
7
|
+
// Create DynamoDB client
|
|
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
|
+
|
|
24
|
+
const runner = new MigrationRunner(client, config);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const executed = await runner.up(limit);
|
|
28
|
+
|
|
29
|
+
if (executed.length > 0) {
|
|
30
|
+
console.log(`\nš Successfully applied ${executed.length} migration(s)`);
|
|
31
|
+
|
|
32
|
+
const currentVersion = await runner.getCurrentVersion();
|
|
33
|
+
console.log(`š Current version: ${currentVersion}\n`);
|
|
34
|
+
}
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
console.error(`\nā Migration failed: ${error.message}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { MigrationConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load migration configuration
|
|
7
|
+
*/
|
|
8
|
+
export class ConfigLoader {
|
|
9
|
+
private configPath: string;
|
|
10
|
+
|
|
11
|
+
constructor(configPath?: string) {
|
|
12
|
+
this.configPath = configPath || this.findConfigFile() || 'dynatable.config.js';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find config file in current directory or parent directories
|
|
17
|
+
*/
|
|
18
|
+
private findConfigFile(): string | null {
|
|
19
|
+
const configFileNames = [
|
|
20
|
+
'dynatable.config.js',
|
|
21
|
+
'dynatable.config.json',
|
|
22
|
+
'.dynatablerc.js',
|
|
23
|
+
'.dynatablerc.json',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let currentDir = process.cwd();
|
|
27
|
+
const root = path.parse(currentDir).root;
|
|
28
|
+
|
|
29
|
+
while (currentDir !== root) {
|
|
30
|
+
for (const fileName of configFileNames) {
|
|
31
|
+
const filePath = path.join(currentDir, fileName);
|
|
32
|
+
if (fs.existsSync(filePath)) {
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
currentDir = path.dirname(currentDir);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load configuration from file
|
|
45
|
+
*/
|
|
46
|
+
async load(): Promise<MigrationConfig> {
|
|
47
|
+
if (!fs.existsSync(this.configPath)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Configuration file not found: ${this.configPath}\n\nPlease create a dynatable.config.js file in your project root.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
let config: MigrationConfig;
|
|
55
|
+
|
|
56
|
+
if (this.configPath.endsWith('.json')) {
|
|
57
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
58
|
+
config = JSON.parse(content);
|
|
59
|
+
} else {
|
|
60
|
+
// JavaScript config file
|
|
61
|
+
const module = await import(this.configPath);
|
|
62
|
+
config = module.default || module;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.validateAndNormalizeConfig(config);
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
throw new Error(`Failed to load configuration: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate and normalize configuration
|
|
73
|
+
*/
|
|
74
|
+
private validateAndNormalizeConfig(config: Partial<MigrationConfig>): MigrationConfig {
|
|
75
|
+
if (!config.tableName) {
|
|
76
|
+
throw new Error("Configuration error: 'tableName' is required");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!config.client?.region) {
|
|
80
|
+
throw new Error("Configuration error: 'client.region' is required");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
tableName: config.tableName,
|
|
85
|
+
client: {
|
|
86
|
+
region: config.client.region,
|
|
87
|
+
endpoint: config.client.endpoint,
|
|
88
|
+
credentials: config.client.credentials,
|
|
89
|
+
},
|
|
90
|
+
migrationsDir: config.migrationsDir || './migrations',
|
|
91
|
+
trackingPrefix: config.trackingPrefix || '_SCHEMA#VERSION',
|
|
92
|
+
gsi1Name: config.gsi1Name || 'GSI1',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create default config file
|
|
98
|
+
*/
|
|
99
|
+
static createDefaultConfig(targetPath: string): void {
|
|
100
|
+
const defaultConfig = `module.exports = {
|
|
101
|
+
// DynamoDB table name
|
|
102
|
+
tableName: "MyTable",
|
|
103
|
+
|
|
104
|
+
// DynamoDB client configuration
|
|
105
|
+
client: {
|
|
106
|
+
region: "us-east-1",
|
|
107
|
+
|
|
108
|
+
// Optional: For local DynamoDB
|
|
109
|
+
endpoint: "http://localhost:8000",
|
|
110
|
+
|
|
111
|
+
// Optional: Credentials (use AWS_PROFILE or IAM role in production)
|
|
112
|
+
credentials: {
|
|
113
|
+
accessKeyId: "local",
|
|
114
|
+
secretAccessKey: "local",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Optional: Migrations directory (default: ./migrations)
|
|
119
|
+
migrationsDir: "./migrations",
|
|
120
|
+
|
|
121
|
+
// Optional: Tracking prefix in single table (default: _SCHEMA#VERSION)
|
|
122
|
+
trackingPrefix: "_SCHEMA#VERSION",
|
|
123
|
+
|
|
124
|
+
// Optional: GSI name for tracking (default: GSI1)
|
|
125
|
+
gsi1Name: "GSI1",
|
|
126
|
+
};
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
fs.writeFileSync(targetPath, defaultConfig, 'utf-8');
|
|
130
|
+
console.log(`ā
Created config file: ${targetPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get config from environment or file
|
|
136
|
+
*/
|
|
137
|
+
export async function loadConfig(configPath?: string): Promise<MigrationConfig> {
|
|
138
|
+
const loader = new ConfigLoader(configPath);
|
|
139
|
+
return loader.load();
|
|
140
|
+
}
|