@ftschopp/dynatable-migrations 1.2.6 → 1.3.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +12 -0
  3. package/dist/cli.js +21 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/create.js +1 -1
  6. package/dist/commands/create.js.map +1 -1
  7. package/dist/commands/down.d.ts +1 -1
  8. package/dist/commands/down.d.ts.map +1 -1
  9. package/dist/commands/down.js +61 -1
  10. package/dist/commands/down.js.map +1 -1
  11. package/dist/commands/unlock.d.ts +8 -0
  12. package/dist/commands/unlock.d.ts.map +1 -0
  13. package/dist/commands/unlock.js +87 -0
  14. package/dist/commands/unlock.js.map +1 -0
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +14 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/loader.d.ts +7 -1
  19. package/dist/core/loader.d.ts.map +1 -1
  20. package/dist/core/loader.js +35 -6
  21. package/dist/core/loader.js.map +1 -1
  22. package/dist/core/tracker.d.ts.map +1 -1
  23. package/dist/core/tracker.js +23 -12
  24. package/dist/core/tracker.js.map +1 -1
  25. package/package.json +35 -4
  26. package/.gitkeep +0 -0
  27. package/jest.config.js +0 -8
  28. package/migrations/.gitkeep +0 -0
  29. package/src/cli-utils.test.ts +0 -45
  30. package/src/cli-utils.ts +0 -18
  31. package/src/cli.ts +0 -110
  32. package/src/commands/create.ts +0 -111
  33. package/src/commands/down.ts +0 -26
  34. package/src/commands/init.ts +0 -37
  35. package/src/commands/status.ts +0 -75
  36. package/src/commands/up.ts +0 -26
  37. package/src/core/client.ts +0 -24
  38. package/src/core/config.ts +0 -140
  39. package/src/core/errors.ts +0 -55
  40. package/src/core/loader.ts +0 -256
  41. package/src/core/lock-heartbeat.test.ts +0 -78
  42. package/src/core/lock-heartbeat.ts +0 -48
  43. package/src/core/runner.test.ts +0 -51
  44. package/src/core/runner.ts +0 -315
  45. package/src/core/semver.test.ts +0 -33
  46. package/src/core/semver.ts +0 -26
  47. package/src/core/tracker.test.ts +0 -281
  48. package/src/core/tracker.ts +0 -559
  49. package/src/index.ts +0 -25
  50. package/src/templates/migration.ts +0 -92
  51. package/src/types/index.ts +0 -220
  52. package/tsconfig.json +0 -19
package/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-migrations",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "DynamoDB migration tool for single table design with schema versioning",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
7
14
  "bin": {
8
15
  "dynatable-migrate": "./bin/dynatable-migrate.js"
9
16
  },
17
+ "files": [
18
+ "dist",
19
+ "bin",
20
+ "README.md",
21
+ "USAGE.md",
22
+ "CHANGELOG.md"
23
+ ],
10
24
  "publishConfig": {
11
25
  "access": "public"
12
26
  },
@@ -14,7 +28,9 @@
14
28
  "build": "tsc",
15
29
  "dev": "tsc --watch",
16
30
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
17
- "test": "jest"
31
+ "lint": "eslint src",
32
+ "test": "jest",
33
+ "prepublishOnly": "yarn build"
18
34
  },
19
35
  "keywords": [
20
36
  "dynamodb",
@@ -29,6 +45,7 @@
29
45
  "@types/jest": "^30.0.0",
30
46
  "@types/node": "^25.0.3",
31
47
  "aws-sdk-client-mock": "^4.1.0",
48
+ "eslint": "^9.39.2",
32
49
  "jest": "^30.2.0",
33
50
  "ts-jest": "^29.4.6",
34
51
  "typescript": "5.9.3"
@@ -36,7 +53,21 @@
36
53
  "dependencies": {
37
54
  "@aws-sdk/client-dynamodb": "^3.965.0",
38
55
  "@aws-sdk/lib-dynamodb": "^3.965.0",
39
- "commander": "^14.0.2",
40
- "ts-node": "^10.9.2"
56
+ "commander": "^14.0.2"
57
+ },
58
+ "peerDependencies": {
59
+ "ts-node": "^10.9.0",
60
+ "typescript": ">=4.5"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "ts-node": {
64
+ "optional": true
65
+ },
66
+ "typescript": {
67
+ "optional": true
68
+ }
69
+ },
70
+ "engines": {
71
+ "node": ">=22"
41
72
  }
42
73
  }
