stepwise-migrations 1.0.14 → 1.0.15
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 +20 -20
- package/dist/{db.js → src/db.js} +73 -28
- package/dist/src/index.js +146 -0
- package/dist/src/state.js +123 -0
- package/dist/src/types.js +13 -0
- package/dist/src/utils.js +184 -0
- package/dist/src/validate.js +1 -0
- package/dist/test/index.test.js +129 -0
- package/dist/test/utils.js +51 -0
- package/docker-compose.yml +21 -0
- package/package.json +12 -6
- package/src/db.ts +92 -37
- package/src/index.ts +115 -80
- package/src/state.ts +166 -0
- package/src/types.ts +49 -4
- package/src/utils.ts +122 -66
- package/test/index.test.ts +166 -0
- package/test/migrations-invalid/v0_get_number.repeatable.sql +6 -0
- package/test/migrations-invalid/v1_first.sql +1 -0
- package/test/migrations-invalid/v2_second.sql +4 -0
- package/test/migrations-invalid/v2_second.undo.sql +1 -0
- package/test/migrations-invalid/v3_third.sql +4 -0
- package/test/migrations-invalid/v3_third.undo.sql +1 -0
- package/test/migrations-template/v0_get_number.repeatable.sql +6 -0
- package/test/migrations-template/v1_first.sql +4 -0
- package/test/migrations-template/v2_second.sql +4 -0
- package/test/migrations-template/v2_second.undo.sql +1 -0
- package/test/migrations-template/v3_third.sql +4 -0
- package/test/migrations-template/v3_third.undo.sql +1 -0
- package/test/migrations-valid/v0_get_number.repeatable.sql +8 -0
- package/test/migrations-valid/v1_first.sql +4 -0
- package/test/migrations-valid/v2_second.sql +4 -0
- package/test/migrations-valid/v2_second.undo.sql +1 -0
- package/test/migrations-valid/v3_third.sql +4 -0
- package/test/migrations-valid/v3_third.undo.sql +1 -0
- package/test/utils.ts +69 -0
- package/tsconfig.json +1 -1
- package/dist/index.js +0 -115
- package/dist/migrate.js +0 -102
- package/dist/types.js +0 -2
- package/dist/utils.js +0 -132
- package/src/migrate.ts +0 -143
@@ -0,0 +1,184 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
13
|
+
};
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
15
|
+
exports.sliceFromFirstNull = exports.exitIfNotInitialized = exports.abortIfErrors = exports.fileExists = exports.printMigrationHistory = exports.printMigrationHistoryAndUnappliedMigrations = exports.readMigrationFiles = exports.filenameToType = exports.validateArgs = exports.usage = void 0;
|
16
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
17
|
+
const path_1 = __importDefault(require("path"));
|
18
|
+
exports.usage = `
|
19
|
+
Usage: stepwise-migrations [command] [options]
|
20
|
+
|
21
|
+
Commands:
|
22
|
+
migrate
|
23
|
+
Migrate the database to the latest version
|
24
|
+
down
|
25
|
+
Rollback the database to the previous version
|
26
|
+
info
|
27
|
+
Show information about the current state of the migrations in the database
|
28
|
+
drop
|
29
|
+
Drop the tables, schema and migration history table
|
30
|
+
|
31
|
+
Options:
|
32
|
+
--connection <connection> The connection string to use to connect to the database
|
33
|
+
--schema <schema> The schema to use for the migrations
|
34
|
+
--path <path> The path to the migrations directory
|
35
|
+
--ssl true/false Whether to use SSL for the connection (default: false)
|
36
|
+
--napply Number of up migrations to apply (default: all)
|
37
|
+
--nundo Number of down migrations to apply (default: 1)
|
38
|
+
|
39
|
+
Example:
|
40
|
+
npx stepwise-migrations migrate \\
|
41
|
+
--connection=postgresql://postgres:postgres@127.0.0.1:5432/mydatabase \\
|
42
|
+
--schema=myschema \\
|
43
|
+
--path=./db/migration/
|
44
|
+
`;
|
45
|
+
const validateArgs = (argv) => {
|
46
|
+
const required = ["connection", "schema", "path", "_"];
|
47
|
+
if (required.some((key) => !(key in argv))) {
|
48
|
+
console.error("Missing required arguments", required.filter((key) => !(key in argv)));
|
49
|
+
console.log(exports.usage);
|
50
|
+
process.exit(1);
|
51
|
+
}
|
52
|
+
if (argv._.length !== 1) {
|
53
|
+
console.error(`Invalid number of arguments: ${argv._.length}`);
|
54
|
+
console.log(exports.usage);
|
55
|
+
process.exit(1);
|
56
|
+
}
|
57
|
+
};
|
58
|
+
exports.validateArgs = validateArgs;
|
59
|
+
const filenameToType = (filename) => {
|
60
|
+
if (filename.endsWith(".undo.sql")) {
|
61
|
+
return "undo";
|
62
|
+
}
|
63
|
+
else if (filename.endsWith(".repeatable.sql")) {
|
64
|
+
return "repeatable";
|
65
|
+
}
|
66
|
+
return "versioned";
|
67
|
+
};
|
68
|
+
exports.filenameToType = filenameToType;
|
69
|
+
const readMigrationFiles = (directory, appliedVersionedMigrations) => __awaiter(void 0, void 0, void 0, function* () {
|
70
|
+
let errors = [];
|
71
|
+
const files = yield promises_1.default.readdir(directory, { withFileTypes: true });
|
72
|
+
const migrationFiles = files
|
73
|
+
.filter((file) => file.isFile() && file.name.endsWith(".sql"))
|
74
|
+
.map((file) => path_1.default.join(directory, file.name));
|
75
|
+
migrationFiles.sort();
|
76
|
+
const results = [];
|
77
|
+
for (const fullFileName of migrationFiles) {
|
78
|
+
const script = yield promises_1.default.readFile(fullFileName, "utf8");
|
79
|
+
results.push({
|
80
|
+
type: (0, exports.filenameToType)(path_1.default.basename(fullFileName)),
|
81
|
+
filename: path_1.default.basename(fullFileName),
|
82
|
+
script,
|
83
|
+
});
|
84
|
+
}
|
85
|
+
for (const appliedMigration of appliedVersionedMigrations) {
|
86
|
+
const file = results.find((f) => f.filename === appliedMigration.filename);
|
87
|
+
if (file &&
|
88
|
+
file.type === "versioned" &&
|
89
|
+
file.script !== appliedMigration.script) {
|
90
|
+
errors.push(`Versioned migration ${appliedMigration.filename} has been altered. Cannot migrate in current state.`);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
return { files: results, errors };
|
94
|
+
});
|
95
|
+
exports.readMigrationFiles = readMigrationFiles;
|
96
|
+
const printMigrationHistoryAndUnappliedMigrations = (state) => {
|
97
|
+
console.log("All applied versioned migrations:");
|
98
|
+
console.table(state.current.appliedVersionedMigrations.map((h) => ({
|
99
|
+
id: h.id,
|
100
|
+
type: h.type,
|
101
|
+
filename: h.filename,
|
102
|
+
applied_by: h.applied_by,
|
103
|
+
applied_at: h.applied_at,
|
104
|
+
})));
|
105
|
+
if (state.current.appliedRepeatableMigrations.length > 0) {
|
106
|
+
console.log("All applied repeatable migrations:");
|
107
|
+
console.table(state.current.appliedRepeatableMigrations.map((h) => ({
|
108
|
+
id: h.id,
|
109
|
+
type: h.type,
|
110
|
+
filename: h.filename,
|
111
|
+
applied_by: h.applied_by,
|
112
|
+
applied_at: h.applied_at,
|
113
|
+
})));
|
114
|
+
}
|
115
|
+
console.log("Unapplied versioned migrations:");
|
116
|
+
console.table(state.files.unappliedVersionedFiles.map((h) => ({
|
117
|
+
type: h.type,
|
118
|
+
filename: h.filename,
|
119
|
+
})));
|
120
|
+
if (state.files.unappliedRepeatableFiles.length > 0) {
|
121
|
+
console.log("Unapplied repeatable migrations:");
|
122
|
+
console.table(state.files.unappliedRepeatableFiles.map((h) => ({
|
123
|
+
type: h.type,
|
124
|
+
filename: h.filename,
|
125
|
+
})));
|
126
|
+
}
|
127
|
+
};
|
128
|
+
exports.printMigrationHistoryAndUnappliedMigrations = printMigrationHistoryAndUnappliedMigrations;
|
129
|
+
const printMigrationHistory = (state) => {
|
130
|
+
console.log("All applied versioned migrations:");
|
131
|
+
console.table(state.current.appliedVersionedMigrations.map((h) => ({
|
132
|
+
id: h.id,
|
133
|
+
type: h.type,
|
134
|
+
filename: h.filename,
|
135
|
+
applied_by: h.applied_by,
|
136
|
+
applied_at: h.applied_at,
|
137
|
+
})));
|
138
|
+
if (state.current.appliedRepeatableMigrations.length > 0) {
|
139
|
+
console.log("All applied repeatable migrations:");
|
140
|
+
console.table(state.current.appliedRepeatableMigrations.map((h) => ({
|
141
|
+
id: h.id,
|
142
|
+
type: h.type,
|
143
|
+
filename: h.filename,
|
144
|
+
applied_by: h.applied_by,
|
145
|
+
applied_at: h.applied_at,
|
146
|
+
})));
|
147
|
+
}
|
148
|
+
};
|
149
|
+
exports.printMigrationHistory = printMigrationHistory;
|
150
|
+
const fileExists = (path) => __awaiter(void 0, void 0, void 0, function* () {
|
151
|
+
try {
|
152
|
+
return (yield promises_1.default.stat(path)).isFile();
|
153
|
+
}
|
154
|
+
catch (error) {
|
155
|
+
return false;
|
156
|
+
}
|
157
|
+
});
|
158
|
+
exports.fileExists = fileExists;
|
159
|
+
const abortIfErrors = (state) => {
|
160
|
+
if (state.errors.length > 0) {
|
161
|
+
console.error(`There were errors loading the migration state. Please fix the errors and try again.`);
|
162
|
+
console.error(state.errors.map((e) => " - " + e).join("\n"));
|
163
|
+
process.exit(1);
|
164
|
+
}
|
165
|
+
};
|
166
|
+
exports.abortIfErrors = abortIfErrors;
|
167
|
+
const exitIfNotInitialized = (schemaExists, tableExists) => {
|
168
|
+
if (!schemaExists) {
|
169
|
+
console.log("Schema does not exist. Run migrate to begin.");
|
170
|
+
process.exit(1);
|
171
|
+
}
|
172
|
+
if (!tableExists) {
|
173
|
+
console.log("Migration table has not been initialised. Run migrate to begin.");
|
174
|
+
process.exit(1);
|
175
|
+
}
|
176
|
+
};
|
177
|
+
exports.exitIfNotInitialized = exitIfNotInitialized;
|
178
|
+
const sliceFromFirstNull = (array) => {
|
179
|
+
const indexOfFirstNull = array.findIndex((x) => x == null);
|
180
|
+
return indexOfFirstNull < 0
|
181
|
+
? array
|
182
|
+
: array.slice(0, indexOfFirstNull);
|
183
|
+
};
|
184
|
+
exports.sliceFromFirstNull = sliceFromFirstNull;
|
@@ -0,0 +1 @@
|
|
1
|
+
"use strict";
|
@@ -0,0 +1,129 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
13
|
+
};
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
15
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
16
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
17
|
+
const node_test_1 = require("node:test");
|
18
|
+
const utils_1 = require("./utils");
|
19
|
+
const connection = "postgresql://postgres:postgres@127.0.0.1:5432/stepwise-db";
|
20
|
+
const schema = "stepwise";
|
21
|
+
const paths = {
|
22
|
+
valid: "./test/migrations-valid",
|
23
|
+
invalid: "./test/migrations-invalid",
|
24
|
+
};
|
25
|
+
const executeCommand = (command, path = "", extraArgs = "") => (0, utils_1.execute)(`npm exec stepwise-migrations ${command} -- \\
|
26
|
+
--connection=${connection} \\
|
27
|
+
--schema=${schema} \\
|
28
|
+
--path=${path} ${extraArgs}
|
29
|
+
`);
|
30
|
+
(0, node_test_1.describe)("valid migrations", () => __awaiter(void 0, void 0, void 0, function* () {
|
31
|
+
(0, node_test_1.beforeEach)(() => __awaiter(void 0, void 0, void 0, function* () {
|
32
|
+
const { output, error, exitCode } = yield executeCommand("drop", "");
|
33
|
+
node_assert_1.default.ok(output.includes("Dropping the tables, schema and migration history table... done!"));
|
34
|
+
node_assert_1.default.ok(exitCode === 0);
|
35
|
+
node_fs_1.default.rmSync(paths.valid, { recursive: true, force: true });
|
36
|
+
node_fs_1.default.cpSync("./test/migrations-template", paths.valid, {
|
37
|
+
recursive: true,
|
38
|
+
});
|
39
|
+
}));
|
40
|
+
(0, node_test_1.it)("migrate without params", () => __awaiter(void 0, void 0, void 0, function* () {
|
41
|
+
(0, utils_1.assertIncludesAll)(yield (0, utils_1.execute)("npm exec stepwise-migrations"), ["Usage"]);
|
42
|
+
}));
|
43
|
+
(0, node_test_1.it)("migrate one versioned and undo, redo, undo", () => __awaiter(void 0, void 0, void 0, function* () {
|
44
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.valid), [
|
45
|
+
"All done! Applied 4 migrations",
|
46
|
+
]);
|
47
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("status"), [
|
48
|
+
"v0_get_number.repeatable.sql",
|
49
|
+
"v1_first.sql",
|
50
|
+
"v2_second.sql",
|
51
|
+
"v3_third.sql",
|
52
|
+
]);
|
53
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("undo", paths.valid), [
|
54
|
+
"All done! Performed 1 undo migration",
|
55
|
+
]);
|
56
|
+
(0, utils_1.assertIncludesExcludesAll)(yield executeCommand("status"), ["v0_get_number.repeatable.sql", "v1_first.sql", "v2_second.sql"], ["v3_third.sql"]);
|
57
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.valid), [
|
58
|
+
"All done! Applied 1 migration",
|
59
|
+
]);
|
60
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("status"), [
|
61
|
+
"v0_get_number.repeatable.sql",
|
62
|
+
"v1_first.sql",
|
63
|
+
"v2_second.sql",
|
64
|
+
"v3_third.sql",
|
65
|
+
]);
|
66
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("undo", paths.valid, "--nundo=2"), [
|
67
|
+
"All done! Performed 2 undo migrations",
|
68
|
+
]);
|
69
|
+
(0, utils_1.assertIncludesExcludesAll)(yield executeCommand("status"), ["v0_get_number.repeatable.sql", "v1_first.sql"], ["v2_second.sql", "v3_third.sql"]);
|
70
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.valid), [
|
71
|
+
"All done! Applied 2 migrations",
|
72
|
+
]);
|
73
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("status"), [
|
74
|
+
"v0_get_number.repeatable.sql",
|
75
|
+
"v1_first.sql",
|
76
|
+
"v2_second.sql",
|
77
|
+
"v3_third.sql",
|
78
|
+
]);
|
79
|
+
}));
|
80
|
+
(0, node_test_1.it)("migrate with altered repeatable migration", () => __awaiter(void 0, void 0, void 0, function* () {
|
81
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.valid), [
|
82
|
+
"All done! Applied 4 migrations",
|
83
|
+
]);
|
84
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("status"), [
|
85
|
+
"v0_get_number.repeatable.sql",
|
86
|
+
"v1_first.sql",
|
87
|
+
"v2_second.sql",
|
88
|
+
"v3_third.sql",
|
89
|
+
]);
|
90
|
+
node_fs_1.default.writeFileSync("./test/migrations-valid/v0_get_number.repeatable.sql", `
|
91
|
+
CREATE OR REPLACE FUNCTION get_number()
|
92
|
+
RETURNS integer AS $$
|
93
|
+
BEGIN
|
94
|
+
RETURN 2;
|
95
|
+
END; $$
|
96
|
+
LANGUAGE plpgsql;
|
97
|
+
`);
|
98
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.valid), [
|
99
|
+
"All done! Applied 1 migration",
|
100
|
+
]);
|
101
|
+
}));
|
102
|
+
}));
|
103
|
+
node_test_1.describe.only("invalid migrations", () => __awaiter(void 0, void 0, void 0, function* () {
|
104
|
+
(0, node_test_1.beforeEach)(() => __awaiter(void 0, void 0, void 0, function* () {
|
105
|
+
const { output, error, exitCode } = yield executeCommand("drop", "");
|
106
|
+
node_assert_1.default.ok(output.includes("Dropping the tables, schema and migration history table... done!"));
|
107
|
+
node_assert_1.default.ok(exitCode === 0);
|
108
|
+
node_fs_1.default.rmSync(paths.invalid, { recursive: true, force: true });
|
109
|
+
node_fs_1.default.cpSync("./test/migrations-template", paths.invalid, {
|
110
|
+
recursive: true,
|
111
|
+
});
|
112
|
+
}));
|
113
|
+
node_test_1.it.only("missing undo migration", () => __awaiter(void 0, void 0, void 0, function* () {
|
114
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.invalid), [
|
115
|
+
"All done!",
|
116
|
+
]);
|
117
|
+
node_fs_1.default.unlinkSync("./test/migrations-invalid/v3_third.undo.sql");
|
118
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("undo", paths.invalid, "--nundos=2"), ["Error: not enough sequential (from last) undo migrations to apply"]);
|
119
|
+
}));
|
120
|
+
(0, node_test_1.it)("alter migration", () => __awaiter(void 0, void 0, void 0, function* () {
|
121
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.invalid), [
|
122
|
+
"All done!",
|
123
|
+
]);
|
124
|
+
node_fs_1.default.writeFileSync("./test/migrations-invalid/v1_first.sql", "ALTER TABLE test ADD COLUMN test_column TEXT;");
|
125
|
+
(0, utils_1.assertIncludesAll)(yield executeCommand("migrate", paths.invalid), [
|
126
|
+
"Versioned migration v1_first.sql has been altered. Cannot migrate in current state.",
|
127
|
+
]);
|
128
|
+
}));
|
129
|
+
}));
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.assertIncludesExcludesAll = exports.assertExcludesAll = exports.assertIncludesAll = exports.includesExcludesAll = exports.excludesAll = exports.includesAll = exports.execute = void 0;
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
5
|
+
const execute = (cmd) => {
|
6
|
+
const child = (0, node_child_process_1.exec)(cmd);
|
7
|
+
let scriptOutput = "";
|
8
|
+
let scriptError = "";
|
9
|
+
if (!child.stdout || !child.stderr) {
|
10
|
+
return { output: "", error: "", exitCode: 1 };
|
11
|
+
}
|
12
|
+
child.stdout.setEncoding("utf8");
|
13
|
+
child.stdout.on("data", (data) => {
|
14
|
+
scriptOutput += data.toString();
|
15
|
+
});
|
16
|
+
child.stderr.setEncoding("utf8");
|
17
|
+
child.stderr.on("data", (data) => {
|
18
|
+
scriptError += data.toString();
|
19
|
+
});
|
20
|
+
return new Promise((res, rej) => child.on("close", function (code) {
|
21
|
+
res({ output: scriptOutput, error: scriptError, exitCode: code !== null && code !== void 0 ? code : 1 });
|
22
|
+
}));
|
23
|
+
};
|
24
|
+
exports.execute = execute;
|
25
|
+
const includesAll = (output, expected) => expected.every((x) => output.includes(x));
|
26
|
+
exports.includesAll = includesAll;
|
27
|
+
const excludesAll = (output, expected) => expected.every((x) => !output.includes(x));
|
28
|
+
exports.excludesAll = excludesAll;
|
29
|
+
const includesExcludesAll = (output, includes, excludes) => (0, exports.includesAll)(output, includes) && (0, exports.excludesAll)(output, excludes);
|
30
|
+
exports.includesExcludesAll = includesExcludesAll;
|
31
|
+
const assertIncludesAll = ({ output, error }, expected) => {
|
32
|
+
if (!(0, exports.includesAll)(output + error, expected)) {
|
33
|
+
console.log(output);
|
34
|
+
console.error(error);
|
35
|
+
throw new Error(`Expected ${expected} to be in output.`);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
exports.assertIncludesAll = assertIncludesAll;
|
39
|
+
const assertExcludesAll = ({ output, error }, expected) => {
|
40
|
+
if (!(0, exports.excludesAll)(output + error, expected)) {
|
41
|
+
console.log(output);
|
42
|
+
console.error(error);
|
43
|
+
throw new Error(`Expected ${expected} to be be excluded from output.`);
|
44
|
+
}
|
45
|
+
};
|
46
|
+
exports.assertExcludesAll = assertExcludesAll;
|
47
|
+
const assertIncludesExcludesAll = ({ output, error }, includes, excludes) => {
|
48
|
+
(0, exports.assertIncludesAll)({ output, error }, includes);
|
49
|
+
(0, exports.assertExcludesAll)({ output, error }, excludes);
|
50
|
+
};
|
51
|
+
exports.assertIncludesExcludesAll = assertIncludesExcludesAll;
|
@@ -0,0 +1,21 @@
|
|
1
|
+
services:
|
2
|
+
stepwise-db:
|
3
|
+
container_name: stepwise-db
|
4
|
+
image: postgres:17.2
|
5
|
+
environment:
|
6
|
+
POSTGRES_DB: stepwise-db
|
7
|
+
POSTGRES_USER: postgres
|
8
|
+
POSTGRES_PASSWORD: postgres
|
9
|
+
healthcheck:
|
10
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
11
|
+
interval: 1s
|
12
|
+
timeout: 5s
|
13
|
+
retries: 5
|
14
|
+
ports:
|
15
|
+
- "5432:5432"
|
16
|
+
networks:
|
17
|
+
- mynet
|
18
|
+
|
19
|
+
networks:
|
20
|
+
mynet:
|
21
|
+
driver: bridge
|
package/package.json
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "stepwise-migrations",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.15",
|
4
4
|
"description": "",
|
5
|
-
"main": "index.js",
|
5
|
+
"main": "src/index.js",
|
6
6
|
"scripts": {
|
7
|
-
"build": "tsc"
|
7
|
+
"build": "tsc",
|
8
|
+
"test": "npm run build; glob -c \"tsx --test --test-reporter spec \" \"./test/**/*.test.ts\"",
|
9
|
+
"test:only": "npm run build; glob -c \"tsx --test --test-only --test-reporter spec \" \"./test/**/*.test.ts\""
|
8
10
|
},
|
9
11
|
"keywords": [
|
10
12
|
"migrations",
|
@@ -17,14 +19,18 @@
|
|
17
19
|
"devDependencies": {
|
18
20
|
"@types/git-diff": "^2.0.7",
|
19
21
|
"@types/pg": "^8.11.10",
|
20
|
-
"@types/yargs": "^17.0.33"
|
22
|
+
"@types/yargs": "^17.0.33",
|
23
|
+
"glob": "^11.0.0",
|
24
|
+
"tsx": "^4.19.2"
|
21
25
|
},
|
22
26
|
"bin": {
|
23
|
-
"stepwise-migrations": "dist/index.js"
|
27
|
+
"stepwise-migrations": "dist/src/index.js"
|
24
28
|
},
|
25
29
|
"dependencies": {
|
26
30
|
"git-diff": "^2.0.6",
|
27
31
|
"pg": "^8.13.1",
|
28
|
-
"
|
32
|
+
"sqlite": "^5.1.1",
|
33
|
+
"yargs": "^17.7.2",
|
34
|
+
"zod": "^3.23.8"
|
29
35
|
}
|
30
36
|
}
|
package/src/db.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import pg, { Pool, PoolClient } from "pg";
|
2
|
-
import {
|
2
|
+
import { EventRow, MigrationFile, MigrationState } from "./types";
|
3
3
|
|
4
4
|
pg.types.setTypeParser(1114, function (stringValue) {
|
5
5
|
return stringValue; //1114 for time without timezone type
|
@@ -27,10 +27,7 @@ export const dbConnect = async (argv: { connection: string; ssl?: string }) => {
|
|
27
27
|
return client;
|
28
28
|
};
|
29
29
|
|
30
|
-
export const
|
31
|
-
client: PoolClient,
|
32
|
-
schema: string
|
33
|
-
) => {
|
30
|
+
export const dbSchemaExists = async (client: PoolClient, schema: string) => {
|
34
31
|
const result = await client.query(
|
35
32
|
`SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '${schema}')`
|
36
33
|
);
|
@@ -39,20 +36,14 @@ export const dbHistorySchemaExists = async (
|
|
39
36
|
|
40
37
|
export const dbTableExists = async (client: PoolClient, schema: string) => {
|
41
38
|
const tableExistsResult = await client.query(
|
42
|
-
`SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = '
|
39
|
+
`SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stepwise_migration_events' and schemaname = '${schema}')`
|
43
40
|
);
|
44
41
|
|
45
42
|
return tableExistsResult.rows[0].exists;
|
46
43
|
};
|
47
44
|
|
48
|
-
export const
|
49
|
-
client
|
50
|
-
schema: string
|
51
|
-
) => {
|
52
|
-
const migrationsQuery = await client.query(
|
53
|
-
`SELECT * FROM ${schema}.stepwise_migrations`
|
54
|
-
);
|
55
|
-
return migrationsQuery.rows as MigrationRow[];
|
45
|
+
export const dbDropAll = async (client: PoolClient, schema: string) => {
|
46
|
+
await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
|
56
47
|
};
|
57
48
|
|
58
49
|
export const dbCreateSchema = async (client: PoolClient, schema: string) => {
|
@@ -61,31 +52,29 @@ export const dbCreateSchema = async (client: PoolClient, schema: string) => {
|
|
61
52
|
console.log(`done!`);
|
62
53
|
};
|
63
54
|
|
64
|
-
export const
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
55
|
+
export const dbEventHistory = async (client: PoolClient, schema: string) => {
|
56
|
+
try {
|
57
|
+
const eventQuery = await client.query(
|
58
|
+
`SELECT * FROM ${schema}.stepwise_migration_events`
|
59
|
+
);
|
60
|
+
return eventQuery.rows.map((row) => EventRow.parse(row));
|
61
|
+
} catch (error) {
|
62
|
+
console.error("Error fetching event history", error);
|
63
|
+
process.exit(1);
|
64
|
+
}
|
69
65
|
};
|
70
66
|
|
71
|
-
export const
|
67
|
+
export const dbCreateEventsTable = async (
|
72
68
|
client: PoolClient,
|
73
69
|
schema: string
|
74
70
|
) => {
|
75
|
-
process.stdout.write(`Creating
|
71
|
+
process.stdout.write(`Creating stepwise_migration_events table... `);
|
76
72
|
await client.query(
|
77
73
|
`
|
78
|
-
CREATE TABLE IF NOT EXISTS ${schema}.
|
79
|
-
id SERIAL PRIMARY KEY,
|
80
|
-
name TEXT UNIQUE NOT NULL,
|
81
|
-
script TEXT NOT NULL,
|
82
|
-
applied_by TEXT NOT NULL DEFAULT current_user,
|
83
|
-
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
84
|
-
);
|
85
|
-
CREATE TABLE IF NOT EXISTS ${schema}.stepwise_audit (
|
74
|
+
CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migration_events (
|
86
75
|
id SERIAL PRIMARY KEY,
|
87
76
|
type TEXT NOT NULL,
|
88
|
-
|
77
|
+
filename TEXT NOT NULL,
|
89
78
|
script TEXT NOT NULL,
|
90
79
|
applied_by TEXT NOT NULL DEFAULT current_user,
|
91
80
|
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
@@ -95,14 +84,80 @@ CREATE TABLE IF NOT EXISTS ${schema}.stepwise_audit (
|
|
95
84
|
console.log(`done!`);
|
96
85
|
};
|
97
86
|
|
98
|
-
export const
|
87
|
+
export const dbGetAppliedScript = async (
|
88
|
+
state: MigrationState,
|
89
|
+
filename: string
|
90
|
+
) => {
|
91
|
+
return state.current.appliedVersionedMigrations
|
92
|
+
.concat(state.current.appliedRepeatableMigrations)
|
93
|
+
.find((file) => file.filename === filename)?.script;
|
94
|
+
};
|
95
|
+
|
96
|
+
export const applyMigration = async (
|
99
97
|
client: PoolClient,
|
100
98
|
schema: string,
|
101
|
-
|
99
|
+
migration: MigrationFile
|
102
100
|
) => {
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
101
|
+
try {
|
102
|
+
process.stdout.write(
|
103
|
+
`Applying ${migration.type} migration ${migration.filename}... `
|
104
|
+
);
|
105
|
+
await client.query("BEGIN");
|
106
|
+
|
107
|
+
await client.query(
|
108
|
+
`SET search_path TO ${schema};
|
109
|
+
${migration.script.toString()}`
|
110
|
+
);
|
111
|
+
|
112
|
+
await client.query(
|
113
|
+
`INSERT INTO ${schema}.stepwise_migration_events (type, filename, script) VALUES ($1, $2, $3)`,
|
114
|
+
[migration.type, migration.filename, migration.script]
|
115
|
+
);
|
116
|
+
|
117
|
+
await client.query("COMMIT");
|
118
|
+
|
119
|
+
console.log(`done!`);
|
120
|
+
} catch (error) {
|
121
|
+
try {
|
122
|
+
await client.query("ROLLBACK");
|
123
|
+
} catch (error) {
|
124
|
+
console.error("Error rolling back transaction", error);
|
125
|
+
}
|
126
|
+
console.error("Error applying migration", error);
|
127
|
+
process.exit(1);
|
128
|
+
}
|
129
|
+
};
|
130
|
+
|
131
|
+
export const applyUndoMigration = async (
|
132
|
+
client: PoolClient,
|
133
|
+
schema: string,
|
134
|
+
filename: string,
|
135
|
+
script: string
|
136
|
+
) => {
|
137
|
+
try {
|
138
|
+
process.stdout.write(`Applying undo migration ${filename}... `);
|
139
|
+
await client.query("BEGIN");
|
140
|
+
|
141
|
+
await client.query(
|
142
|
+
`SET search_path TO ${schema};
|
143
|
+
${script.toString()}`
|
144
|
+
);
|
145
|
+
|
146
|
+
await client.query(
|
147
|
+
`INSERT INTO ${schema}.stepwise_migration_events (type, filename, script) VALUES ($1, $2, $3)`,
|
148
|
+
["undo", filename, script]
|
149
|
+
);
|
150
|
+
|
151
|
+
await client.query("COMMIT");
|
152
|
+
|
153
|
+
console.log(`done!`);
|
154
|
+
} catch (error) {
|
155
|
+
try {
|
156
|
+
await client.query("ROLLBACK");
|
157
|
+
} catch (error) {
|
158
|
+
console.error("Error rolling back transaction", error);
|
159
|
+
}
|
160
|
+
console.error("Error applying undo migration", error);
|
161
|
+
process.exit(1);
|
162
|
+
}
|
108
163
|
};
|