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
package/src/index.ts
CHANGED
@@ -2,26 +2,22 @@
|
|
2
2
|
|
3
3
|
import yargs from "yargs";
|
4
4
|
import {
|
5
|
-
|
5
|
+
applyMigration,
|
6
|
+
applyUndoMigration,
|
6
7
|
dbConnect,
|
7
|
-
|
8
|
+
dbCreateEventsTable,
|
8
9
|
dbCreateSchema,
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
dbDropAll,
|
11
|
+
dbGetAppliedScript,
|
12
|
+
dbSchemaExists,
|
12
13
|
dbTableExists,
|
13
14
|
} from "./db";
|
15
|
+
import { getUndoFilename, loadState } from "./state";
|
14
16
|
import {
|
15
|
-
|
16
|
-
|
17
|
-
validateDownMigrationFiles,
|
18
|
-
validateMigrationFiles,
|
19
|
-
} from "./migrate";
|
20
|
-
import {
|
21
|
-
printMigrationHistory,
|
17
|
+
abortIfErrors,
|
18
|
+
exitIfNotInitialized,
|
22
19
|
printMigrationHistoryAndUnappliedMigrations,
|
23
|
-
|
24
|
-
readMigrationFiles,
|
20
|
+
sliceFromFirstNull,
|
25
21
|
usage,
|
26
22
|
validateArgs,
|
27
23
|
} from "./utils";
|
@@ -33,134 +29,173 @@ const main = async () => {
|
|
33
29
|
|
34
30
|
const schema = argv.schema;
|
35
31
|
const command = argv._[0];
|
32
|
+
const napply = argv.napply || Infinity;
|
33
|
+
const nundo = argv.nundo || 1;
|
34
|
+
const filePath = argv.path;
|
36
35
|
|
37
36
|
const client = await dbConnect(argv);
|
38
|
-
const
|
37
|
+
const schemaExists = await dbSchemaExists(client, schema);
|
39
38
|
const tableExists = await dbTableExists(client, schema);
|
40
39
|
|
41
40
|
if (command === "migrate") {
|
42
|
-
|
43
|
-
if (!historySchemaExists) {
|
41
|
+
if (!schemaExists) {
|
44
42
|
await dbCreateSchema(client, schema);
|
45
43
|
}
|
46
44
|
if (!tableExists) {
|
47
|
-
await
|
45
|
+
await dbCreateEventsTable(client, schema);
|
48
46
|
}
|
49
47
|
|
50
|
-
const
|
51
|
-
const migrationFiles = await readMigrationFiles(argv.path);
|
48
|
+
const state = await loadState(client, schema, argv.path);
|
52
49
|
|
53
|
-
|
50
|
+
abortIfErrors(state);
|
54
51
|
|
55
|
-
if (
|
52
|
+
if (
|
53
|
+
state.files.unappliedVersionedFiles.length === 0 &&
|
54
|
+
state.files.unappliedRepeatableFiles.length === 0
|
55
|
+
) {
|
56
56
|
console.log("All migrations are already applied");
|
57
57
|
process.exit(0);
|
58
58
|
}
|
59
59
|
|
60
|
-
const migrationsToApply =
|
61
|
-
|
62
|
-
|
63
|
-
);
|
60
|
+
const migrationsToApply = [
|
61
|
+
...state.files.unappliedVersionedFiles,
|
62
|
+
...state.files.unappliedRepeatableFiles,
|
63
|
+
].slice(0, napply);
|
64
64
|
|
65
|
-
for (const
|
66
|
-
await applyMigration(client, schema,
|
65
|
+
for (const migration of migrationsToApply) {
|
66
|
+
await applyMigration(client, schema, migration);
|
67
67
|
}
|
68
68
|
|
69
|
-
console.log(
|
69
|
+
console.log(
|
70
|
+
`All done! Applied ${migrationsToApply.length} migration${
|
71
|
+
migrationsToApply.length === 1 ? "" : "s"
|
72
|
+
}`
|
73
|
+
);
|
70
74
|
|
71
75
|
printMigrationHistoryAndUnappliedMigrations(
|
72
|
-
await
|
73
|
-
await dbMigrationHistory(client, schema)
|
76
|
+
await loadState(client, schema, filePath)
|
74
77
|
);
|
75
78
|
} else if (command === "info") {
|
76
|
-
if (!
|
79
|
+
if (!schemaExists) {
|
77
80
|
console.log("Schema does not exist");
|
78
81
|
}
|
79
82
|
|
80
83
|
if (!tableExists) {
|
81
|
-
console.log(
|
84
|
+
console.log(
|
85
|
+
"Migration table has not been initialised. Run migrate to begin."
|
86
|
+
);
|
82
87
|
}
|
83
88
|
|
84
|
-
if (
|
85
|
-
|
89
|
+
if (schemaExists && tableExists) {
|
90
|
+
printMigrationHistoryAndUnappliedMigrations(
|
91
|
+
await loadState(client, schema, filePath)
|
92
|
+
);
|
86
93
|
}
|
87
|
-
} else if (command === "
|
88
|
-
if (!
|
94
|
+
} else if (command === "status") {
|
95
|
+
if (!schemaExists) {
|
89
96
|
console.log("Schema does not exist");
|
90
97
|
}
|
91
98
|
|
92
99
|
if (!tableExists) {
|
93
|
-
console.log(
|
100
|
+
console.log(
|
101
|
+
"Migration table has not been initialised. Run migrate to begin."
|
102
|
+
);
|
94
103
|
}
|
95
104
|
|
96
|
-
if (
|
97
|
-
|
98
|
-
await
|
99
|
-
await dbMigrationHistory(client, schema)
|
105
|
+
if (schemaExists && tableExists) {
|
106
|
+
printMigrationHistoryAndUnappliedMigrations(
|
107
|
+
await loadState(client, schema, filePath)
|
100
108
|
);
|
101
109
|
}
|
110
|
+
} else if (command === "validate") {
|
111
|
+
exitIfNotInitialized(schemaExists, tableExists);
|
112
|
+
|
113
|
+
const state = await loadState(client, schema, argv.path);
|
114
|
+
if (schemaExists && tableExists) {
|
115
|
+
abortIfErrors(state);
|
116
|
+
}
|
102
117
|
console.log("Validation passed");
|
103
118
|
|
104
|
-
printMigrationHistoryAndUnappliedMigrations(
|
105
|
-
await readMigrationFiles(argv.path),
|
106
|
-
await dbMigrationHistory(client, schema)
|
107
|
-
);
|
119
|
+
printMigrationHistoryAndUnappliedMigrations(state);
|
108
120
|
} else if (command === "drop") {
|
109
121
|
process.stdout.write(
|
110
122
|
`Dropping the tables, schema and migration history table... `
|
111
123
|
);
|
112
|
-
await client
|
124
|
+
await dbDropAll(client, schema);
|
113
125
|
console.log(`done!`);
|
114
|
-
} else if (command === "
|
115
|
-
const
|
116
|
-
|
117
|
-
const migrationHistory = await dbMigrationHistory(client, schema);
|
118
|
-
validateMigrationFiles(
|
119
|
-
await readMigrationFiles(argv.path),
|
120
|
-
migrationHistory,
|
121
|
-
false
|
122
|
-
);
|
126
|
+
} else if (command === "undo") {
|
127
|
+
const state = await loadState(client, schema, filePath);
|
123
128
|
|
124
|
-
|
125
|
-
const downMigrationFilesToApply = await readDownMigrationFiles(
|
126
|
-
argv.path,
|
127
|
-
reverseMigrationHistory
|
128
|
-
);
|
129
|
+
abortIfErrors(state);
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
const reversedAppliedVersionedMigrations =
|
132
|
+
state.current.appliedVersionedMigrations.slice().reverse();
|
133
|
+
|
134
|
+
const undosToApplyAll = reversedAppliedVersionedMigrations.map(
|
135
|
+
(migration) =>
|
136
|
+
state.files.undoFiles.find(
|
137
|
+
(file) => file.filename === getUndoFilename(migration.filename)
|
138
|
+
)
|
133
139
|
);
|
134
|
-
|
135
|
-
|
140
|
+
|
141
|
+
const undosToApply = sliceFromFirstNull(undosToApplyAll).slice(0, nundo);
|
142
|
+
|
143
|
+
if (undosToApply.length < nundo) {
|
144
|
+
console.error(
|
145
|
+
`Error: not enough sequential (from last) undo migrations to apply ${nundo} undos.`
|
146
|
+
);
|
147
|
+
process.exit(1);
|
148
|
+
}
|
149
|
+
|
150
|
+
console.log(undosToApply);
|
151
|
+
for (const { filename, script } of undosToApply) {
|
152
|
+
await applyUndoMigration(client, schema, filename, script);
|
136
153
|
}
|
137
154
|
console.log(
|
138
|
-
`All done!
|
139
|
-
|
155
|
+
`All done! Performed ${undosToApply.length} undo migration${
|
156
|
+
undosToApply.length === 1 ? "" : "s"
|
140
157
|
}`
|
141
158
|
);
|
142
159
|
|
143
|
-
printMigrationHistoryAndUnappliedMigrations(
|
144
|
-
await readMigrationFiles(argv.path),
|
145
|
-
await dbMigrationHistory(client, schema)
|
146
|
-
);
|
160
|
+
printMigrationHistoryAndUnappliedMigrations(state);
|
147
161
|
} else if (command === "audit") {
|
148
|
-
|
149
|
-
|
162
|
+
exitIfNotInitialized(schemaExists, tableExists);
|
163
|
+
|
164
|
+
const state = await loadState(client, schema, argv.path);
|
165
|
+
console.log("Event history:");
|
150
166
|
console.table(
|
151
|
-
|
167
|
+
state.events.map((row) => ({
|
152
168
|
id: row.id,
|
153
169
|
type: row.type,
|
154
|
-
|
170
|
+
filename: row.filename,
|
155
171
|
applied_by: row.applied_by,
|
156
172
|
applied_at: row.applied_at,
|
157
173
|
}))
|
158
174
|
);
|
159
|
-
} else if (command === "get-script") {
|
160
|
-
|
161
|
-
|
175
|
+
} else if (command === "get-applied-script") {
|
176
|
+
if (!schemaExists) {
|
177
|
+
console.log("Schema does not exist");
|
178
|
+
process.exit(1);
|
179
|
+
}
|
180
|
+
|
181
|
+
if (!tableExists) {
|
182
|
+
console.log(
|
183
|
+
"Migration table has not been initialised. Run migrate to begin."
|
184
|
+
);
|
185
|
+
process.exit(1);
|
186
|
+
}
|
187
|
+
|
188
|
+
const state = await loadState(client, schema, argv.path);
|
189
|
+
const script = await dbGetAppliedScript(state, argv.filename);
|
190
|
+
if (script) {
|
191
|
+
console.log(script);
|
192
|
+
} else {
|
193
|
+
console.error(
|
194
|
+
`Script for ${argv.filename} not found, use the audit command to check all applied migrations`
|
195
|
+
);
|
196
|
+
}
|
162
197
|
} else {
|
163
|
-
console.error(`Invalid command: ${
|
198
|
+
console.error(`Invalid command: ${command}`);
|
164
199
|
console.log(usage);
|
165
200
|
process.exit(1);
|
166
201
|
}
|
package/src/state.ts
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
import gitDiff from "git-diff";
|
2
|
+
import path from "path";
|
3
|
+
import { PoolClient } from "pg";
|
4
|
+
import { dbEventHistory } from "./db";
|
5
|
+
import { AppliedMigration, MigrationState } from "./types";
|
6
|
+
import { readMigrationFiles } from "./utils";
|
7
|
+
|
8
|
+
export const validateMigrationFiles = (state: MigrationState) => {
|
9
|
+
let errors: string[] = [];
|
10
|
+
|
11
|
+
if (state.files.allFiles.length === 0) {
|
12
|
+
return ["No migration files found"];
|
13
|
+
}
|
14
|
+
|
15
|
+
if (
|
16
|
+
state.current.appliedVersionedMigrations.length >
|
17
|
+
state.files.versionedFiles.length
|
18
|
+
) {
|
19
|
+
errors.push(
|
20
|
+
"Migration history is in a bad state: more applied versioned migrations than files"
|
21
|
+
);
|
22
|
+
}
|
23
|
+
|
24
|
+
for (let i = 0; i < state.current.appliedVersionedMigrations.length; i++) {
|
25
|
+
const { filename, script } = state.files.versionedFiles[i];
|
26
|
+
if (state.current.appliedVersionedMigrations[i].filename !== filename) {
|
27
|
+
errors.push(
|
28
|
+
`Migration history is in a bad state: applied versioned migration ${state.current.appliedVersionedMigrations[i].filename} does not match file ${filename}`
|
29
|
+
);
|
30
|
+
break;
|
31
|
+
}
|
32
|
+
if (state.current.appliedVersionedMigrations[i].script !== script) {
|
33
|
+
errors.push(
|
34
|
+
`Migration history is in a bad state: applied versioned migration ${state.current.appliedVersionedMigrations[i].filename} has been modified.\n\n` +
|
35
|
+
gitDiff(state.current.appliedVersionedMigrations[i].script, script, {
|
36
|
+
color: true,
|
37
|
+
noHeaders: true,
|
38
|
+
})
|
39
|
+
);
|
40
|
+
break;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
if (
|
45
|
+
state.current.appliedRepeatableMigrations.length >
|
46
|
+
state.files.repeatableFiles.length
|
47
|
+
) {
|
48
|
+
errors.push(
|
49
|
+
"Migration history is in a bad state: more applied repeatable migrations than files"
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
for (let i = 0; i < state.current.appliedRepeatableMigrations.length; i++) {
|
54
|
+
const { filename } = state.files.repeatableFiles[i];
|
55
|
+
if (state.current.appliedRepeatableMigrations[i].filename !== filename) {
|
56
|
+
errors.push(
|
57
|
+
`Migration history is in a bad state: applied repeatable migration ${state.current.appliedRepeatableMigrations[i].filename} does not match file ${filename}`
|
58
|
+
);
|
59
|
+
break;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
return errors;
|
64
|
+
};
|
65
|
+
|
66
|
+
export const loadState = async (
|
67
|
+
client: PoolClient,
|
68
|
+
schema: string,
|
69
|
+
migrationPath: string
|
70
|
+
): Promise<MigrationState> => {
|
71
|
+
const events = await dbEventHistory(client, schema);
|
72
|
+
const {
|
73
|
+
appliedVersionedMigrations,
|
74
|
+
appliedRepeatableMigrations,
|
75
|
+
errors: appliedErrors,
|
76
|
+
} = eventsToApplied(events);
|
77
|
+
const { files: allFiles, errors: readFileErrors } = await readMigrationFiles(
|
78
|
+
path.join(process.cwd(), migrationPath),
|
79
|
+
appliedVersionedMigrations
|
80
|
+
);
|
81
|
+
const unappliedVersionedFiles = allFiles
|
82
|
+
.filter((file) => file.type === "versioned")
|
83
|
+
.filter(
|
84
|
+
(file) =>
|
85
|
+
!appliedVersionedMigrations.find(
|
86
|
+
(event) => event.filename === file.filename
|
87
|
+
)
|
88
|
+
);
|
89
|
+
const unappliedRepeatableFiles = allFiles
|
90
|
+
.filter((file) => file.type === "repeatable")
|
91
|
+
.filter(
|
92
|
+
(file) =>
|
93
|
+
!appliedRepeatableMigrations.find(
|
94
|
+
(event) =>
|
95
|
+
event.filename === file.filename && event.script === file.script
|
96
|
+
)
|
97
|
+
);
|
98
|
+
|
99
|
+
return {
|
100
|
+
schema,
|
101
|
+
current: {
|
102
|
+
appliedVersionedMigrations,
|
103
|
+
appliedRepeatableMigrations,
|
104
|
+
},
|
105
|
+
events,
|
106
|
+
files: {
|
107
|
+
allFiles,
|
108
|
+
unappliedVersionedFiles,
|
109
|
+
unappliedRepeatableFiles,
|
110
|
+
versionedFiles: allFiles.filter((file) => file.type === "versioned"),
|
111
|
+
undoFiles: allFiles.filter((file) => file.type === "undo"),
|
112
|
+
repeatableFiles: allFiles.filter((file) => file.type === "repeatable"),
|
113
|
+
},
|
114
|
+
errors: appliedErrors.concat(readFileErrors),
|
115
|
+
};
|
116
|
+
};
|
117
|
+
|
118
|
+
export const getUndoFilename = (filename: string) => {
|
119
|
+
return filename.replace(".sql", ".undo.sql");
|
120
|
+
};
|
121
|
+
|
122
|
+
export const eventsToApplied = (
|
123
|
+
events: MigrationState["events"]
|
124
|
+
): {
|
125
|
+
errors: string[];
|
126
|
+
appliedVersionedMigrations: AppliedMigration[];
|
127
|
+
appliedRepeatableMigrations: AppliedMigration[];
|
128
|
+
} => {
|
129
|
+
let errors: string[] = [];
|
130
|
+
let appliedVersionedMigrations: AppliedMigration[] = [];
|
131
|
+
let appliedRepeatableMigrations: AppliedMigration[] = [];
|
132
|
+
|
133
|
+
for (const event of events) {
|
134
|
+
if (event.type === "versioned") {
|
135
|
+
appliedVersionedMigrations.push(event);
|
136
|
+
} else if (event.type === "undo") {
|
137
|
+
if (appliedVersionedMigrations.length === 0) {
|
138
|
+
errors.push(
|
139
|
+
"Events table is in a bad state: undo event without a migration to undo"
|
140
|
+
);
|
141
|
+
break;
|
142
|
+
} else if (
|
143
|
+
getUndoFilename(
|
144
|
+
appliedVersionedMigrations[appliedVersionedMigrations.length - 1]
|
145
|
+
.filename
|
146
|
+
) !== event.filename
|
147
|
+
) {
|
148
|
+
errors.push(
|
149
|
+
"Events table is in a bad state: down migration does not match the most recently applied migration"
|
150
|
+
);
|
151
|
+
break;
|
152
|
+
} else {
|
153
|
+
appliedVersionedMigrations.pop();
|
154
|
+
}
|
155
|
+
} else if (event.type === "repeatable") {
|
156
|
+
appliedRepeatableMigrations.push(event);
|
157
|
+
} else {
|
158
|
+
errors.push(
|
159
|
+
`Events table is in a bad state: unknown event type ${event.type}`
|
160
|
+
);
|
161
|
+
break;
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
return { errors, appliedVersionedMigrations, appliedRepeatableMigrations };
|
166
|
+
};
|
package/src/types.ts
CHANGED
@@ -1,9 +1,54 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
import { z } from "zod";
|
2
|
+
|
3
|
+
export type MigrationType = "versioned" | "undo" | "repeatable";
|
4
|
+
|
5
|
+
export interface MigrationFile {
|
6
|
+
type: MigrationType;
|
7
|
+
filename: string;
|
8
|
+
script: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
export interface AppliedMigration {
|
12
|
+
id: number;
|
13
|
+
type: MigrationType;
|
14
|
+
filename: string;
|
4
15
|
script: string;
|
5
16
|
applied_by: string;
|
6
17
|
applied_at: string;
|
7
18
|
}
|
8
19
|
|
9
|
-
export
|
20
|
+
export interface MigrationState {
|
21
|
+
schema: string;
|
22
|
+
current: {
|
23
|
+
appliedVersionedMigrations: AppliedMigration[];
|
24
|
+
appliedRepeatableMigrations: AppliedMigration[];
|
25
|
+
};
|
26
|
+
events: {
|
27
|
+
id: number;
|
28
|
+
type: MigrationType;
|
29
|
+
filename: string;
|
30
|
+
script: string;
|
31
|
+
applied_by: string;
|
32
|
+
applied_at: string;
|
33
|
+
}[];
|
34
|
+
files: {
|
35
|
+
allFiles: MigrationFile[];
|
36
|
+
unappliedVersionedFiles: MigrationFile[];
|
37
|
+
unappliedRepeatableFiles: MigrationFile[];
|
38
|
+
versionedFiles: MigrationFile[];
|
39
|
+
undoFiles: MigrationFile[];
|
40
|
+
repeatableFiles: MigrationFile[];
|
41
|
+
};
|
42
|
+
errors: string[];
|
43
|
+
}
|
44
|
+
|
45
|
+
const MigrationType = z.enum(["versioned", "undo", "repeatable"]);
|
46
|
+
|
47
|
+
export const EventRow = z.object({
|
48
|
+
id: z.number(),
|
49
|
+
type: MigrationType,
|
50
|
+
filename: z.string(),
|
51
|
+
script: z.string(),
|
52
|
+
applied_by: z.string(),
|
53
|
+
applied_at: z.string(),
|
54
|
+
});
|