package/.gitkeep DELETED
File without changes
package/jest.config.js DELETED
@@ -1,8 +0,0 @@
1
- /** @type {import('jest').Config} */
2
- module.exports = {
3
- preset: 'ts-jest',
4
- testEnvironment: 'node',
5
- testMatch: ['**/?(*.)+(test).[tj]s'],
6
- testPathIgnorePatterns: ['/node_modules/', '/dist/'],
7
- roots: ['<rootDir>/src'],
8
- };
File without changes
@@ -1,45 +0,0 @@
1
- import { parsePositiveInt } from './cli-utils';
2
-
3
- describe('parsePositiveInt', () => {
4
- const parser = parsePositiveInt('limit');
5
-
6
- test('accepts positive integers', () => {
7
- expect(parser('1')).toBe(1);
8
- expect(parser('42')).toBe(42);
9
- expect(parser('1000')).toBe(1000);
10
- });
11
-
12
- test('trims surrounding whitespace before parsing', () => {
13
- expect(parser(' 5 ')).toBe(5);
14
- });
15
-
16
- test('rejects 0', () => {
17
- expect(() => parser('0')).toThrow(/--limit must be a positive integer/);
18
- });
19
-
20
- test('rejects negative numbers', () => {
21
- expect(() => parser('-5')).toThrow(/--limit must be a positive integer/);
22
- });
23
-
24
- test('rejects non-numeric input', () => {
25
- expect(() => parser('abc')).toThrow(/--limit must be a positive integer/);
26
- });
27
-
28
- test('rejects floats (1.5 would parseInt to 1, but the trailing decimals are still wrong)', () => {
29
- expect(() => parser('1.5')).toThrow(/--limit must be a positive integer/);
30
- });
31
-
32
- test('rejects empty string', () => {
33
- expect(() => parser('')).toThrow(/--limit must be a positive integer/);
34
- });
35
-
36
- test('rejects strings with trailing junk that parseInt would silently swallow', () => {
37
- // parseInt('5abc', 10) === 5, but we should refuse it because the input
38
- // is clearly not a positive integer.
39
- expect(() => parser('5abc')).toThrow(/--limit must be a positive integer/);
40
- });
41
-
42
- test('error mentions the flag name passed to the factory', () => {
43
- expect(() => parsePositiveInt('steps')('-1')).toThrow(/--steps/);
44
- });
45
- });
package/src/cli-utils.ts DELETED
@@ -1,18 +0,0 @@
1
- import { InvalidArgumentError } from 'commander';
2
-
3
- /**
4
- * Commander value parser that accepts only positive integers. Used for
5
- * `--limit` and `--steps` so `0`, negative numbers, and non-numeric input
6
- * fail at parse time with a clean usage error instead of silently feeding
7
- * `Array.prototype.slice` and producing surprising rollbacks.
8
- */
9
- export function parsePositiveInt(name: string): (raw: string) => number {
10
- return (raw: string) => {
11
- const trimmed = raw.trim();
12
- const n = Number.parseInt(trimmed, 10);
13
- if (!Number.isInteger(n) || n <= 0 || String(n) !== trimmed) {
14
- throw new InvalidArgumentError(`--${name} must be a positive integer (got "${raw}").`);
15
- }
16
- return n;
17
- };
18
- }
package/src/cli.ts DELETED
@@ -1,110 +0,0 @@
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
- import { parsePositiveInt } from './cli-utils';
11
-
12
- // Read version from package.json
13
- const packageJson = require('../package.json');
14
-
15
- const program = new Command();
16
-
17
- program
18
- .name('dynatable-migrate')
19
- .description('DynamoDB migration tool for single table design')
20
- .version(packageJson.version);
21
-
22
- // Init command
23
- program
24
- .command('init')
25
- .description('Initialize migrations in current project')
26
- .action(async () => {
27
- try {
28
- await initProject();
29
- } catch (error: any) {
30
- console.error(`Error: ${error.message}`);
31
- process.exit(1);
32
- }
33
- });
34
-
35
- // Create command
36
- program
37
- .command('create <name>')
38
- .description('Create a new migration file')
39
- .option('-c, --config <path>', 'Path to config file')
40
- .option('-t, --type <type>', 'Version bump type: major, minor, or patch (default: patch)')
41
- .option('-e, --explicit <version>', 'Explicit version (e.g., 2.0.0)')
42
- .action(async (name: string, options) => {
43
- try {
44
- let migrationsDir = './migrations';
45
-
46
- // Try to load config to get migrations directory
47
- try {
48
- const config = await loadConfig(options.config);
49
- migrationsDir = config.migrationsDir || './migrations';
50
- } catch {
51
- // If config doesn't exist, use default
52
- }
53
-
54
- await createMigration(name, migrationsDir, options.type, options.explicit);
55
- } catch (error: any) {
56
- console.error(`Error: ${error.message}`);
57
- process.exit(1);
58
- }
59
- });
60
-
61
- // Up command
62
- program
63
- .command('up')
64
- .description('Run pending migrations')
65
- .option('-c, --config <path>', 'Path to config file')
66
- .option('-l, --limit <number>', 'Limit number of migrations to run', parsePositiveInt('limit'))
67
- .option('-d, --dry-run', 'Show what would be done without making changes')
68
- .action(async (options) => {
69
- try {
70
- const config = await loadConfig(options.config);
71
- await runMigrations(config, options.limit, options.dryRun);
72
- } catch (error: any) {
73
- console.error(`Error: ${error.message}`);
74
- process.exit(1);
75
- }
76
- });
77
-
78
- // Down command
79
- program
80
- .command('down')
81
- .description('Rollback migrations')
82
- .option('-c, --config <path>', 'Path to config file')
83
- .option('-s, --steps <number>', 'Number of migrations to rollback', parsePositiveInt('steps'), 1)
84
- .option('-d, --dry-run', 'Show what would be done without making changes')
85
- .action(async (options) => {
86
- try {
87
- const config = await loadConfig(options.config);
88
- await rollbackMigrations(config, options.steps, options.dryRun);
89
- } catch (error: any) {
90
- console.error(`Error: ${error.message}`);
91
- process.exit(1);
92
- }
93
- });
94
-
95
- // Status command
96
- program
97
- .command('status')
98
- .description('Show migration status')
99
- .option('-c, --config <path>', 'Path to config file')
100
- .action(async (options) => {
101
- try {
102
- const config = await loadConfig(options.config);
103
- await showStatus(config);
104
- } catch (error: any) {
105
- console.error(`Error: ${error.message}`);
106
- process.exit(1);
107
- }
108
- });
109
-
110
- program.parse(process.argv);
@@ -1,111 +0,0 @@
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
- const lastMigration = migrations.length > 0 ? migrations[migrations.length - 1] : null;
15
- const lastVersion = lastMigration?.version;
16
- const parts = lastVersion?.split('.');
17
-
18
- if (!parts || parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
19
- switch (bumpType) {
20
- case 'major':
21
- return '1.0.0';
22
- case 'minor':
23
- return '0.1.0';
24
- case 'patch':
25
- return '0.0.1';
26
- default:
27
- return '0.1.0';
28
- }
29
- }
30
-
31
- const major = parseInt(parts[0], 10);
32
- const minor = parseInt(parts[1], 10);
33
- const patch = parseInt(parts[2], 10);
34
-
35
- switch (bumpType) {
36
- case 'major':
37
- return `${major + 1}.0.0`;
38
- case 'minor':
39
- return `${major}.${minor + 1}.0`;
40
- case 'patch':
41
- return `${major}.${minor}.${patch + 1}`;
42
- default:
43
- return `${major}.${minor}.${patch + 1}`;
44
- }
45
- }
46
-
47
- export async function createMigration(
48
- name: string,
49
- migrationsDir: string = './migrations',
50
- bumpType?: string,
51
- explicitVersion?: string
52
- ): Promise<void> {
53
- // Ensure migrations directory exists
54
- if (!fs.existsSync(migrationsDir)) {
55
- fs.mkdirSync(migrationsDir, { recursive: true });
56
- console.log(`📁 Created migrations directory: ${migrationsDir}`);
57
- }
58
-
59
- // Generate version
60
- const loader = new MigrationLoader(migrationsDir);
61
- let version: string;
62
-
63
- if (explicitVersion) {
64
- // Validate explicit version is semver
65
- if (!/^\d+\.\d+\.\d+$/.test(explicitVersion)) {
66
- throw new Error(
67
- `Invalid version format: ${explicitVersion}. Expected semver format (e.g., 1.0.0)`
68
- );
69
- }
70
- version = explicitVersion;
71
- } else if (bumpType) {
72
- // Validate bump type
73
- const validTypes: VersionBumpType[] = ['major', 'minor', 'patch'];
74
- if (!validTypes.includes(bumpType as VersionBumpType)) {
75
- throw new Error(`Invalid bump type: ${bumpType}. Expected: major, minor, or patch`);
76
- }
77
- version = await getNextVersion(loader, bumpType as VersionBumpType);
78
- } else {
79
- // Default: increment patch
80
- version = await getNextVersion(loader, 'patch');
81
- }
82
-
83
- // Sanitize name (replace spaces and special chars with underscores)
84
- const sanitizedName = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
85
-
86
- // Generate file name
87
- const fileName = `${version}_${sanitizedName}.ts`;
88
- const filePath = path.join(migrationsDir, fileName);
89
-
90
- // Check if file already exists
91
- if (fs.existsSync(filePath)) {
92
- throw new Error(`Migration file already exists: ${filePath}`);
93
- }
94
-
95
- // Generate migration content
96
- const content = generateMigrationTemplate(version, sanitizedName);
97
-
98
- // Write file
99
- fs.writeFileSync(filePath, content, 'utf-8');
100
-
101
- // Determine version type for informational message
102
- const versionType = bumpType ? bumpType.toUpperCase() : explicitVersion ? 'EXPLICIT' : 'PATCH';
103
-
104
- console.log(`\n✅ Created migration: ${fileName}`);
105
- console.log(` Path: ${filePath}`);
106
- console.log(` Version: ${version} (${versionType})\n`);
107
- console.log(`Next steps:`);
108
- console.log(` 1. Edit ${fileName}`);
109
- console.log(` 2. Implement up() and down() functions`);
110
- console.log(` 3. Run: dynatable-migrate up\n`);
111
- }
@@ -1,26 +0,0 @@
1
- import { MigrationRunner } from '../core/runner';
2
- import { createDynamoDBClient } from '../core/client';
3
- import { MigrationConfig } from '../types';
4
-
5
- export async function rollbackMigrations(
6
- config: MigrationConfig,
7
- steps: number = 1,
8
- dryRun: boolean = false
9
- ): Promise<void> {
10
- const client = createDynamoDBClient(config);
11
- const runner = new MigrationRunner(client, config);
12
-
13
- try {
14
- const rolledBack = await runner.down(steps, dryRun);
15
-
16
- if (!dryRun && rolledBack.length > 0) {
17
- console.log(`\n🎉 Successfully rolled back ${rolledBack.length} migration(s)`);
18
-
19
- const currentVersion = await runner.getCurrentVersion();
20
- console.log(`📌 Current version: ${currentVersion}\n`);
21
- }
22
- } catch (error: any) {
23
- console.error(`\n❌ Rollback failed: ${error.message}\n`);
24
- process.exit(1);
25
- }
26
- }
@@ -1,37 +0,0 @@
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
- }
@@ -1,75 +0,0 @@
1
- import { MigrationRunner } from '../core/runner';
2
- import { createDynamoDBClient } from '../core/client';
3
- import { MigrationConfig } from '../types';
4
-
5
- export async function showStatus(config: MigrationConfig): Promise<void> {
6
- const client = createDynamoDBClient(config);
7
- const runner = new MigrationRunner(client, config);
8
-
9
- try {
10
- const statuses = await runner.status();
11
- const currentVersion = await runner.getCurrentVersion();
12
-
13
- console.log(`\n📊 Migration Status\n`);
14
- console.log(`Table: ${config.tableName}`);
15
- console.log(`Current version: ${currentVersion || 'v0000 (no migrations)'}`);
16
- console.log(`Migrations directory: ${config.migrationsDir || './migrations'}\n`);
17
-
18
- if (statuses.length === 0) {
19
- console.log('No migrations found.\n');
20
- return;
21
- }
22
-
23
- // Group by status
24
- const pending = statuses.filter((s) => s.status === 'pending');
25
- const applied = statuses.filter((s) => s.status === 'applied');
26
- const failed = statuses.filter((s) => s.status === 'failed');
27
- const rolledBack = statuses.filter((s) => s.status === 'rolled_back');
28
-
29
- // Show applied migrations
30
- if (applied.length > 0) {
31
- console.log(`✅ Applied (${applied.length}):`);
32
- for (const status of applied) {
33
- const date = status.appliedAt ? new Date(status.appliedAt).toLocaleString() : '';
34
- console.log(` ${status.version} - ${status.name} (${date})`);
35
- }
36
- console.log();
37
- }
38
-
39
- // Show pending migrations
40
- if (pending.length > 0) {
41
- console.log(`⏳ Pending (${pending.length}):`);
42
- for (const status of pending) {
43
- console.log(` ${status.version} - ${status.name}`);
44
- }
45
- console.log();
46
- }
47
-
48
- // Show rolled back migrations
49
- if (rolledBack.length > 0) {
50
- console.log(`⬅️ Rolled Back (${rolledBack.length}):`);
51
- for (const status of rolledBack) {
52
- console.log(` ${status.version} - ${status.name}`);
53
- }
54
- console.log();
55
- }
56
-
57
- // Show failed migrations
58
- if (failed.length > 0) {
59
- console.log(`❌ Failed (${failed.length}):`);
60
- for (const status of failed) {
61
- console.log(` ${status.version} - ${status.name}`);
62
- if (status.error) {
63
- console.log(` Error: ${status.error}`);
64
- }
65
- }
66
- console.log();
67
- }
68
-
69
- // Summary
70
- console.log(`Total: ${statuses.length} migration(s)\n`);
71
- } catch (error: any) {
72
- console.error(`\n❌ Failed to get status: ${error.message}\n`);
73
- process.exit(1);
74
- }
75
- }
@@ -1,26 +0,0 @@
1
- import { MigrationRunner } from '../core/runner';
2
- import { createDynamoDBClient } from '../core/client';
3
- import { MigrationConfig } from '../types';
4
-
5
- export async function runMigrations(
6
- config: MigrationConfig,
7
- limit?: number,
8
- dryRun: boolean = false
9
- ): Promise<void> {
10
- const client = createDynamoDBClient(config);
11
- const runner = new MigrationRunner(client, config);
12
-
13
- try {
14
- const executed = await runner.up({ limit, dryRun });
15
-
16
- if (!dryRun && executed.length > 0) {
17
- console.log(`\n🎉 Successfully applied ${executed.length} migration(s)`);
18
-
19
- const currentVersion = await runner.getCurrentVersion();
20
- console.log(`📌 Current version: ${currentVersion}\n`);
21
- }
22
- } catch (error: any) {
23
- console.error(`\n❌ Migration failed: ${error.message}\n`);
24
- process.exit(1);
25
- }
26
- }
@@ -1,24 +0,0 @@
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
- }