bunsql-native-migrate 0.4.0 → 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 +89 -35
- package/package.json +1 -1
- package/src/api/create.ts +2 -1
- package/src/api/down.ts +41 -37
- package/src/api/init.ts +2 -2
- package/src/api/load-migration.ts +46 -7
- package/src/api/mark.ts +6 -9
- package/src/api/options.ts +35 -0
- package/src/api/pending.ts +2 -2
- package/src/api/status.ts +2 -2
- package/src/api/tracking-table.ts +11 -0
- package/src/api/up.ts +8 -19
- package/src/cli/exit-codes.ts +31 -0
- package/src/cli/main.ts +101 -79
- package/src/core/config.ts +131 -0
- package/src/core/console.ts +4 -1
- 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 +10 -6
- package/src/drivers/sqlite-lock.ts +87 -0
- package/src/drivers/sqlite.ts +28 -11
- package/src/index.ts +8 -0
package/src/api/pending.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listMigrationFiles } from "../core/fs.js";
|
|
2
2
|
import { log } from "../core/console.js";
|
|
3
3
|
import { MigrationNotFoundError } from "./options.js";
|
|
4
4
|
|
|
@@ -29,7 +29,7 @@ export async function assertTargetOptions(
|
|
|
29
29
|
throw new Error(`Invalid ${command} options: "to" and "steps" cannot be combined`);
|
|
30
30
|
}
|
|
31
31
|
if (target !== undefined) {
|
|
32
|
-
assertTargetInFiles(await
|
|
32
|
+
assertTargetInFiles(await listMigrationFiles(listDir), target);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
package/src/api/status.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listMigrationFiles, resolveListDir } from "../core/fs.js";
|
|
2
2
|
import type { MigrateOptions, MigrateStatusResult } from "./options.js";
|
|
3
3
|
import { runWithDriver } from "./run-with-driver.js";
|
|
4
4
|
import { ensureTrackingTable } from "./tracking-table.js";
|
|
@@ -9,7 +9,7 @@ export async function migrateStatus(options: MigrateOptions = {}): Promise<Migra
|
|
|
9
9
|
return runWithDriver(options, async (driver) => {
|
|
10
10
|
await ensureTrackingTable(driver);
|
|
11
11
|
|
|
12
|
-
const files = await
|
|
12
|
+
const files = await listMigrationFiles(listDir);
|
|
13
13
|
const executed = await driver.listExecuted();
|
|
14
14
|
const appliedNames = new Set(executed.map((entry) => entry.name));
|
|
15
15
|
|
|
@@ -13,3 +13,14 @@ export async function listExecutedForPlan(driver: MigrationDriver): Promise<Exec
|
|
|
13
13
|
}
|
|
14
14
|
return driver.listExecuted();
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export async function loadExecutedHistory(
|
|
18
|
+
driver: MigrationDriver,
|
|
19
|
+
dryRun: boolean,
|
|
20
|
+
): Promise<ExecutedMigration[]> {
|
|
21
|
+
if (dryRun) {
|
|
22
|
+
return listExecutedForPlan(driver);
|
|
23
|
+
}
|
|
24
|
+
await ensureTrackingTable(driver);
|
|
25
|
+
return driver.listExecuted();
|
|
26
|
+
}
|
package/src/api/up.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { checksumFile, listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
|
|
1
|
+
import { checksumFiles, listMigrationFiles, resolveListDir } from "../core/fs.js";
|
|
3
2
|
import { log } from "../core/console.js";
|
|
4
3
|
import { formatDuration } from "../core/duration.js";
|
|
5
4
|
import { type MigrateUpOptions, type MigrateUpResult, ChecksumDriftError } from "./options.js";
|
|
@@ -8,7 +7,7 @@ import { runMigrationStep } from "./run-step.js";
|
|
|
8
7
|
import { loadMigration } from "./load-migration.js";
|
|
9
8
|
import { resolvePendingToTarget } from "./pending.js";
|
|
10
9
|
import { resolveLockTimeout, withMigrationLock } from "./lock.js";
|
|
11
|
-
import {
|
|
10
|
+
import { loadExecutedHistory } from "./tracking-table.js";
|
|
12
11
|
|
|
13
12
|
export async function migrateUp(options: MigrateUpOptions = {}): Promise<MigrateUpResult> {
|
|
14
13
|
const listDir = resolveListDir(options.listDir);
|
|
@@ -17,22 +16,10 @@ export async function migrateUp(options: MigrateUpOptions = {}): Promise<Migrate
|
|
|
17
16
|
const lockTimeout = resolveLockTimeout(options.lockTimeout);
|
|
18
17
|
|
|
19
18
|
return runWithDriver(options, async (driver) => {
|
|
20
|
-
if (!dryRun) {
|
|
21
|
-
await ensureTrackingTable(driver);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
const run = async (): Promise<MigrateUpResult> => {
|
|
25
|
-
const allFiles = await
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
await Promise.all(
|
|
29
|
-
allFiles.map(
|
|
30
|
-
async (file) => [file, await checksumFile(path.join(listDir, file))] as const,
|
|
31
|
-
),
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const executed = dryRun ? await listExecutedForPlan(driver) : await driver.listExecuted();
|
|
20
|
+
const allFiles = await listMigrationFiles(listDir);
|
|
21
|
+
const checksums = await checksumFiles(listDir, allFiles);
|
|
22
|
+
const executed = await loadExecutedHistory(driver, dryRun);
|
|
36
23
|
const executedByName = new Map(executed.map((entry) => [entry.name, entry]));
|
|
37
24
|
|
|
38
25
|
for (const [file, checksum] of checksums) {
|
|
@@ -81,7 +68,9 @@ export async function migrateUp(options: MigrateUpOptions = {}): Promise<Migrate
|
|
|
81
68
|
|
|
82
69
|
for (const file of pending) {
|
|
83
70
|
const checksum = checksums.get(file);
|
|
84
|
-
if (
|
|
71
|
+
if (checksum === undefined) {
|
|
72
|
+
throw new Error(`checksum for ${file} was not computed`);
|
|
73
|
+
}
|
|
85
74
|
try {
|
|
86
75
|
const { up } = await loadMigration(listDir, file);
|
|
87
76
|
if (up === null) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChecksumDriftError,
|
|
3
|
+
InvalidMigrationNameError,
|
|
4
|
+
MigrationLockError,
|
|
5
|
+
} from "../api/options.js";
|
|
6
|
+
import { InvalidIdentifierError } from "../core/identifiers.js";
|
|
7
|
+
import { InvalidConfigError } from "../core/config.js";
|
|
8
|
+
|
|
9
|
+
export const EXIT_SUCCESS = 0;
|
|
10
|
+
export const EXIT_GENERIC = 1;
|
|
11
|
+
export const EXIT_PENDING = 2;
|
|
12
|
+
export const EXIT_CHECKSUM_DRIFT = 3;
|
|
13
|
+
export const EXIT_LOCK_TIMEOUT = 4;
|
|
14
|
+
export const EXIT_USAGE = 5;
|
|
15
|
+
|
|
16
|
+
export function exitCodeForError(error: unknown): number {
|
|
17
|
+
if (error instanceof ChecksumDriftError) {
|
|
18
|
+
return EXIT_CHECKSUM_DRIFT;
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof MigrationLockError) {
|
|
21
|
+
return EXIT_LOCK_TIMEOUT;
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
error instanceof InvalidIdentifierError ||
|
|
25
|
+
error instanceof InvalidConfigError ||
|
|
26
|
+
error instanceof InvalidMigrationNameError
|
|
27
|
+
) {
|
|
28
|
+
return EXIT_USAGE;
|
|
29
|
+
}
|
|
30
|
+
return EXIT_GENERIC;
|
|
31
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -9,20 +9,25 @@ import { createMigrationCommand, type MigrationLang } from "../api/create.js";
|
|
|
9
9
|
import { initMigrations } from "../api/init.js";
|
|
10
10
|
import { markMigrationsApplied } from "../api/mark.js";
|
|
11
11
|
import { resolveSecondsOption } from "../core/duration.js";
|
|
12
|
+
import { loadProjectConfig, InvalidConfigError } from "../core/config.js";
|
|
12
13
|
import {
|
|
13
14
|
ChecksumDriftError,
|
|
14
15
|
DatabaseWaitTimeoutError,
|
|
16
|
+
InvalidMigrationNameError,
|
|
17
|
+
MigrationFileMissingError,
|
|
15
18
|
MigrationLockError,
|
|
16
19
|
MigrationNotFoundError,
|
|
17
20
|
} from "../api/options.js";
|
|
18
21
|
import { InvalidIdentifierError } from "../core/identifiers.js";
|
|
19
22
|
import { log } from "../core/console.js";
|
|
23
|
+
import { EXIT_PENDING, EXIT_SUCCESS, EXIT_USAGE, exitCodeForError } from "./exit-codes.js";
|
|
20
24
|
|
|
21
25
|
interface CliArgs {
|
|
22
26
|
command: string | undefined;
|
|
23
27
|
positional: string[];
|
|
24
28
|
url?: string | undefined;
|
|
25
29
|
dir?: string | undefined;
|
|
30
|
+
config?: string | undefined;
|
|
26
31
|
git: boolean;
|
|
27
32
|
lang?: MigrationLang | undefined;
|
|
28
33
|
to?: string | undefined;
|
|
@@ -37,7 +42,7 @@ interface CliArgs {
|
|
|
37
42
|
version: boolean;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
type FlagKey = Exclude<keyof CliArgs, "command" | "positional" | "help" | "version">;
|
|
45
|
+
type FlagKey = Exclude<keyof CliArgs, "command" | "positional" | "help" | "version" | "config">;
|
|
41
46
|
|
|
42
47
|
const FLAG_NAMES: Record<FlagKey, string> = {
|
|
43
48
|
url: "--url",
|
|
@@ -74,7 +79,7 @@ const COMMAND_SPECS: Record<string, CommandSpec> = {
|
|
|
74
79
|
positionalLimit: 1,
|
|
75
80
|
},
|
|
76
81
|
init: { flags: new Set(["dir", "lang"]), positionalLimit: 0 },
|
|
77
|
-
install: { flags: new Set(["url", "
|
|
82
|
+
install: { flags: new Set(["url", "wait", "table", "schema"]), positionalLimit: 0 },
|
|
78
83
|
create: { flags: new Set(["dir", "lang", "git"]), positionalLimit: 1 },
|
|
79
84
|
mark: { flags: new Set(["url", "dir", "wait", "table", "schema", "all"]), positionalLimit: 1 },
|
|
80
85
|
status: {
|
|
@@ -91,7 +96,7 @@ function rejectDisallowedFlags(command: string, args: CliArgs): void {
|
|
|
91
96
|
const [extra] = args.positional.slice(spec.positionalLimit);
|
|
92
97
|
if (extra !== undefined) {
|
|
93
98
|
log({ text: `Unexpected argument for ${command}: ${extra}`, type: "error" });
|
|
94
|
-
usage(
|
|
99
|
+
usage(EXIT_USAGE);
|
|
95
100
|
}
|
|
96
101
|
for (const flag of Object.keys(FLAG_NAMES) as FlagKey[]) {
|
|
97
102
|
const value = args[flag];
|
|
@@ -99,7 +104,7 @@ function rejectDisallowedFlags(command: string, args: CliArgs): void {
|
|
|
99
104
|
continue;
|
|
100
105
|
}
|
|
101
106
|
log({ text: `${command} does not support ${FLAG_NAMES[flag]}.`, type: "error" });
|
|
102
|
-
usage(
|
|
107
|
+
usage(EXIT_USAGE);
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -111,14 +116,41 @@ function parseSecondsValue(flag: string, value: string | undefined): number {
|
|
|
111
116
|
text: `Invalid ${flag}: ${value ?? "(missing)"} (expected a non-negative integer of seconds)`,
|
|
112
117
|
type: "error",
|
|
113
118
|
});
|
|
114
|
-
usage(
|
|
119
|
+
usage(EXIT_USAGE);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
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);
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
function parseArgs(argv: string[]): CliArgs {
|
|
119
150
|
const positional: string[] = [];
|
|
120
151
|
let url: string | undefined;
|
|
121
152
|
let dir: string | undefined;
|
|
153
|
+
let config: string | undefined;
|
|
122
154
|
let git = false;
|
|
123
155
|
let lang: MigrationLang | undefined;
|
|
124
156
|
let to: string | undefined;
|
|
@@ -134,13 +166,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
134
166
|
for (let i = 0; i < argv.length; i++) {
|
|
135
167
|
const arg = argv[i]!;
|
|
136
168
|
if (arg === "--url") {
|
|
137
|
-
url = argv
|
|
138
|
-
if (url === undefined) {
|
|
139
|
-
log({ text: "--url requires a database URL", type: "error" });
|
|
140
|
-
usage(1);
|
|
141
|
-
}
|
|
169
|
+
url = requireFlagValue(argv, ++i, "--url", "a database URL");
|
|
142
170
|
} else if (arg === "--dir") {
|
|
143
|
-
dir = argv
|
|
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");
|
|
144
174
|
} else if (arg === "--git") {
|
|
145
175
|
git = true;
|
|
146
176
|
} else if (arg === "--lang") {
|
|
@@ -150,31 +180,19 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
150
180
|
text: `Unknown --lang value: ${value ?? "(missing)"} (expected js or ts)`,
|
|
151
181
|
type: "error",
|
|
152
182
|
});
|
|
153
|
-
usage(
|
|
183
|
+
usage(EXIT_USAGE);
|
|
154
184
|
}
|
|
155
185
|
lang = value;
|
|
156
186
|
} else if (arg === "--to") {
|
|
157
|
-
to = argv
|
|
158
|
-
if (to === undefined) {
|
|
159
|
-
log({ text: "--to requires a migration file name", type: "error" });
|
|
160
|
-
usage(1);
|
|
161
|
-
}
|
|
187
|
+
to = requireFlagValue(argv, ++i, "--to", "a migration file name");
|
|
162
188
|
} else if (arg === "--lock-timeout") {
|
|
163
189
|
lockTimeout = parseSecondsValue("--lock-timeout", argv[++i]);
|
|
164
190
|
} else if (arg === "--wait") {
|
|
165
191
|
wait = parseSecondsValue("--wait", argv[++i]);
|
|
166
192
|
} else if (arg === "--table") {
|
|
167
|
-
table = argv
|
|
168
|
-
if (table === undefined) {
|
|
169
|
-
log({ text: "--table requires a tracking table name", type: "error" });
|
|
170
|
-
usage(1);
|
|
171
|
-
}
|
|
193
|
+
table = requireFlagValue(argv, ++i, "--table", "a tracking table name");
|
|
172
194
|
} else if (arg === "--schema") {
|
|
173
|
-
schema = argv
|
|
174
|
-
if (schema === undefined) {
|
|
175
|
-
log({ text: "--schema requires a postgres schema name", type: "error" });
|
|
176
|
-
usage(1);
|
|
177
|
-
}
|
|
195
|
+
schema = requireFlagValue(argv, ++i, "--schema", "a postgres schema name");
|
|
178
196
|
} else if (arg === "--all") {
|
|
179
197
|
all = true;
|
|
180
198
|
} else if (arg === "--dry-run") {
|
|
@@ -186,6 +204,10 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
186
204
|
} else if (arg === "--version") {
|
|
187
205
|
version = true;
|
|
188
206
|
} else {
|
|
207
|
+
if (arg === "") {
|
|
208
|
+
log({ text: "Empty argument is not allowed", type: "error" });
|
|
209
|
+
usage(EXIT_USAGE);
|
|
210
|
+
}
|
|
189
211
|
positional.push(arg);
|
|
190
212
|
}
|
|
191
213
|
}
|
|
@@ -194,6 +216,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
194
216
|
positional,
|
|
195
217
|
url,
|
|
196
218
|
dir,
|
|
219
|
+
config,
|
|
197
220
|
git,
|
|
198
221
|
lang,
|
|
199
222
|
to,
|
|
@@ -211,7 +234,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
211
234
|
|
|
212
235
|
function usage(exitCode: number): never {
|
|
213
236
|
log({
|
|
214
|
-
text: "Usage: bunsql-native-migrate <init|up|down [n]|redo [n]|install|create [name]|mark [name]|status|version> [--url <url>] [--dir <migrations-dir>] [--to <name>] [--lock-timeout <seconds>] [--wait <seconds>] [--table <name>] [--schema <name>] [--dry-run] [--all] [--lang <js|ts>] [--git] [--strict] [--version] [--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]",
|
|
215
238
|
type: "info",
|
|
216
239
|
});
|
|
217
240
|
process.exit(exitCode);
|
|
@@ -223,33 +246,45 @@ async function printVersion(): Promise<void> {
|
|
|
223
246
|
}
|
|
224
247
|
|
|
225
248
|
const args = parseArgs(process.argv.slice(2));
|
|
226
|
-
const urlOptions = args.url !== undefined ? { databaseUrl: args.url } : {};
|
|
227
|
-
const waitOptions = args.wait !== undefined && args.wait > 0 ? { waitTimeout: args.wait } : {};
|
|
228
|
-
const listDirOptions = args.dir ? { listDir: args.dir } : {};
|
|
229
|
-
const tableOptions = {
|
|
230
|
-
...(args.table !== undefined ? { tableName: args.table } : {}),
|
|
231
|
-
...(args.schema !== undefined ? { schema: args.schema } : {}),
|
|
232
|
-
};
|
|
233
|
-
const connectOptions = {
|
|
234
|
-
...urlOptions,
|
|
235
|
-
...listDirOptions,
|
|
236
|
-
...tableOptions,
|
|
237
|
-
...waitOptions,
|
|
238
|
-
};
|
|
239
249
|
|
|
240
250
|
if (args.help) {
|
|
241
|
-
usage(
|
|
251
|
+
usage(EXIT_SUCCESS);
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
if (args.version) {
|
|
245
255
|
await printVersion();
|
|
246
|
-
process.exit(
|
|
256
|
+
process.exit(EXIT_SUCCESS);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (args.command !== undefined) {
|
|
260
|
+
rejectDisallowedFlags(args.command, args);
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
|
|
253
288
|
switch (args.command) {
|
|
254
289
|
case "version": {
|
|
255
290
|
await printVersion();
|
|
@@ -258,8 +293,8 @@ try {
|
|
|
258
293
|
case "up": {
|
|
259
294
|
const { applied, planned } = await migrateUp({
|
|
260
295
|
...connectOptions,
|
|
261
|
-
...(args.to ? { to: args.to } : {}),
|
|
262
|
-
...(
|
|
296
|
+
...(args.to !== undefined ? { to: args.to } : {}),
|
|
297
|
+
...(lockTimeout !== undefined ? { lockTimeout } : {}),
|
|
263
298
|
...(args.dryRun ? { dryRun: true } : {}),
|
|
264
299
|
});
|
|
265
300
|
if (planned !== undefined && planned.length > 0) {
|
|
@@ -274,28 +309,20 @@ try {
|
|
|
274
309
|
const [stepsArg] = args.positional;
|
|
275
310
|
if (args.all && stepsArg !== undefined) {
|
|
276
311
|
log({ text: "Use either --all or a number of steps, not both.", type: "error" });
|
|
277
|
-
usage(
|
|
312
|
+
usage(EXIT_USAGE);
|
|
278
313
|
}
|
|
279
314
|
if (args.to !== undefined && (args.all || stepsArg !== undefined)) {
|
|
280
315
|
log({
|
|
281
316
|
text: "Use either --to, --all, or a number of steps, not more than one of them.",
|
|
282
317
|
type: "error",
|
|
283
318
|
});
|
|
284
|
-
usage(
|
|
319
|
+
usage(EXIT_USAGE);
|
|
285
320
|
}
|
|
286
321
|
let steps: number | "all" = 1;
|
|
287
322
|
if (args.all) {
|
|
288
323
|
steps = "all";
|
|
289
324
|
} else if (stepsArg !== undefined) {
|
|
290
|
-
|
|
291
|
-
steps = parseSteps(Number(stepsArg));
|
|
292
|
-
} catch {
|
|
293
|
-
log({
|
|
294
|
-
text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
|
|
295
|
-
type: "error",
|
|
296
|
-
});
|
|
297
|
-
usage(1);
|
|
298
|
-
}
|
|
325
|
+
steps = parseStepsArg(stepsArg);
|
|
299
326
|
}
|
|
300
327
|
const { reverted, planned } = await migrateDown({
|
|
301
328
|
...connectOptions,
|
|
@@ -314,25 +341,17 @@ try {
|
|
|
314
341
|
const [stepsArg] = args.positional;
|
|
315
342
|
if (stepsArg !== undefined && args.to !== undefined) {
|
|
316
343
|
log({ text: "Use either --to or a step count, not both.", type: "error" });
|
|
317
|
-
usage(
|
|
344
|
+
usage(EXIT_USAGE);
|
|
318
345
|
}
|
|
319
346
|
let steps: number | undefined;
|
|
320
347
|
if (stepsArg !== undefined) {
|
|
321
|
-
|
|
322
|
-
steps = parseSteps(Number(stepsArg));
|
|
323
|
-
} catch {
|
|
324
|
-
log({
|
|
325
|
-
text: `Invalid step count: ${stepsArg} (expected a positive integer)`,
|
|
326
|
-
type: "error",
|
|
327
|
-
});
|
|
328
|
-
usage(1);
|
|
329
|
-
}
|
|
348
|
+
steps = parseStepsArg(stepsArg);
|
|
330
349
|
}
|
|
331
350
|
const { reverted } = await migrateRedo({
|
|
332
351
|
...connectOptions,
|
|
333
352
|
...(steps !== undefined ? { steps } : {}),
|
|
334
353
|
...(args.to !== undefined ? { to: args.to } : {}),
|
|
335
|
-
...(
|
|
354
|
+
...(lockTimeout !== undefined ? { lockTimeout } : {}),
|
|
336
355
|
});
|
|
337
356
|
if (reverted.length > 0) {
|
|
338
357
|
log({ text: `Redid ${reverted.length} migration(s).`, type: "success" });
|
|
@@ -342,7 +361,7 @@ try {
|
|
|
342
361
|
case "init": {
|
|
343
362
|
await initMigrations({
|
|
344
363
|
...listDirOptions,
|
|
345
|
-
...(
|
|
364
|
+
...(lang !== undefined ? { lang } : {}),
|
|
346
365
|
});
|
|
347
366
|
break;
|
|
348
367
|
}
|
|
@@ -353,8 +372,8 @@ try {
|
|
|
353
372
|
case "create": {
|
|
354
373
|
const [name] = args.positional;
|
|
355
374
|
await createMigrationCommand({
|
|
356
|
-
...(name ? { name } : {}),
|
|
357
|
-
...(
|
|
375
|
+
...(name !== undefined ? { name } : {}),
|
|
376
|
+
...(lang !== undefined ? { lang } : {}),
|
|
358
377
|
git: args.git,
|
|
359
378
|
...listDirOptions,
|
|
360
379
|
});
|
|
@@ -364,11 +383,11 @@ try {
|
|
|
364
383
|
const [name] = args.positional;
|
|
365
384
|
if (args.all && name !== undefined) {
|
|
366
385
|
log({ text: "Use either --all or a migration file name, not both.", type: "error" });
|
|
367
|
-
usage(
|
|
386
|
+
usage(EXIT_USAGE);
|
|
368
387
|
}
|
|
369
388
|
if (!args.all && name === undefined) {
|
|
370
389
|
log({ text: "mark requires a migration file name or --all.", type: "error" });
|
|
371
|
-
usage(
|
|
390
|
+
usage(EXIT_USAGE);
|
|
372
391
|
}
|
|
373
392
|
const { marked } = await markMigrationsApplied({
|
|
374
393
|
...connectOptions,
|
|
@@ -390,24 +409,27 @@ try {
|
|
|
390
409
|
log({ text: `${applied.length} applied, ${pending.length} pending`, type: "info" });
|
|
391
410
|
if (args.strict && pending.length > 0) {
|
|
392
411
|
log({ text: `Strict mode: ${pending.length} pending migration(s).`, type: "warn" });
|
|
393
|
-
process.exit(
|
|
412
|
+
process.exit(EXIT_PENDING);
|
|
394
413
|
}
|
|
395
414
|
break;
|
|
396
415
|
}
|
|
397
416
|
default:
|
|
398
|
-
usage(
|
|
417
|
+
usage(EXIT_USAGE);
|
|
399
418
|
}
|
|
400
419
|
} catch (error) {
|
|
401
420
|
if (
|
|
402
421
|
error instanceof ChecksumDriftError ||
|
|
403
422
|
error instanceof MigrationNotFoundError ||
|
|
423
|
+
error instanceof MigrationFileMissingError ||
|
|
404
424
|
error instanceof MigrationLockError ||
|
|
405
425
|
error instanceof DatabaseWaitTimeoutError ||
|
|
406
|
-
error instanceof InvalidIdentifierError
|
|
426
|
+
error instanceof InvalidIdentifierError ||
|
|
427
|
+
error instanceof InvalidConfigError ||
|
|
428
|
+
error instanceof InvalidMigrationNameError
|
|
407
429
|
) {
|
|
408
430
|
log({ text: error.message, type: "error" });
|
|
409
431
|
} else {
|
|
410
432
|
log({ text: "Migration command failed", type: "error", error });
|
|
411
433
|
}
|
|
412
|
-
process.exit(
|
|
434
|
+
process.exit(exitCodeForError(error));
|
|
413
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) {
|