stepwise-migrations 1.0.5 → 1.0.7
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/README.md +62 -5
- package/dist/db.js +1 -1
- package/dist/index.js +20 -3
- package/dist/migrate.js +35 -3
- package/dist/utils.js +47 -10
- package/package.json +1 -1
- package/src/db.ts +3 -2
- package/src/index.ts +51 -5
- package/src/migrate.ts +55 -2
- package/src/utils.ts +64 -7
package/README.md
CHANGED
@@ -4,16 +4,49 @@
|
|
4
4
|
|
5
5
|
A tool for managing Raw SQL migrations in a Postgres database.
|
6
6
|
Loosely based on flyway.
|
7
|
-
Only "up" migrations are supported so far, but what more do you need?
|
8
7
|
|
9
|
-
##
|
8
|
+
## Instructions
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
Name up migration files as `.sql` and down migration files with the same name but suffixed with `.down.sql`.
|
11
|
+
e.g. `v1_users.sql` and `v1_users.down.sql`.
|
12
|
+
NOTE: Down migrations are optional.
|
13
|
+
|
14
|
+
Up migrations are first sorted in ascending order based on filename.
|
13
15
|
No subdirectories are read below the migration directory.
|
14
16
|
|
15
17
|
## Usage
|
16
18
|
|
19
|
+
```
|
20
|
+
Usage: stepwise-migrations [command] [options]
|
21
|
+
|
22
|
+
Commands:
|
23
|
+
migrate
|
24
|
+
Migrate the database to the latest version
|
25
|
+
down
|
26
|
+
Rollback the database to the previous version
|
27
|
+
info
|
28
|
+
Show information about the current state of the migrations in the database
|
29
|
+
drop
|
30
|
+
Drop all tables, schema and migration history table
|
31
|
+
|
32
|
+
Options:
|
33
|
+
--connection <connection> The connection string to use to connect to the database
|
34
|
+
--schema <schema> The schema to use for the migrations
|
35
|
+
--path <path> The path to the migrations directory
|
36
|
+
--ssl true/false Whether to use SSL for the connection (default: false)
|
37
|
+
--nup Number of up migrations to apply (default: all)
|
38
|
+
--ndown Number of down migrations to apply (default: 1)
|
39
|
+
|
40
|
+
Example:
|
41
|
+
npx stepwise-migrations \
|
42
|
+
--connection=postgresql://postgres:postgres@127.0.0.1:5432/mydatabase \
|
43
|
+
--schema=myschema \
|
44
|
+
--path=./db/migration/ \
|
45
|
+
migrate
|
46
|
+
```
|
47
|
+
|
48
|
+
## Examples
|
49
|
+
|
17
50
|
### Migrate
|
18
51
|
|
19
52
|
Command:
|
@@ -36,8 +69,33 @@ Migration history table created
|
|
36
69
|
Found 2 migration files
|
37
70
|
Applied migration V0_01__connect_session_table.sql
|
38
71
|
Applied migration V0_02__auth.sql
|
72
|
+
All done!
|
73
|
+
```
|
74
|
+
|
75
|
+
### Down
|
76
|
+
|
77
|
+
Command:
|
78
|
+
|
79
|
+
```bash
|
80
|
+
npx stepwise-migrations down \
|
81
|
+
--connection=postgresql://postgres:postgres@127.0.0.1:5432/mydb \
|
82
|
+
--schema=myschema \
|
83
|
+
--path=./db/migration/
|
84
|
+
```
|
39
85
|
|
86
|
+
Outputs:
|
87
|
+
|
88
|
+
```
|
89
|
+
|
90
|
+
Connected to the database
|
91
|
+
Applied down migration v2_auth.down.sql
|
40
92
|
All done!
|
93
|
+
New migration history:
|
94
|
+
┌─────────┬────┬────────────────────────────────┬────────────────────────────────────────────────────────────────────┬────────────┬──────────────────────────────┐
|
95
|
+
│ (index) │ id │ name │ hash │ applied_by │ applied_at │
|
96
|
+
├─────────┼────┼────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┼──────────────────────────────┤
|
97
|
+
│ 0 │ 1 │ 'v1_connect_session_table.sql' │ 'f08638e58139ae0e2dda24b1bdba29f3f2128597066a23d2bb382d448bbe9d7e' │ 'postgres' │ '2024-11-23 18:13:36.518495' │
|
98
|
+
└─────────┴────┴────────────────────────────────┴────────────────────────────────────────────────────────────────────┴────────────┴──────────────────────────────┘
|
41
99
|
```
|
42
100
|
|
43
101
|
### Info
|
@@ -78,6 +136,5 @@ Outputs:
|
|
78
136
|
```
|
79
137
|
Connected to the database
|
80
138
|
Dropping the tables, schema and migration history table
|
81
|
-
|
82
139
|
All done!
|
83
140
|
```
|
package/dist/db.js
CHANGED
@@ -93,7 +93,7 @@ const dbCreateHistoryTable = (client, schema) => __awaiter(void 0, void 0, void
|
|
93
93
|
console.log(`Creating migration history table`);
|
94
94
|
yield client.query(`CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
|
95
95
|
id SERIAL PRIMARY KEY,
|
96
|
-
name TEXT NOT NULL,
|
96
|
+
name TEXT UNIQUE NOT NULL,
|
97
97
|
hash TEXT NOT NULL,
|
98
98
|
applied_by TEXT NOT NULL DEFAULT current_user,
|
99
99
|
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
package/dist/index.js
CHANGED
@@ -26,6 +26,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
26
|
const historySchemaExists = yield (0, db_1.dbHistorySchemaExists)(client, schema);
|
27
27
|
const tableExists = yield (0, db_1.dbTableExists)(client, schema);
|
28
28
|
if (command === "migrate") {
|
29
|
+
const nUp = argv.nup || Infinity;
|
29
30
|
if (!historySchemaExists) {
|
30
31
|
yield (0, db_1.dbCreateSchema)(client, schema);
|
31
32
|
}
|
@@ -36,11 +37,13 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
37
|
const migrationFiles = yield (0, utils_1.readMigrationFiles)(argv.path);
|
37
38
|
console.log(`Found ${migrationFiles.length} migration files`);
|
38
39
|
(0, migrate_1.validateMigrationFiles)(migrationFiles, migrationHistory);
|
39
|
-
const migrationsToApply = migrationFiles.slice(migrationHistory.length);
|
40
|
+
const migrationsToApply = migrationFiles.slice(migrationHistory.length, migrationHistory.length + nUp);
|
40
41
|
for (const { filename, contents, hash } of migrationsToApply) {
|
41
42
|
yield (0, migrate_1.applyMigration)(client, schema, filename, contents, hash);
|
42
43
|
}
|
43
|
-
console.log("
|
44
|
+
console.log("All done!");
|
45
|
+
console.log("New migration history:");
|
46
|
+
console.table(yield (0, db_1.dbMigrationHistory)(client, schema));
|
44
47
|
}
|
45
48
|
else if (command === "info") {
|
46
49
|
console.log("Showing information about the current state of the migrations in the database");
|
@@ -54,7 +57,21 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
57
|
else if (command === "drop") {
|
55
58
|
console.log("Dropping the tables, schema and migration history table");
|
56
59
|
yield client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
|
57
|
-
console.log("
|
60
|
+
console.log("All done!");
|
61
|
+
}
|
62
|
+
else if (command === "down") {
|
63
|
+
const nDown = argv.ndown || 1;
|
64
|
+
const migrationHistory = yield (0, db_1.dbMigrationHistory)(client, schema);
|
65
|
+
(0, migrate_1.validateMigrationFiles)(yield (0, utils_1.readMigrationFiles)(argv.path), migrationHistory, false);
|
66
|
+
const reverseMigrationHistory = migrationHistory.reverse().slice(0, nDown);
|
67
|
+
const downMigrationFilesToApply = yield (0, utils_1.readDownMigrationFiles)(argv.path, reverseMigrationHistory);
|
68
|
+
(0, migrate_1.validateDownMigrationFiles)(downMigrationFilesToApply, reverseMigrationHistory);
|
69
|
+
for (const { filename, contents, upFilename, } of downMigrationFilesToApply) {
|
70
|
+
yield (0, migrate_1.applyDownMigration)(client, schema, filename, contents, upFilename);
|
71
|
+
}
|
72
|
+
console.log("All done!");
|
73
|
+
console.log("New migration history:");
|
74
|
+
console.table(yield (0, db_1.dbMigrationHistory)(client, schema));
|
58
75
|
}
|
59
76
|
client.release();
|
60
77
|
process.exit(0);
|
package/dist/migrate.js
CHANGED
@@ -9,8 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
9
9
|
});
|
10
10
|
};
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
-
exports.applyMigration = exports.validateMigrationFiles = void 0;
|
13
|
-
const validateMigrationFiles = (migrationFiles, migrationHistory) => {
|
12
|
+
exports.applyDownMigration = exports.validateDownMigrationFiles = exports.applyMigration = exports.validateMigrationFiles = void 0;
|
13
|
+
const validateMigrationFiles = (migrationFiles, migrationHistory, isUp = true) => {
|
14
14
|
if (migrationFiles.length === 0) {
|
15
15
|
console.log("No migrations found");
|
16
16
|
process.exit(0);
|
@@ -19,7 +19,7 @@ const validateMigrationFiles = (migrationFiles, migrationHistory) => {
|
|
19
19
|
console.error("Error: migration history is longer than the number of migration files, aborting.");
|
20
20
|
process.exit(1);
|
21
21
|
}
|
22
|
-
if (migrationFiles.length === migrationHistory.length) {
|
22
|
+
if (migrationFiles.length === migrationHistory.length && isUp) {
|
23
23
|
console.log("All migrations are already applied");
|
24
24
|
process.exit(0);
|
25
25
|
}
|
@@ -60,3 +60,35 @@ const applyMigration = (client, schema, filename, contents, hash) => __awaiter(v
|
|
60
60
|
}
|
61
61
|
});
|
62
62
|
exports.applyMigration = applyMigration;
|
63
|
+
const validateDownMigrationFiles = (downMigrationFilesToApply, reverseMigrationHistory) => {
|
64
|
+
for (let i = 0; i < downMigrationFilesToApply.length; i++) {
|
65
|
+
const { filename } = downMigrationFilesToApply[i];
|
66
|
+
if (filename.split(".down.sql")[0] !==
|
67
|
+
reverseMigrationHistory[i].name.split(".sql")[0]) {
|
68
|
+
console.error(`Migration ${filename} does not match the expected migration ${reverseMigrationHistory[i].name}`);
|
69
|
+
process.exit(1);
|
70
|
+
}
|
71
|
+
}
|
72
|
+
};
|
73
|
+
exports.validateDownMigrationFiles = validateDownMigrationFiles;
|
74
|
+
const applyDownMigration = (client, schema, filename, contents, upFilename) => __awaiter(void 0, void 0, void 0, function* () {
|
75
|
+
try {
|
76
|
+
yield client.query("BEGIN");
|
77
|
+
yield client.query(`SET search_path TO ${schema};
|
78
|
+
${contents.toString()}`);
|
79
|
+
yield client.query(`DELETE FROM ${schema}.stepwise_migrations WHERE name = $1`, [upFilename]);
|
80
|
+
yield client.query("COMMIT");
|
81
|
+
console.log(`Applied down migration ${filename}`);
|
82
|
+
}
|
83
|
+
catch (error) {
|
84
|
+
try {
|
85
|
+
yield client.query("ROLLBACK");
|
86
|
+
}
|
87
|
+
catch (error) {
|
88
|
+
console.error("Error rolling back transaction", error);
|
89
|
+
}
|
90
|
+
console.error("Error applying down migration", error);
|
91
|
+
process.exit(1);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
exports.applyDownMigration = applyDownMigration;
|
package/dist/utils.js
CHANGED
@@ -12,21 +12,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
13
13
|
};
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
15
|
-
exports.readMigrationFiles = exports.validateArgs = exports.usage = exports.
|
15
|
+
exports.readDownMigrationFiles = exports.fileExists = exports.readMigrationFiles = exports.validateArgs = exports.usage = exports.calculateHash = void 0;
|
16
16
|
const crypto_1 = __importDefault(require("crypto"));
|
17
17
|
const promises_1 = __importDefault(require("fs/promises"));
|
18
18
|
const path_1 = __importDefault(require("path"));
|
19
|
-
const
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
exports.hashFile = hashFile;
|
19
|
+
const calculateHash = (contents) => {
|
20
|
+
return crypto_1.default.createHash("sha256").update(contents).digest("hex");
|
21
|
+
};
|
22
|
+
exports.calculateHash = calculateHash;
|
24
23
|
exports.usage = `
|
25
24
|
Usage: stepwise-migrations [command] [options]
|
26
25
|
|
27
26
|
Commands:
|
28
27
|
migrate
|
29
28
|
Migrate the database to the latest version
|
29
|
+
down
|
30
|
+
Rollback the database to the previous version
|
30
31
|
info
|
31
32
|
Show information about the current state of the migrations in the database
|
32
33
|
drop
|
@@ -37,6 +38,8 @@ Options:
|
|
37
38
|
--schema <schema> The schema to use for the migrations
|
38
39
|
--path <path> The path to the migrations directory
|
39
40
|
--ssl true/false Whether to use SSL for the connection (default: false)
|
41
|
+
--nup Number of up migrations to apply (default: all)
|
42
|
+
--ndown Number of down migrations to apply (default: 1)
|
40
43
|
|
41
44
|
Example:
|
42
45
|
npx stepwise-migrations \
|
@@ -57,7 +60,10 @@ const validateArgs = (argv) => {
|
|
57
60
|
console.log(exports.usage);
|
58
61
|
process.exit(1);
|
59
62
|
}
|
60
|
-
if (argv._[0] !== "migrate" &&
|
63
|
+
if (argv._[0] !== "migrate" &&
|
64
|
+
argv._[0] !== "info" &&
|
65
|
+
argv._[0] !== "drop" &&
|
66
|
+
argv._[0] !== "down") {
|
61
67
|
console.error(`Invalid command: ${argv._[0]}`);
|
62
68
|
console.log(exports.usage);
|
63
69
|
process.exit(1);
|
@@ -67,20 +73,51 @@ exports.validateArgs = validateArgs;
|
|
67
73
|
const readMigrationFiles = (directory) => __awaiter(void 0, void 0, void 0, function* () {
|
68
74
|
const files = yield promises_1.default.readdir(directory, { withFileTypes: true });
|
69
75
|
const migrationFiles = files
|
70
|
-
.filter((file) => file.isFile() &&
|
76
|
+
.filter((file) => file.isFile() &&
|
77
|
+
file.name.endsWith(".sql") &&
|
78
|
+
!file.name.endsWith(".down.sql"))
|
71
79
|
.map((file) => path_1.default.join(directory, file.name));
|
72
80
|
migrationFiles.sort();
|
73
81
|
const results = [];
|
74
82
|
for (const fullFilePath of migrationFiles) {
|
75
|
-
const hash = yield (0, exports.hashFile)(fullFilePath);
|
76
83
|
const contents = yield promises_1.default.readFile(fullFilePath, "utf8");
|
77
84
|
results.push({
|
85
|
+
type: "up",
|
78
86
|
fullFilePath,
|
79
87
|
filename: path_1.default.basename(fullFilePath),
|
80
|
-
hash,
|
88
|
+
hash: (0, exports.calculateHash)(contents),
|
81
89
|
contents,
|
82
90
|
});
|
83
91
|
}
|
84
92
|
return results;
|
85
93
|
});
|
86
94
|
exports.readMigrationFiles = readMigrationFiles;
|
95
|
+
const fileExists = (path) => __awaiter(void 0, void 0, void 0, function* () {
|
96
|
+
try {
|
97
|
+
return (yield promises_1.default.stat(path)).isFile();
|
98
|
+
}
|
99
|
+
catch (error) {
|
100
|
+
return false;
|
101
|
+
}
|
102
|
+
});
|
103
|
+
exports.fileExists = fileExists;
|
104
|
+
const readDownMigrationFiles = (directory, migrationHistory) => __awaiter(void 0, void 0, void 0, function* () {
|
105
|
+
const results = [];
|
106
|
+
for (const migration of migrationHistory) {
|
107
|
+
const fullFilePath = path_1.default.join(directory, `${migration.name.split(".sql")[0]}.down.sql`);
|
108
|
+
if (!(yield (0, exports.fileExists)(fullFilePath))) {
|
109
|
+
console.error(`Down migration file not found: ${fullFilePath}`);
|
110
|
+
process.exit(1);
|
111
|
+
}
|
112
|
+
const contents = yield promises_1.default.readFile(fullFilePath, "utf8");
|
113
|
+
results.push({
|
114
|
+
type: "down",
|
115
|
+
fullFilePath,
|
116
|
+
filename: path_1.default.basename(fullFilePath),
|
117
|
+
upFilename: migration.name,
|
118
|
+
contents,
|
119
|
+
});
|
120
|
+
}
|
121
|
+
return results;
|
122
|
+
});
|
123
|
+
exports.readDownMigrationFiles = readDownMigrationFiles;
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import pg, { Pool, PoolClient } from "pg";
|
2
|
+
import { MigrationRow } from "./types";
|
2
3
|
|
3
4
|
pg.types.setTypeParser(1114, function (stringValue) {
|
4
5
|
return stringValue; //1114 for time without timezone type
|
@@ -52,7 +53,7 @@ export const dbMigrationHistory = async (
|
|
52
53
|
const migrationsQuery = await client.query(
|
53
54
|
`SELECT * FROM ${schema}.stepwise_migrations`
|
54
55
|
);
|
55
|
-
return migrationsQuery.rows;
|
56
|
+
return migrationsQuery.rows as MigrationRow[];
|
56
57
|
};
|
57
58
|
|
58
59
|
export const dbCreateSchema = async (client: PoolClient, schema: string) => {
|
@@ -69,7 +70,7 @@ export const dbCreateHistoryTable = async (
|
|
69
70
|
await client.query(
|
70
71
|
`CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
|
71
72
|
id SERIAL PRIMARY KEY,
|
72
|
-
name TEXT NOT NULL,
|
73
|
+
name TEXT UNIQUE NOT NULL,
|
73
74
|
hash TEXT NOT NULL,
|
74
75
|
applied_by TEXT NOT NULL DEFAULT current_user,
|
75
76
|
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
package/src/index.ts
CHANGED
@@ -9,8 +9,17 @@ import {
|
|
9
9
|
dbMigrationHistory,
|
10
10
|
dbTableExists,
|
11
11
|
} from "./db";
|
12
|
-
import {
|
13
|
-
|
12
|
+
import {
|
13
|
+
applyDownMigration,
|
14
|
+
applyMigration,
|
15
|
+
validateDownMigrationFiles,
|
16
|
+
validateMigrationFiles,
|
17
|
+
} from "./migrate";
|
18
|
+
import {
|
19
|
+
readDownMigrationFiles,
|
20
|
+
readMigrationFiles,
|
21
|
+
validateArgs,
|
22
|
+
} from "./utils";
|
14
23
|
|
15
24
|
const main = async () => {
|
16
25
|
const argv: any = yargs(process.argv.slice(2)).argv;
|
@@ -25,6 +34,7 @@ const main = async () => {
|
|
25
34
|
const tableExists = await dbTableExists(client, schema);
|
26
35
|
|
27
36
|
if (command === "migrate") {
|
37
|
+
const nUp = argv.nup || Infinity;
|
28
38
|
if (!historySchemaExists) {
|
29
39
|
await dbCreateSchema(client, schema);
|
30
40
|
}
|
@@ -38,12 +48,18 @@ const main = async () => {
|
|
38
48
|
|
39
49
|
validateMigrationFiles(migrationFiles, migrationHistory);
|
40
50
|
|
41
|
-
const migrationsToApply = migrationFiles.slice(
|
51
|
+
const migrationsToApply = migrationFiles.slice(
|
52
|
+
migrationHistory.length,
|
53
|
+
migrationHistory.length + nUp
|
54
|
+
);
|
42
55
|
|
43
56
|
for (const { filename, contents, hash } of migrationsToApply) {
|
44
57
|
await applyMigration(client, schema, filename, contents, hash);
|
45
58
|
}
|
46
|
-
|
59
|
+
|
60
|
+
console.log("All done!");
|
61
|
+
console.log("New migration history:");
|
62
|
+
console.table(await dbMigrationHistory(client, schema));
|
47
63
|
} else if (command === "info") {
|
48
64
|
console.log(
|
49
65
|
"Showing information about the current state of the migrations in the database"
|
@@ -61,7 +77,37 @@ const main = async () => {
|
|
61
77
|
} else if (command === "drop") {
|
62
78
|
console.log("Dropping the tables, schema and migration history table");
|
63
79
|
await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
|
64
|
-
console.log("
|
80
|
+
console.log("All done!");
|
81
|
+
} else if (command === "down") {
|
82
|
+
const nDown = argv.ndown || 1;
|
83
|
+
|
84
|
+
const migrationHistory = await dbMigrationHistory(client, schema);
|
85
|
+
validateMigrationFiles(
|
86
|
+
await readMigrationFiles(argv.path),
|
87
|
+
migrationHistory,
|
88
|
+
false
|
89
|
+
);
|
90
|
+
|
91
|
+
const reverseMigrationHistory = migrationHistory.reverse().slice(0, nDown);
|
92
|
+
const downMigrationFilesToApply = await readDownMigrationFiles(
|
93
|
+
argv.path,
|
94
|
+
reverseMigrationHistory
|
95
|
+
);
|
96
|
+
|
97
|
+
validateDownMigrationFiles(
|
98
|
+
downMigrationFilesToApply,
|
99
|
+
reverseMigrationHistory
|
100
|
+
);
|
101
|
+
for (const {
|
102
|
+
filename,
|
103
|
+
contents,
|
104
|
+
upFilename,
|
105
|
+
} of downMigrationFilesToApply) {
|
106
|
+
await applyDownMigration(client, schema, filename, contents, upFilename);
|
107
|
+
}
|
108
|
+
console.log("All done!");
|
109
|
+
console.log("New migration history:");
|
110
|
+
console.table(await dbMigrationHistory(client, schema));
|
65
111
|
}
|
66
112
|
|
67
113
|
client.release();
|
package/src/migrate.ts
CHANGED
@@ -3,7 +3,8 @@ import { MigrationRow } from "./types";
|
|
3
3
|
|
4
4
|
export const validateMigrationFiles = (
|
5
5
|
migrationFiles: { fullFilePath: string; filename: string; hash: string }[],
|
6
|
-
migrationHistory: MigrationRow[]
|
6
|
+
migrationHistory: MigrationRow[],
|
7
|
+
isUp: boolean = true
|
7
8
|
) => {
|
8
9
|
if (migrationFiles.length === 0) {
|
9
10
|
console.log("No migrations found");
|
@@ -17,7 +18,7 @@ export const validateMigrationFiles = (
|
|
17
18
|
process.exit(1);
|
18
19
|
}
|
19
20
|
|
20
|
-
if (migrationFiles.length === migrationHistory.length) {
|
21
|
+
if (migrationFiles.length === migrationHistory.length && isUp) {
|
21
22
|
console.log("All migrations are already applied");
|
22
23
|
process.exit(0);
|
23
24
|
}
|
@@ -73,3 +74,55 @@ export const applyMigration = async (
|
|
73
74
|
process.exit(1);
|
74
75
|
}
|
75
76
|
};
|
77
|
+
|
78
|
+
export const validateDownMigrationFiles = (
|
79
|
+
downMigrationFilesToApply: { filename: string }[],
|
80
|
+
reverseMigrationHistory: MigrationRow[]
|
81
|
+
) => {
|
82
|
+
for (let i = 0; i < downMigrationFilesToApply.length; i++) {
|
83
|
+
const { filename } = downMigrationFilesToApply[i];
|
84
|
+
if (
|
85
|
+
filename.split(".down.sql")[0] !==
|
86
|
+
reverseMigrationHistory[i].name.split(".sql")[0]
|
87
|
+
) {
|
88
|
+
console.error(
|
89
|
+
`Migration ${filename} does not match the expected migration ${reverseMigrationHistory[i].name}`
|
90
|
+
);
|
91
|
+
process.exit(1);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
};
|
95
|
+
|
96
|
+
export const applyDownMigration = async (
|
97
|
+
client: PoolClient,
|
98
|
+
schema: string,
|
99
|
+
filename: string,
|
100
|
+
contents: string,
|
101
|
+
upFilename: string
|
102
|
+
) => {
|
103
|
+
try {
|
104
|
+
await client.query("BEGIN");
|
105
|
+
|
106
|
+
await client.query(
|
107
|
+
`SET search_path TO ${schema};
|
108
|
+
${contents.toString()}`
|
109
|
+
);
|
110
|
+
|
111
|
+
await client.query(
|
112
|
+
`DELETE FROM ${schema}.stepwise_migrations WHERE name = $1`,
|
113
|
+
[upFilename]
|
114
|
+
);
|
115
|
+
|
116
|
+
await client.query("COMMIT");
|
117
|
+
|
118
|
+
console.log(`Applied down migration ${filename}`);
|
119
|
+
} catch (error) {
|
120
|
+
try {
|
121
|
+
await client.query("ROLLBACK");
|
122
|
+
} catch (error) {
|
123
|
+
console.error("Error rolling back transaction", error);
|
124
|
+
}
|
125
|
+
console.error("Error applying down migration", error);
|
126
|
+
process.exit(1);
|
127
|
+
}
|
128
|
+
};
|
package/src/utils.ts
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
import crypto from "crypto";
|
2
2
|
import fs from "fs/promises";
|
3
3
|
import path from "path";
|
4
|
+
import { MigrationRow } from "./types";
|
4
5
|
|
5
|
-
export const
|
6
|
-
|
7
|
-
return crypto.createHash("sha256").update(file).digest("hex");
|
6
|
+
export const calculateHash = (contents: string) => {
|
7
|
+
return crypto.createHash("sha256").update(contents).digest("hex");
|
8
8
|
};
|
9
9
|
|
10
10
|
export const usage = `
|
@@ -13,6 +13,8 @@ Usage: stepwise-migrations [command] [options]
|
|
13
13
|
Commands:
|
14
14
|
migrate
|
15
15
|
Migrate the database to the latest version
|
16
|
+
down
|
17
|
+
Rollback the database to the previous version
|
16
18
|
info
|
17
19
|
Show information about the current state of the migrations in the database
|
18
20
|
drop
|
@@ -23,6 +25,8 @@ Options:
|
|
23
25
|
--schema <schema> The schema to use for the migrations
|
24
26
|
--path <path> The path to the migrations directory
|
25
27
|
--ssl true/false Whether to use SSL for the connection (default: false)
|
28
|
+
--nup Number of up migrations to apply (default: all)
|
29
|
+
--ndown Number of down migrations to apply (default: 1)
|
26
30
|
|
27
31
|
Example:
|
28
32
|
npx stepwise-migrations \
|
@@ -47,7 +51,12 @@ export const validateArgs = (argv: any) => {
|
|
47
51
|
console.log(usage);
|
48
52
|
process.exit(1);
|
49
53
|
}
|
50
|
-
if (
|
54
|
+
if (
|
55
|
+
argv._[0] !== "migrate" &&
|
56
|
+
argv._[0] !== "info" &&
|
57
|
+
argv._[0] !== "drop" &&
|
58
|
+
argv._[0] !== "down"
|
59
|
+
) {
|
51
60
|
console.error(`Invalid command: ${argv._[0]}`);
|
52
61
|
console.log(usage);
|
53
62
|
process.exit(1);
|
@@ -57,22 +66,70 @@ export const validateArgs = (argv: any) => {
|
|
57
66
|
export const readMigrationFiles = async (directory: string) => {
|
58
67
|
const files = await fs.readdir(directory, { withFileTypes: true });
|
59
68
|
const migrationFiles = files
|
60
|
-
.filter(
|
69
|
+
.filter(
|
70
|
+
(file) =>
|
71
|
+
file.isFile() &&
|
72
|
+
file.name.endsWith(".sql") &&
|
73
|
+
!file.name.endsWith(".down.sql")
|
74
|
+
)
|
61
75
|
.map((file) => path.join(directory, file.name));
|
62
76
|
migrationFiles.sort();
|
63
77
|
const results: {
|
78
|
+
type: "up";
|
64
79
|
fullFilePath: string;
|
65
80
|
filename: string;
|
66
81
|
hash: string;
|
67
82
|
contents: string;
|
68
83
|
}[] = [];
|
69
84
|
for (const fullFilePath of migrationFiles) {
|
70
|
-
const
|
85
|
+
const contents = await fs.readFile(fullFilePath, "utf8");
|
86
|
+
|
87
|
+
results.push({
|
88
|
+
type: "up",
|
89
|
+
fullFilePath,
|
90
|
+
filename: path.basename(fullFilePath),
|
91
|
+
hash: calculateHash(contents),
|
92
|
+
contents,
|
93
|
+
});
|
94
|
+
}
|
95
|
+
return results;
|
96
|
+
};
|
97
|
+
|
98
|
+
export const fileExists = async (path: string) => {
|
99
|
+
try {
|
100
|
+
return (await fs.stat(path)).isFile();
|
101
|
+
} catch (error) {
|
102
|
+
return false;
|
103
|
+
}
|
104
|
+
};
|
105
|
+
|
106
|
+
export const readDownMigrationFiles = async (
|
107
|
+
directory: string,
|
108
|
+
migrationHistory: MigrationRow[]
|
109
|
+
) => {
|
110
|
+
const results: {
|
111
|
+
type: "down";
|
112
|
+
fullFilePath: string;
|
113
|
+
filename: string;
|
114
|
+
upFilename: string;
|
115
|
+
|
116
|
+
contents: string;
|
117
|
+
}[] = [];
|
118
|
+
for (const migration of migrationHistory) {
|
119
|
+
const fullFilePath = path.join(
|
120
|
+
directory,
|
121
|
+
`${migration.name.split(".sql")[0]}.down.sql`
|
122
|
+
);
|
123
|
+
if (!(await fileExists(fullFilePath))) {
|
124
|
+
console.error(`Down migration file not found: ${fullFilePath}`);
|
125
|
+
process.exit(1);
|
126
|
+
}
|
71
127
|
const contents = await fs.readFile(fullFilePath, "utf8");
|
72
128
|
results.push({
|
129
|
+
type: "down",
|
73
130
|
fullFilePath,
|
74
131
|
filename: path.basename(fullFilePath),
|
75
|
-
|
132
|
+
upFilename: migration.name,
|
76
133
|
contents,
|
77
134
|
});
|
78
135
|
}
|