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.
@@ -1,14 +1,21 @@
1
1
  import path from "node:path";
2
2
  import { type SQL } from "bun";
3
+ import { MigrationFileMissingError } from "./options.js";
3
4
 
4
5
  export type MigrationStep = (tx?: SQL) => Promise<void>;
5
6
 
7
+ export interface MigrationStepPlan {
8
+ step: MigrationStep;
9
+ noTransaction: boolean;
10
+ }
11
+
6
12
  export interface MigrationFunctions {
7
- up: MigrationStep | null;
8
- down: MigrationStep | null;
13
+ up: MigrationStepPlan | null;
14
+ down: MigrationStepPlan | null;
9
15
  }
10
16
 
11
17
  const SQL_UP_SUFFIX = ".up.sql";
18
+ const NO_TRANSACTION_DIRECTIVE = "-- bunsql-migrate:no-transaction";
12
19
 
13
20
  export function isSqlMigration(file: string): boolean {
14
21
  return file.endsWith(SQL_UP_SUFFIX);
@@ -27,19 +34,76 @@ function sqlFileStep(filePath: string): MigrationStep {
27
34
  };
28
35
  }
29
36
 
37
+ function hasNoTransactionDirective(content: string): boolean {
38
+ for (const line of content.split("\n")) {
39
+ const trimmed = line.trim();
40
+ if (trimmed === "") continue;
41
+ if (!trimmed.startsWith("--")) return false;
42
+ if (trimmed === NO_TRANSACTION_DIRECTIVE) return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ function hasParameterList(step: MigrationStep): boolean {
48
+ const source = step.toString();
49
+ const open = source.indexOf("(");
50
+ if (open === -1) return false;
51
+ let depth = 0;
52
+ for (let index = open; index < source.length; index++) {
53
+ const char = source[index];
54
+ if (char === "(") depth++;
55
+ else if (char === ")") {
56
+ depth--;
57
+ if (depth === 0) {
58
+ return source.slice(open + 1, index).trim().length > 0;
59
+ }
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+
65
+ function assertExplicitTransactionMode(
66
+ file: string,
67
+ direction: "up" | "down",
68
+ step: MigrationStep,
69
+ ): void {
70
+ if (step.length > 0 || !hasParameterList(step)) return;
71
+ throw new Error(
72
+ `${file}: ${direction}() declares its parameter with a default value or as a rest parameter — ` +
73
+ `function.length is 0, so the step would silently run outside the migration transaction; ` +
74
+ `declare the parameter without a default (async (tx) => …) or add "export const noTransaction = true" to opt out explicitly`,
75
+ );
76
+ }
77
+
78
+ async function sqlFilePlan(filePath: string): Promise<MigrationStepPlan> {
79
+ const content = await Bun.file(filePath).text();
80
+ return {
81
+ step: sqlFileStep(filePath),
82
+ noTransaction: hasNoTransactionDirective(content),
83
+ };
84
+ }
85
+
30
86
  export async function loadMigration(listDir: string, file: string): Promise<MigrationFunctions> {
87
+ const migrationPath = path.join(listDir, file);
88
+ if (!(await Bun.file(migrationPath).exists())) {
89
+ throw new MigrationFileMissingError(file);
90
+ }
91
+
31
92
  if (isSqlMigration(file)) {
32
- const upPath = path.join(listDir, file);
33
93
  const downPath = path.join(listDir, sqlDownFile(file));
34
94
  return {
35
- up: sqlFileStep(upPath),
36
- down: (await Bun.file(downPath).exists()) ? sqlFileStep(downPath) : null,
95
+ up: await sqlFilePlan(migrationPath),
96
+ down: (await Bun.file(downPath).exists()) ? await sqlFilePlan(downPath) : null,
37
97
  };
38
98
  }
39
99
 
40
- const mod = await import(path.join(listDir, file));
41
- return {
42
- up: typeof mod.up === "function" ? mod.up : null,
43
- down: typeof mod.down === "function" ? mod.down : null,
44
- };
100
+ const mod = await import(migrationPath);
101
+ const noTransaction = mod.noTransaction === true;
102
+ const up = typeof mod.up === "function" ? { step: mod.up, noTransaction } : null;
103
+ const down = typeof mod.down === "function" ? { step: mod.down, noTransaction } : null;
104
+ if (!noTransaction) {
105
+ if (up !== null) assertExplicitTransactionMode(file, "up", up.step);
106
+ if (down !== null) assertExplicitTransactionMode(file, "down", down.step);
107
+ }
108
+ return { up, down };
45
109
  }
package/src/api/lock.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { log } from "../core/console.js";
2
+ import { resolveSecondsOption } from "../core/duration.js";
2
3
  import type { MigrationDriver } from "../core/driver.js";
3
4
  import { MigrationLockError } from "./options.js";
4
5
 
@@ -7,13 +8,7 @@ export const DEFAULT_LOCK_TIMEOUT_SECONDS = 30;
7
8
  const LOCK_RETRY_DELAY_MS = 100;
8
9
 
9
10
  export function resolveLockTimeout(lockTimeout: number | undefined): number {
10
- if (lockTimeout === undefined) return DEFAULT_LOCK_TIMEOUT_SECONDS;
11
- if (!Number.isInteger(lockTimeout) || lockTimeout < 0) {
12
- throw new Error(
13
- `Invalid lockTimeout: ${String(lockTimeout)} — expected a non-negative integer of seconds`,
14
- );
15
- }
16
- return lockTimeout;
11
+ return resolveSecondsOption("lockTimeout", lockTimeout, DEFAULT_LOCK_TIMEOUT_SECONDS);
17
12
  }
18
13
 
19
14
  export async function withMigrationLock<T>(
package/src/api/mark.ts CHANGED
@@ -1,5 +1,4 @@
1
- import path from "node:path";
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 { type MarkOptions, type MarkResult } from "./options.js";
5
4
  import { resolvePendingToTarget } from "./pending.js";
@@ -13,7 +12,7 @@ export async function markMigrationsApplied(options: MarkOptions = {}): Promise<
13
12
  return runWithDriver(options, async (driver) => {
14
13
  await ensureTrackingTable(driver);
15
14
 
16
- const allFiles = await listFiles(listDir, MIGRATION_EXTENSIONS);
15
+ const allFiles = await listMigrationFiles(listDir);
17
16
  const executed = await driver.listExecuted();
18
17
 
19
18
  const { pending, targetApplied } = resolvePendingToTarget({
@@ -25,16 +24,14 @@ export async function markMigrationsApplied(options: MarkOptions = {}): Promise<
25
24
  return { marked: [] };
26
25
  }
27
26
 
28
- const checksums = new Map(
29
- await Promise.all(
30
- pending.map(async (file) => [file, await checksumFile(path.join(listDir, file))] as const),
31
- ),
32
- );
27
+ const checksums = await checksumFiles(listDir, pending);
33
28
 
34
29
  const marked: string[] = [];
35
30
  for (const file of pending) {
36
31
  const checksum = checksums.get(file);
37
- if (!checksum) continue;
32
+ if (checksum === undefined) {
33
+ throw new Error(`checksum for ${file} was not computed`);
34
+ }
38
35
  await driver.record(file, checksum);
39
36
  marked.push(file);
40
37
  log({ text: `${file} marked as applied`, type: "success" });
@@ -5,6 +5,7 @@ export interface MigrateOptions {
5
5
  listDir?: string;
6
6
  tableName?: string;
7
7
  schema?: string;
8
+ waitTimeout?: number;
8
9
  }
9
10
 
10
11
  export interface MigrateUpOptions extends MigrateOptions {
@@ -15,9 +16,16 @@ export interface MigrateUpOptions extends MigrateOptions {
15
16
 
16
17
  export interface MigrateDownOptions extends MigrateOptions {
17
18
  steps?: number | "all";
19
+ to?: string;
18
20
  dryRun?: boolean;
19
21
  }
20
22
 
23
+ export interface RedoOptions extends MigrateOptions {
24
+ steps?: number;
25
+ to?: string;
26
+ lockTimeout?: number;
27
+ }
28
+
21
29
  export interface MarkOptions extends MigrateOptions {
22
30
  to?: string;
23
31
  }
@@ -32,6 +40,11 @@ export interface MigrateDownResult {
32
40
  planned?: string[];
33
41
  }
34
42
 
43
+ export interface RedoResult {
44
+ reverted: string[];
45
+ applied: string[];
46
+ }
47
+
35
48
  export interface MarkResult {
36
49
  marked: string[];
37
50
  }
@@ -63,6 +76,18 @@ export class MigrationNotFoundError extends Error {
63
76
  }
64
77
  }
65
78
 
79
+ export class MigrationFileMissingError extends Error {
80
+ readonly file: string;
81
+
82
+ constructor(file: string) {
83
+ super(
84
+ `${file} is missing from the migrations directory — restore the file or remove its tracking record manually`,
85
+ );
86
+ this.name = "MigrationFileMissingError";
87
+ this.file = file;
88
+ }
89
+ }
90
+
66
91
  export class MigrationLockError extends Error {
67
92
  readonly timeoutSeconds: number;
68
93
 
@@ -75,6 +100,17 @@ export class MigrationLockError extends Error {
75
100
  }
76
101
  }
77
102
 
103
+ export class DatabaseWaitTimeoutError extends Error {
104
+ readonly timeoutSeconds: number;
105
+
106
+ constructor(timeoutSeconds: number, cause?: unknown) {
107
+ const reason = cause instanceof Error ? `: ${cause.message}` : "";
108
+ super(`database was not ready within ${timeoutSeconds}s${reason}`);
109
+ this.name = "DatabaseWaitTimeoutError";
110
+ this.timeoutSeconds = timeoutSeconds;
111
+ }
112
+ }
113
+
78
114
  export class GitStageError extends Error {
79
115
  readonly file: string;
80
116
  readonly exitCode: number;
@@ -89,3 +125,26 @@ export class GitStageError extends Error {
89
125
  this.exitCode = exitCode;
90
126
  }
91
127
  }
128
+
129
+ export const MIGRATION_NAME_MAX_LENGTH = 128;
130
+
131
+ const MIGRATION_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
132
+
133
+ export class InvalidMigrationNameError extends Error {
134
+ readonly value: string;
135
+
136
+ constructor(value: string) {
137
+ super(
138
+ `Invalid migration name: "${value}" — expected letters, digits, hyphens and underscores only ` +
139
+ `(no path separators, dots or spaces), at most ${MIGRATION_NAME_MAX_LENGTH} characters`,
140
+ );
141
+ this.name = "InvalidMigrationNameError";
142
+ this.value = value;
143
+ }
144
+ }
145
+
146
+ export function validateMigrationName(name: string): void {
147
+ if (!MIGRATION_NAME_PATTERN.test(name) || name.length > MIGRATION_NAME_MAX_LENGTH) {
148
+ throw new InvalidMigrationNameError(name);
149
+ }
150
+ }
@@ -1,3 +1,4 @@
1
+ import { listMigrationFiles } from "../core/fs.js";
1
2
  import { log } from "../core/console.js";
2
3
  import { MigrationNotFoundError } from "./options.js";
3
4
 
@@ -12,6 +13,26 @@ interface ResolvePendingOptions {
12
13
  target: string | undefined;
13
14
  }
14
15
 
16
+ export function assertTargetInFiles(allFiles: readonly string[], target: string): void {
17
+ if (!allFiles.includes(target)) {
18
+ throw new MigrationNotFoundError(target);
19
+ }
20
+ }
21
+
22
+ export async function assertTargetOptions(
23
+ command: string,
24
+ listDir: string,
25
+ target: string | undefined,
26
+ steps: unknown,
27
+ ): Promise<void> {
28
+ if (target !== undefined && steps !== undefined) {
29
+ throw new Error(`Invalid ${command} options: "to" and "steps" cannot be combined`);
30
+ }
31
+ if (target !== undefined) {
32
+ assertTargetInFiles(await listMigrationFiles(listDir), target);
33
+ }
34
+ }
35
+
15
36
  export function resolvePendingToTarget({
16
37
  allFiles,
17
38
  executedNames,
@@ -19,8 +40,8 @@ export function resolvePendingToTarget({
19
40
  }: ResolvePendingOptions): PendingToTargetResult {
20
41
  const executed = new Set(executedNames);
21
42
 
22
- if (target !== undefined && !allFiles.includes(target)) {
23
- throw new MigrationNotFoundError(target);
43
+ if (target !== undefined) {
44
+ assertTargetInFiles(allFiles, target);
24
45
  }
25
46
 
26
47
  let pending = allFiles.filter((file) => !executed.has(file));
@@ -0,0 +1,44 @@
1
+ import { resolveListDir } from "../core/fs.js";
2
+ import { log } from "../core/console.js";
3
+ import { type RedoOptions, type RedoResult } from "./options.js";
4
+ import { migrateDown, parseSteps } from "./down.js";
5
+ import { migrateUp } from "./up.js";
6
+ import { migrateStatus } from "./status.js";
7
+ import { assertTargetOptions } from "./pending.js";
8
+
9
+ export async function migrateRedo(options: RedoOptions = {}): Promise<RedoResult> {
10
+ const listDir = resolveListDir(options.listDir);
11
+ const target = options.to;
12
+ const steps = parseSteps(options.steps);
13
+
14
+ await assertTargetOptions("redo", listDir, target, options.steps);
15
+
16
+ const { applied } = await migrateStatus(options);
17
+ const lastApplied = applied.at(-1)?.name;
18
+ if (lastApplied === undefined) {
19
+ log({ text: "No migrations to redo.", type: "warn" });
20
+ return { reverted: [], applied: [] };
21
+ }
22
+
23
+ const appliedNames = applied.map((entry) => entry.name);
24
+ if (target !== undefined && !appliedNames.includes(target)) {
25
+ log({ text: `${target} is not applied — nothing to redo.`, type: "warn" });
26
+ return { reverted: [], applied: [] };
27
+ }
28
+
29
+ let reverted: string[] = [];
30
+ try {
31
+ ({ reverted } =
32
+ target !== undefined ? await migrateDown(options) : await migrateDown({ ...options, steps }));
33
+ const up = await migrateUp({ ...options, to: lastApplied });
34
+ return { reverted, applied: up.applied };
35
+ } catch (error) {
36
+ if (reverted.length > 0) {
37
+ log({
38
+ text: "Redo: the up phase failed — the rollbacks above stay reverted; run up to re-apply them",
39
+ type: "warn",
40
+ });
41
+ }
42
+ throw error;
43
+ }
44
+ }
@@ -1,15 +1,33 @@
1
1
  import { type SQL } from "bun";
2
2
  import type { MigrationDriver } from "../core/driver.js";
3
+ import type { MigrationStepPlan } from "./load-migration.js";
3
4
 
4
5
  export async function runMigrationStep(
5
6
  driver: MigrationDriver,
6
- step: (tx?: SQL) => Promise<void>,
7
+ plan: MigrationStepPlan,
7
8
  ): Promise<number> {
8
9
  const startedAt = performance.now();
9
- if (step.length > 0) {
10
- await driver.transaction((tx) => step(tx));
10
+ if (plan.noTransaction) {
11
+ await runOutsideTransaction(driver, plan.step);
12
+ return performance.now() - startedAt;
13
+ }
14
+ if (plan.step.length > 0) {
15
+ await driver.transaction((tx) => plan.step(tx));
11
16
  return performance.now() - startedAt;
12
17
  }
13
- await step();
18
+ await plan.step();
14
19
  return performance.now() - startedAt;
15
20
  }
21
+
22
+ function runOutsideTransaction(
23
+ driver: MigrationDriver,
24
+ step: (tx?: SQL) => Promise<void>,
25
+ ): Promise<void> {
26
+ const client = driver.client?.();
27
+ if (client === undefined) {
28
+ throw new Error(
29
+ "this driver does not expose a non-transactional client — the noTransaction marker is unsupported here",
30
+ );
31
+ }
32
+ return step(client);
33
+ }
@@ -1,16 +1,21 @@
1
1
  import { getDatabaseUrl } from "../core/env.js";
2
- import { createDriver, type MigrationDriver } from "../core/driver.js";
2
+ import type { MigrationDriver } from "../core/driver.js";
3
3
  import type { MigrateOptions } from "./options.js";
4
+ import { connectDriver, resolveWaitTimeout } from "./wait.js";
4
5
 
5
6
  export async function runWithDriver<T>(
6
7
  options: MigrateOptions,
7
8
  run: (driver: MigrationDriver) => Promise<T>,
8
9
  ): Promise<T> {
9
10
  const url = getDatabaseUrl(options.databaseUrl);
10
- const driver = await createDriver(url, {
11
- ...(options.tableName !== undefined ? { tableName: options.tableName } : {}),
12
- ...(options.schema !== undefined ? { schema: options.schema } : {}),
13
- });
11
+ const driver = await connectDriver(
12
+ url,
13
+ {
14
+ ...(options.tableName !== undefined ? { tableName: options.tableName } : {}),
15
+ ...(options.schema !== undefined ? { schema: options.schema } : {}),
16
+ },
17
+ resolveWaitTimeout(options.waitTimeout),
18
+ );
14
19
  try {
15
20
  return await run(driver);
16
21
  } finally {
package/src/api/status.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { listFiles, MIGRATION_EXTENSIONS, resolveListDir } from "../core/fs.js";
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 listFiles(listDir, MIGRATION_EXTENSIONS);
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 path from "node:path";
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 { ensureTrackingTable, listExecutedForPlan } from "./tracking-table.js";
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 listFiles(listDir, MIGRATION_EXTENSIONS);
26
-
27
- const checksums = new Map(
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 (!checksum) continue;
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,67 @@
1
+ import { log } from "../core/console.js";
2
+ import { createDriver, type DriverTableOptions, type MigrationDriver } from "../core/driver.js";
3
+ import { resolveSecondsOption } from "../core/duration.js";
4
+ import { DatabaseWaitTimeoutError } from "./options.js";
5
+
6
+ export const WAIT_RETRY_DELAY_MS = 500;
7
+
8
+ export function resolveWaitTimeout(waitTimeout: number | undefined): number {
9
+ return resolveSecondsOption("waitTimeout", waitTimeout, 0);
10
+ }
11
+
12
+ export async function waitForDatabase<T>(
13
+ attempt: () => Promise<T>,
14
+ timeoutSeconds: number,
15
+ retryDelayMs: number = WAIT_RETRY_DELAY_MS,
16
+ ): Promise<T> {
17
+ const deadline = Date.now() + timeoutSeconds * 1000;
18
+ let waitingLogged = false;
19
+ while (true) {
20
+ try {
21
+ return await attempt();
22
+ } catch (error) {
23
+ if (Date.now() >= deadline) {
24
+ throw new DatabaseWaitTimeoutError(timeoutSeconds, error);
25
+ }
26
+ if (!waitingLogged) {
27
+ log({ text: `database is not ready — waiting up to ${timeoutSeconds}s`, type: "info" });
28
+ waitingLogged = true;
29
+ }
30
+ await Bun.sleep(retryDelayMs);
31
+ }
32
+ }
33
+ }
34
+
35
+ async function connectProbed(
36
+ databaseUrl: string,
37
+ tableOptions: DriverTableOptions,
38
+ ): Promise<MigrationDriver> {
39
+ const driver = await createDriver(databaseUrl, tableOptions);
40
+ try {
41
+ await driver.transaction(async () => {});
42
+ return driver;
43
+ } catch (error) {
44
+ await driver.close().catch(() => undefined);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async function assertDriverConfig(
50
+ databaseUrl: string,
51
+ tableOptions: DriverTableOptions,
52
+ ): Promise<void> {
53
+ const driver = await createDriver(databaseUrl, tableOptions);
54
+ await driver.close();
55
+ }
56
+
57
+ export async function connectDriver(
58
+ databaseUrl: string,
59
+ tableOptions: DriverTableOptions,
60
+ waitTimeout: number,
61
+ ): Promise<MigrationDriver> {
62
+ if (waitTimeout <= 0) {
63
+ return createDriver(databaseUrl, tableOptions);
64
+ }
65
+ await assertDriverConfig(databaseUrl, tableOptions);
66
+ return waitForDatabase(() => connectProbed(databaseUrl, tableOptions), waitTimeout);
67
+ }
@@ -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
+ }