bunsql-native-migrate 0.3.2 → 0.4.1
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 +173 -51
- package/package.json +1 -1
- package/src/api/create.ts +2 -1
- package/src/api/down.ts +49 -30
- package/src/api/init.ts +2 -2
- package/src/api/load-migration.ts +74 -10
- package/src/api/lock.ts +2 -7
- package/src/api/mark.ts +6 -9
- package/src/api/options.ts +59 -0
- package/src/api/pending.ts +23 -2
- package/src/api/redo.ts +44 -0
- package/src/api/run-step.ts +22 -4
- package/src/api/run-with-driver.ts +10 -5
- package/src/api/status.ts +2 -2
- package/src/api/tracking-table.ts +11 -0
- package/src/api/up.ts +8 -19
- package/src/api/wait.ts +67 -0
- package/src/cli/exit-codes.ts +31 -0
- package/src/cli/main.ts +243 -67
- package/src/core/config.ts +131 -0
- package/src/core/console.ts +4 -1
- package/src/core/driver.ts +1 -0
- package/src/core/duration.ts +16 -0
- package/src/core/env.ts +4 -0
- package/src/core/fs.ts +28 -4
- package/src/drivers/mariadb.ts +4 -1
- package/src/drivers/postgres.ts +4 -1
- package/src/drivers/shared.ts +15 -7
- package/src/drivers/sqlite-lock.ts +87 -0
- package/src/drivers/sqlite.ts +28 -11
- package/src/index.ts +16 -0
package/src/cli/main.ts
CHANGED
|
@@ -1,49 +1,176 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { migrateUp } from "../api/up.js";
|
|
3
4
|
import { migrateDown, parseSteps } from "../api/down.js";
|
|
5
|
+
import { migrateRedo } from "../api/redo.js";
|
|
4
6
|
import { migrateStatus } from "../api/status.js";
|
|
5
7
|
import { installMigrations } from "../api/install.js";
|
|
6
8
|
import { createMigrationCommand, type MigrationLang } from "../api/create.js";
|
|
7
9
|
import { initMigrations } from "../api/init.js";
|
|
8
10
|
import { markMigrationsApplied } from "../api/mark.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { resolveSecondsOption } from "../core/duration.js";
|
|
12
|
+
import { loadProjectConfig, InvalidConfigError } from "../core/config.js";
|
|
13
|
+
import {
|
|
14
|
+
ChecksumDriftError,
|
|
15
|
+
DatabaseWaitTimeoutError,
|
|
16
|
+
InvalidMigrationNameError,
|
|
17
|
+
MigrationFileMissingError,
|
|
18
|
+
MigrationLockError,
|
|
19
|
+
MigrationNotFoundError,
|
|
20
|
+
} from "../api/options.js";
|
|
11
21
|
import { InvalidIdentifierError } from "../core/identifiers.js";
|
|
12
22
|
import { log } from "../core/console.js";
|
|
23
|
+
import { EXIT_PENDING, EXIT_SUCCESS, EXIT_USAGE, exitCodeForError } from "./exit-codes.js";
|
|
13
24
|
|
|
14
25
|
interface CliArgs {
|
|
15
26
|
command: string | undefined;
|
|
16
27
|
positional: string[];
|
|
28
|
+
url?: string | undefined;
|
|
17
29
|
dir?: string | undefined;
|
|
30
|
+
config?: string | undefined;
|
|
18
31
|
git: boolean;
|
|
19
32
|
lang?: MigrationLang | undefined;
|
|
20
33
|
to?: string | undefined;
|
|
21
34
|
lockTimeout?: number | undefined;
|
|
35
|
+
wait?: number | undefined;
|
|
22
36
|
table?: string | undefined;
|
|
23
37
|
schema?: string | undefined;
|
|
24
38
|
dryRun: boolean;
|
|
25
39
|
all: boolean;
|
|
26
40
|
strict: boolean;
|
|
27
41
|
help: boolean;
|
|
42
|
+
version: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type FlagKey = Exclude<keyof CliArgs, "command" | "positional" | "help" | "version" | "config">;
|
|
46
|
+
|
|
47
|
+
const FLAG_NAMES: Record<FlagKey, string> = {
|
|
48
|
+
url: "--url",
|
|
49
|
+
dir: "--dir",
|
|
50
|
+
git: "--git",
|
|
51
|
+
lang: "--lang",
|
|
52
|
+
to: "--to",
|
|
53
|
+
lockTimeout: "--lock-timeout",
|
|
54
|
+
wait: "--wait",
|
|
55
|
+
table: "--table",
|
|
56
|
+
schema: "--schema",
|
|
57
|
+
dryRun: "--dry-run",
|
|
58
|
+
all: "--all",
|
|
59
|
+
strict: "--strict",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
interface CommandSpec {
|
|
63
|
+
flags: ReadonlySet<FlagKey>;
|
|
64
|
+
positionalLimit: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const COMMAND_SPECS: Record<string, CommandSpec> = {
|
|
68
|
+
version: { flags: new Set<FlagKey>([]), positionalLimit: 0 },
|
|
69
|
+
up: {
|
|
70
|
+
flags: new Set(["url", "dir", "to", "lockTimeout", "wait", "table", "schema", "dryRun"]),
|
|
71
|
+
positionalLimit: 0,
|
|
72
|
+
},
|
|
73
|
+
down: {
|
|
74
|
+
flags: new Set(["url", "dir", "to", "wait", "table", "schema", "dryRun", "all"]),
|
|
75
|
+
positionalLimit: 1,
|
|
76
|
+
},
|
|
77
|
+
redo: {
|
|
78
|
+
flags: new Set(["url", "dir", "to", "lockTimeout", "wait", "table", "schema"]),
|
|
79
|
+
positionalLimit: 1,
|
|
80
|
+
},
|
|
81
|
+
init: { flags: new Set(["dir", "lang"]), positionalLimit: 0 },
|
|
82
|
+
install: { flags: new Set(["url", "wait", "table", "schema"]), positionalLimit: 0 },
|
|
83
|
+
create: { flags: new Set(["dir", "lang", "git"]), positionalLimit: 1 },
|
|
84
|
+
mark: { flags: new Set(["url", "dir", "wait", "table", "schema", "all"]), positionalLimit: 1 },
|
|
85
|
+
status: {
|
|
86
|
+
flags: new Set(["url", "dir", "wait", "table", "schema", "strict"]),
|
|
87
|
+
positionalLimit: 0,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function rejectDisallowedFlags(command: string, args: CliArgs): void {
|
|
92
|
+
const spec = COMMAND_SPECS[command];
|
|
93
|
+
if (spec === undefined) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const [extra] = args.positional.slice(spec.positionalLimit);
|
|
97
|
+
if (extra !== undefined) {
|
|
98
|
+
log({ text: `Unexpected argument for ${command}: ${extra}`, type: "error" });
|
|
99
|
+
usage(EXIT_USAGE);
|
|
100
|
+
}
|
|
101
|
+
for (const flag of Object.keys(FLAG_NAMES) as FlagKey[]) {
|
|
102
|
+
const value = args[flag];
|
|
103
|
+
if (value === undefined || value === false || spec.flags.has(flag)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
log({ text: `${command} does not support ${FLAG_NAMES[flag]}.`, type: "error" });
|
|
107
|
+
usage(EXIT_USAGE);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseSecondsValue(flag: string, value: string | undefined): number {
|
|
112
|
+
try {
|
|
113
|
+
return resolveSecondsOption(flag, Number(value), 0);
|
|
114
|
+
} catch {
|
|
115
|
+
log({
|
|
116
|
+
text: `Invalid ${flag}: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
|
|
117
|
+
type: "error",
|
|
118
|
+
});
|
|
119
|
+
usage(EXIT_USAGE);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseStepsArg(stepsArg: string): number {
|
|
124
|
+
try {
|
|
125
|
+
return parseSteps(Number(stepsArg));
|
|
126
|
+
} catch {
|
|
127
|
+
log({
|
|
128
|
+
text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
|
|
129
|
+
type: "error",
|
|
130
|
+
});
|
|
131
|
+
usage(EXIT_USAGE);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requireFlagValue(
|
|
136
|
+
argv: string[],
|
|
137
|
+
index: number,
|
|
138
|
+
flag: string,
|
|
139
|
+
description: string,
|
|
140
|
+
): string {
|
|
141
|
+
const value = argv[index];
|
|
142
|
+
if (value !== undefined && value !== "" && !value.startsWith("-")) {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
log({ text: `${flag} requires ${description}`, type: "error" });
|
|
146
|
+
usage(EXIT_USAGE);
|
|
28
147
|
}
|
|
29
148
|
|
|
30
149
|
function parseArgs(argv: string[]): CliArgs {
|
|
31
150
|
const positional: string[] = [];
|
|
151
|
+
let url: string | undefined;
|
|
32
152
|
let dir: string | undefined;
|
|
153
|
+
let config: string | undefined;
|
|
33
154
|
let git = false;
|
|
34
155
|
let lang: MigrationLang | undefined;
|
|
35
156
|
let to: string | undefined;
|
|
36
157
|
let lockTimeout: number | undefined;
|
|
158
|
+
let wait: number | undefined;
|
|
37
159
|
let table: string | undefined;
|
|
38
160
|
let schema: string | undefined;
|
|
39
161
|
let dryRun = false;
|
|
40
162
|
let all = false;
|
|
41
163
|
let strict = false;
|
|
42
164
|
let help = false;
|
|
165
|
+
let version = false;
|
|
43
166
|
for (let i = 0; i < argv.length; i++) {
|
|
44
167
|
const arg = argv[i]!;
|
|
45
|
-
if (arg === "--
|
|
46
|
-
|
|
168
|
+
if (arg === "--url") {
|
|
169
|
+
url = requireFlagValue(argv, ++i, "--url", "a database URL");
|
|
170
|
+
} else if (arg === "--dir") {
|
|
171
|
+
dir = requireFlagValue(argv, ++i, "--dir", "a migrations directory path");
|
|
172
|
+
} else if (arg === "--config") {
|
|
173
|
+
config = requireFlagValue(argv, ++i, "--config", "a config file path");
|
|
47
174
|
} else if (arg === "--git") {
|
|
48
175
|
git = true;
|
|
49
176
|
} else if (arg === "--lang") {
|
|
@@ -53,38 +180,19 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
53
180
|
text: `Unknown --lang value: ${value ?? "(missing)"} (expected js or ts)`,
|
|
54
181
|
type: "error",
|
|
55
182
|
});
|
|
56
|
-
usage(
|
|
183
|
+
usage(EXIT_USAGE);
|
|
57
184
|
}
|
|
58
185
|
lang = value;
|
|
59
186
|
} else if (arg === "--to") {
|
|
60
|
-
to = argv
|
|
61
|
-
if (to === undefined) {
|
|
62
|
-
log({ text: "--to requires a migration file name", type: "error" });
|
|
63
|
-
usage(1);
|
|
64
|
-
}
|
|
187
|
+
to = requireFlagValue(argv, ++i, "--to", "a migration file name");
|
|
65
188
|
} else if (arg === "--lock-timeout") {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch {
|
|
70
|
-
log({
|
|
71
|
-
text: `Invalid --lock-timeout: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
|
|
72
|
-
type: "error",
|
|
73
|
-
});
|
|
74
|
-
usage(1);
|
|
75
|
-
}
|
|
189
|
+
lockTimeout = parseSecondsValue("--lock-timeout", argv[++i]);
|
|
190
|
+
} else if (arg === "--wait") {
|
|
191
|
+
wait = parseSecondsValue("--wait", argv[++i]);
|
|
76
192
|
} else if (arg === "--table") {
|
|
77
|
-
table = argv
|
|
78
|
-
if (table === undefined) {
|
|
79
|
-
log({ text: "--table requires a tracking table name", type: "error" });
|
|
80
|
-
usage(1);
|
|
81
|
-
}
|
|
193
|
+
table = requireFlagValue(argv, ++i, "--table", "a tracking table name");
|
|
82
194
|
} else if (arg === "--schema") {
|
|
83
|
-
schema = argv
|
|
84
|
-
if (schema === undefined) {
|
|
85
|
-
log({ text: "--schema requires a postgres schema name", type: "error" });
|
|
86
|
-
usage(1);
|
|
87
|
-
}
|
|
195
|
+
schema = requireFlagValue(argv, ++i, "--schema", "a postgres schema name");
|
|
88
196
|
} else if (arg === "--all") {
|
|
89
197
|
all = true;
|
|
90
198
|
} else if (arg === "--dry-run") {
|
|
@@ -93,54 +201,100 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
93
201
|
strict = true;
|
|
94
202
|
} else if (arg === "--help" || arg === "-h") {
|
|
95
203
|
help = true;
|
|
204
|
+
} else if (arg === "--version") {
|
|
205
|
+
version = true;
|
|
96
206
|
} else {
|
|
207
|
+
if (arg === "") {
|
|
208
|
+
log({ text: "Empty argument is not allowed", type: "error" });
|
|
209
|
+
usage(EXIT_USAGE);
|
|
210
|
+
}
|
|
97
211
|
positional.push(arg);
|
|
98
212
|
}
|
|
99
213
|
}
|
|
100
214
|
return {
|
|
101
215
|
command: positional.shift(),
|
|
102
216
|
positional,
|
|
217
|
+
url,
|
|
103
218
|
dir,
|
|
219
|
+
config,
|
|
104
220
|
git,
|
|
105
221
|
lang,
|
|
106
222
|
to,
|
|
107
223
|
lockTimeout,
|
|
224
|
+
wait,
|
|
108
225
|
table,
|
|
109
226
|
schema,
|
|
110
227
|
dryRun,
|
|
111
228
|
all,
|
|
112
229
|
strict,
|
|
113
230
|
help,
|
|
231
|
+
version,
|
|
114
232
|
};
|
|
115
233
|
}
|
|
116
234
|
|
|
117
235
|
function usage(exitCode: number): never {
|
|
118
236
|
log({
|
|
119
|
-
text: "Usage: bunsql-native-migrate <init|up|down [n]|install|create [name]|mark [name]|status> [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--table <name>] [--schema <name>] [--dry-run] [--all] [--lang <js|ts>] [--git] [--strict] [--help]",
|
|
237
|
+
text: "Usage: bunsql-native-migrate <init|up|down [n]|redo [n]|install|create [name]|mark [name]|status|version> [--url <url>] [--dir <migrations-dir>] [--config <path>] [--to <name>] [--lock-timeout <seconds>] [--wait <seconds>] [--table <name>] [--schema <name>] [--dry-run] [--all] [--lang <js|ts>] [--git] [--strict] [--version] [--help]",
|
|
120
238
|
type: "info",
|
|
121
239
|
});
|
|
122
240
|
process.exit(exitCode);
|
|
123
241
|
}
|
|
124
242
|
|
|
243
|
+
async function printVersion(): Promise<void> {
|
|
244
|
+
const manifest = await Bun.file(path.resolve(import.meta.dir, "..", "..", "package.json")).json();
|
|
245
|
+
log({ text: String(manifest.version), type: "info" });
|
|
246
|
+
}
|
|
247
|
+
|
|
125
248
|
const args = parseArgs(process.argv.slice(2));
|
|
126
|
-
const listDirOptions = args.dir ? { listDir: args.dir } : {};
|
|
127
|
-
const tableOptions = {
|
|
128
|
-
...(args.table !== undefined ? { tableName: args.table } : {}),
|
|
129
|
-
...(args.schema !== undefined ? { schema: args.schema } : {}),
|
|
130
|
-
};
|
|
131
249
|
|
|
132
250
|
if (args.help) {
|
|
133
|
-
usage(
|
|
251
|
+
usage(EXIT_SUCCESS);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (args.version) {
|
|
255
|
+
await printVersion();
|
|
256
|
+
process.exit(EXIT_SUCCESS);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (args.command !== undefined) {
|
|
260
|
+
rejectDisallowedFlags(args.command, args);
|
|
134
261
|
}
|
|
135
262
|
|
|
136
263
|
try {
|
|
264
|
+
const config = args.command === "version" ? {} : await loadProjectConfig(args.config);
|
|
265
|
+
|
|
266
|
+
const databaseUrl = args.url ?? config.databaseUrl;
|
|
267
|
+
const listDir = args.dir ?? config.listDir;
|
|
268
|
+
const tableName = args.table ?? config.tableName;
|
|
269
|
+
const schema = args.schema ?? config.schema;
|
|
270
|
+
const lang = args.lang ?? config.lang;
|
|
271
|
+
const lockTimeout = args.lockTimeout ?? config.lockTimeout;
|
|
272
|
+
const waitTimeout = args.wait ?? config.waitTimeout;
|
|
273
|
+
|
|
274
|
+
const urlOptions = databaseUrl !== undefined ? { databaseUrl } : {};
|
|
275
|
+
const waitOptions = waitTimeout !== undefined && waitTimeout > 0 ? { waitTimeout } : {};
|
|
276
|
+
const listDirOptions = listDir !== undefined ? { listDir } : {};
|
|
277
|
+
const tableOptions = {
|
|
278
|
+
...(tableName !== undefined ? { tableName } : {}),
|
|
279
|
+
...(schema !== undefined ? { schema } : {}),
|
|
280
|
+
};
|
|
281
|
+
const connectOptions = {
|
|
282
|
+
...urlOptions,
|
|
283
|
+
...listDirOptions,
|
|
284
|
+
...tableOptions,
|
|
285
|
+
...waitOptions,
|
|
286
|
+
};
|
|
287
|
+
|
|
137
288
|
switch (args.command) {
|
|
289
|
+
case "version": {
|
|
290
|
+
await printVersion();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
138
293
|
case "up": {
|
|
139
294
|
const { applied, planned } = await migrateUp({
|
|
140
|
-
...
|
|
141
|
-
...
|
|
142
|
-
...(
|
|
143
|
-
...(args.lockTimeout !== undefined ? { lockTimeout: args.lockTimeout } : {}),
|
|
295
|
+
...connectOptions,
|
|
296
|
+
...(args.to !== undefined ? { to: args.to } : {}),
|
|
297
|
+
...(lockTimeout !== undefined ? { lockTimeout } : {}),
|
|
144
298
|
...(args.dryRun ? { dryRun: true } : {}),
|
|
145
299
|
});
|
|
146
300
|
if (planned !== undefined && planned.length > 0) {
|
|
@@ -155,26 +309,24 @@ try {
|
|
|
155
309
|
const [stepsArg] = args.positional;
|
|
156
310
|
if (args.all && stepsArg !== undefined) {
|
|
157
311
|
log({ text: "Use either --all or a number of steps, not both.", type: "error" });
|
|
158
|
-
usage(
|
|
312
|
+
usage(EXIT_USAGE);
|
|
313
|
+
}
|
|
314
|
+
if (args.to !== undefined && (args.all || stepsArg !== undefined)) {
|
|
315
|
+
log({
|
|
316
|
+
text: "Use either --to, --all, or a number of steps, not more than one of them.",
|
|
317
|
+
type: "error",
|
|
318
|
+
});
|
|
319
|
+
usage(EXIT_USAGE);
|
|
159
320
|
}
|
|
160
321
|
let steps: number | "all" = 1;
|
|
161
322
|
if (args.all) {
|
|
162
323
|
steps = "all";
|
|
163
324
|
} else if (stepsArg !== undefined) {
|
|
164
|
-
|
|
165
|
-
steps = parseSteps(Number(stepsArg));
|
|
166
|
-
} catch {
|
|
167
|
-
log({
|
|
168
|
-
text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
|
|
169
|
-
type: "error",
|
|
170
|
-
});
|
|
171
|
-
usage(1);
|
|
172
|
-
}
|
|
325
|
+
steps = parseStepsArg(stepsArg);
|
|
173
326
|
}
|
|
174
327
|
const { reverted, planned } = await migrateDown({
|
|
175
|
-
...
|
|
176
|
-
...
|
|
177
|
-
steps,
|
|
328
|
+
...connectOptions,
|
|
329
|
+
...(args.to !== undefined ? { to: args.to } : { steps }),
|
|
178
330
|
...(args.dryRun ? { dryRun: true } : {}),
|
|
179
331
|
});
|
|
180
332
|
if (planned !== undefined && planned.length > 0) {
|
|
@@ -185,22 +337,43 @@ try {
|
|
|
185
337
|
}
|
|
186
338
|
break;
|
|
187
339
|
}
|
|
340
|
+
case "redo": {
|
|
341
|
+
const [stepsArg] = args.positional;
|
|
342
|
+
if (stepsArg !== undefined && args.to !== undefined) {
|
|
343
|
+
log({ text: "Use either --to or a step count, not both.", type: "error" });
|
|
344
|
+
usage(EXIT_USAGE);
|
|
345
|
+
}
|
|
346
|
+
let steps: number | undefined;
|
|
347
|
+
if (stepsArg !== undefined) {
|
|
348
|
+
steps = parseStepsArg(stepsArg);
|
|
349
|
+
}
|
|
350
|
+
const { reverted } = await migrateRedo({
|
|
351
|
+
...connectOptions,
|
|
352
|
+
...(steps !== undefined ? { steps } : {}),
|
|
353
|
+
...(args.to !== undefined ? { to: args.to } : {}),
|
|
354
|
+
...(lockTimeout !== undefined ? { lockTimeout } : {}),
|
|
355
|
+
});
|
|
356
|
+
if (reverted.length > 0) {
|
|
357
|
+
log({ text: `Redid ${reverted.length} migration(s).`, type: "success" });
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
188
361
|
case "init": {
|
|
189
362
|
await initMigrations({
|
|
190
363
|
...listDirOptions,
|
|
191
|
-
...(
|
|
364
|
+
...(lang !== undefined ? { lang } : {}),
|
|
192
365
|
});
|
|
193
366
|
break;
|
|
194
367
|
}
|
|
195
368
|
case "install": {
|
|
196
|
-
await installMigrations(
|
|
369
|
+
await installMigrations(connectOptions);
|
|
197
370
|
break;
|
|
198
371
|
}
|
|
199
372
|
case "create": {
|
|
200
373
|
const [name] = args.positional;
|
|
201
374
|
await createMigrationCommand({
|
|
202
|
-
...(name ? { name } : {}),
|
|
203
|
-
...(
|
|
375
|
+
...(name !== undefined ? { name } : {}),
|
|
376
|
+
...(lang !== undefined ? { lang } : {}),
|
|
204
377
|
git: args.git,
|
|
205
378
|
...listDirOptions,
|
|
206
379
|
});
|
|
@@ -210,15 +383,14 @@ try {
|
|
|
210
383
|
const [name] = args.positional;
|
|
211
384
|
if (args.all && name !== undefined) {
|
|
212
385
|
log({ text: "Use either --all or a migration file name, not both.", type: "error" });
|
|
213
|
-
usage(
|
|
386
|
+
usage(EXIT_USAGE);
|
|
214
387
|
}
|
|
215
388
|
if (!args.all && name === undefined) {
|
|
216
389
|
log({ text: "mark requires a migration file name or --all.", type: "error" });
|
|
217
|
-
usage(
|
|
390
|
+
usage(EXIT_USAGE);
|
|
218
391
|
}
|
|
219
392
|
const { marked } = await markMigrationsApplied({
|
|
220
|
-
...
|
|
221
|
-
...tableOptions,
|
|
393
|
+
...connectOptions,
|
|
222
394
|
...(name !== undefined ? { to: name } : {}),
|
|
223
395
|
});
|
|
224
396
|
if (marked.length > 0) {
|
|
@@ -227,7 +399,7 @@ try {
|
|
|
227
399
|
break;
|
|
228
400
|
}
|
|
229
401
|
case "status": {
|
|
230
|
-
const { applied, pending } = await migrateStatus(
|
|
402
|
+
const { applied, pending } = await migrateStatus(connectOptions);
|
|
231
403
|
for (const entry of applied) {
|
|
232
404
|
log({ text: `${entry.name} applied`, type: "info" });
|
|
233
405
|
}
|
|
@@ -237,23 +409,27 @@ try {
|
|
|
237
409
|
log({ text: `${applied.length} applied, ${pending.length} pending`, type: "info" });
|
|
238
410
|
if (args.strict && pending.length > 0) {
|
|
239
411
|
log({ text: `Strict mode: ${pending.length} pending migration(s).`, type: "warn" });
|
|
240
|
-
process.exit(
|
|
412
|
+
process.exit(EXIT_PENDING);
|
|
241
413
|
}
|
|
242
414
|
break;
|
|
243
415
|
}
|
|
244
416
|
default:
|
|
245
|
-
usage(
|
|
417
|
+
usage(EXIT_USAGE);
|
|
246
418
|
}
|
|
247
419
|
} catch (error) {
|
|
248
420
|
if (
|
|
249
421
|
error instanceof ChecksumDriftError ||
|
|
250
422
|
error instanceof MigrationNotFoundError ||
|
|
423
|
+
error instanceof MigrationFileMissingError ||
|
|
251
424
|
error instanceof MigrationLockError ||
|
|
252
|
-
error instanceof
|
|
425
|
+
error instanceof DatabaseWaitTimeoutError ||
|
|
426
|
+
error instanceof InvalidIdentifierError ||
|
|
427
|
+
error instanceof InvalidConfigError ||
|
|
428
|
+
error instanceof InvalidMigrationNameError
|
|
253
429
|
) {
|
|
254
430
|
log({ text: error.message, type: "error" });
|
|
255
431
|
} else {
|
|
256
432
|
log({ text: "Migration command failed", type: "error", error });
|
|
257
433
|
}
|
|
258
|
-
process.exit(
|
|
434
|
+
process.exit(exitCodeForError(error));
|
|
259
435
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { type MigrationLang } from "../api/create.js";
|
|
5
|
+
|
|
6
|
+
export interface ProjectConfig {
|
|
7
|
+
databaseUrl?: string;
|
|
8
|
+
listDir?: string;
|
|
9
|
+
tableName?: string;
|
|
10
|
+
schema?: string;
|
|
11
|
+
lang?: MigrationLang;
|
|
12
|
+
lockTimeout?: number;
|
|
13
|
+
waitTimeout?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CONFIG_FILE_NAMES = ["bunsql-migrate.config.ts", "bunsql-migrate.config.js"];
|
|
17
|
+
|
|
18
|
+
type ConfigKeyType = "string" | "lang" | "seconds";
|
|
19
|
+
|
|
20
|
+
const CONFIG_KEYS: Record<string, ConfigKeyType> = {
|
|
21
|
+
databaseUrl: "string",
|
|
22
|
+
listDir: "string",
|
|
23
|
+
tableName: "string",
|
|
24
|
+
schema: "string",
|
|
25
|
+
lang: "lang",
|
|
26
|
+
lockTimeout: "seconds",
|
|
27
|
+
waitTimeout: "seconds",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class InvalidConfigError extends Error {
|
|
31
|
+
readonly configPath: string;
|
|
32
|
+
|
|
33
|
+
constructor(configPath: string, detail: string) {
|
|
34
|
+
super(`${configPath}: ${detail}`);
|
|
35
|
+
this.name = "InvalidConfigError";
|
|
36
|
+
this.configPath = configPath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveConfigPath(override?: string): string | null {
|
|
41
|
+
if (override !== undefined) {
|
|
42
|
+
const resolved = path.resolve(override);
|
|
43
|
+
if (!existsSync(resolved)) {
|
|
44
|
+
throw new InvalidConfigError(override, "config file not found");
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
49
|
+
const candidate = path.resolve(name);
|
|
50
|
+
if (existsSync(candidate)) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function errorMessage(error: unknown): string {
|
|
58
|
+
return error instanceof Error ? error.message : String(error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function importConfig(filePath: string): Promise<Record<string, unknown>> {
|
|
62
|
+
let moduleNamespace: Record<string, unknown>;
|
|
63
|
+
try {
|
|
64
|
+
moduleNamespace = (await import(pathToFileURL(filePath).href)) as Record<string, unknown>;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new InvalidConfigError(
|
|
67
|
+
filePath,
|
|
68
|
+
`could not load the config file — ${errorMessage(error)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const exported =
|
|
72
|
+
moduleNamespace["default"] !== undefined ? moduleNamespace["default"] : moduleNamespace;
|
|
73
|
+
if (typeof exported !== "object" || exported === null || Array.isArray(exported)) {
|
|
74
|
+
throw new InvalidConfigError(filePath, "must export a config object");
|
|
75
|
+
}
|
|
76
|
+
return exported as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function quoteKeys(keys: string[]): string {
|
|
80
|
+
return keys.map((key) => `"${key}"`).join(", ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateConfig(filePath: string, raw: Record<string, unknown>): ProjectConfig {
|
|
84
|
+
const unknownKeys = Object.keys(raw).filter((key) => !(key in CONFIG_KEYS));
|
|
85
|
+
if (unknownKeys.length > 0) {
|
|
86
|
+
const label = unknownKeys.length > 1 ? "unknown config keys" : "unknown config key";
|
|
87
|
+
throw new InvalidConfigError(filePath, `${label} ${quoteKeys(unknownKeys)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const problems: string[] = [];
|
|
91
|
+
const config: Record<string, string | number> = {};
|
|
92
|
+
for (const [key, kind] of Object.entries(CONFIG_KEYS)) {
|
|
93
|
+
const value = raw[key];
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (kind === "string") {
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
config[key] = value;
|
|
100
|
+
} else {
|
|
101
|
+
problems.push(`expected a string for "${key}"`);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (kind === "lang") {
|
|
106
|
+
if (value === "js" || value === "ts") {
|
|
107
|
+
config[key] = value;
|
|
108
|
+
} else {
|
|
109
|
+
problems.push(`expected "js" or "ts" for "${key}"`);
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
114
|
+
config[key] = value;
|
|
115
|
+
} else {
|
|
116
|
+
problems.push(`expected a non-negative integer for "${key}"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (problems.length > 0) {
|
|
120
|
+
throw new InvalidConfigError(filePath, problems.join("; "));
|
|
121
|
+
}
|
|
122
|
+
return config as ProjectConfig;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function loadProjectConfig(configPath?: string): Promise<ProjectConfig> {
|
|
126
|
+
const filePath = resolveConfigPath(configPath);
|
|
127
|
+
if (filePath === null) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
return validateConfig(filePath, await importConfig(filePath));
|
|
131
|
+
}
|
package/src/core/console.ts
CHANGED
|
@@ -15,8 +15,11 @@ interface LogOptions {
|
|
|
15
15
|
|
|
16
16
|
function formatError(error: unknown): void {
|
|
17
17
|
if (error instanceof Error) {
|
|
18
|
+
if (error.stack) {
|
|
19
|
+
console.log(error.stack);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
console.log(error.message);
|
|
19
|
-
if (error.stack) console.log(error.stack);
|
|
20
23
|
return;
|
|
21
24
|
}
|
|
22
25
|
if (typeof error !== "object" || error === null) {
|
package/src/core/driver.ts
CHANGED
package/src/core/duration.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export function resolveSecondsOption(
|
|
2
|
+
option: string,
|
|
3
|
+
value: number | undefined,
|
|
4
|
+
fallback: number,
|
|
5
|
+
): number {
|
|
6
|
+
if (value === undefined) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Invalid ${option}: ${String(value)} — expected a non-negative integer of seconds`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export function formatDuration(durationMs: number): string {
|
|
2
18
|
const roundedMs = Math.round(durationMs);
|
|
3
19
|
if (roundedMs < 1000) {
|