From ff28a80d7ccf8ddaf02162d4a1f2010fc8cae554 Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Wed, 29 Apr 2026 22:39:37 +0200 Subject: [PATCH] feat: migration-tool (#269) * feat: working on migration backup tool * feat: add migration sequences * fix: responsive for migration tool * fix: truncate database name in migration * fix: filter on databases success for migration tool --------- Co-authored-by: charles-gauthereau --- .../(organization)/migration/page.tsx | 65 + .../common/sidebar/menu-sidebar-main.tsx | 31 +- .../organization/migration/migration-flow.tsx | 272 ++ .../organization/migration/migration-tool.tsx | 172 ++ .../migration/migration.action.ts | 192 ++ .../organization/migration/source-panel.tsx | 309 ++ .../organization/migration/target-panel.tsx | 214 ++ .../projects/database/database-content.tsx | 8 +- src/db/migrations/0053_lyrical_union_jack.sql | 1 + src/db/migrations/meta/0053_snapshot.json | 2661 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/06_project.ts | 8 +- src/db/schema/07_database.ts | 1 + src/features/dashboard/backup/columns.tsx | 6 + .../storages/providers/google-drive/index.ts | 76 +- src/features/storages/providers/index.ts | 9 +- src/features/storages/providers/local.ts | 56 + src/features/storages/providers/s3.ts | 42 +- src/features/storages/types.ts | 9 +- src/utils/date-formatting.ts | 8 + 20 files changed, 4133 insertions(+), 14 deletions(-) create mode 100644 app/(customer)/dashboard/(organization)/migration/page.tsx create mode 100644 src/components/wrappers/dashboard/organization/migration/migration-flow.tsx create mode 100644 src/components/wrappers/dashboard/organization/migration/migration-tool.tsx create mode 100644 src/components/wrappers/dashboard/organization/migration/migration.action.ts create mode 100644 src/components/wrappers/dashboard/organization/migration/source-panel.tsx create mode 100644 src/components/wrappers/dashboard/organization/migration/target-panel.tsx create mode 100644 src/db/migrations/0053_lyrical_union_jack.sql create mode 100644 src/db/migrations/meta/0053_snapshot.json diff --git a/app/(customer)/dashboard/(organization)/migration/page.tsx b/app/(customer)/dashboard/(organization)/migration/page.tsx new file mode 100644 index 00000000..78187682 --- /dev/null +++ b/app/(customer)/dashboard/(organization)/migration/page.tsx @@ -0,0 +1,65 @@ +import {PageParams} from "@/types/next"; +import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; +import {notFound} from "next/navigation"; +import {getOrganization} from "@/lib/auth/auth"; +import {Metadata} from "next"; +import {db} from "@/db"; +import {MigrationTool} from "@/components/wrappers/dashboard/organization/migration/migration-tool"; + +export const metadata: Metadata = { + title: "Projects", +}; + +export default async function RoutePage(props: PageParams<{}>) { + + const organization = await getOrganization({}); + + if (!organization) { + notFound(); + } + + const projects = await db.query.project.findMany({ + where: (project, {eq, and, not}) => + and( + eq(project.organizationId, organization.id), + not(eq(project.isArchived, true)) + ), + with: { + organization: true, + databases: { + where: (database, { isNull, not, inArray, and }) => + and( + isNull(database.deletedAt), + not(inArray(database.dbms, ["valkey", "redis"])) + ), + + with: { + backups: { + where: (backup, { isNull, eq, and }) => + and( + isNull(backup.deletedAt), + eq(backup.status, "success") + ), + orderBy: (backup, {desc}) => [desc(backup.createdAt)], + limit: 15, + } + } + }, + }, + }); + return ( + + + + Database Migration + +

+ Import backups from a source project into your target database +

+
+ + + +
+ ); +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx index f9693fce..64cca330 100644 --- a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx +++ b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx @@ -7,7 +7,8 @@ import { Layers, ChartArea, ShieldHalf, - Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse, BookOpen + Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse, BookOpen, Hammer, + ChevronsLeftRightEllipsis } from "lucide-react"; import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar"; import {authClient} from "@/lib/auth/auth-client"; @@ -32,7 +33,17 @@ export const SidebarMenuCustomMain = () => { const groupContent: SidebarGroupItem["group_content"] = [ {title: "Projects", url: "/projects", icon: Layers, details: true, type: "item"}, {title: "Statistics", url: "/statistics", icon: ChartArea, type: "item"}, - {title: "Settings", url: "/settings", icon: Settings, details: true, type: "item"} + {title: "Settings", url: "/settings", icon: Settings, details: true, type: "item"}, + { + title: "Tools", + url: "/tools", + icon: Hammer, + details: true, + type: "collapse", + submenu: [ + {title: "Migration", url: "/migration", icon: ChevronsLeftRightEllipsis, type: "item"}, + ] + } ]; @@ -69,8 +80,8 @@ export const SidebarMenuCustomMain = () => { details: true, type: "collapse", submenu: [ - { title: "Channels", url: "/notifications/channels", icon: Blocks, type: "item" }, - { title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" }, + {title: "Channels", url: "/notifications/channels", icon: Blocks, type: "item"}, + {title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item"}, ], }, { @@ -80,7 +91,7 @@ export const SidebarMenuCustomMain = () => { details: true, type: "collapse", submenu: [ - { title: "Channels", url: "/storages/channels", icon: Blocks, type: "item" }, + {title: "Channels", url: "/storages/channels", icon: Blocks, type: "item"}, ], }, { @@ -91,7 +102,13 @@ export const SidebarMenuCustomMain = () => { type: "collapse", submenu: [ {title: "Users", url: "/admin/users", icon: Users, type: "item"}, - {title: "Organizations", url: "/admin/organizations", icon: Building, type: "item", details: true}, + { + title: "Organizations", + url: "/admin/organizations", + icon: Building, + type: "item", + details: true + }, ], }, { @@ -105,7 +122,7 @@ export const SidebarMenuCustomMain = () => { } items.push( - { + { label: "Resources", type: "list", group_content: [ diff --git a/src/components/wrappers/dashboard/organization/migration/migration-flow.tsx b/src/components/wrappers/dashboard/organization/migration/migration-flow.tsx new file mode 100644 index 00000000..d69af063 --- /dev/null +++ b/src/components/wrappers/dashboard/organization/migration/migration-flow.tsx @@ -0,0 +1,272 @@ +"use client" + +import { + ArrowRight, + CheckCircle2, + Circle, + Loader2, + Play, + Server, + FolderOpen, + HardDrive, +} from "lucide-react" +import {cn} from "@/lib/utils" +import {ProjectWithDatabasesAndBackups as ProjectWith} from "@/db/schema/06_project"; +import {Backup, DatabaseWith} from "@/db/schema/07_database"; +import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; + +interface MigrationFlowProps { + sourceProject: ProjectWith | null + sourceDatabase: DatabaseWith | null + selectedBackups: Backup[] + targetProject: ProjectWith | null + targetDatabase: DatabaseWith | null + status: MigrationStatus + onStartMigration: () => void + canStart: boolean +} + +export type MigrationStatus = "idle" | "migrating" | "completed" | "error" + + +export function MigrationFlow({ + sourceProject, + sourceDatabase, + selectedBackups, + targetProject, + targetDatabase, + status, + onStartMigration, + canStart, + }: MigrationFlowProps) { + + const steps = [ + { + id: "source-project", + label: "Source Project", + description: "Choose source project", + completed: sourceProject !== null, + active: status === "idle" && !sourceProject, + }, + { + id: "source-database", + label: "Source Database", + description: "Select database", + completed: sourceDatabase !== null, + active: status === "idle" && sourceProject !== null && !sourceDatabase, + }, + { + id: "backups", + label: "Select Backups", + description: "Choose backups to migrate", + completed: selectedBackups.length > 0, + active: status === "idle" && sourceDatabase !== null && selectedBackups.length === 0, + }, + { + id: "target-project", + label: "Target Project", + description: "Choose destination project", + completed: targetProject !== null, + active: status === "idle" && selectedBackups.length > 0 && !targetProject, + }, + { + id: "target-database", + label: "Target Database", + description: "Select destination database", + completed: targetDatabase !== null, + active: status === "idle" && targetProject !== null && !targetDatabase, + }, + { + id: "migrate", + label: "Migrate Data", + description: "Transfer to target", + completed: status === "completed", + active: status === "migrating", + }, + ] + + return ( +
+
+
+
+ +
+
+

Migration Flow

+

+ {status === "idle" && "Configure your migration"} + {status === "migrating" && "Migration in progress..."} + {status === "completed" && "Migration completed!"} +

+
+
+
+ +
+
+ {steps.map((step, index) => ( +
+
+
+ {step.completed ? ( + + ) : step.active && status === "migrating" ? ( + + ) : ( + + )} +
+ {index < steps.length - 1 && ( +
+ )} +
+
+

+ {step.label} +

+

{step.description}

+
+
+ ))} +
+ + {(sourceProject || targetProject) && ( +
+

+ Migration Summary +

+
+ {/* Source */} + {sourceProject && ( +
+ +
+ From: + + {sourceProject.name} + + {sourceDatabase && ( + + {" "}/ {sourceDatabase.name} + + )} +
+
+ )} + + {selectedBackups.length > 0 && ( +
+ + Backups: + + {selectedBackups.length} + +
+ )} + + {targetProject && ( +
+ +
+ To: + + {targetProject.name} + + {targetDatabase && ( + + {" "}/ {targetDatabase.name} + + )} +
+
+ )} +
+
+ )} + + {status === "completed" && ( +
+
+ + + Migration completed successfully! + +
+
+ )} +
+ +
+ + + + {status === "migrating" ? ( + <> + Migrating... + + ) : status === "completed" ? ( + <> + + Completed + + ) : ( + <> + + Start Migration + + )} + + + {!canStart && status === "idle" && ( +

+ {!sourceProject + ? "Select a source project" + : !sourceDatabase + ? "Select a source database" + : selectedBackups.length === 0 + ? "Select at least one backup" + : !targetProject + ? "Select a target project" + : !targetDatabase + ? "Select a target database" + : "Ready to start"} +

+ )} +
+
+ ) +} diff --git a/src/components/wrappers/dashboard/organization/migration/migration-tool.tsx b/src/components/wrappers/dashboard/organization/migration/migration-tool.tsx new file mode 100644 index 00000000..ce0a4faf --- /dev/null +++ b/src/components/wrappers/dashboard/organization/migration/migration-tool.tsx @@ -0,0 +1,172 @@ +"use client" + +import {useState, useMemo} from "react" +import {ProjectWithDatabasesAndBackups as ProjectWith} from "@/db/schema/06_project" +import {SourcePanel} from "@/components/wrappers/dashboard/organization/migration/source-panel" +import {Backup, DatabaseWith} from "@/db/schema/07_database" +import {MigrationFlow, MigrationStatus} from "@/components/wrappers/dashboard/organization/migration/migration-flow" +import {TargetPanel} from "@/components/wrappers/dashboard/organization/migration/target-panel" +import {useMutation} from "@tanstack/react-query"; +import {toast} from "sonner"; +import {migrationAction} from "@/components/wrappers/dashboard/organization/migration/migration.action"; +import {useRouter} from "next/navigation"; + +interface MigrationToolProps { + projects: ProjectWith[] +} + +export const MigrationTool = ({projects}: MigrationToolProps) => { + const [sourceProject, setSourceProject] = useState(null) + const [sourceDatabase, setSourceDatabase] = useState(null) + const [selectedBackups, setSelectedBackups] = useState([]) + const router = useRouter() + const [targetProject, setTargetProject] = useState(null) + const [targetDatabase, setTargetDatabase] = useState(null) + + const [migrationStatus, setMigrationStatus] = useState("idle") + const [_migrationProgress, setMigrationProgress] = useState(0) + + const sourceDbKind = sourceDatabase?.dbms + + const isTargetEnabled = useMemo(() => { + return migrationStatus === "idle" && + sourceDatabase !== null && + selectedBackups.length > 0 + }, [sourceDatabase, migrationStatus, selectedBackups]) + + const filteredProjects = useMemo(() => { + if (!sourceDbKind) return [] + + return projects.map((project) => ({ + ...project, + databases: project.databases.filter((db) => { + const sameKind = db.dbms === sourceDbKind + const notSource = db.id !== sourceDatabase?.id + return sameKind && notSource + }), + })) + }, [projects, sourceDbKind, sourceDatabase]) + + const handleSelectSourceProject = (project: ProjectWith | null) => { + setSourceProject(project) + setSourceDatabase(null) + setSelectedBackups([]) + setTargetProject(null) + setTargetDatabase(null) + } + + const handleSelectSourceDatabase = (database: DatabaseWith | null) => { + setSourceDatabase(database) + setSelectedBackups([]) + + if (targetDatabase && database?.dbms !== targetDatabase.dbms) { + setTargetProject(null) + setTargetDatabase(null) + } + + if (targetDatabase && database?.id === targetDatabase.id) { + setTargetProject(null) + setTargetDatabase(null) + } + } + + const handleSelectBackup = (backup: Backup) => { + setSelectedBackups((prev) => { + const isSelected = prev.some((b) => b.id === backup.id) + if (isSelected) { + return prev.filter((b) => b.id !== backup.id) + } + return [...prev, backup] + }) + } + + const handleSelectTargetProject = (project: ProjectWith | null) => { + setTargetProject(project) + setTargetDatabase(null) + } + + const handleSelectTargetDatabase = (database: DatabaseWith | null) => { + setTargetDatabase(database) + } + + const mutation = useMutation({ + mutationFn: async () => { + if (selectedBackups.length === 0 || !targetDatabase) return + setMigrationStatus("migrating") + + const result = await migrationAction({ + targetDatabaseId: targetDatabase?.id, + backupIds: selectedBackups.map((backup) => backup.id) + }) + const inner = result?.data; + console.log(inner) + if (inner?.success) { + toast.success(inner.actionSuccess?.message); + handleReset() + router.refresh(); + } else { + toast.error(inner?.actionError?.message); + } + }, + }); + + const handleReset = () => { + setMigrationStatus("idle") + setMigrationProgress(0) + setSelectedBackups([]) + setSourceProject(null) + setSourceDatabase(null) + setTargetProject(null) + setTargetDatabase(null) + } + + const canStartMigration = + selectedBackups.length > 0 && + targetDatabase !== null && + migrationStatus === "idle" + + return ( + +
+
+
+ +
+ +
+ mutation.mutateAsync()} + canStart={canStartMigration} + /> +
+ +
+ +
+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/organization/migration/migration.action.ts b/src/components/wrappers/dashboard/organization/migration/migration.action.ts new file mode 100644 index 00000000..07557c5b --- /dev/null +++ b/src/components/wrappers/dashboard/organization/migration/migration.action.ts @@ -0,0 +1,192 @@ +"use server" +import {userAction} from "@/lib/safe-actions/actions"; +import {ServerActionResult} from "@/types/action-type"; +import {z} from "zod"; +import {db} from "@/db"; +import * as drizzleDb from "@/db"; +import {eq, inArray} from "drizzle-orm"; +import {dispatchStorage} from "@/features/storages/dispatch"; +import {v4 as uuidv4} from "uuid"; +import {getTodayISODate} from "@/utils/date-formatting"; +import {withUpdatedAt} from "@/db/utils"; + +export const migrationAction = userAction.schema( + z.object({ + targetDatabaseId: z.string(), + backupIds: z.array(z.string()), + }) +).action(async ({ parsedInput }): Promise> => { + + const { targetDatabaseId, backupIds } = parsedInput; + + let hasError = false; + + try { + + const targetDatabase = await db.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.id, targetDatabaseId), + with: { + project: true, + retentionPolicy: true, + alertPolicies: true, + storagePolicies: true + } + }); + + if (!targetDatabase) { + return { + success: false, + actionError: { + message: "Unable to find target database", + status: 404, + }, + }; + } + + const backups = await db.query.backup.findMany({ + where: inArray(drizzleDb.schemas.backup.id, backupIds), + with: { + database: true, + storages: true + } + }); + + if (backups.length !== backupIds.length) { + return { + success: false, + actionError: { + message: "Some backups were not found", + status: 400, + }, + }; + } + + await Promise.all( + backups.map(async (backup) => { + + const [migratedBackup] = await db + .insert(drizzleDb.schemas.backup) + .values({ + status: "ongoing", + databaseId: targetDatabaseId, + fileSize: backup.fileSize, + migrated: true, + }) + .returning(); + + const storageResults = await Promise.allSettled( + backup.storages.map(async (storage) => { + + const [backupStorage] = await db + .insert(drizzleDb.schemas.backupStorage) + .values({ + backupId: migratedBackup.id, + storageChannelId: storage.storageChannelId, + status: "pending", + }) + .returning(); + + const fileName = `${uuidv4()}.tar.gz`; + const pathTo = `backups/${getTodayISODate()}/${fileName}`; + + try { + const result = await dispatchStorage( + { + action: "copy", + data: { + from: storage.path ?? "", + to: pathTo, + }, + metadata: { + storageId: storage.storageChannelId, + fileKind: "backups", + }, + }, + undefined, + storage.storageChannelId + ); + + if (!result.success) { + hasError = true; + throw new Error("dispatchStorage failed"); + } + + await db + .update(drizzleDb.schemas.backupStorage) + .set( + withUpdatedAt({ + status: "success", + path: pathTo, + size: backup.fileSize, + }) + ) + .where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id)); + + return { success: true }; + + } catch (error) { + + hasError = true; + + await db + .update(drizzleDb.schemas.backupStorage) + .set( + withUpdatedAt({ + status: "failed", + }) + ) + .where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id)); + + return { success: false }; + } + }) + ); + + const failed = storageResults.some( + r => r.status === "rejected" || (r.status === "fulfilled" && !r.value.success) + ); + + if (failed) hasError = true; + + await db + .update(drizzleDb.schemas.backup) + .set( + withUpdatedAt({ + status: failed ? "failed" : "success", + }) + ) + .where(eq(drizzleDb.schemas.backup.id, migratedBackup.id)); + + }) + ); + + if (hasError) { + return { + success: false, + actionError: { + message: "Migration completed with errors", + status: 500, + }, + }; + } + + return { + success: true, + value: {}, + actionSuccess: { + message: "Backups successfully migrated.", + messageParams: {}, + }, + }; + + } catch (error) { + return { + success: false, + actionError: { + message: "Migration failed unexpectedly", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + }, + }; + } +}); \ No newline at end of file diff --git a/src/components/wrappers/dashboard/organization/migration/source-panel.tsx b/src/components/wrappers/dashboard/organization/migration/source-panel.tsx new file mode 100644 index 00000000..88d8b979 --- /dev/null +++ b/src/components/wrappers/dashboard/organization/migration/source-panel.tsx @@ -0,0 +1,309 @@ +"use client" + +import { + Database, + HardDrive, + Calendar, + Check, + ChevronLeft, + FolderOpen, + Server, +} from "lucide-react" +import {cn} from "@/lib/utils" +import {ProjectWithDatabasesAndBackups as ProjectWith} from "@/db/schema/06_project"; +import {Backup, DatabaseWith} from "@/db/schema/07_database"; +import {ScrollArea} from "@/components/ui/scroll-area"; +import {formatLocalizedDate} from "@/utils/date-formatting"; +import {formatBytes, truncateWords} from "@/utils/text"; + +type ViewState = "projects" | "databases" | "backups" + +interface SourcePanelProps { + projects: ProjectWith[] + selectedProject: ProjectWith | null + selectedDatabase: DatabaseWith | null + selectedBackups: Backup[] + onSelectProject: (project: ProjectWith | null) => void + onSelectDatabase: (database: DatabaseWith | null) => void + onSelectBackup: (backup: Backup) => void + disabled?: boolean +} + +export function SourcePanel({ + projects, + selectedProject, + selectedDatabase, + selectedBackups, + onSelectProject, + onSelectDatabase, + onSelectBackup, + disabled, + }: SourcePanelProps) { + const currentView: ViewState = selectedDatabase + ? "backups" + : selectedProject + ? "databases" + : "projects" + + + const handleBack = () => { + if (currentView === "backups") { + onSelectDatabase(null) + } else if (currentView === "databases") { + onSelectProject(null) + } + } + + const getHeaderInfo = () => { + if (currentView === "backups" && selectedDatabase) { + return { + title: selectedDatabase.name, + subtitle: selectedDatabase.backups?.length? `${selectedBackups.length} of ${selectedDatabase.backups.length} backups selected` : "", + icon: , + } + } + if (currentView === "databases" && selectedProject) { + return { + title: selectedProject.name, + subtitle: `${selectedProject.databases.length} database${selectedProject.databases.length !== 1 ? "s" : ""} available`, + icon: , + } + } + return { + title: "Source", + subtitle: "Select a project to start", + icon: , + } + } + + const headerInfo = getHeaderInfo() + + return ( +
+
+
+ {currentView !== "projects" ? ( + + ) : ( +
+ {headerInfo.icon} +
+ )} +
+

+ {headerInfo.title} +

+

{headerInfo.subtitle}

+
+
+ + {currentView !== "projects" && ( +
+ + {selectedProject && ( + <> + / + + + )} + {selectedDatabase && ( + <> + / + + {selectedDatabase.name} + + + )} +
+ )} +
+ + +
+ {currentView === "projects" && ( +
+ {projects.length === 0 ? ( +
+

+ No projects available +

+
+ ) : ( + projects.map((project) => ( + + )) + )} +
+ )} + + {currentView === "databases" && selectedProject && ( +
+ {selectedProject.databases.length === 0 ? ( +
+ +

+ No databases in this project +

+
+ ) : ( + selectedProject.databases.map((database) => ( + + )) + )} +
+ )} + + {currentView === "backups" && selectedDatabase && ( +
+ {selectedDatabase?.backups?.length === 0 ? ( +
+ +

+ No backups available for this database +

+
+ ) : ( + selectedDatabase?.backups?.map((backup) => { + const isSelected = selectedBackups.some((b) => b.id === backup.id) + return ( + + ) + }) + )} +
+ )} +
+
+
+ ) +} diff --git a/src/components/wrappers/dashboard/organization/migration/target-panel.tsx b/src/components/wrappers/dashboard/organization/migration/target-panel.tsx new file mode 100644 index 00000000..9ae9c85a --- /dev/null +++ b/src/components/wrappers/dashboard/organization/migration/target-panel.tsx @@ -0,0 +1,214 @@ +"use client" + +import { + Database, + Check, + ChevronLeft, + FolderOpen, + Server, +} from "lucide-react" +import {cn} from "@/lib/utils" +import {ProjectWithDatabasesAndBackups as ProjectWith} from "@/db/schema/06_project"; +import {DatabaseWith} from "@/db/schema/07_database"; +import {ScrollArea} from "@/components/ui/scroll-area"; +import {truncateWords} from "@/utils/text"; + +type ViewState = "projects" | "databases" + +interface TargetPanelProps { + projects: ProjectWith[] + selectedProject: ProjectWith | null + selectedDatabase: DatabaseWith | null + onSelectProject: (project: ProjectWith | null) => void + onSelectDatabase: (database: DatabaseWith | null) => void + disabled?: boolean +} + +export function TargetPanel({ + projects, + selectedProject, + selectedDatabase, + onSelectProject, + onSelectDatabase, + disabled, + }: TargetPanelProps) { + const currentView: ViewState = selectedProject ? "databases" : "projects" + + const handleBack = () => { + onSelectDatabase(null) + onSelectProject(null) + } + + const getHeaderInfo = () => { + if (selectedProject) { + return { + title: selectedProject.name, + subtitle: selectedDatabase + ? selectedDatabase.name + : "Select a target database", + icon: , + } + } + return { + title: "Target", + subtitle: "Select destination project", + icon: , + } + } + + const headerInfo = getHeaderInfo() + + return ( +
+
+
+ {currentView !== "projects" ? ( + + ) : ( +
+ {headerInfo.icon} +
+ )} +
+

+ {headerInfo.title} +

+

{headerInfo.subtitle}

+
+
+ + {currentView !== "projects" && ( +
+ + {selectedProject && ( + <> + / + + {selectedProject.name} + + + )} +
+ )} +
+ + +
+ {currentView === "projects" && ( +
+ {projects.map((project) => ( + + ))} +
+ )} + + {currentView === "databases" && selectedProject && ( +
+ {selectedProject.databases.length === 0 ? ( +
+ +

+ No databases in this project +

+
+ ) : ( + selectedProject.databases.map((database) => { + const isSelected = selectedDatabase?.id === database.id + + return ( + + ) + }) + )} +
+ )} +
+
+
+ ) +} diff --git a/src/components/wrappers/dashboard/projects/database/database-content.tsx b/src/components/wrappers/dashboard/projects/database/database-content.tsx index fcda5f47..77b5e5a1 100644 --- a/src/components/wrappers/dashboard/projects/database/database-content.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-content.tsx @@ -22,6 +22,7 @@ import {ImportModal} from "@/components/wrappers/dashboard/database/import/impor import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; import {HealthModal} from "@/components/wrappers/dashboard/database/health/health-modal"; import {HealthcheckLog} from "@/db/schema/15_healthcheck-log"; +import {Badge} from "@/components/ui/badge"; export type DatabaseContentProps = { settings: Setting; @@ -99,8 +100,13 @@ export const DatabaseContent = (props: DatabaseContentProps) => { <>
-
+
{capitalizeFirstLetter(database.name)} +
+ + {database.dbms} + +
{!isMember && (
diff --git a/src/db/migrations/0053_lyrical_union_jack.sql b/src/db/migrations/0053_lyrical_union_jack.sql new file mode 100644 index 00000000..44db7612 --- /dev/null +++ b/src/db/migrations/0053_lyrical_union_jack.sql @@ -0,0 +1 @@ +ALTER TABLE "backups" ADD COLUMN "migrated" boolean DEFAULT false; \ No newline at end of file diff --git a/src/db/migrations/meta/0053_snapshot.json b/src/db/migrations/meta/0053_snapshot.json new file mode 100644 index 00000000..5d56659f --- /dev/null +++ b/src/db/migrations/meta/0053_snapshot.json @@ -0,0 +1,2661 @@ +{ + "id": "5b172f27-7fb7-4168-b410-2ae8100a8a03", + "prevId": "cee595d3-b9bc-4973-ae23-322de576a907", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_notification_channel_id": { + "name": "default_notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_default_notification_channel_id_notification_channel_id_fk": { + "name": "settings_default_notification_channel_id_notification_channel_id_fk", + "tableFrom": "settings", + "tableTo": "notification_channel", + "columnsFrom": [ + "default_notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "migrated": { + "name": "migrated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agents_organization_id_organization_id_fk": { + "name": "agents_organization_id_organization_id_fk", + "tableFrom": "agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_agents": { + "name": "organization_agents", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_agents_organization_id_organization_id_fk": { + "name": "organization_agents_organization_id_organization_id_fk", + "tableFrom": "organization_agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_agents_agent_id_agents_id_fk": { + "name": "organization_agents_agent_id_agents_id_fk", + "tableFrom": "organization_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_agents_organization_id_agent_id_unique": { + "name": "organization_agents_organization_id_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "agent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.healthcheck_log": { + "name": "healthcheck_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "healthcheck_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "healthcheck_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report", + "error_health_agent", + "error_health_database" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.healthcheck_status": { + "name": "healthcheck_status", + "schema": "public", + "values": [ + "success", + "failed" + ] + }, + "public.healthcheck_kind": { + "name": "healthcheck_kind", + "schema": "public", + "values": [ + "database", + "agent" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "redis", + "valkey", + "firebird" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 2a2ce2a4..7b1ce9e3 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -372,6 +372,13 @@ "when": 1775803959723, "tag": "0052_cute_punisher", "breakpoints": true + }, + { + "idx": 53, + "version": "7", + "when": 1777204042208, + "tag": "0053_lyrical_union_jack", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/06_project.ts b/src/db/schema/06_project.ts index 6f396f99..216ade61 100644 --- a/src/db/schema/06_project.ts +++ b/src/db/schema/06_project.ts @@ -3,7 +3,7 @@ import {relations} from "drizzle-orm"; import {Organization, organization} from "./03_organization"; import {createSelectSchema} from "drizzle-zod"; import {z} from "zod"; -import {Database, database} from "./07_database"; +import {Database, database, DatabaseWith} from "./07_database"; import {timestamps} from "@/db/schema/00_common"; export const project = pgTable("projects", { @@ -32,3 +32,9 @@ export type ProjectWith = Project & { databases: Database[]; organization: Organization; }; + +export type ProjectWithDatabasesAndBackups = Project & { + databases: DatabaseWith[]; + organization: Organization; +}; + diff --git a/src/db/schema/07_database.ts b/src/db/schema/07_database.ts index 2944a369..d4cc6aff 100644 --- a/src/db/schema/07_database.ts +++ b/src/db/schema/07_database.ts @@ -42,6 +42,7 @@ export const backup = pgTable( .notNull() .references(() => database.id, {onDelete: "cascade"}), imported: boolean('imported').default(false), + migrated: boolean('migrated').default(false), ...timestamps }, ); diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index ca6547b1..25b916ef 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -60,6 +60,7 @@ export function backupColumns( cell: ({row}) => { const reference = row.original.id const isImported = row.original.imported + const isMigrated = row.original.migrated return (
{reference} @@ -68,6 +69,11 @@ export function backupColumns( Imported )} + {isMigrated && ( + + Migrated + + )}
) }, diff --git a/src/features/storages/providers/google-drive/index.ts b/src/features/storages/providers/google-drive/index.ts index 9372308c..da87f201 100644 --- a/src/features/storages/providers/google-drive/index.ts +++ b/src/features/storages/providers/google-drive/index.ts @@ -1,5 +1,12 @@ "use server" -import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../../types'; +import { + StorageCopyInput, + StorageDeleteInput, + StorageGetInput, + StorageMetaData, + StorageResult, + StorageUploadInput +} from '../../types'; import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types"; import { ensureFolderPath, @@ -144,4 +151,71 @@ export async function pingGoogleDrive(config: GoogleDriveConfig): Promise { + const client = await getGoogleDriveClient(config); + + const sourceFileId = await resolveFilePath( + client, + input.data.from, + config.folderId, + ); + + if (!sourceFileId) { + return { + success: false, + provider: "google-drive", + error: "Source file not found", + }; + } + + const fullPath = input.data.to; + const parts = fullPath.split("/").filter(Boolean); + const fileName = parts.pop()!; + const folderPath = parts.join("/"); + + const folderId = folderPath + ? await ensureFolderPath(client, folderPath, config.folderId) + : config.folderId; + + try { + const copied = await client.files.copy({ + fileId: sourceFileId, + requestBody: { + name: fileName, + parents: [folderId], + }, + fields: "id", + supportsAllDrives: true, + }); + + const newFileId = copied.data.id; + + if (!newFileId) { + return { + success: false, + provider: "google-drive", + error: "Copy failed (no file id returned)", + }; + } + + return { + success: true, + provider: "google-drive", + }; + } catch (err: any) { + return { + success: false, + provider: "google-drive", + error: err.message || "Copy failed", + }; + } } \ No newline at end of file diff --git a/src/features/storages/providers/index.ts b/src/features/storages/providers/index.ts index 2654c397..5bdef7ee 100644 --- a/src/features/storages/providers/index.ts +++ b/src/features/storages/providers/index.ts @@ -4,9 +4,10 @@ import { StorageResult, } from '../types'; -import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local'; -import {deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3"; +import {uploadLocal, getLocal, deleteLocal, pingLocal, copyLocal} from './local'; +import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3"; import { + copyGoogleDrive, deleteGoogleDrive, getGoogleDrive, pingGoogleDrive, @@ -18,6 +19,7 @@ type ProviderHandler = { get: (config: any, input: StorageInput & { action: 'get' }) => Promise; delete: (config: any, input: StorageInput & { action: 'delete' }) => Promise; ping: (config: any, input: { action: 'ping' }) => Promise; + copy: (config: any, input: StorageInput & { action: 'copy' }) => Promise; }; const handlers: Record = { @@ -26,18 +28,21 @@ const handlers: Record = { get: getLocal, delete: deleteLocal, ping: pingLocal, + copy: copyLocal, }, s3: { upload: uploadS3, get: getS3, delete: deleteS3, ping: pingS3, + copy: copyS3 }, "google-drive": { upload: uploadGoogleDrive, get: getGoogleDrive, delete: deleteGoogleDrive, ping: pingGoogleDrive, + copy: copyGoogleDrive, } }; diff --git a/src/features/storages/providers/local.ts b/src/features/storages/providers/local.ts index b28bb831..ada5e1bf 100644 --- a/src/features/storages/providers/local.ts +++ b/src/features/storages/providers/local.ts @@ -2,6 +2,7 @@ import { mkdir, unlink } from "fs/promises"; import path from "path"; import { + StorageCopyInput, StorageDeleteInput, StorageGetInput, StorageMetaData, @@ -159,3 +160,58 @@ export async function pingLocal(config: { response: "Local storage OK", }; } + +export async function copyLocal( + config: { baseDir?: string }, + input: { + data: StorageCopyInput, + metadata?: StorageMetaData; + }, +): Promise { + const base = config.baseDir + ? path.join(process.cwd(), config.baseDir ?? "") + : BASE_DIR; + + const sourcePath = path.join(base, input.data.from); + const destinationPath = path.join(base, input.data.to); + + const dir = path.dirname(destinationPath); + await mkdir(dir, { recursive: true }); + + if (!fs.existsSync(sourcePath)) { + return { + success: false, + provider: "local", + error: "Source file not found", + }; + } + + try { + await new Promise((resolve, reject) => { + const readStream = fs.createReadStream(sourcePath); + const writeStream = fs.createWriteStream(destinationPath); + + readStream.on("error", reject); + writeStream.on("error", reject); + writeStream.on("finish", resolve); + + readStream.pipe(writeStream); + }); + + + return { + success: true, + provider: "local", + }; + } catch (err: any) { + try { + await unlink(destinationPath); + } catch {} + + return { + success: false, + provider: "local", + error: err.message || "Copy failed", + }; + } +} \ No newline at end of file diff --git a/src/features/storages/providers/s3.ts b/src/features/storages/providers/s3.ts index 6bdc8b29..7fba4069 100644 --- a/src/features/storages/providers/s3.ts +++ b/src/features/storages/providers/s3.ts @@ -1,5 +1,12 @@ import * as Minio from "minio"; -import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from "../types"; +import { + StorageCopyInput, + StorageDeleteInput, + StorageGetInput, + StorageMetaData, + StorageResult, + StorageUploadInput +} from "../types"; import {Readable} from "node:stream"; type S3Config = { @@ -132,3 +139,36 @@ export async function pingS3(config: S3Config): Promise { }; } } + + +export async function copyS3( + config: S3Config, + input: { + data: StorageCopyInput, + }, +): Promise { + const client = await getS3Client(config); + await ensureBucket(config); + + const sourceKey = `${BASE_DIR}${input.data.from}`; + const destinationKey = `${BASE_DIR}${input.data.to}`; + + try { + await client.copyObject( + config.bucketName, + destinationKey, + `/${config.bucketName}/${sourceKey}` + ); + + return { + success: true, + provider: "s3", + }; + } catch (err: any) { + return { + success: false, + provider: "s3", + error: err.message, + }; + } +} \ No newline at end of file diff --git a/src/features/storages/types.ts b/src/features/storages/types.ts index d0041b7c..5dfc5815 100644 --- a/src/features/storages/types.ts +++ b/src/features/storages/types.ts @@ -38,11 +38,18 @@ export interface StorageDeleteInput { path: string; } + +export interface StorageCopyInput { + from: string; + to: string; +} + export type StorageInput = | { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData } | { action: 'get'; data: StorageGetInput, metadata: StorageMetaData } | { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData } - | { action: 'ping'; }; + | { action: 'ping'; } + | { action: 'copy'; data: StorageCopyInput, metadata?: StorageMetaData }; export interface StorageResult { success: boolean; diff --git a/src/utils/date-formatting.ts b/src/utils/date-formatting.ts index eda54bd7..92a230dc 100644 --- a/src/utils/date-formatting.ts +++ b/src/utils/date-formatting.ts @@ -47,3 +47,11 @@ export function formatDayOnly(date: Date) { }).format(date); } +export function getTodayISODate() { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}