stepwise-migrations 1.0.7 → 1.0.9

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 CHANGED
@@ -24,10 +24,16 @@ Commands:
24
24
  Migrate the database to the latest version
25
25
  down
26
26
  Rollback the database to the previous version
27
+ validate
28
+ Validate the migration files and the migration history table
29
+ audit
30
+ Show the audit history for the migrations in the database
27
31
  info
28
32
  Show information about the current state of the migrations in the database
29
33
  drop
30
34
  Drop all tables, schema and migration history table
35
+ get-script
36
+ Get the script for the last applied migration
31
37
 
32
38
  Options:
33
39
  --connection <connection> The connection string to use to connect to the database
@@ -36,20 +42,20 @@ Options:
36
42
  --ssl true/false Whether to use SSL for the connection (default: false)
37
43
  --nup Number of up migrations to apply (default: all)
38
44
  --ndown Number of down migrations to apply (default: 1)
45
+ --filename The filename to get the script for (default: last applied migration)
39
46
 
40
47
  Example:
41
- npx stepwise-migrations \
48
+ npx stepwise-migrations migrate \
42
49
  --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydatabase \
43
50
  --schema=myschema \
44
- --path=./db/migration/ \
45
- migrate
51
+ --path=./db/migration/
46
52
  ```
47
53
 
48
54
  ## Examples
49
55
 
50
56
  ### Migrate
51
57
 
52
- Command:
58
+ If all files are in a valid state, runs all the "up" migrations that have not been applied yet.
53
59
 
54
60
  ```bash
55
61
  npx stepwise-migrations migrate \
@@ -58,22 +64,11 @@ npx stepwise-migrations migrate \
58
64
  --path=./db/migration/
59
65
  ```
60
66
 
61
- Outputs:
62
-
63
- ```
64
- Connected to the database
65
- Creating schema collie
66
- Schema collie created
67
- Creating migration history table
68
- Migration history table created
69
- Found 2 migration files
70
- Applied migration V0_01__connect_session_table.sql
71
- Applied migration V0_02__auth.sql
72
- All done!
73
- ```
74
-
75
67
  ### Down
76
68
 
69
+ Runs a single down migration for the last applied migration.
70
+ Can run multiple down migrations if the `--ndown` option is provided.
71
+
77
72
  Command:
78
73
 
79
74
  ```bash
@@ -83,23 +78,34 @@ npx stepwise-migrations down \
83
78
  --path=./db/migration/
84
79
  ```
85
80
 
86
- Outputs:
81
+ ### Validate
87
82
 
83
+ Validates the migration files and the migration history table.
84
+
85
+ ```bash
86
+ npx stepwise-migrations validate \
87
+ --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydb \
88
+ --schema=myschema \
89
+ --path=./db/migration/
88
90
  ```
89
91
 
90
- Connected to the database
91
- Applied down migration v2_auth.down.sql
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
- └─────────┴────┴────────────────────────────────┴────────────────────────────────────────────────────────────────────┴────────────┴──────────────────────────────┘
92
+ ### Audit
93
+
94
+ Shows the audit history for the migrations in the database.
95
+
96
+ ```bash
97
+ npx stepwise-migrations audit \
98
+ --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydb \
99
+ --schema=myschema \
100
+ --path=./db/migration/
99
101
  ```
100
102
 
101
103
  ### Info
102
104
 
105
+ Shows the current state of the migrations in the database.
106
+
107
+ Command:
108
+
103
109
  ```bash
104
110
  npx stepwise-migrations info \
105
111
  --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydb \
@@ -107,23 +113,11 @@ npx stepwise-migrations info \
107
113
  --path=./db/migration/
108
114
  ```
109
115
 
110
- Outputs:
116
+ ### Drop
111
117
 
112
- ```
113
- Connected to the database
114
- Showing information about the current state of the migrations in the database
115
- Migration history schema exists
116
- Migration history table exists
117
- Migration history:
118
- ┌─────────┬────┬────────────────────────────────────┬────────────────────────────────────────────────────────────────────┬────────────┬──────────────────────────────┐
119
- │ (index) │ id │ name │ hash │ applied_by │ applied_at │
120
- ├─────────┼────┼────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┼──────────────────────────────┤
121
- │ 0 │ 1 │ 'V0_01__connect_session_table.sql' │ 'f08638e58139ae0e2dda24b1bdba29f3f2128597066a23d2bb382d448bbe9d7e' │ 'postgres' │ '2024-11-23 16:24:50.437496' │
122
- │ 1 │ 2 │ 'V0_02__auth.sql' │ '0a4c5df39f03df85cb68ef0b297b913d7c15477fa9dcba13b6e0577d88258a8e' │ 'postgres' │ '2024-11-23 16:24:50.440493' │
123
- └─────────┴────┴────────────────────────────────────┴────────────────────────────────────────────────────────────────────┴────────────┴──────────────────────────────┘
124
- ```
118
+ Drops the tables, schema and migration history table.
125
119
 
126
- ### Drop
120
+ Command:
127
121
 
128
122
  ```bash
129
123
  npx stepwise-migrations drop \
@@ -131,10 +125,16 @@ npx stepwise-migrations drop \
131
125
  --schema=myschema
