Adds drag-and-drop functionality to reorder backup schedules Introduces a sort order field to the database schema and implements API endpoints to update the schedule order. This allows users to visually prioritize their backup jobs.
208 lines
9.3 KiB
TypeScript
208 lines
9.3 KiB
TypeScript
import { relations, sql } from "drizzle-orm";
|
|
import { int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core";
|
|
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
|
|
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
|
|
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
|
|
|
|
/**
|
|
* Volumes Table
|
|
*/
|
|
export const volumesTable = sqliteTable("volumes_table", {
|
|
id: int().primaryKey({ autoIncrement: true }),
|
|
shortId: text("short_id").notNull().unique(),
|
|
name: text().notNull().unique(),
|
|
type: text().$type<BackendType>().notNull(),
|
|
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
|
lastError: text("last_error"),
|
|
lastHealthCheck: integer("last_health_check", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
createdAt: integer("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: integer("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
|
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
|
});
|
|
export type Volume = typeof volumesTable.$inferSelect;
|
|
|
|
/**
|
|
* Users Table
|
|
*/
|
|
export const usersTable = sqliteTable("users_table", {
|
|
id: int().primaryKey({ autoIncrement: true }),
|
|
username: text().notNull().unique(),
|
|
passwordHash: text("password_hash").notNull(),
|
|
hasDownloadedResticPassword: int("has_downloaded_restic_password", { mode: "boolean" }).notNull().default(false),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
export type User = typeof usersTable.$inferSelect;
|
|
export const sessionsTable = sqliteTable("sessions_table", {
|
|
id: text().primaryKey(),
|
|
userId: int("user_id")
|
|
.notNull()
|
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
|
expiresAt: int("expires_at", { mode: "number" }).notNull(),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
export type Session = typeof sessionsTable.$inferSelect;
|
|
|
|
/**
|
|
* Repositories Table
|
|
*/
|
|
export const repositoriesTable = sqliteTable("repositories_table", {
|
|
id: text().primaryKey(),
|
|
shortId: text("short_id").notNull().unique(),
|
|
name: text().notNull().unique(),
|
|
type: text().$type<RepositoryBackend>().notNull(),
|
|
config: text("config", { mode: "json" }).$type<typeof repositoryConfigSchema.inferOut>().notNull(),
|
|
compressionMode: text("compression_mode").$type<CompressionMode>().default("auto"),
|
|
status: text().$type<RepositoryStatus>().default("unknown"),
|
|
lastChecked: int("last_checked", { mode: "number" }),
|
|
lastError: text("last_error"),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
export type Repository = typeof repositoriesTable.$inferSelect;
|
|
|
|
/**
|
|
* Backup Schedules Table
|
|
*/
|
|
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|
id: int().primaryKey({ autoIncrement: true }),
|
|
name: text().notNull().unique(),
|
|
volumeId: int("volume_id")
|
|
.notNull()
|
|
.references(() => volumesTable.id, { onDelete: "cascade" }),
|
|
repositoryId: text("repository_id")
|
|
.notNull()
|
|
.references(() => repositoriesTable.id, { onDelete: "cascade" }),
|
|
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
|
|
cronExpression: text("cron_expression").notNull(),
|
|
retentionPolicy: text("retention_policy", { mode: "json" }).$type<{
|
|
keepLast?: number;
|
|
keepHourly?: number;
|
|
keepDaily?: number;
|
|
keepWeekly?: number;
|
|
keepMonthly?: number;
|
|
keepYearly?: number;
|
|
keepWithinDuration?: string;
|
|
}>(),
|
|
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
|
excludeIfPresent: text("exclude_if_present", { mode: "json" }).$type<string[]>().default([]),
|
|
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
|
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
|
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
|
|
lastBackupError: text("last_backup_error"),
|
|
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
|
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
|
|
export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({
|
|
volume: one(volumesTable, {
|
|
fields: [backupSchedulesTable.volumeId],
|
|
references: [volumesTable.id],
|
|
}),
|
|
repository: one(repositoriesTable, {
|
|
fields: [backupSchedulesTable.repositoryId],
|
|
references: [repositoriesTable.id],
|
|
}),
|
|
notifications: many(backupScheduleNotificationsTable),
|
|
mirrors: many(backupScheduleMirrorsTable),
|
|
}));
|
|
export type BackupSchedule = typeof backupSchedulesTable.$inferSelect;
|
|
|
|
/**
|
|
* Notification Destinations Table
|
|
*/
|
|
export const notificationDestinationsTable = sqliteTable("notification_destinations_table", {
|
|
id: int().primaryKey({ autoIncrement: true }),
|
|
name: text().notNull().unique(),
|
|
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
|
|
type: text().$type<NotificationType>().notNull(),
|
|
config: text("config", { mode: "json" }).$type<typeof notificationConfigSchema.inferOut>().notNull(),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
export const notificationDestinationRelations = relations(notificationDestinationsTable, ({ many }) => ({
|
|
schedules: many(backupScheduleNotificationsTable),
|
|
}));
|
|
export type NotificationDestination = typeof notificationDestinationsTable.$inferSelect;
|
|
|
|
/**
|
|
* Backup Schedule Notifications Junction Table (Many-to-Many)
|
|
*/
|
|
export const backupScheduleNotificationsTable = sqliteTable(
|
|
"backup_schedule_notifications_table",
|
|
{
|
|
scheduleId: int("schedule_id")
|
|
.notNull()
|
|
.references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
|
|
destinationId: int("destination_id")
|
|
.notNull()
|
|
.references(() => notificationDestinationsTable.id, { onDelete: "cascade" }),
|
|
notifyOnStart: int("notify_on_start", { mode: "boolean" }).notNull().default(false),
|
|
notifyOnSuccess: int("notify_on_success", { mode: "boolean" }).notNull().default(false),
|
|
notifyOnWarning: int("notify_on_warning", { mode: "boolean" }).notNull().default(true),
|
|
notifyOnFailure: int("notify_on_failure", { mode: "boolean" }).notNull().default(true),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.scheduleId, table.destinationId] })],
|
|
);
|
|
export const backupScheduleNotificationRelations = relations(backupScheduleNotificationsTable, ({ one }) => ({
|
|
schedule: one(backupSchedulesTable, {
|
|
fields: [backupScheduleNotificationsTable.scheduleId],
|
|
references: [backupSchedulesTable.id],
|
|
}),
|
|
destination: one(notificationDestinationsTable, {
|
|
fields: [backupScheduleNotificationsTable.destinationId],
|
|
references: [notificationDestinationsTable.id],
|
|
}),
|
|
}));
|
|
export type BackupScheduleNotification = typeof backupScheduleNotificationsTable.$inferSelect;
|
|
|
|
/**
|
|
* Backup Schedule Mirrors Junction Table (Many-to-Many)
|
|
* Allows copying snapshots to secondary repositories after backup completes
|
|
*/
|
|
export const backupScheduleMirrorsTable = sqliteTable(
|
|
"backup_schedule_mirrors_table",
|
|
{
|
|
id: int().primaryKey({ autoIncrement: true }),
|
|
scheduleId: int("schedule_id")
|
|
.notNull()
|
|
.references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
|
|
repositoryId: text("repository_id")
|
|
.notNull()
|
|
.references(() => repositoriesTable.id, { onDelete: "cascade" }),
|
|
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
|
|
lastCopyAt: int("last_copy_at", { mode: "number" }),
|
|
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
|
|
lastCopyError: text("last_copy_error"),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
},
|
|
(table) => [unique().on(table.scheduleId, table.repositoryId)],
|
|
);
|
|
|
|
export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({
|
|
schedule: one(backupSchedulesTable, {
|
|
fields: [backupScheduleMirrorsTable.scheduleId],
|
|
references: [backupSchedulesTable.id],
|
|
}),
|
|
repository: one(repositoriesTable, {
|
|
fields: [backupScheduleMirrorsTable.repositoryId],
|
|
references: [repositoriesTable.id],
|
|
}),
|
|
}));
|
|
export type BackupScheduleMirror = typeof backupScheduleMirrorsTable.$inferSelect;
|
|
|
|
/**
|
|
* App Metadata Table
|
|
* Used for storing key-value pairs like migration checkpoints
|
|
*/
|
|
export const appMetadataTable = sqliteTable("app_metadata", {
|
|
key: text().primaryKey(),
|
|
value: text().notNull(),
|
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
|
});
|
|
export type AppMetadata = typeof appMetadataTable.$inferSelect;
|