feat(backup): add one-shot database backups
This commit is contained in:
1 parent
f0cc5aefcf
commit
0facea2740
17 files changed
+888
-179
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { databaseBackupRunsToPrune, normalizeDatabaseBackupRun, type DatabaseBackupRun } from "../../backup/scheduledDatabaseBackup";
|
||||
|
||||
const startedAt = "2026-08-12T00:00:00.000Z";
|
||||
|
||||
function run(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "run-1",
|
||||
scheduleName: "One-time backup",
|
||||
connectionId: "mysql-1",
|
||||
connectionName: "Local MySQL",
|
||||
trigger: "manual",
|
||||
source: "one-shot",
|
||||
status: "success",
|
||||
startedAt,
|
||||
files: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("database backup run persistence", () => {
|
||||
it("accepts a one-shot run without a schedule id", () => {
|
||||
const normalized = normalizeDatabaseBackupRun(run());
|
||||
|
||||
expect(normalized).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "run-1",
|
||||
scheduleId: undefined,
|
||||
source: "one-shot",
|
||||
trigger: "manual",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps legacy runs compatible and treats them as scheduled history", () => {
|
||||
const normalized = normalizeDatabaseBackupRun(run({ scheduleId: "schedule-1", source: undefined, scheduleName: "Nightly backup" }));
|
||||
|
||||
expect(normalized).toEqual(expect.objectContaining({ scheduleId: "schedule-1", source: "scheduled", scheduleName: "Nightly backup" }));
|
||||
});
|
||||
|
||||
it("does not include one-shot runs in schedule retention pruning", () => {
|
||||
const scheduledRuns = [run({ id: "scheduled-old", scheduleId: "schedule-1", source: "scheduled", startedAt: "2026-08-10T00:00:00.000Z" }), run({ id: "scheduled-new", scheduleId: "schedule-1", source: "scheduled", startedAt: "2026-08-11T00:00:00.000Z" })];
|
||||
const oneShotRun = run({ id: "one-shot", scheduleId: undefined, source: "one-shot", startedAt: "2026-08-12T00:00:00.000Z" });
|
||||
|
||||
const normalizedRuns = [...scheduledRuns, oneShotRun].map((value) => normalizeDatabaseBackupRun(value)!).filter((value): value is DatabaseBackupRun => !!value);
|
||||
expect(databaseBackupRunsToPrune(normalizedRuns, "schedule-1", 1).map((value) => value.id)).toEqual(["scheduled-old"]);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ export type DatabaseBackupFrequency = "hourly" | "daily" | "weekly";
|
||||
export type DatabaseBackupExportStatus = "Running" | "Done" | "Error" | "Cancelled";
|
||||
export type DatabaseBackupRunStatus = "running" | "success" | "failed" | "cancelled";
|
||||
export type DatabaseBackupRunTrigger = "manual" | "scheduled";
|
||||
export type DatabaseBackupRunSource = "scheduled" | "one-shot";
|
||||
export type DatabaseBackupTableFilterMode = "all" | "include" | "exclude";
|
||||
|
||||
const CONSISTENT_BACKUP_DATABASE_TYPES = new Set(["mysql", "postgres"]);
|
||||
@@ -106,23 +107,26 @@ export function resolveScheduledDatabaseBackupTableScope(mode: DatabaseBackupTab
|
||||
};
|
||||
}
|
||||
|
||||
export interface DatabaseBackupSchedule {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
export interface DatabaseBackupExecutionConfig {
|
||||
connectionId: string;
|
||||
databases: string[];
|
||||
tableFilterMode: DatabaseBackupTableFilterMode;
|
||||
tablePatterns: string[];
|
||||
destinationDirectory: string;
|
||||
frequency: DatabaseBackupFrequency;
|
||||
intervalHours: number;
|
||||
timeOfDay: string;
|
||||
weekday: number;
|
||||
includeStructure: boolean;
|
||||
includeData: boolean;
|
||||
includeObjects: boolean;
|
||||
dropTableIfExists: boolean;
|
||||
}
|
||||
|
||||
export interface DatabaseBackupSchedule extends DatabaseBackupExecutionConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
frequency: DatabaseBackupFrequency;
|
||||
intervalHours: number;
|
||||
timeOfDay: string;
|
||||
weekday: number;
|
||||
retentionCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -131,6 +135,20 @@ export interface DatabaseBackupSchedule {
|
||||
lastRunStatus?: Exclude<DatabaseBackupRunStatus, "running">;
|
||||
}
|
||||
|
||||
export function toDatabaseBackupExecutionConfig(schedule: DatabaseBackupSchedule): DatabaseBackupExecutionConfig {
|
||||
return {
|
||||
connectionId: schedule.connectionId,
|
||||
databases: [...schedule.databases],
|
||||
tableFilterMode: schedule.tableFilterMode,
|
||||
tablePatterns: [...schedule.tablePatterns],
|
||||
destinationDirectory: schedule.destinationDirectory,
|
||||
includeStructure: schedule.includeStructure,
|
||||
includeData: schedule.includeData,
|
||||
includeObjects: schedule.includeObjects,
|
||||
dropTableIfExists: schedule.dropTableIfExists,
|
||||
};
|
||||
}
|
||||
|
||||
export interface DatabaseBackupFile {
|
||||
database: string;
|
||||
schema: string;
|
||||
@@ -140,12 +158,13 @@ export interface DatabaseBackupFile {
|
||||
|
||||
export interface DatabaseBackupRun {
|
||||
id: string;
|
||||
scheduleId: string;
|
||||
scheduleId?: string;
|
||||
scheduleName: string;
|
||||
displayName?: string;
|
||||
connectionId: string;
|
||||
connectionName: string;
|
||||
trigger: DatabaseBackupRunTrigger;
|
||||
source: DatabaseBackupRunSource;
|
||||
status: DatabaseBackupRunStatus;
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
@@ -278,9 +297,9 @@ function normalizeDatabaseBackupFile(value: unknown): DatabaseBackupFile | null
|
||||
export function normalizeDatabaseBackupRun(value: unknown): DatabaseBackupRun | null {
|
||||
const input = recordValue(value);
|
||||
const id = stringValue(input.id).trim();
|
||||
const scheduleId = stringValue(input.scheduleId).trim();
|
||||
const scheduleId = stringValue(input.scheduleId).trim() || undefined;
|
||||
const startedAt = validIsoDate(input.startedAt, "");
|
||||
if (!id || !scheduleId || !startedAt) return null;
|
||||
if (!id || !startedAt) return null;
|
||||
const status: DatabaseBackupRunStatus = input.status === "success" || input.status === "failed" || input.status === "cancelled" ? input.status : "failed";
|
||||
return {
|
||||
id,
|
||||
@@ -290,6 +309,7 @@ export function normalizeDatabaseBackupRun(value: unknown): DatabaseBackupRun |
|
||||
connectionId: stringValue(input.connectionId).trim(),
|
||||
connectionName: stringValue(input.connectionName).trim(),
|
||||
trigger: input.trigger === "scheduled" ? "scheduled" : "manual",
|
||||
source: input.source === "one-shot" ? "one-shot" : "scheduled",
|
||||
status,
|
||||
startedAt,
|
||||
completedAt: typeof input.completedAt === "string" && Number.isFinite(Date.parse(input.completedAt)) ? input.completedAt : undefined,
|
||||
|
||||
Reference in new issue
Block a user