132
126
  ```
133
127
 
134
- Outputs:
128
+ ### Get Script
135
129
 
136
- ```
137
- Connected to the database
138
- Dropping the tables, schema and migration history table
139
- All done!
130
+ Gets the script for the last applied migration.
131
+ Can get the script for a specific migration if the `--filename` option is provided.
132
+
133
+ Command:
134
+
135
+ ```bash
136
+ npx stepwise-migrations get-script \
137
+ --filename v1_users.sql \
138
+ --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydb \
139
+ --schema=myschema
140
140
  ```
package/dist/db.js CHANGED
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.dbCreateHistoryTable = exports.dbCreateSchema = exports.dbMigrationHistory = exports.dbTableExists = exports.dbHistorySchemaExists = exports.dbConnect = void 0;
45
+ exports.dbGetScript = exports.dbCreateHistoryTable = exports.dbAuditHistory = exports.dbCreateSchema = exports.dbMigrationHistory = exports.dbTableExists = exports.dbHistorySchemaExists = exports.dbConnect = void 0;
46
46
  const pg_1 = __importStar(require("pg"));
47
47
  pg_1.default.types.setTypeParser(1114, function (stringValue) {
48
48
  return stringValue; //1114 for time without timezone type
@@ -59,7 +59,6 @@ const dbConnect = (argv) => __awaiter(void 0, void 0, void 0, function* () {
59
59
  try {
60
60
  client = yield pool.connect();
61
61
  yield client.query("SELECT 1");
62
- console.log("Connected to the database");
63
62
  }
64
63
  catch (error) {
65
64
  console.error("Failed to connect to the database", error);
@@ -84,20 +83,40 @@ const dbMigrationHistory = (client, schema) => __awaiter(void 0, void 0, void 0,
84
83
  });
85
84
  exports.dbMigrationHistory = dbMigrationHistory;
86
85
  const dbCreateSchema = (client, schema) => __awaiter(void 0, void 0, void 0, function* () {
87
- console.log(`Creating schema ${schema}`);
86
+ process.stdout.write(`Creating schema ${schema}... `);
88
87
  yield client.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
89
- console.log(`Schema ${schema} created`);
88
+ console.log(`done!`);
90
89
  });
91
90
  exports.dbCreateSchema = dbCreateSchema;
91
+ const dbAuditHistory = (client, schema) => __awaiter(void 0, void 0, void 0, function* () {
92
+ const auditQuery = yield client.query(`SELECT * FROM ${schema}.stepwise_audit`);
93
+ return auditQuery.rows;
94
+ });
95
+ exports.dbAuditHistory = dbAuditHistory;
92
96
  const dbCreateHistoryTable = (client, schema) => __awaiter(void 0, void 0, void 0, function* () {
93
- console.log(`Creating migration history table`);
94
- yield client.query(`CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
95
- id SERIAL PRIMARY KEY,
96
- name TEXT UNIQUE NOT NULL,
97
- hash TEXT NOT NULL,
98
- applied_by TEXT NOT NULL DEFAULT current_user,
99
- applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
100
- )`);
101
- console.log(`Migration history table created`);
97
+ process.stdout.write(`Creating migration history table... `);
98
+ yield client.query(`
99
+ CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
100
+ id SERIAL PRIMARY KEY,
101
+ name TEXT UNIQUE NOT NULL,
102
+ script TEXT NOT NULL,
103
+ applied_by TEXT NOT NULL DEFAULT current_user,
104
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
105
+ );
106
+ CREATE TABLE IF NOT EXISTS ${schema}.stepwise_audit (
107
+ id SERIAL PRIMARY KEY,
108
+ type TEXT NOT NULL,
109
+ name TEXT UNIQUE NOT NULL,
110
+ script TEXT NOT NULL,
111
+ applied_by TEXT NOT NULL DEFAULT current_user,
112
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
113
+ );
114
+ `);
115
+ console.log(`done!`);
102
116
  });
103
117
  exports.dbCreateHistoryTable = dbCreateHistoryTable;
118
+ const dbGetScript = (client, schema, filename) => __awaiter(void 0, void 0, void 0, function* () {
119
+ const script = yield client.query(`SELECT script FROM ${schema}.stepwise_audit WHERE name = $1`, [filename]);
120
+ return script.rows[0].script;
121
+ });
122
+ exports.dbGetScript = dbGetScript;
package/dist/index.js CHANGED
@@ -35,29 +35,46 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
35
35
  }
36
36
  const migrationHistory = yield (0, db_1.dbMigrationHistory)(client, schema);
37
37
  const migrationFiles = yield (0, utils_1.readMigrationFiles)(argv.path);
38
- console.log(`Found ${migrationFiles.length} migration files`);
39
38
  (0, migrate_1.validateMigrationFiles)(migrationFiles, migrationHistory);
39
+ if (migrationFiles.length === migrationHistory.length) {
40
+ console.log("All migrations are already applied");
41
+ process.exit(0);
42
+ }
40
43
  const migrationsToApply = migrationFiles.slice(migrationHistory.length, migrationHistory.length + nUp);
41
- for (const { filename, contents, hash } of migrationsToApply) {
42
- yield (0, migrate_1.applyMigration)(client, schema, filename, contents, hash);
44
+ for (const { filename, script } of migrationsToApply) {
45
+ yield (0, migrate_1.applyMigration)(client, schema, filename, script);
43
46
  }
44
- console.log("All done!");
45
- console.log("New migration history:");
46
- console.table(yield (0, db_1.dbMigrationHistory)(client, schema));
47
+ console.log(`All done! Applied ${migrationsToApply.length} migrations`);
48
+ (0, utils_1.printMigrationHistoryAndUnappliedMigrations)(yield (0, utils_1.readMigrationFiles)(argv.path), yield (0, db_1.dbMigrationHistory)(client, schema));
47
49
  }
48
50
  else if (command === "info") {
49
- console.log("Showing information about the current state of the migrations in the database");
50
- console.log(historySchemaExists ? "Schema exists" : "Schema does not exist");
51
- console.log(tableExists
52
- ? "Migration history table exists"
53
- : "Migration history table does not exist");
54
- console.log("Migration history:");
55
- console.table(yield (0, db_1.dbMigrationHistory)(client, schema));
51
+ if (!historySchemaExists) {
52
+ console.log("Schema does not exist");
53
+ }
54
+ if (!tableExists) {
55
+ console.log("Migration history table does not exist");
56
+ }
57
+ if (historySchemaExists && tableExists) {
58
+ (0, utils_1.printMigrationHistory)(yield (0, db_1.dbMigrationHistory)(client, schema));
59
+ }
60
+ }
61
+ else if (command === "validate") {
62
+ if (!historySchemaExists) {
63
+ console.log("Schema does not exist");
64
+ }
65
+ if (!tableExists) {
66
+ console.log("Migration history table does not exist");
67
+ }
68
+ if (historySchemaExists && tableExists) {
69
+ (0, migrate_1.validateMigrationFiles)(yield (0, utils_1.readMigrationFiles)(argv.path), yield (0, db_1.dbMigrationHistory)(client, schema));
70
+ }
71
+ console.log("Validation passed");
72
+ (0, utils_1.printMigrationHistoryAndUnappliedMigrations)(yield (0, utils_1.readMigrationFiles)(argv.path), yield (0, db_1.dbMigrationHistory)(client, schema));
56
73
  }
57
74
  else if (command === "drop") {
58
- console.log("Dropping the tables, schema and migration history table");
75
+ process.stdout.write(`Dropping the tables, schema and migration history table... `);
59
76
  yield client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
60
- console.log("All done!");
77
+ console.log(`done!`);
61
78
  }
62
79
  else if (command === "down") {
63
80
  const nDown = argv.ndown || 1;
@@ -66,12 +83,31 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
66
83
  const reverseMigrationHistory = migrationHistory.reverse().slice(0, nDown);
67
84
  const downMigrationFilesToApply = yield (0, utils_1.readDownMigrationFiles)(argv.path, reverseMigrationHistory);
68
85
  (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);
86
+ for (const { filename, script, upFilename } of downMigrationFilesToApply) {
87
+ yield (0, migrate_1.applyDownMigration)(client, schema, filename, script, upFilename);
71
88
  }
72
- console.log("All done!");
73
- console.log("New migration history:");
74
- console.table(yield (0, db_1.dbMigrationHistory)(client, schema));
89
+ console.log(`All done! Applied ${downMigrationFilesToApply.length} down migration${downMigrationFilesToApply.length === 1 ? "" : "s"}`);
90
+ (0, utils_1.printMigrationHistoryAndUnappliedMigrations)(yield (0, utils_1.readMigrationFiles)(argv.path), yield (0, db_1.dbMigrationHistory)(client, schema));
91
+ }
92
+ else if (command === "audit") {
93
+ const auditHistory = yield (0, db_1.dbAuditHistory)(client, schema);
94
+ console.log("Audit history:");
95
+ console.table(auditHistory.map((row) => ({
96
+ id: row.id,
97
+ type: row.type,
98
+ name: row.name,
99
+ applied_by: row.applied_by,
100
+ applied_at: row.applied_at,
101
+ })));
102
+ }
103
+ else if (command === "get-script") {
104
+ const script = yield (0, db_1.dbGetScript)(client, schema, argv.filename);
105
+ console.log(script);
106
+ }
107
+ else {
108
+ console.error(`Invalid command: ${argv._[0]}`);
109
+ console.log(utils_1.usage);
110
+ process.exit(1);
75
111
  }
76
112
  client.release();
77
113
  process.exit(0);
package/dist/migrate.js CHANGED
@@ -8,8 +8,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.applyDownMigration = exports.validateDownMigrationFiles = exports.applyMigration = exports.validateMigrationFiles = void 0;
16
+ const git_diff_1 = __importDefault(require("git-diff"));
13
17
  const validateMigrationFiles = (migrationFiles, migrationHistory, isUp = true) => {
14
18
  if (migrationFiles.length === 0) {
15
19
  console.log("No migrations found");
@@ -19,12 +23,8 @@ const validateMigrationFiles = (migrationFiles, migrationHistory, isUp = true) =
19
23
  console.error("Error: migration history is longer than the number of migration files, aborting.");
20
24
  process.exit(1);
21
25
  }
22
- if (migrationFiles.length === migrationHistory.length && isUp) {
23
- console.log("All migrations are already applied");
24
- process.exit(0);
25
- }
26
26
  for (let i = 0; i < migrationFiles.length; i++) {
27
- const { filename, hash: migrationHash } = migrationFiles[i];
27
+ const { filename, script: migrationScript } = migrationFiles[i];
28
28
  if (i >= migrationHistory.length) {
29
29
  continue;
30
30
  }
@@ -32,21 +32,27 @@ const validateMigrationFiles = (migrationFiles, migrationHistory, isUp = true) =
32
32
  console.error(`Error: migration ${filename} has been renamed, aborting.`);
33
33
  process.exit(1);
34
34
  }
35
- if (migrationHistory[i].hash !== migrationHash) {
35
+ if (migrationHistory[i].script !== migrationScript) {
36
36
  console.error(`Error: migration ${filename} has been modified, aborting.`);
37
+ console.log((0, git_diff_1.default)(migrationHistory[i].script, migrationScript, {
38
+ color: true,
39
+ noHeaders: true,
40
+ }));
37
41
  process.exit(1);
38
42
  }
39
43
  }
40
44
  };
41
45
  exports.validateMigrationFiles = validateMigrationFiles;
42
- const applyMigration = (client, schema, filename, contents, hash) => __awaiter(void 0, void 0, void 0, function* () {
46
+ const applyMigration = (client, schema, filename, script) => __awaiter(void 0, void 0, void 0, function* () {
43
47
  try {
48
+ process.stdout.write(`Applying migration ${filename}... `);
44
49
  yield client.query("BEGIN");
45
50
  yield client.query(`SET search_path TO ${schema};
46
- ${contents.toString()}`);
47
- yield client.query(`INSERT INTO ${schema}.stepwise_migrations (name, hash) VALUES ($1, $2)`, [filename, hash]);
51
+ ${script.toString()}`);
52
+ yield client.query(`INSERT INTO ${schema}.stepwise_migrations (name, script) VALUES ($1, $2)`, [filename, script]);
53
+ yield client.query(`INSERT INTO ${schema}.stepwise_audit (type, name, script) VALUES ($1, $2, $3)`, ["up", filename, script]);
48
54
  yield client.query("COMMIT");
49
- console.log(`Applied migration ${filename}`);
55
+ console.log(`done!`);
50
56
  }
51
57
  catch (error) {
52
58
  try {
@@ -71,14 +77,16 @@ const validateDownMigrationFiles = (downMigrationFilesToApply, reverseMigrationH
71
77
  }
72
78
  };
73
79
  exports.validateDownMigrationFiles = validateDownMigrationFiles;
74
- const applyDownMigration = (client, schema, filename, contents, upFilename) => __awaiter(void 0, void 0, void 0, function* () {
80
+ const applyDownMigration = (client, schema, filename, script, upFilename) => __awaiter(void 0, void 0, void 0, function* () {
75
81
  try {
82
+ process.stdout.write(`Applying down migration ${filename}... `);
76
83
  yield client.query("BEGIN");
77
84
  yield client.query(`SET search_path TO ${schema};
78
- ${contents.toString()}`);
85
+ ${script.toString()}`);
79
86
  yield client.query(`DELETE FROM ${schema}.stepwise_migrations WHERE name = $1`, [upFilename]);
87
+ yield client.query(`INSERT INTO ${schema}.stepwise_audit (type, name, script) VALUES ($1, $2, $3)`, ["down", filename, script]);
80
88
  yield client.query("COMMIT");
81
- console.log(`Applied down migration ${filename}`);
89
+ console.log(`done!`);
82
90
  }
83
91
  catch (error) {
84
92
  try {
package/dist/utils.js CHANGED
@@ -12,14 +12,9 @@ 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.readDownMigrationFiles = exports.fileExists = exports.readMigrationFiles = exports.validateArgs = exports.usage = exports.calculateHash = void 0;
16
- const crypto_1 = __importDefault(require("crypto"));
15
+ exports.readDownMigrationFiles = exports.fileExists = exports.printMigrationHistory = exports.printMigrationHistoryAndUnappliedMigrations = exports.readMigrationFiles = exports.validateArgs = exports.usage = void 0;
17
16
  const promises_1 = __importDefault(require("fs/promises"));
18
17
  const path_1 = __importDefault(require("path"));
19
- const calculateHash = (contents) => {
20
- return crypto_1.default.createHash("sha256").update(contents).digest("hex");
21
- };
22
- exports.calculateHash = calculateHash;
23
18
  exports.usage = `
24
19
  Usage: stepwise-migrations [command] [options]
25
20
 
@@ -42,11 +37,10 @@ Options:
42
37
  --ndown Number of down migrations to apply (default: 1)
43
38
 
44
39
  Example:
45
- npx stepwise-migrations \
40
+ npx stepwise-migrations migrate \
46
41
  --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydatabase \
47
42
  --schema=myschema \
48
- --path=./db/migration/ \
49
- migrate
43
+ --path=./db/migration/
50
44
  `;
51
45
  const validateArgs = (argv) => {
52
46
  const required = ["connection", "schema", "path", "_"];
@@ -60,14 +54,6 @@ const validateArgs = (argv) => {
60
54
  console.log(exports.usage);
61
55
  process.exit(1);
62
56
  }
63
- if (argv._[0] !== "migrate" &&
64
- argv._[0] !== "info" &&
65
- argv._[0] !== "drop" &&
66
- argv._[0] !== "down") {
67
- console.error(`Invalid command: ${argv._[0]}`);
68
- console.log(exports.usage);
69
- process.exit(1);
70
- }
71
57
  };
72
58
  exports.validateArgs = validateArgs;
73
59
  const readMigrationFiles = (directory) => __awaiter(void 0, void 0, void 0, function* () {
@@ -80,18 +66,41 @@ const readMigrationFiles = (directory) => __awaiter(void 0, void 0, void 0, func
80
66
  migrationFiles.sort();
81
67
  const results = [];
82
68
  for (const fullFilePath of migrationFiles) {
83
- const contents = yield promises_1.default.readFile(fullFilePath, "utf8");
69
+ const script = yield promises_1.default.readFile(fullFilePath, "utf8");
84
70
  results.push({
85
71
  type: "up",
86
72
  fullFilePath,
87
73
  filename: path_1.default.basename(fullFilePath),
88
- hash: (0, exports.calculateHash)(contents),
89
- contents,
74
+ script,
90
75
  });
91
76
  }
92
77
  return results;
93
78
  });
94
79
  exports.readMigrationFiles = readMigrationFiles;
80
+ const printMigrationHistoryAndUnappliedMigrations = (migrationFiles, migrationHistory) => {
81
+ console.log("Migration history:");
82
+ console.table(migrationHistory.map((h) => ({
83
+ id: h.id,
84
+ name: h.name,
85
+ applied_by: h.applied_by,
86
+ applied_at: h.applied_at,
87
+ })));
88
+ console.log("Unapplied migrations:");
89
+ console.table(migrationFiles.slice(migrationHistory.length).map((m) => ({
90
+ filename: m.filename,
91
+ })));
92
+ };
93
+ exports.printMigrationHistoryAndUnappliedMigrations = printMigrationHistoryAndUnappliedMigrations;
94
+ const printMigrationHistory = (migrationHistory) => {
95
+ console.log("Migration history:");
96
+ console.table(migrationHistory.map((h) => ({
97
+ id: h.id,
98
+ name: h.name,
99
+ applied_by: h.applied_by,
100
+ applied_at: h.applied_at,
101
+ })));
102
+ };
103
+ exports.printMigrationHistory = printMigrationHistory;
95
104
  const fileExists = (path) => __awaiter(void 0, void 0, void 0, function* () {
96
105
  try {
97
106
  return (yield promises_1.default.stat(path)).isFile();
@@ -109,13 +118,13 @@ const readDownMigrationFiles = (directory, migrationHistory) => __awaiter(void 0
109
118
  console.error(`Down migration file not found: ${fullFilePath}`);
110
119
  process.exit(1);
111
120
  }
112
- const contents = yield promises_1.default.readFile(fullFilePath, "utf8");
121
+ const script = yield promises_1.default.readFile(fullFilePath, "utf8");
113
122
  results.push({
114
123
  type: "down",
115
124
  fullFilePath,
116
125
  filename: path_1.default.basename(fullFilePath),
117
126
  upFilename: migration.name,
118
- contents,
127
+ script,
119
128
  });
120
129
  }
121
130
  return results;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stepwise-migrations",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -15,6 +15,7 @@
15
15
  "author": "github.com/mj1618",
16
16
  "license": "MIT",
17
17
  "devDependencies": {
18
+ "@types/git-diff": "^2.0.7",
18
19
  "@types/pg": "^8.11.10",
19
20
  "@types/yargs": "^17.0.33"
20
21
  },
@@ -22,6 +23,7 @@
22
23
  "stepwise-migrations": "dist/index.js"
23
24
  },
24
25
  "dependencies": {
26
+ "git-diff": "^2.0.6",
25
27
  "pg": "^8.13.1",
26
28
  "yargs": "^17.7.2"
27
29
  }
package/src/db.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import pg, { Pool, PoolClient } from "pg";
2
- import { MigrationRow } from "./types";
2
+ import { AuditRow, MigrationRow } from "./types";
3
3
 
4
4
  pg.types.setTypeParser(1114, function (stringValue) {
5
5
  return stringValue; //1114 for time without timezone type
@@ -19,7 +19,6 @@ export const dbConnect = async (argv: { connection: string; ssl?: string }) => {
19
19
  try {
20
20
  client = await pool.connect();
21
21
  await client.query("SELECT 1");
22
- console.log("Connected to the database");
23
22
  } catch (error) {
24
23
  console.error("Failed to connect to the database", error);
25
24
  process.exit(1);
@@ -57,24 +56,53 @@ export const dbMigrationHistory = async (
57
56
  };
58
57
 
59
58
  export const dbCreateSchema = async (client: PoolClient, schema: string) => {
60
- console.log(`Creating schema ${schema}`);
59
+ process.stdout.write(`Creating schema ${schema}... `);
61
60
  await client.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
62
- console.log(`Schema ${schema} created`);
61
+ console.log(`done!`);
62
+ };
63
+
64
+ export const dbAuditHistory = async (client: PoolClient, schema: string) => {
65
+ const auditQuery = await client.query(
66
+ `SELECT * FROM ${schema}.stepwise_audit`
67
+ );
68
+ return auditQuery.rows as AuditRow[];
63
69
  };
64
70
 
65
71
  export const dbCreateHistoryTable = async (
66
72
  client: PoolClient,
67
73
  schema: string
68
74
  ) => {
69
- console.log(`Creating migration history table`);
75
+ process.stdout.write(`Creating migration history table... `);
70
76
  await client.query(
71
- `CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
72
- id SERIAL PRIMARY KEY,
73
- name TEXT UNIQUE NOT NULL,
74
- hash TEXT NOT NULL,
75
- applied_by TEXT NOT NULL DEFAULT current_user,
76
- applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
77
- )`
77
+ `
78
+ CREATE TABLE IF NOT EXISTS ${schema}.stepwise_migrations (
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 (
86
+ id SERIAL PRIMARY KEY,
87
+ type TEXT NOT NULL,
88
+ name TEXT UNIQUE NOT NULL,
89
+ script TEXT NOT NULL,
90
+ applied_by TEXT NOT NULL DEFAULT current_user,
91
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
92
+ );
93
+ `
94
+ );
95
+ console.log(`done!`);
96
+ };
97
+
98
+ export const dbGetScript = async (
99
+ client: PoolClient,
100
+ schema: string,
101
+ filename: string
102
+ ) => {
103
+ const script = await client.query(
104
+ `SELECT script FROM ${schema}.stepwise_audit WHERE name = $1`,
105
+ [filename]
78
106
  );
79
- console.log(`Migration history table created`);
107
+ return script.rows[0].script;
80
108
  };
package/src/index.ts CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  import yargs from "yargs";
4
4
  import {
5
+ dbAuditHistory,
5
6
  dbConnect,
6
7
  dbCreateHistoryTable,
7
8
  dbCreateSchema,
9
+ dbGetScript,
8
10
  dbHistorySchemaExists,
9
11
  dbMigrationHistory,
10
12
  dbTableExists,
@@ -16,8 +18,11 @@ import {
16
18
  validateMigrationFiles,
17
19
  } from "./migrate";
18
20
  import {
21
+ printMigrationHistory,
22
+ printMigrationHistoryAndUnappliedMigrations,
19
23
  readDownMigrationFiles,
20
24
  readMigrationFiles,
25
+ usage,
21
26
  validateArgs,
22
27
  } from "./utils";
23
28
 
@@ -44,40 +49,68 @@ const main = async () => {
44
49
 
45
50
  const migrationHistory = await dbMigrationHistory(client, schema);
46
51
  const migrationFiles = await readMigrationFiles(argv.path);
47
- console.log(`Found ${migrationFiles.length} migration files`);
48
52
 
49
53
  validateMigrationFiles(migrationFiles, migrationHistory);
50
54
 
55
+ if (migrationFiles.length === migrationHistory.length) {
56
+ console.log("All migrations are already applied");
57
+ process.exit(0);
58
+ }
59
+
51
60
  const migrationsToApply = migrationFiles.slice(
52
61
  migrationHistory.length,
53
62
  migrationHistory.length + nUp
54
63
  );
55
64
 
56
- for (const { filename, contents, hash } of migrationsToApply) {
57
- await applyMigration(client, schema, filename, contents, hash);
65
+ for (const { filename, script } of migrationsToApply) {
66
+ await applyMigration(client, schema, filename, script);
58
67
  }
59
68
 
60
- console.log("All done!");
61
- console.log("New migration history:");
62
- console.table(await dbMigrationHistory(client, schema));
63
- } else if (command === "info") {
64
- console.log(
65
- "Showing information about the current state of the migrations in the database"
66
- );
67
- console.log(
68
- historySchemaExists ? "Schema exists" : "Schema does not exist"
69
+ console.log(`All done! Applied ${migrationsToApply.length} migrations`);
70
+
71
+ printMigrationHistoryAndUnappliedMigrations(
72
+ await readMigrationFiles(argv.path),
73
+ await dbMigrationHistory(client, schema)
69
74
  );
70
- console.log(
71
- tableExists
72
- ? "Migration history table exists"
73
- : "Migration history table does not exist"
75
+ } else if (command === "info") {
76
+ if (!historySchemaExists) {
77
+ console.log("Schema does not exist");
78
+ }
79
+
80
+ if (!tableExists) {
81
+ console.log("Migration history table does not exist");
82
+ }
83
+
84
+ if (historySchemaExists && tableExists) {
85
+ printMigrationHistory(await dbMigrationHistory(client, schema));
86
+ }
87
+ } else if (command === "validate") {
88
+ if (!historySchemaExists) {
89
+ console.log("Schema does not exist");
90
+ }
91
+
92
+ if (!tableExists) {
93
+ console.log("Migration history table does not exist");
94
+ }
95
+
96
+ if (historySchemaExists && tableExists) {
97
+ validateMigrationFiles(
98
+ await readMigrationFiles(argv.path),
99
+ await dbMigrationHistory(client, schema)
100
+ );
101
+ }
102
+ console.log("Validation passed");
103
+
104
+ printMigrationHistoryAndUnappliedMigrations(
105
+ await readMigrationFiles(argv.path),
106
+ await dbMigrationHistory(client, schema)
74
107
  );
75
- console.log("Migration history:");
76
- console.table(await dbMigrationHistory(client, schema));
77
108
  } else if (command === "drop") {
78
- console.log("Dropping the tables, schema and migration history table");
109
+ process.stdout.write(
110
+ `Dropping the tables, schema and migration history table... `
111
+ );
79
112
  await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
80
- console.log("All done!");
113
+ console.log(`done!`);
81
114
  } else if (command === "down") {
82
115
  const nDown = argv.ndown || 1;
83
116
 
@@ -98,16 +131,38 @@ const main = async () => {
98
131
  downMigrationFilesToApply,
99
132
  reverseMigrationHistory
100
133
  );
101
- for (const {
102
- filename,
103
- contents,
104
- upFilename,
105
- } of downMigrationFilesToApply) {
106
- await applyDownMigration(client, schema, filename, contents, upFilename);
134
+ for (const { filename, script, upFilename } of downMigrationFilesToApply) {
135
+ await applyDownMigration(client, schema, filename, script, upFilename);
107
136
  }
108
- console.log("All done!");
109
- console.log("New migration history:");
110
- console.table(await dbMigrationHistory(client, schema));
137
+ console.log(
138
+ `All done! Applied ${downMigrationFilesToApply.length} down migration${
139
+ downMigrationFilesToApply.length === 1 ? "" : "s"
140
+ }`
141
+ );
142
+
143
+ printMigrationHistoryAndUnappliedMigrations(
144
+ await readMigrationFiles(argv.path),
145
+ await dbMigrationHistory(client, schema)
146
+ );
147
+ } else if (command === "audit") {
148
+ const auditHistory = await dbAuditHistory(client, schema);
149
+ console.log("Audit history:");
150
+ console.table(
151
+ auditHistory.map((row) => ({
152
+ id: row.id,
153
+ type: row.type,
154
+ name: row.name,
155
+ applied_by: row.applied_by,
156
+ applied_at: row.applied_at,
157
+ }))
158
+ );
159
+ } else if (command === "get-script") {
160
+ const script = await dbGetScript(client, schema, argv.filename);
161
+ console.log(script);
162
+ } else {
163
+ console.error(`Invalid command: ${argv._[0]}`);
164
+ console.log(usage);
165
+ process.exit(1);
111
166
  }
112
167
 
113
168
  client.release();
package/src/migrate.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import gitDiff from "git-diff";
1
2
  import { PoolClient } from "pg";
2
3
  import { MigrationRow } from "./types";
3
4
 
4
5
  export const validateMigrationFiles = (
5
- migrationFiles: { fullFilePath: string; filename: string; hash: string }[],
6
+ migrationFiles: { fullFilePath: string; filename: string; script: string }[],
6
7
  migrationHistory: MigrationRow[],
7
8
  isUp: boolean = true
8
9
  ) => {
@@ -18,13 +19,8 @@ export const validateMigrationFiles = (
18
19
  process.exit(1);
19
20
  }
20
21
 
21
- if (migrationFiles.length === migrationHistory.length && isUp) {
22
- console.log("All migrations are already applied");
23
- process.exit(0);
24
- }
25
-
26
22
  for (let i = 0; i < migrationFiles.length; i++) {
27
- const { filename, hash: migrationHash } = migrationFiles[i];
23
+ const { filename, script: migrationScript } = migrationFiles[i];
28
24
  if (i >= migrationHistory.length) {
29
25
  continue;
30
26
  }
@@ -32,10 +28,18 @@ export const validateMigrationFiles = (
32
28
  console.error(`Error: migration ${filename} has been renamed, aborting.`);
33
29
  process.exit(1);
34
30
  }
35
- if (migrationHistory[i].hash !== migrationHash) {
31
+ if (migrationHistory[i].script !== migrationScript) {
36
32
  console.error(
37
33
  `Error: migration ${filename} has been modified, aborting.`
38
34
  );
35
+
36
+ console.log(
37
+ gitDiff(migrationHistory[i].script, migrationScript, {
38
+ color: true,
39
+ noHeaders: true,
40
+ })
41
+ );
42
+
39
43
  process.exit(1);
40
44
  }
41
45
  }
@@ -45,25 +49,30 @@ export const applyMigration = async (
45
49
  client: PoolClient,
46
50
  schema: string,
47
51
  filename: string,
48
- contents: string,
49
- hash: string
52
+ script: string
50
53
  ) => {
51
54
  try {
55
+ process.stdout.write(`Applying migration ${filename}... `);
52
56
  await client.query("BEGIN");
53
57
 
54
58
  await client.query(
55
59
  `SET search_path TO ${schema};
56
- ${contents.toString()}`
60
+ ${script.toString()}`
57
61
  );
58
62
 
59
63
  await client.query(
60
- `INSERT INTO ${schema}.stepwise_migrations (name, hash) VALUES ($1, $2)`,
61
- [filename, hash]
64
+ `INSERT INTO ${schema}.stepwise_migrations (name, script) VALUES ($1, $2)`,
65
+ [filename, script]
66
+ );
67
+
68
+ await client.query(
69
+ `INSERT INTO ${schema}.stepwise_audit (type, name, script) VALUES ($1, $2, $3)`,
70
+ ["up", filename, script]
62
71
  );
63
72
 
64
73
  await client.query("COMMIT");
65
74
 
66
- console.log(`Applied migration ${filename}`);
75
+ console.log(`done!`);
67
76
  } catch (error) {
68
77
  try {
69
78
  await client.query("ROLLBACK");
@@ -97,15 +106,16 @@ export const applyDownMigration = async (
97
106
  client: PoolClient,
98
107
  schema: string,
99
108
  filename: string,
100
- contents: string,
109
+ script: string,
101
110
  upFilename: string
102
111
  ) => {
103
112
  try {
113
+ process.stdout.write(`Applying down migration ${filename}... `);
104
114
  await client.query("BEGIN");
105
115
 
106
116
  await client.query(
107
117
  `SET search_path TO ${schema};
108
- ${contents.toString()}`
118
+ ${script.toString()}`
109
119
  );
110
120
 
111
121
  await client.query(
@@ -113,9 +123,14 @@ export const applyDownMigration = async (
113
123
  [upFilename]
114
124
  );
115
125
 
126
+ await client.query(
127
+ `INSERT INTO ${schema}.stepwise_audit (type, name, script) VALUES ($1, $2, $3)`,
128
+ ["down", filename, script]
129
+ );
130
+
116
131
  await client.query("COMMIT");
117
132
 
118
- console.log(`Applied down migration ${filename}`);
133
+ console.log(`done!`);
119
134
  } catch (error) {
120
135
  try {
121
136
  await client.query("ROLLBACK");
package/src/types.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export interface MigrationRow {
2
2
  id: string;
3
3
  name: string;
4
- hash: string;
4
+ script: string;
5
5
  applied_by: string;
6
6
  applied_at: string;
7
7
  }
8
+
9
+ export type AuditRow = MigrationRow & { type: "up" | "down" };
package/src/utils.ts CHANGED
@@ -1,12 +1,7 @@
1
- import crypto from "crypto";
2
1
  import fs from "fs/promises";
3
2
  import path from "path";
4
3
  import { MigrationRow } from "./types";
5
4
 
6
- export const calculateHash = (contents: string) => {
7
- return crypto.createHash("sha256").update(contents).digest("hex");
8
- };
9
-
10
5
  export const usage = `
11
6
  Usage: stepwise-migrations [command] [options]
12
7
 
@@ -29,11 +24,10 @@ Options:
29
24
  --ndown Number of down migrations to apply (default: 1)
30
25
 
31
26
  Example:
32
- npx stepwise-migrations \
27
+ npx stepwise-migrations migrate \
33
28
  --connection=postgresql://postgres:postgres@127.0.0.1:5432/mydatabase \
34
29
  --schema=myschema \
35
- --path=./db/migration/ \
36
- migrate
30
+ --path=./db/migration/
37
31
  `;
38
32
 
39
33
  export const validateArgs = (argv: any) => {
@@ -51,16 +45,6 @@ export const validateArgs = (argv: any) => {
51
45
  console.log(usage);
52
46
  process.exit(1);
53
47
  }
54
- if (
55
- argv._[0] !== "migrate" &&
56
- argv._[0] !== "info" &&
57
- argv._[0] !== "drop" &&
58
- argv._[0] !== "down"
59
- ) {
60
- console.error(`Invalid command: ${argv._[0]}`);
61
- console.log(usage);
62
- process.exit(1);
63
- }
64
48
  };
65
49
 
66
50
  export const readMigrationFiles = async (directory: string) => {
@@ -78,23 +62,54 @@ export const readMigrationFiles = async (directory: string) => {
78
62
  type: "up";
79
63
  fullFilePath: string;
80
64
  filename: string;
81
- hash: string;
82
- contents: string;
65
+ script: string;
83
66
  }[] = [];
84
67
  for (const fullFilePath of migrationFiles) {
85
- const contents = await fs.readFile(fullFilePath, "utf8");
68
+ const script = await fs.readFile(fullFilePath, "utf8");
86
69
 
87
70
  results.push({
88
71
  type: "up",
89
72
  fullFilePath,
90
73
  filename: path.basename(fullFilePath),
91
- hash: calculateHash(contents),
92
- contents,
74
+ script,
93
75
  });
94
76
  }
95
77
  return results;
96
78
  };
97
79
 
80
+ export const printMigrationHistoryAndUnappliedMigrations = (
81
+ migrationFiles: { filename: string }[],
82
+ migrationHistory: MigrationRow[]
83
+ ) => {
84
+ console.log("Migration history:");
85
+ console.table(
86
+ migrationHistory.map((h) => ({
87
+ id: h.id,
88
+ name: h.name,
89
+ applied_by: h.applied_by,
90
+ applied_at: h.applied_at,
91
+ }))
92
+ );
93
+ console.log("Unapplied migrations:");
94
+ console.table(
95
+ migrationFiles.slice(migrationHistory.length).map((m) => ({
96
+ filename: m.filename,
97
+ }))
98
+ );
99
+ };
100
+
101
+ export const printMigrationHistory = (migrationHistory: MigrationRow[]) => {
102
+ console.log("Migration history:");
103
+ console.table(
104
+ migrationHistory.map((h) => ({
105
+ id: h.id,
106
+ name: h.name,
107
+ applied_by: h.applied_by,
108
+ applied_at: h.applied_at,
109
+ }))
110
+ );
111
+ };
112
+
98
113
  export const fileExists = async (path: string) => {
99
114
  try {
100
115
  return (await fs.stat(path)).isFile();
@@ -113,7 +128,7 @@ export const readDownMigrationFiles = async (
113
128
  filename: string;
114
129
  upFilename: string;
115
130
 
116
- contents: string;
131
+ script: string;
117
132
  }[] = [];
118
133
  for (const migration of migrationHistory) {
119
134
  const fullFilePath = path.join(
@@ -124,13 +139,13 @@ export const readDownMigrationFiles = async (
124
139
  console.error(`Down migration file not found: ${fullFilePath}`);
125
140
  process.exit(1);
126
141
  }
127
- const contents = await fs.readFile(fullFilePath, "utf8");
142
+ const script = await fs.readFile(fullFilePath, "utf8");
128
143
  results.push({
129
144
  type: "down",
130
145
  fullFilePath,
131
146
  filename: path.basename(fullFilePath),
132
147
  upFilename: migration.name,
133
- contents,
148
+ script,
134
149
  });
135
150
  }
136
151
  return results;