From 2ea2605ab181139260276a7653ddbfff9916d8e3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:17:49 -0600 Subject: [PATCH 01/23] Enhance backup functionality by introducing support for compose backups. Update backup schema to include serviceName and backupType fields. Modify related components to handle new backup types and integrate service selection for compose backups. Update API routes and database schema accordingly. --- .../dashboard/database/backups/add-backup.tsx | 206 +- .../database/backups/restore-backup.tsx | 10 +- .../database/backups/show-backups.tsx | 104 +- .../database/backups/update-backup.tsx | 164 +- apps/dokploy/drizzle/0088_same_ezekiel.sql | 5 + apps/dokploy/drizzle/meta/0088_snapshot.json | 5448 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + .../services/compose/[composeId].tsx | 47 +- .../services/mariadb/[mariadbId].tsx | 2 +- .../[projectId]/services/mongo/[mongoId].tsx | 6 +- .../[projectId]/services/mysql/[mysqlId].tsx | 6 +- .../services/postgres/[postgresId].tsx | 6 +- .../pages/dashboard/settings/server.tsx | 6 +- apps/dokploy/server/api/routers/backup.ts | 12 + apps/dokploy/server/api/routers/compose.ts | 14 + packages/server/src/db/schema/backups.ts | 23 +- packages/server/src/db/schema/compose.ts | 2 + packages/server/src/services/backup.ts | 1 + packages/server/src/services/compose.ts | 5 + 19 files changed, 6005 insertions(+), 69 deletions(-) create mode 100644 apps/dokploy/drizzle/0088_same_ezekiel.sql create mode 100644 apps/dokploy/drizzle/meta/0088_snapshot.json diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx index 0fa568a9..c4920730 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Command, @@ -31,42 +32,59 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; +import { DatabaseZap, PlusIcon, RefreshCw } from "lucide-react"; import { CheckIcon, ChevronsUpDown } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +type CacheType = "cache" | "fetch"; + const AddPostgresBackup1Schema = z.object({ destinationId: z.string().min(1, "Destination required"), schedule: z.string().min(1, "Schedule (Cron) required"), - // .regex( - // new RegExp( - // /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/, - // ), - // "Invalid Cron", - // ), prefix: z.string().min(1, "Prefix required"), enabled: z.boolean(), database: z.string().min(1, "Database required"), keepLatestCount: z.coerce.number().optional(), + serviceName: z.string().nullable(), }); type AddPostgresBackup = z.infer; interface Props { - databaseId: string; + id: string; databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; refetch: () => void; + backupType: "database" | "compose"; } -export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { +export const AddBackup = ({ + id, + databaseType, + refetch, + backupType = "database", +}: Props) => { const { data, isLoading } = api.destination.all.useQuery(); + const [cacheType, setCacheType] = useState("cache"); const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } = api.backup.create.useMutation(); @@ -79,10 +97,28 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { prefix: "/", schedule: "", keepLatestCount: undefined, + serviceName: null, }, resolver: zodResolver(AddPostgresBackup1Schema), }); + const { + data: services, + isFetching: isLoadingServices, + error: errorServices, + refetch: refetchServices, + } = api.compose.loadServices.useQuery( + { + composeId: id, + type: cacheType, + }, + { + retry: false, + refetchOnWindowFocus: false, + enabled: backupType === "compose", + }, + ); + useEffect(() => { form.reset({ database: databaseType === "web-server" ? "dokploy" : "", @@ -91,32 +127,45 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { prefix: "/", schedule: "", keepLatestCount: undefined, + serviceName: null, }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + }, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]); const onSubmit = async (data: AddPostgresBackup) => { + if (backupType === "compose" && !data.serviceName) { + form.setError("serviceName", { + type: "manual", + message: "Service name is required for compose backups", + }); + return; + } + const getDatabaseId = - databaseType === "postgres" + backupType === "compose" ? { - postgresId: databaseId, + composeId: id, } - : databaseType === "mariadb" + : databaseType === "postgres" ? { - mariadbId: databaseId, + postgresId: id, } - : databaseType === "mysql" + : databaseType === "mariadb" ? { - mysqlId: databaseId, + mariadbId: id, } - : databaseType === "mongo" + : databaseType === "mysql" ? { - mongoId: databaseId, + mysqlId: id, } - : databaseType === "web-server" + : databaseType === "mongo" ? { - userId: databaseId, + mongoId: id, } - : undefined; + : databaseType === "web-server" + ? { + userId: id, + } + : undefined; await createBackup({ destinationId: data.destinationId, @@ -125,8 +174,10 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { enabled: data.enabled, database: data.database, keepLatestCount: data.keepLatestCount, - databaseType, + databaseType: databaseType, + serviceName: data.serviceName, ...getDatabaseId, + backupType, }) .then(async () => { toast.success("Backup Created"); @@ -157,6 +208,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { className="grid w-full gap-4" >
+ {errorServices && ( + + {errorServices?.message} + + )} { )} /> + {backupType === "compose" && ( +
+ ( + + Service Name +
+ + + + + + + +

+ Fetch: Will clone the repository and load the + services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this + compose, it will read the services from the + last deployment/fetch from the repository +

+
+
+
+
+ + +
+ )} + /> +
+ )} | "web-server"; serverId?: string | null; } @@ -85,11 +85,7 @@ const formatBytes = (bytes: number): string => { return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; }; -export const RestoreBackup = ({ - databaseId, - databaseType, - serverId, -}: Props) => { +export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); @@ -136,7 +132,7 @@ export const RestoreBackup = ({ api.backup.restoreBackupWithLogs.useSubscription( { - databaseId, + databaseId: id, databaseType, databaseName: form.watch("databaseName"), backupFile: form.watch("backupFile"), diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 1c2b527b..53bfad75 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -22,41 +22,60 @@ import type { ServiceType } from "../../application/advanced/show-resources"; import { AddBackup } from "./add-backup"; import { RestoreBackup } from "./restore-backup"; import { UpdateBackup } from "./update-backup"; +import { AlertBlock } from "@/components/shared/alert-block"; interface Props { id: string; - type: Exclude | "web-server"; + databaseType: Exclude | "web-server"; + backupType?: "database" | "compose"; } -export const ShowBackups = ({ id, type }: Props) => { +export const ShowBackups = ({ + id, + databaseType, + backupType = "database", +}: Props) => { const [activeManualBackup, setActiveManualBackup] = useState< string | undefined >(); - const queryMap = { - postgres: () => - api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), - mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), - mariadb: () => - api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), - mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), - "web-server": () => api.user.getBackups.useQuery(), - }; + const queryMap = + backupType === "database" + ? { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + mysql: () => + api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + mongo: () => + api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + "web-server": () => api.user.getBackups.useQuery(), + } + : { + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + }; const { data } = api.destination.all.useQuery(); - const { data: postgres, refetch } = queryMap[type] - ? queryMap[type]() + const key = backupType === "database" ? databaseType : "compose"; + const query = queryMap[key as keyof typeof queryMap]; + const { data: postgres, refetch } = query + ? query() : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + console.log(postgres); + const mutationMap = { postgres: () => api.backup.manualBackupPostgres.useMutation(), mysql: () => api.backup.manualBackupMySql.useMutation(), mariadb: () => api.backup.manualBackupMariadb.useMutation(), mongo: () => api.backup.manualBackupMongo.useMutation(), "web-server": () => api.backup.manualBackupWebServer.useMutation(), + compose: () => api.backup.manualBackupCompose.useMutation(), }; const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[ - type + databaseType ] - ? mutationMap[type]() + ? mutationMap[databaseType]() : api.backup.manualBackupMongo.useMutation(); const { mutateAsync: deleteBackup, isLoading: isRemoving } = @@ -78,16 +97,17 @@ export const ShowBackups = ({ id, type }: Props) => { {postgres && postgres?.backups?.length > 0 && (
- {type !== "web-server" && ( + {databaseType !== "web-server" && ( )}
@@ -110,7 +130,7 @@ export const ShowBackups = ({ id, type }: Props) => {
) : ( -
+
{postgres?.backups.length === 0 ? (
@@ -119,13 +139,14 @@ export const ShowBackups = ({ id, type }: Props) => {
{
) : ( -
+
+
+ {backupType === "compose" && ( + + Deploy is required to apply changes after creating or + updating a backup. + + )} +
{postgres?.backups.map((backup) => (
-
+
+ {backup.backupType === "compose" && ( + <> +
+ + Service Name + + + {backup.serviceName} + +
+ +
+ + Database Type + + + {backup.databaseType} + +
+ + )}
Destination diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index 2cf7b7a5..6163d2b1 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Command, @@ -31,16 +32,37 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react"; +import { + CheckIcon, + ChevronsUpDown, + DatabaseZap, + PenBoxIcon, + RefreshCw, +} from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +type CacheType = "cache" | "fetch"; + const UpdateBackupSchema = z.object({ destinationId: z.string().min(1, "Destination required"), schedule: z.string().min(1, "Schedule (Cron) required"), @@ -48,6 +70,7 @@ const UpdateBackupSchema = z.object({ enabled: z.boolean(), database: z.string().min(1, "Database required"), keepLatestCount: z.coerce.number().optional(), + serviceName: z.string().nullable(), }); type UpdateBackup = z.infer; @@ -59,6 +82,7 @@ interface Props { export const UpdateBackup = ({ backupId, refetch }: Props) => { const [isOpen, setIsOpen] = useState(false); + const [cacheType, setCacheType] = useState("cache"); const { data, isLoading } = api.destination.all.useQuery(); const { data: backup } = api.backup.one.useQuery( { @@ -69,6 +93,24 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { }, ); + const { + data: services, + isFetching: isLoadingServices, + error: errorServices, + refetch: refetchServices, + } = api.compose.loadServices.useQuery( + { + composeId: backup?.composeId || "", + type: cacheType, + }, + { + retry: false, + refetchOnWindowFocus: false, + enabled: + isOpen && backup?.backupType === "compose" && !!backup?.composeId, + }, + ); + const { mutateAsync, isLoading: isLoadingUpdate } = api.backup.update.useMutation(); @@ -80,6 +122,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { prefix: "/", schedule: "", keepLatestCount: undefined, + serviceName: null, }, resolver: zodResolver(UpdateBackupSchema), }); @@ -92,6 +135,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { enabled: backup.enabled || false, prefix: backup.prefix, schedule: backup.schedule, + serviceName: backup.serviceName || null, keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined, @@ -100,6 +144,14 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { }, [form, form.reset, backup]); const onSubmit = async (data: UpdateBackup) => { + if (backup?.backupType === "compose" && !data.serviceName) { + form.setError("serviceName", { + type: "manual", + message: "Service name is required for compose backups", + }); + return; + } + await mutateAsync({ backupId, destinationId: data.destinationId, @@ -107,6 +159,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { schedule: data.schedule, enabled: data.enabled, database: data.database, + serviceName: data.serviceName, keepLatestCount: data.keepLatestCount as number | null, }) .then(async () => { @@ -143,6 +196,11 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { className="grid w-full gap-4" >
+ {errorServices && ( + + {errorServices?.message} + + )} { )} /> + {backup?.backupType === "compose" && ( +
+ ( + + Service Name +
+ + + + + + + +

+ Fetch: Will clone the repository and load the + services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this + compose, it will read the services from the + last deployment/fetch from the repository +

+
+
+
+
+ + +
+ )} + /> +
+ )} statement-breakpoint +ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint +ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint +ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint +ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0088_snapshot.json b/apps/dokploy/drizzle/meta/0088_snapshot.json new file mode 100644 index 00000000..644b268c --- /dev/null +++ b/apps/dokploy/drizzle/meta/0088_snapshot.json @@ -0,0 +1,5448 @@ +{ + "id": "7fea81ef-e2a7-4a8b-b755-e98903a08b57", + "prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_temp": { + "name": "user_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_temp_email_unique": { + "name": "user_temp_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_temp_id_fk": { + "name": "backup_userId_user_temp_id_fk", + "tableFrom": "backup", + "tableTo": "user_temp", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_temp_id_fk": { + "name": "session_temp_user_id_user_temp_id_fk", + "tableFrom": "session_temp", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "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 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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": "text", + "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 + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_temp_id_fk": { + "name": "account_user_id_user_temp_id_fk", + "tableFrom": "account", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_temp_id_fk": { + "name": "apikey_user_id_user_temp_id_fk", + "tableFrom": "apikey", + "tableTo": "user_temp", + "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": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "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_temp_id_fk": { + "name": "invitation_inviter_id_user_temp_id_fk", + "tableFrom": "invitation", + "tableTo": "user_temp", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "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_temp_id_fk": { + "name": "member_user_id_user_temp_id_fk", + "tableFrom": "member", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_temp_id_fk": { + "name": "organization_owner_id_user_temp_id_fk", + "tableFrom": "organization", + "tableTo": "user_temp", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_temp_id_fk": { + "name": "two_factor_user_id_user_temp_id_fk", + "tableFrom": "two_factor", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "gotify" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index e29f6bd9..1b3d986d 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -617,6 +617,13 @@ "when": 1745723563822, "tag": "0087_lively_risque", "breakpoints": true + }, + { + "idx": 88, + "version": "7", + "when": 1745801614194, + "tag": "0088_same_ezekiel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 3bba9eb2..38a0a1b9 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -9,6 +9,7 @@ import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show" import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack"; import { UpdateCompose } from "@/components/dashboard/compose/update-compose"; +import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring"; import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring"; import { ProjectLayout } from "@/components/layouts/project-layout"; @@ -30,6 +31,13 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; @@ -57,6 +65,8 @@ type TabState = | "domains" | "monitoring"; +type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo"; + const Service = ( props: InferGetServerSidePropsType, ) => { @@ -65,6 +75,8 @@ const Service = ( const router = useRouter(); const { projectId } = router.query; const [tab, setTab] = useState(activeTab); + const [selectedDatabaseType, setSelectedDatabaseType] = + useState("postgres"); useEffect(() => { if (router.query.tab) { @@ -217,16 +229,17 @@ const Service = ( className={cn( "lg:grid lg:w-fit max-md:overflow-y-scroll justify-start", isCloud && data?.serverId - ? "lg:grid-cols-7" + ? "lg:grid-cols-8" : data?.serverId - ? "lg:grid-cols-6" - : "lg:grid-cols-7", + ? "lg:grid-cols-7" + : "lg:grid-cols-8", )} > General Environment Domains Deployments + Backups Logs {((data?.serverId && isCloud) || !data?.server) && ( Monitoring @@ -245,6 +258,34 @@ const Service = (
+ +
+
+ + +
+ +
+
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index 1f74adbe..09aa309f 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -271,7 +271,7 @@ const Mariadb = (
- +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 8a035579..fd4c75e0 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -272,7 +272,11 @@ const Mongo = (
- +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index 6dbbd6a2..767e4211 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -252,7 +252,11 @@ const MySql = (
- +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index 2441bcb6..875e8d85 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -251,7 +251,11 @@ const Postgresql = (
- +
diff --git a/apps/dokploy/pages/dashboard/settings/server.tsx b/apps/dokploy/pages/dashboard/settings/server.tsx index f3df395f..5e16ef42 100644 --- a/apps/dokploy/pages/dashboard/settings/server.tsx +++ b/apps/dokploy/pages/dashboard/settings/server.tsx @@ -20,7 +20,11 @@ const Page = () => {
- +
diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 0aeebb02..10957359 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -82,6 +82,11 @@ export const backupRouter = createTRPCRouter({ serverId = backup.mongo.serverId; } else if (databaseType === "mariadb" && backup.mariadb?.serverId) { serverId = backup.mariadb.serverId; + } else if ( + backup.backupType === "compose" && + backup.compose?.serverId + ) { + serverId = backup.compose.serverId; } const server = await findServerById(serverId); @@ -232,6 +237,13 @@ export const backupRouter = createTRPCRouter({ }); } }), + manualBackupCompose: protectedProcedure + .input(apiFindOneBackup) + .mutation(async ({ input }) => { + // const backup = await findBackupById(input.backupId); + // await runComposeBackup(backup); + return true; + }), manualBackupMongo: protectedProcedure .input(apiFindOneBackup) .mutation(async ({ input }) => { diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index c4f9b317..7ded3f7a 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -723,4 +723,18 @@ export const composeRouter = createTRPCRouter({ }); } }), + manualBackup: protectedProcedure + .input(z.object({ composeId: z.string() })) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to backup this compose", + }); + } + await createBackup({ + composeId: compose.composeId, + }); + }), }); diff --git a/packages/server/src/db/schema/backups.ts b/packages/server/src/db/schema/backups.ts index 951652d4..f7428960 100644 --- a/packages/server/src/db/schema/backups.ts +++ b/packages/server/src/db/schema/backups.ts @@ -16,6 +16,8 @@ import { mongo } from "./mongo"; import { mysql } from "./mysql"; import { postgres } from "./postgres"; import { users_temp } from "./user"; +import { compose } from "./compose"; + export const databaseType = pgEnum("databaseType", [ "postgres", "mariadb", @@ -24,6 +26,8 @@ export const databaseType = pgEnum("databaseType", [ "web-server", ]); +export const backupType = pgEnum("backupType", ["database", "compose"]); + export const backups = pgTable("backup", { backupId: text("backupId") .notNull() @@ -33,14 +37,19 @@ export const backups = pgTable("backup", { enabled: boolean("enabled"), database: text("database").notNull(), prefix: text("prefix").notNull(), - + serviceName: text("serviceName"), destinationId: text("destinationId") .notNull() .references(() => destinations.destinationId, { onDelete: "cascade" }), - keepLatestCount: integer("keepLatestCount"), - + backupType: backupType("backupType").notNull().default("database"), databaseType: databaseType("databaseType").notNull(), + composeId: text("composeId").references( + (): AnyPgColumn => compose.composeId, + { + onDelete: "cascade", + }, + ), postgresId: text("postgresId").references( (): AnyPgColumn => postgres.postgresId, { @@ -87,6 +96,10 @@ export const backupsRelations = relations(backups, ({ one }) => ({ fields: [backups.userId], references: [users_temp.id], }), + compose: one(compose, { + fields: [backups.composeId], + references: [compose.composeId], + }), })); const createSchema = createInsertSchema(backups, { @@ -118,6 +131,9 @@ export const apiCreateBackup = createSchema.pick({ mongoId: true, databaseType: true, userId: true, + backupType: true, + composeId: true, + serviceName: true, }); export const apiFindOneBackup = createSchema @@ -141,5 +157,6 @@ export const apiUpdateBackup = createSchema destinationId: true, database: true, keepLatestCount: true, + serviceName: true, }) .required(); diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts index 5e62ce55..e4a7bde8 100644 --- a/packages/server/src/db/schema/compose.ts +++ b/packages/server/src/db/schema/compose.ts @@ -15,6 +15,7 @@ import { server } from "./server"; import { applicationStatus, triggerType } from "./shared"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; +import { backups } from "./backups"; export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ "git", @@ -135,6 +136,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({ fields: [compose.serverId], references: [server.serverId], }), + backups: many(backups), })); const createSchema = createInsertSchema(compose, { diff --git a/packages/server/src/services/backup.ts b/packages/server/src/services/backup.ts index 32705786..40faf6d9 100644 --- a/packages/server/src/services/backup.ts +++ b/packages/server/src/services/backup.ts @@ -35,6 +35,7 @@ export const findBackupById = async (backupId: string) => { mariadb: true, mongo: true, destination: true, + compose: true, }, }); if (!backup) { diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index d855ff9e..ec6ed171 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -131,6 +131,11 @@ export const findComposeById = async (composeId: string) => { bitbucket: true, gitea: true, server: true, + backups: { + with: { + destination: true, + }, + }, }, }); if (!result) { From 7c2eb636252d56f905fca9035fd641fe835e441d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:14:06 -0600 Subject: [PATCH 02/23] Implement metadata handling for database and compose backups. Update backup schemas to include metadata fields for various database types. Enhance backup creation and update processes to accommodate new metadata requirements. Modify UI components to support metadata input for different database types during backup operations. --- .../dashboard/database/backups/add-backup.tsx | 154 +- .../database/backups/show-backups.tsx | 29 +- .../database/backups/update-backup.tsx | 158 +- apps/dokploy/drizzle/0089_dazzling_marrow.sql | 1 + apps/dokploy/drizzle/meta/0089_snapshot.json | 5454 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/server/api/routers/backup.ts | 17 +- apps/dokploy/server/api/routers/compose.ts | 18 +- packages/server/src/db/schema/backups.ts | 20 + packages/server/src/services/mongo.ts | 26 +- packages/server/src/utils/backups/compose.ts | 91 + packages/server/src/utils/backups/utils.ts | 47 +- packages/server/src/utils/builders/compose.ts | 6 +- packages/server/src/utils/docker/backup.ts | 4 + packages/server/src/utils/docker/domain.ts | 55 +- 15 files changed, 6010 insertions(+), 77 deletions(-) create mode 100644 apps/dokploy/drizzle/0089_dazzling_marrow.sql create mode 100644 apps/dokploy/drizzle/meta/0089_snapshot.json create mode 100644 packages/server/src/utils/backups/compose.ts create mode 100644 packages/server/src/utils/docker/backup.ts diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx index c4920730..8fe2a976 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx @@ -58,7 +58,36 @@ import { z } from "zod"; type CacheType = "cache" | "fetch"; -const AddPostgresBackup1Schema = z.object({ +const getMetadataSchema = ( + backupType: "database" | "compose", + databaseType: Props["databaseType"], +) => { + if (backupType !== "compose") return z.object({}).optional(); + + const schemas = { + postgres: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + }), + mariadb: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + databasePassword: z.string().min(1, "Database password is required"), + }), + mongo: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + databasePassword: z.string().min(1, "Database password is required"), + }), + mysql: z.object({ + databaseRootPassword: z.string().min(1, "Root password is required"), + }), + "web-server": z.object({}), + }; + + return z.object({ + [databaseType]: schemas[databaseType], + }); +}; + +const Schema = z.object({ destinationId: z.string().min(1, "Destination required"), schedule: z.string().min(1, "Schedule (Cron) required"), prefix: z.string().min(1, "Prefix required"), @@ -68,7 +97,7 @@ const AddPostgresBackup1Schema = z.object({ serviceName: z.string().nullable(), }); -type AddPostgresBackup = z.infer; +type Schema = z.infer; interface Props { id: string; @@ -89,7 +118,11 @@ export const AddBackup = ({ const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } = api.backup.create.useMutation(); - const form = useForm({ + const schema = Schema.extend({ + metadata: getMetadataSchema(backupType, databaseType), + }); + + const form = useForm>({ defaultValues: { database: "", destinationId: "", @@ -98,8 +131,9 @@ export const AddBackup = ({ schedule: "", keepLatestCount: undefined, serviceName: null, + metadata: {}, }, - resolver: zodResolver(AddPostgresBackup1Schema), + resolver: zodResolver(schema), }); const { @@ -128,10 +162,11 @@ export const AddBackup = ({ schedule: "", keepLatestCount: undefined, serviceName: null, + metadata: {}, }); }, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]); - const onSubmit = async (data: AddPostgresBackup) => { + const onSubmit = async (data: Schema) => { if (backupType === "compose" && !data.serviceName) { form.setError("serviceName", { type: "manual", @@ -489,6 +524,115 @@ export const AddBackup = ({ )} /> + {backupType === "compose" && ( + <> + {databaseType === "postgres" && ( + ( + + Database User + + + + + + )} + /> + )} + + {databaseType === "mariadb" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {databaseType === "mongo" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {databaseType === "mysql" && ( + ( + + Root Password + + + + + + )} + /> + )} + + )}
- + Update Backup Update the backup @@ -238,6 +262,27 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { {errorServices?.message} )} + {backup?.backupType === "compose" && ( + + Database Type + + + )} { /> {backup?.backupType === "compose" && ( <> - {backup.databaseType === "postgres" && ( + {selectedDatabaseType === "postgres" && ( { /> )} - {backup.databaseType === "mariadb" && ( + {selectedDatabaseType === "mariadb" && ( <> { )} - {backup.databaseType === "mongo" && ( + {selectedDatabaseType === "mongo" && ( <> { )} - {backup.databaseType === "mysql" && ( + {selectedDatabaseType === "mysql" && ( , ) => { @@ -75,8 +66,6 @@ const Service = ( const router = useRouter(); const { projectId } = router.query; const [tab, setTab] = useState(activeTab); - const [selectedDatabaseType, setSelectedDatabaseType] = - useState("postgres"); useEffect(() => { if (router.query.tab) { @@ -260,30 +249,7 @@ const Service = (
-
- - -
- +
diff --git a/packages/server/src/db/schema/backups.ts b/packages/server/src/db/schema/backups.ts index 83f49344..94461043 100644 --- a/packages/server/src/db/schema/backups.ts +++ b/packages/server/src/db/schema/backups.ts @@ -178,5 +178,6 @@ export const apiUpdateBackup = createSchema keepLatestCount: true, serviceName: true, metadata: true, + databaseType: true, }) .required(); From 19bf4f27b683330170d3117d7b4de2e18cd3d311 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:06:38 -0600 Subject: [PATCH 05/23] Update UpdateBackup component to enforce DatabaseType casting for backup databaseType. Modify backups schema to allow optional metadata field, enhancing flexibility for various database types. --- .../database/backups/update-backup.tsx | 2 +- packages/server/src/db/schema/backups.ts | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index d3ce1aa1..a57cf01d 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -221,7 +221,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { databaseType: backup?.backupType === "compose" ? selectedDatabaseType - : backup?.databaseType, + : (backup?.databaseType as DatabaseType), }) .then(async () => { toast.success("Backup Updated"); diff --git a/packages/server/src/db/schema/backups.ts b/packages/server/src/db/schema/backups.ts index 94461043..6dca5caa 100644 --- a/packages/server/src/db/schema/backups.ts +++ b/packages/server/src/db/schema/backups.ts @@ -71,22 +71,25 @@ export const backups = pgTable("backup", { }), userId: text("userId").references(() => users_temp.id), // Only for compose backups - metadata: jsonb("metadata").$type<{ - postgres?: { - databaseUser: string; - }; - mariadb?: { - databaseUser: string; - databasePassword: string; - }; - mongo?: { - databaseUser: string; - databasePassword: string; - }; - mysql?: { - databaseRootPassword: string; - }; - }>(), + metadata: jsonb("metadata").$type< + | { + postgres?: { + databaseUser: string; + }; + mariadb?: { + databaseUser: string; + databasePassword: string; + }; + mongo?: { + databaseUser: string; + databasePassword: string; + }; + mysql?: { + databaseRootPassword: string; + }; + } + | undefined + >(), }); export const backupsRelations = relations(backups, ({ one }) => ({ @@ -134,6 +137,7 @@ const createSchema = createInsertSchema(backups, { mysqlId: z.string().optional(), mongoId: z.string().optional(), userId: z.string().optional(), + metadata: z.object({}).optional(), }); export const apiCreateBackup = createSchema.pick({ From 77d7dc1f2233442962f095ed59507489a0a33f32 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:09:39 -0600 Subject: [PATCH 06/23] Enhance backup functionality by adding support for compose backups in various components. Update ShowBackups to clarify backup update requirements, modify initCronJobs to include compose backups, and improve logging for backup scheduling. Additionally, ensure that the scheduleBackup function retains the latest backups for compose types and document the addition of domains and backups in the Docker compose process. --- .../components/dashboard/database/backups/show-backups.tsx | 2 +- packages/server/src/utils/backups/index.ts | 7 ++++--- packages/server/src/utils/backups/utils.ts | 1 + packages/server/src/utils/docker/domain.ts | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index e6de42e5..e219c3e3 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -161,7 +161,7 @@ export const ShowBackups = ({ {backupType === "compose" && ( Deploy is required to apply changes after creating or - updating a backup. + updating the service name in the backup. )}
diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index 6c940406..38a0b446 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -70,6 +70,7 @@ export const initCronJobs = async () => { mysql: true, mongo: true, user: true, + compose: true, }, }); @@ -77,10 +78,10 @@ export const initCronJobs = async () => { try { if (backup.enabled) { scheduleBackup(backup); + console.log( + `[Backup] ${backup.databaseType} Enabled with cron: [${backup.schedule}]`, + ); } - console.log( - `[Backup] ${backup.databaseType} Enabled with cron: [${backup.schedule}]`, - ); } catch (error) { console.error(`[Backup] ${backup.databaseType} Error`, error); } diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 00d1aa0e..c91a7ac7 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -40,6 +40,7 @@ export const scheduleBackup = (backup: BackupSchedule) => { } } else if (backup.backupType === "compose" && compose) { await runComposeBackup(compose, backup); + await keepLatestNBackups(backup, compose.serverId); } }); }; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index fac4d02a..8d314364 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -210,6 +210,7 @@ export const addDomainToCompose = async ( result = randomized; } + // Add domains to the compose for (const domain of domains) { const { serviceName, https } = domain; if (!serviceName) { @@ -264,6 +265,7 @@ export const addDomainToCompose = async ( } } + // Add backups to the compose for (const backup of backups) { const { backupId, serviceName, enabled } = backup; From ddcb22dff93c1618b67819058adebadcbb58ecb9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:12:11 -0600 Subject: [PATCH 07/23] Implement support for compose backups in runJobs function. Enhance backup handling by checking server status and executing runComposeBackup for compose database types. Update backup structure to include compose details. --- apps/schedules/src/utils.ts | 11 ++++++++++- packages/server/src/utils/backups/compose.ts | 1 - 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/schedules/src/utils.ts b/apps/schedules/src/utils.ts index b0d9b877..4868e77d 100644 --- a/apps/schedules/src/utils.ts +++ b/apps/schedules/src/utils.ts @@ -16,13 +16,14 @@ import { eq } from "drizzle-orm"; import { logger } from "./logger.js"; import { scheduleJob } from "./queue.js"; import type { QueueJob } from "./schema.js"; +import { runComposeBackup } from "@dokploy/server/src/utils/backups/compose.js"; export const runJobs = async (job: QueueJob) => { try { if (job.type === "backup") { const { backupId } = job; const backup = await findBackupById(backupId); - const { databaseType, postgres, mysql, mongo, mariadb } = backup; + const { databaseType, postgres, mysql, mongo, mariadb, compose } = backup; if (databaseType === "postgres" && postgres) { const server = await findServerById(postgres.serverId as string); @@ -56,6 +57,14 @@ export const runJobs = async (job: QueueJob) => { } await runMariadbBackup(mariadb, backup); await keepLatestNBackups(backup, server.serverId); + } else if (databaseType === "compose" && compose) { + const server = await findServerById(compose.serverId as string); + if (server.serverStatus === "inactive") { + logger.info("Server is inactive"); + return; + } + await runComposeBackup(compose, backup); + await keepLatestNBackups(backup, server.serverId); } } if (job.type === "server") { diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 443820a1..06d0465f 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -88,4 +88,3 @@ export const runComposeBackup = async ( throw error; } }; -// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz From 5055994bd30fcae43509afc9463912d24792233c Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 28 Apr 2025 02:17:42 -0600 Subject: [PATCH 08/23] Enhance RestoreBackup component to support compose backups by adding a database type selection and metadata handling. Update related API routes and schemas to accommodate new backup types, ensuring flexibility for various database configurations. Modify UI components to allow dynamic input for service names and database credentials based on the selected database type. --- .../database/backups/restore-backup.tsx | 378 +++++++++++++++++- .../database/backups/show-backups.tsx | 1 + .../database/backups/update-backup.tsx | 1 - apps/dokploy/server/api/routers/backup.ts | 143 ++++--- apps/dokploy/server/api/routers/compose.ts | 4 +- packages/server/src/db/schema/backups.ts | 2 +- packages/server/src/utils/backups/compose.ts | 25 +- packages/server/src/utils/builders/compose.ts | 6 +- packages/server/src/utils/docker/domain.ts | 57 +-- packages/server/src/utils/restore/compose.ts | 99 +++++ packages/server/src/utils/restore/index.ts | 1 + 11 files changed, 585 insertions(+), 132 deletions(-) create mode 100644 packages/server/src/utils/restore/compose.ts diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 243201e1..d7323e5b 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -32,25 +32,76 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import copy from "copy-to-clipboard"; import { debounce } from "lodash"; -import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react"; +import { + CheckIcon, + ChevronsUpDown, + Copy, + RotateCcw, + RefreshCw, + DatabaseZap, +} from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import type { ServiceType } from "../../application/advanced/show-resources"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface Props { id: string; databaseType: Exclude | "web-server"; serverId?: string | null; + backupType?: "database" | "compose"; } +const getMetadataSchema = ( + backupType: "database" | "compose", + databaseType: string, +) => { + if (backupType !== "compose") return z.object({}).optional(); + + const schemas = { + postgres: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + }), + mariadb: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + databasePassword: z.string().min(1, "Database password is required"), + }), + mongo: z.object({ + databaseUser: z.string().min(1, "Database user is required"), + databasePassword: z.string().min(1, "Database password is required"), + }), + mysql: z.object({ + databaseRootPassword: z.string().min(1, "Root password is required"), + }), + "web-server": z.object({}), + }; + + return z.object({ + [databaseType]: schemas[databaseType as keyof typeof schemas], + serviceName: z.string().min(1, "Service name is required"), + }); +}; + const RestoreBackupSchema = z.object({ destinationId: z .string({ @@ -73,10 +124,16 @@ const RestoreBackupSchema = z.object({ .min(1, { message: "Database name is required", }), + databaseType: z + .string({ + required_error: "Please select a database type", + }) + .min(1, { + message: "Database type is required", + }), + metadata: z.object({}).optional(), }); -type RestoreBackup = z.infer; - const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; @@ -85,24 +142,41 @@ const formatBytes = (bytes: number): string => { return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; }; -export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { +export const RestoreBackup = ({ + id, + databaseType, + serverId, + backupType = "database", +}: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + const [selectedDatabaseType, setSelectedDatabaseType] = useState( + backupType === "compose" ? "" : databaseType, + ); const { data: destinations = [] } = api.destination.all.useQuery(); - const form = useForm({ + const schema = RestoreBackupSchema.extend({ + metadata: getMetadataSchema(backupType, selectedDatabaseType), + }); + + const form = useForm>({ defaultValues: { destinationId: "", backupFile: "", databaseName: databaseType === "web-server" ? "dokploy" : "", + databaseType: backupType === "compose" ? "" : databaseType, + metadata: {}, }, - resolver: zodResolver(RestoreBackupSchema), + resolver: zodResolver(schema), }); const destionationId = form.watch("destinationId"); + const metadata = form.watch("metadata"); + // console.log({ metadata }); + const debouncedSetSearch = debounce((value: string) => { setDebouncedSearchTerm(value); }, 350); @@ -127,16 +201,15 @@ export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { const [filteredLogs, setFilteredLogs] = useState([]); const [isDeploying, setIsDeploying] = useState(false); - // const { mutateAsync: restore, isLoading: isRestoring } = - // api.backup.restoreBackup.useMutation(); - api.backup.restoreBackupWithLogs.useSubscription( { databaseId: id, - databaseType, + databaseType: form.watch("databaseType"), databaseName: form.watch("databaseName"), backupFile: form.watch("backupFile"), destinationId: form.watch("destinationId"), + backupType: backupType, + metadata: metadata, }, { enabled: isDeploying, @@ -158,10 +231,32 @@ export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { }, ); - const onSubmit = async (_data: RestoreBackup) => { + const onSubmit = async (data: z.infer) => { + if (backupType === "compose" && !data.databaseType) { + toast.error("Please select a database type"); + return; + } + console.log({ data }); setIsDeploying(true); }; + const [cacheType, setCacheType] = useState<"fetch" | "cache">("cache"); + const { + data: services = [], + isLoading: isLoadingServices, + refetch: refetchServices, + } = api.compose.loadServices.useQuery( + { + composeId: id, + type: cacheType, + }, + { + retry: false, + refetchOnWindowFocus: false, + enabled: backupType === "compose", + }, + ); + return ( @@ -170,7 +265,7 @@ export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { Restore Backup - + @@ -373,25 +468,270 @@ export const RestoreBackup = ({ id, databaseType, serverId }: Props) => { control={form.control} name="databaseName" render={({ field }) => ( - + Database Name - + )} /> + + {backupType === "compose" && ( + <> + ( + + Database Type + + + + )} + /> + + ( + + Service Name +
+ + + + + + + +

+ Fetch: Will clone the repository and load the + services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this compose, + it will read the services from the last + deployment/fetch from the repository +

+
+
+
+
+ + +
+ )} + /> + + {selectedDatabaseType === "postgres" && ( + ( + + Database User + + + + + + )} + /> + )} + + {selectedDatabaseType === "mariadb" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {selectedDatabaseType === "mongo" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {selectedDatabaseType === "mysql" && ( + ( + + Root Password + + + + + + )} + /> + )} + + )} + diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index e219c3e3..062e7be7 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -110,6 +110,7 @@ export const ShowBackups = ({
diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index a57cf01d..8a12c535 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -192,7 +192,6 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { form.reset( { ...currentValues, - metadata: {}, }, { keepDefaultValues: true }, ); diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 50368d16..32d011d6 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -11,6 +11,7 @@ import { createBackup, findBackupById, findComposeByBackupId, + findComposeById, findMariadbByBackupId, findMariadbById, findMongoByBackupId, @@ -42,6 +43,7 @@ import { execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { + restoreComposeBackup, restoreMariadbBackup, restoreMongoBackup, restoreMySqlBackup, @@ -129,6 +131,7 @@ export const backupRouter = createTRPCRouter({ .input(apiUpdateBackup) .mutation(async ({ input }) => { try { + console.log(input); await updateBackupById(input.backupId, input); const backup = await findBackupById(input.backupId); @@ -374,78 +377,96 @@ export const backupRouter = createTRPCRouter({ "mongo", "web-server", ]), + backupType: z.enum(["database", "compose"]), databaseName: z.string().min(1), backupFile: z.string().min(1), destinationId: z.string().min(1), + metadata: z.any(), }), ) .subscription(async ({ input }) => { const destination = await findDestinationById(input.destinationId); - if (input.databaseType === "postgres") { - const postgres = await findPostgresById(input.databaseId); + if (input.backupType === "database") { + if (input.databaseType === "postgres") { + const postgres = await findPostgresById(input.databaseId); - return observable((emit) => { - restorePostgresBackup( - postgres, - destination, - input.databaseName, - input.backupFile, - (log) => { - emit.next(log); - }, - ); - }); - } - if (input.databaseType === "mysql") { - const mysql = await findMySqlById(input.databaseId); - return observable((emit) => { - restoreMySqlBackup( - mysql, - destination, - input.databaseName, - input.backupFile, - (log) => { - emit.next(log); - }, - ); - }); - } - if (input.databaseType === "mariadb") { - const mariadb = await findMariadbById(input.databaseId); - return observable((emit) => { - restoreMariadbBackup( - mariadb, - destination, - input.databaseName, - input.backupFile, - (log) => { - emit.next(log); - }, - ); - }); - } - if (input.databaseType === "mongo") { - const mongo = await findMongoById(input.databaseId); - return observable((emit) => { - restoreMongoBackup( - mongo, - destination, - input.databaseName, - input.backupFile, - (log) => { - emit.next(log); - }, - ); - }); - } - if (input.databaseType === "web-server") { - return observable((emit) => { - restoreWebServerBackup(destination, input.backupFile, (log) => { - emit.next(log); + return observable((emit) => { + restorePostgresBackup( + postgres, + destination, + input.databaseName, + input.backupFile, + (log) => { + emit.next(log); + }, + ); }); + } + if (input.databaseType === "mysql") { + const mysql = await findMySqlById(input.databaseId); + return observable((emit) => { + restoreMySqlBackup( + mysql, + destination, + input.databaseName, + input.backupFile, + (log) => { + emit.next(log); + }, + ); + }); + } + if (input.databaseType === "mariadb") { + const mariadb = await findMariadbById(input.databaseId); + return observable((emit) => { + restoreMariadbBackup( + mariadb, + destination, + input.databaseName, + input.backupFile, + (log) => { + emit.next(log); + }, + ); + }); + } + if (input.databaseType === "mongo") { + const mongo = await findMongoById(input.databaseId); + return observable((emit) => { + restoreMongoBackup( + mongo, + destination, + input.databaseName, + input.backupFile, + (log) => { + emit.next(log); + }, + ); + }); + } + if (input.databaseType === "web-server") { + return observable((emit) => { + restoreWebServerBackup(destination, input.backupFile, (log) => { + emit.next(log); + }); + }); + } + } + if (input.backupType === "compose") { + const compose = await findComposeById(input.databaseId); + return observable((emit) => { + restoreComposeBackup( + compose, + destination, + input.databaseName, + input.backupFile, + input.metadata, + (log) => { + emit.next(log); + }, + ); }); } - return true; }), }); diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index ad9f2086..c4f9b317 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -27,6 +27,7 @@ import { createMount, deleteMount, findComposeById, + findDomainsByComposeId, findProjectById, findServerById, findUserById, @@ -267,7 +268,8 @@ export const composeRouter = createTRPCRouter({ message: "You are not authorized to get this compose", }); } - const composeFile = await addDomainToCompose(compose); + const domains = await findDomainsByComposeId(input.composeId); + const composeFile = await addDomainToCompose(compose, domains); return dump(composeFile, { lineWidth: 1000, }); diff --git a/packages/server/src/db/schema/backups.ts b/packages/server/src/db/schema/backups.ts index 6dca5caa..67d8698c 100644 --- a/packages/server/src/db/schema/backups.ts +++ b/packages/server/src/db/schema/backups.ts @@ -137,7 +137,7 @@ const createSchema = createInsertSchema(backups, { mysqlId: z.string().optional(), mongoId: z.string().optional(), userId: z.string().optional(), - metadata: z.object({}).optional(), + metadata: z.any().optional(), }); export const apiCreateBackup = createSchema.pick({ diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 06d0465f..85b9669e 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -21,7 +21,7 @@ export const runComposeBackup = async ( const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - const command = `docker ps --filter "status=running" --filter "label=dokploy.backup.id=${backup.backupId}" --format "{{.ID}}" | head -n 1`; + const command = getFindContainerCommand(compose, backup.serviceName || ""); if (compose.serverId) { const { stdout } = await execAsyncRemote(compose.serverId, command); if (!stdout) { @@ -88,3 +88,26 @@ export const runComposeBackup = async ( throw error; } }; + +export const getFindContainerCommand = ( + compose: Compose, + serviceName: string, +) => { + const { appName, composeType } = compose; + const labels = + composeType === "stack" + ? { + namespace: `label=com.docker.stack.namespace=${appName}`, + service: `label=com.docker.swarm.service.name=${appName}_${serviceName}`, + } + : { + project: `label=com.docker.compose.project=${appName}`, + service: `label=com.docker.compose.service=${serviceName}`, + }; + + const command = `docker ps --filter "status=running" \ + --filter "${Object.values(labels).join('" --filter "')}" \ + --format "{{.ID}}" | head -n 1`; + + return command.trim(); +}; diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 47d4c71e..19e7d152 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -22,15 +22,15 @@ import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< "compose", - { project: true; mounts: true; domains: true; backups: true } + { project: true; mounts: true; domains: true } >; export const buildCompose = async (compose: ComposeNested, logPath: string) => { const writeStream = createWriteStream(logPath, { flags: "a" }); - const { sourceType, appName, mounts, composeType } = compose; + const { sourceType, appName, mounts, composeType, domains } = compose; try { const { COMPOSE_PATH } = paths(); const command = createCommand(compose); - await writeDomainsToCompose(compose); + await writeDomainsToCompose(compose, domains); createEnvFile(compose); if (compose.isolatedDeployment) { diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 8d314364..4f008397 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -38,8 +38,6 @@ import type { PropertiesNetworks, } from "./types"; import { encodeBase64 } from "./utils"; -import type { Backup } from "@dokploy/server/services/backup"; -import { createBackupLabels } from "./backup"; export const cloneCompose = async (compose: Compose) => { if (compose.sourceType === "github") { @@ -134,13 +132,13 @@ export const readComposeFile = async (compose: Compose) => { }; export const writeDomainsToCompose = async ( - compose: Compose & { domains: Domain[]; backups: Backup[] }, + compose: Compose, + domains: Domain[], ) => { - const { domains, backups } = compose; - if (!domains.length && !backups.length) { + if (!domains.length) { return; } - const composeConverted = await addDomainToCompose(compose); + const composeConverted = await addDomainToCompose(compose, domains); const path = getComposePath(compose); const composeString = dump(composeConverted, { lineWidth: 1000 }); @@ -152,7 +150,7 @@ export const writeDomainsToCompose = async ( }; export const writeDomainsToComposeRemote = async ( - compose: Compose & { domains: Domain[]; backups: Backup[] }, + compose: Compose, domains: Domain[], logPath: string, ) => { @@ -161,7 +159,7 @@ export const writeDomainsToComposeRemote = async ( } try { - const composeConverted = await addDomainToCompose(compose); + const composeConverted = await addDomainToCompose(compose, domains); const path = getComposePath(compose); if (!composeConverted) { @@ -182,20 +180,22 @@ exit 1; `; } }; +// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit export const addDomainToCompose = async ( - compose: Compose & { domains: Domain[]; backups: Backup[] }, + compose: Compose, + domains: Domain[], ) => { - const { appName, domains, backups } = compose; + const { appName } = compose; let result: ComposeSpecification | null; if (compose.serverId) { - result = await loadDockerComposeRemote(compose); + result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor } else { result = await loadDockerCompose(compose); } - if (!result || (domains.length === 0 && backups.length === 0)) { + if (!result || domains.length === 0) { return null; } @@ -210,7 +210,6 @@ export const addDomainToCompose = async ( result = randomized; } - // Add domains to the compose for (const domain of domains) { const { serviceName, https } = domain; if (!serviceName) { @@ -265,38 +264,6 @@ export const addDomainToCompose = async ( } } - // Add backups to the compose - for (const backup of backups) { - const { backupId, serviceName, enabled } = backup; - - if (!enabled) { - continue; - } - - if (!serviceName) { - throw new Error( - "Service name not found, please check the backups to use a valid service name", - ); - } - - if (!result?.services?.[serviceName]) { - throw new Error(`The service ${serviceName} not found in the compose`); - } - - const backupLabels = createBackupLabels(backupId); - - if (!result.services[serviceName].labels) { - result.services[serviceName].labels = []; - } - - result.services[serviceName].labels = [ - ...(Array.isArray(result.services[serviceName].labels) - ? result.services[serviceName].labels - : []), - ...backupLabels, - ]; - } - // Add dokploy-network to the root of the compose file if (!compose.isolatedDeployment) { result.networks = addDokployNetworkToRoot(result.networks); diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts new file mode 100644 index 00000000..f9deb52b --- /dev/null +++ b/packages/server/src/utils/restore/compose.ts @@ -0,0 +1,99 @@ +import type { Destination } from "@dokploy/server/services/destination"; +import type { Compose } from "@dokploy/server/services/compose"; +import { getS3Credentials } from "../backups/utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import type { Backup } from "@dokploy/server/services/backup"; +import { getFindContainerCommand } from "../backups/compose"; + +export const restoreComposeBackup = async ( + compose: Compose, + destination: Destination, + database: string, + backupFile: string, + metadata: Backup["metadata"] & { serviceName: string }, + emit: (log: string) => void, +) => { + try { + console.log({ metadata }); + const { serverId } = compose; + + const rcloneFlags = getS3Credentials(destination); + const bucketPath = `:s3:${destination.bucket}`; + const backupPath = `${bucketPath}/${backupFile}`; + + const command = getFindContainerCommand(compose, metadata.serviceName); + + console.log("command", command); + let containerId = ""; + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + emit(stdout); + emit(stderr); + containerId = stdout.trim(); + } else { + const { stdout, stderr } = await execAsync(command); + console.log("stdout", stdout); + console.log("stderr", stderr); + emit(stdout); + emit(stderr); + containerId = stdout.trim(); + } + let restoreCommand = ""; + + if (metadata.postgres) { + restoreCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} pg_restore -U ${metadata.postgres.databaseUser} -d ${database} --clean --if-exists`; + } else if (metadata.mariadb) { + restoreCommand = ` + rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} mariadb -u ${metadata.mariadb.databaseUser} -p${metadata.mariadb.databasePassword} ${database} + `; + } else if (metadata.mysql) { + restoreCommand = ` + rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} mysql -u root -p${metadata.mysql.databaseRootPassword} ${database} + `; + } else if (metadata.mongo) { + const tempDir = "/tmp/dokploy-restore"; + const fileName = backupFile.split("/").pop() || "backup.dump.gz"; + const decompressedName = fileName.replace(".gz", ""); + restoreCommand = `\ + rm -rf ${tempDir} && \ + mkdir -p ${tempDir} && \ + rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \ + cd ${tempDir} && \ + gunzip -f "${fileName}" && \ + docker exec -i ${containerId} mongorestore --username ${metadata.mongo.databaseUser} --password ${metadata.mongo.databasePassword} --authenticationDatabase admin --db ${database} --archive < "${decompressedName}" && \ + rm -rf ${tempDir}`; + } + + emit("Starting restore..."); + emit(`Backup path: ${backupPath}`); + + emit(`Executing command: ${restoreCommand}`); + + if (serverId) { + const { stdout, stderr } = await execAsyncRemote( + serverId, + restoreCommand, + ); + emit(stdout); + emit(stderr); + } else { + const { stdout, stderr } = await execAsync(restoreCommand); + console.log("stdout", stdout); + console.log("stderr", stderr); + emit(stdout); + emit(stderr); + } + + emit("Restore completed successfully!"); + } catch (error) { + console.error(error); + emit( + `Error: ${ + error instanceof Error ? error.message : "Error restoring mongo backup" + }`, + ); + throw new Error( + error instanceof Error ? error.message : "Error restoring mongo backup", + ); + } +}; diff --git a/packages/server/src/utils/restore/index.ts b/packages/server/src/utils/restore/index.ts index 972e1d1d..615e53d3 100644 --- a/packages/server/src/utils/restore/index.ts +++ b/packages/server/src/utils/restore/index.ts @@ -3,3 +3,4 @@ export { restoreMySqlBackup } from "./mysql"; export { restoreMariadbBackup } from "./mariadb"; export { restoreMongoBackup } from "./mongo"; export { restoreWebServerBackup } from "./web-server"; +export { restoreComposeBackup } from "./compose"; From 24f3be3c00d78c2413e0d86c6ee8a2cee668569b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:47:55 -0600 Subject: [PATCH 09/23] Add HandleBackup component to manage backup creation and updates, replacing the deprecated UpdateBackup component. Integrate dynamic form handling for various database types and metadata requirements. Update ShowBackups to utilize HandleBackup for both creating and updating backups, enhancing the user interface for better backup management. --- .../{add-backup.tsx => handle-backup.tsx} | 307 +++++--- .../database/backups/show-backups.tsx | 18 +- .../database/backups/update-backup.tsx | 681 ------------------ packages/server/src/utils/restore/compose.ts | 3 - packages/server/src/utils/restore/postgres.ts | 2 - 5 files changed, 210 insertions(+), 801 deletions(-) rename apps/dokploy/components/dashboard/database/backups/{add-backup.tsx => handle-backup.tsx} (71%) delete mode 100644 apps/dokploy/components/dashboard/database/backups/update-backup.tsx diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx similarity index 71% rename from apps/dokploy/components/dashboard/database/backups/add-backup.tsx rename to apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index 1b3828e6..2d8a6b6d 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -49,7 +49,7 @@ import { import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { DatabaseZap, PlusIcon, RefreshCw } from "lucide-react"; +import { DatabaseZap, PenBoxIcon, PlusIcon, RefreshCw } from "lucide-react"; import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -58,75 +58,140 @@ import { z } from "zod"; type CacheType = "cache" | "fetch"; -const getMetadataSchema = ( - backupType: "database" | "compose", - databaseType: DatabaseType, -) => { - if (backupType !== "compose") return z.object({}).optional(); +type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - const schemas = { - postgres: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - }), - mariadb: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mongo: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mysql: z.object({ - databaseRootPassword: z.string().min(1, "Root password is required"), - }), - "web-server": z.object({}), - }; - - return z.object({ - [databaseType]: schemas[databaseType], +const Schema = z + .object({ + destinationId: z.string().min(1, "Destination required"), + schedule: z.string().min(1, "Schedule (Cron) required"), + prefix: z.string().min(1, "Prefix required"), + enabled: z.boolean(), + database: z.string().min(1, "Database required"), + keepLatestCount: z.coerce.number().optional(), + serviceName: z.string().nullable(), + databaseType: z + .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]) + .optional(), + backupType: z.enum(["database", "compose"]), + metadata: z + .object({ + postgres: z + .object({ + databaseUser: z.string(), + }) + .optional(), + mariadb: z + .object({ + databaseUser: z.string(), + databasePassword: z.string(), + }) + .optional(), + mongo: z + .object({ + databaseUser: z.string(), + databasePassword: z.string(), + }) + .optional(), + mysql: z + .object({ + databaseRootPassword: z.string(), + }) + .optional(), + }) + .optional(), + }) + .superRefine((data, ctx) => { + if (data.backupType === "compose" && !data.databaseType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database type is required for compose backups", + path: ["databaseType"], + }); + } + if (data.backupType === "compose" && data.databaseType) { + if (data.databaseType === "postgres") { + if (!data.metadata?.postgres?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for PostgreSQL", + path: ["metadata", "postgres", "databaseUser"], + }); + } + } else if (data.databaseType === "mariadb") { + if (!data.metadata?.mariadb?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for MariaDB", + path: ["metadata", "mariadb", "databaseUser"], + }); + } + if (!data.metadata?.mariadb?.databasePassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database password is required for MariaDB", + path: ["metadata", "mariadb", "databasePassword"], + }); + } + } else if (data.databaseType === "mongo") { + if (!data.metadata?.mongo?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for MongoDB", + path: ["metadata", "mongo", "databaseUser"], + }); + } + if (!data.metadata?.mongo?.databasePassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database password is required for MongoDB", + path: ["metadata", "mongo", "databasePassword"], + }); + } + } else if (data.databaseType === "mysql") { + if (!data.metadata?.mysql?.databaseRootPassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Root password is required for MySQL", + path: ["metadata", "mysql", "databaseRootPassword"], + }); + } + } + } }); -}; - -const Schema = z.object({ - destinationId: z.string().min(1, "Destination required"), - schedule: z.string().min(1, "Schedule (Cron) required"), - prefix: z.string().min(1, "Prefix required"), - enabled: z.boolean(), - database: z.string().min(1, "Database required"), - keepLatestCount: z.coerce.number().optional(), - serviceName: z.string().nullable(), -}); - -type Schema = z.infer; interface Props { - id: string; + id?: string; + backupId?: string; databaseType?: DatabaseType; refetch: () => void; backupType: "database" | "compose"; } -type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - -export const AddBackup = ({ +export const HandleBackup = ({ id, + backupId, databaseType = "postgres", refetch, backupType = "database", }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const { data, isLoading } = api.destination.all.useQuery(); + const { data: backup } = api.backup.one.useQuery( + { + backupId: backupId ?? "", + }, + { + enabled: !!backupId, + }, + ); const [cacheType, setCacheType] = useState("cache"); - const [selectedDatabaseType, setSelectedDatabaseType] = - useState(databaseType as DatabaseType); - const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } = - api.backup.create.useMutation(); + backupId + ? api.backup.update.useMutation() + : api.backup.create.useMutation(); - const schema = Schema.extend({ - metadata: getMetadataSchema(backupType, selectedDatabaseType), - }); - - const form = useForm>({ + const form = useForm>({ defaultValues: { database: databaseType === "web-server" ? "dokploy" : "", destinationId: "", @@ -135,11 +200,15 @@ export const AddBackup = ({ schedule: "", keepLatestCount: undefined, serviceName: null, + databaseType: backupType === "compose" ? undefined : databaseType, + backupType: backupType, metadata: {}, }, - resolver: zodResolver(schema), + resolver: zodResolver(Schema), }); + console.log(backup); + const { data: services, isFetching: isLoadingServices, @@ -147,38 +216,33 @@ export const AddBackup = ({ refetch: refetchServices, } = api.compose.loadServices.useQuery( { - composeId: id, + composeId: backup?.composeId ?? id ?? "", type: cacheType, }, { retry: false, refetchOnWindowFocus: false, - enabled: backupType === "compose", + enabled: backupType === "compose" && !!backup?.composeId && !!id, }, ); useEffect(() => { form.reset({ - database: databaseType === "web-server" ? "dokploy" : "", - destinationId: "", - enabled: true, - prefix: "/", - schedule: "", - keepLatestCount: undefined, - serviceName: null, - metadata: {}, + database: + (backup?.database ?? databaseType === "web-server") ? "dokploy" : "", + destinationId: backup?.destinationId ?? "", + enabled: backup?.enabled ?? true, + prefix: backup?.prefix ?? "/", + schedule: backup?.schedule ?? "", + keepLatestCount: backup?.keepLatestCount ?? undefined, + serviceName: backup?.serviceName ?? null, + databaseType: backup?.databaseType ?? databaseType, + backupType: backup?.backupType ?? backupType, + metadata: backup?.metadata ?? {}, }); - }, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]); - - const onSubmit = async (data: Schema) => { - if (backupType === "compose" && !data.serviceName) { - form.setError("serviceName", { - type: "manual", - message: "Service name is required for compose backups", - }); - return; - } + }, [form, form.reset, backupId, backup]); + const onSubmit = async (data: z.infer) => { const getDatabaseId = backupType === "compose" ? { @@ -212,33 +276,50 @@ export const AddBackup = ({ schedule: data.schedule, enabled: data.enabled, database: data.database, - keepLatestCount: data.keepLatestCount, - databaseType: - backupType === "compose" ? selectedDatabaseType : databaseType, + keepLatestCount: data.keepLatestCount ?? null, + databaseType: data.databaseType || databaseType, serviceName: data.serviceName, ...getDatabaseId, + backupId: backupId ?? "", backupType, + metadata: data.metadata, }) .then(async () => { - toast.success("Backup Created"); + toast.success(`Backup ${backupId ? "Updated" : "Created"}`); refetch(); + setIsOpen(false); }) .catch(() => { - toast.error("Error creating a backup"); + toast.error(`Error ${backupId ? "updating" : "creating"} a backup`); }); }; + return ( - + - + {backupId ? ( + + ) : ( + + )} - Create a backup - Add a new backup + + {backupId ? "Update Backup" : "Create Backup"} + + + {backupId ? "Update a backup" : "Add a new backup"} +
@@ -254,25 +335,33 @@ export const AddBackup = ({ )} {backupType === "compose" && ( - - Database Type - - + ( + + Database Type + + + + )} + /> )} {backupType === "compose" && ( <> - {selectedDatabaseType === "postgres" && ( + {form.watch("databaseType") === "postgres" && ( )} - {selectedDatabaseType === "mariadb" && ( + {form.watch("databaseType") === "mariadb" && ( <> )} - {selectedDatabaseType === "mongo" && ( + {form.watch("databaseType") === "mongo" && ( <> )} - {selectedDatabaseType === "mysql" && ( + {form.watch("databaseType") === "mysql" && ( - Create + {backupId ? "Update" : "Create"} diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 062e7be7..0a6517f9 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -19,10 +19,10 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; -import { AddBackup } from "./add-backup"; import { RestoreBackup } from "./restore-backup"; -import { UpdateBackup } from "./update-backup"; import { AlertBlock } from "@/components/shared/alert-block"; +import { HandleBackup } from "./handle-backup"; +import { cn } from "@/lib/utils"; interface Props { id: string; @@ -100,7 +100,7 @@ export const ShowBackups = ({ {postgres && postgres?.backups?.length > 0 && (
{databaseType !== "web-server" && ( -
- (
-
+
{backup.backupType === "compose" && ( <>
@@ -265,7 +270,8 @@ export const ShowBackups = ({ - diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx deleted file mode 100644 index 8a12c535..00000000 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ /dev/null @@ -1,681 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - CheckIcon, - ChevronsUpDown, - DatabaseZap, - PenBoxIcon, - RefreshCw, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -type CacheType = "cache" | "fetch"; - -const getMetadataSchema = ( - backupType: "database" | "compose", - databaseType: DatabaseType, -) => { - if (backupType !== "compose") return z.object({}).optional(); - - const schemas = { - postgres: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - }), - mariadb: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mongo: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mysql: z.object({ - databaseRootPassword: z.string().min(1, "Root password is required"), - }), - "web-server": z.object({}), - }; - - return z.object({ - [databaseType]: schemas[databaseType as keyof typeof schemas], - }); -}; - -const Schema = z.object({ - destinationId: z.string().min(1, "Destination required"), - schedule: z.string().min(1, "Schedule (Cron) required"), - prefix: z.string().min(1, "Prefix required"), - enabled: z.boolean(), - database: z.string().min(1, "Database required"), - keepLatestCount: z.coerce.number().optional(), - serviceName: z.string().nullable(), - metadata: z.object({}).optional(), -}); - -interface Props { - backupId: string; - refetch: () => void; -} - -type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - -export const UpdateBackup = ({ backupId, refetch }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const [cacheType, setCacheType] = useState("cache"); - - const { data, isLoading } = api.destination.all.useQuery(); - const { data: backup } = api.backup.one.useQuery( - { - backupId, - }, - { - enabled: !!backupId, - }, - ); - const [selectedDatabaseType, setSelectedDatabaseType] = - useState( - (backup?.databaseType as DatabaseType) || "postgres", - ); - const { - data: services, - isFetching: isLoadingServices, - error: errorServices, - refetch: refetchServices, - } = api.compose.loadServices.useQuery( - { - composeId: backup?.composeId || "", - type: cacheType, - }, - { - retry: false, - refetchOnWindowFocus: false, - enabled: - isOpen && backup?.backupType === "compose" && !!backup?.composeId, - }, - ); - - const { mutateAsync, isLoading: isLoadingUpdate } = - api.backup.update.useMutation(); - - const schema = Schema.extend({ - metadata: getMetadataSchema( - backup?.backupType || "database", - selectedDatabaseType, - ), - }); - - const form = useForm>({ - defaultValues: { - database: "", - destinationId: "", - enabled: true, - prefix: "/", - schedule: "", - keepLatestCount: undefined, - serviceName: null, - metadata: {}, - }, - resolver: zodResolver(schema), - }); - - useEffect(() => { - if (backup) { - form.reset({ - database: backup.database, - destinationId: backup.destinationId, - enabled: backup.enabled || false, - prefix: backup.prefix, - schedule: backup.schedule, - serviceName: backup.serviceName || null, - keepLatestCount: backup.keepLatestCount - ? Number(backup.keepLatestCount) - : undefined, - metadata: backup.metadata || {}, - }); - } - }, [form, form.reset, backup]); - - useEffect(() => { - if (backup?.backupType === "compose") { - const currentValues = form.getValues(); - form.reset( - { - ...currentValues, - }, - { keepDefaultValues: true }, - ); - } - }, [selectedDatabaseType, backup?.backupType, form]); - - const onSubmit = async (data: z.infer) => { - if (backup?.backupType === "compose" && !data.serviceName) { - form.setError("serviceName", { - type: "manual", - message: "Service name is required for compose backups", - }); - return; - } - - await mutateAsync({ - backupId, - destinationId: data.destinationId, - prefix: data.prefix, - schedule: data.schedule, - enabled: data.enabled, - database: data.database, - serviceName: data.serviceName, - keepLatestCount: data.keepLatestCount as number | null, - metadata: data.metadata || {}, - databaseType: - backup?.backupType === "compose" - ? selectedDatabaseType - : (backup?.databaseType as DatabaseType), - }) - .then(async () => { - toast.success("Backup Updated"); - refetch(); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error updating the Backup"); - }); - }; - - return ( - - - - - - - Update Backup - Update the backup - - -
- -
- {errorServices && ( - - {errorServices?.message} - - )} - {backup?.backupType === "compose" && ( - - Database Type - - - )} - ( - - Destination - - - - - - - - - - {isLoading && ( - - Loading Destinations.... - - )} - No destinations found. - - - {data?.map((destination) => ( - { - form.setValue( - "destinationId", - destination.destinationId, - ); - }} - > - {destination.name} - - - ))} - - - - - - - - - )} - /> - {backup?.backupType === "compose" && ( -
- ( - - Service Name -
- - - - - - - -

- Fetch: Will clone the repository and load the - services -

-
-
-
- - - - - - -

- Cache: If you previously deployed this - compose, it will read the services from the - last deployment/fetch from the repository -

-
-
-
-
- - -
- )} - /> -
- )} - { - return ( - - Database - - - - - - ); - }} - /> - { - return ( - - Schedule (Cron) - - - - - - ); - }} - /> - { - return ( - - Prefix Destination - - - - - Use if you want to back up in a specific path of your - destination/bucket - - - - - ); - }} - /> - { - return ( - - Keep the latest - - - - - Optional. If provided, only keeps the latest N backups - in the cloud. - - - - ); - }} - /> - ( - -
- Enabled - - Enable or disable the backup - -
- - - -
- )} - /> - {backup?.backupType === "compose" && ( - <> - {selectedDatabaseType === "postgres" && ( - ( - - Database User - - - - - - )} - /> - )} - - {selectedDatabaseType === "mariadb" && ( - <> - ( - - Database User - - - - - - )} - /> - ( - - Database Password - - - - - - )} - /> - - )} - - {selectedDatabaseType === "mongo" && ( - <> - ( - - Database User - - - - - - )} - /> - ( - - Database Password - - - - - - )} - /> - - )} - - {selectedDatabaseType === "mysql" && ( - ( - - Root Password - - - - - - )} - /> - )} - - )} -
- - - -
- -
-
- ); -}; diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index f9deb52b..1b7f9fd9 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -14,7 +14,6 @@ export const restoreComposeBackup = async ( emit: (log: string) => void, ) => { try { - console.log({ metadata }); const { serverId } = compose; const rcloneFlags = getS3Credentials(destination); @@ -78,8 +77,6 @@ export const restoreComposeBackup = async ( emit(stderr); } else { const { stdout, stderr } = await execAsync(restoreCommand); - console.log("stdout", stdout); - console.log("stderr", stderr); emit(stdout); emit(stderr); } diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 5ab7c74e..d6eae283 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -40,8 +40,6 @@ rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${ emit(stderr); } else { const { stdout, stderr } = await execAsync(command); - console.log("stdout", stdout); - console.log("stderr", stderr); emit(stdout); emit(stderr); } From c4045795ee574a94a02467798eb913c5b9aee7f7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:01:30 -0600 Subject: [PATCH 10/23] Refactor ShowBackups component to improve UI and enhance backup information display. Introduce database type icons for better visual representation and reorganize backup details layout for clarity. Update styles for hover effects and button sizes to enhance user experience. --- .../database/backups/show-backups.tsx | 163 +++++++++++------- 1 file changed, 100 insertions(+), 63 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 0a6517f9..ff31da75 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -23,6 +23,12 @@ import { RestoreBackup } from "./restore-backup"; import { AlertBlock } from "@/components/shared/alert-block"; import { HandleBackup } from "./handle-backup"; import { cn } from "@/lib/utils"; +import { + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, +} from "@/components/icons/data-tools-icons"; interface Props { id: string; @@ -169,78 +175,109 @@ export const ShowBackups = ({
{postgres?.backups.map((backup) => (
-
-
- {backup.backupType === "compose" && ( - <> -
- - Service Name - - - {backup.serviceName} +
+
+
+ {backup.backupType === "compose" && ( +
+ {backup.databaseType === "postgres" && ( + + )} + {backup.databaseType === "mysql" && ( + + )} + {backup.databaseType === "mariadb" && ( + + )} + {backup.databaseType === "mongo" && ( + + )} +
+ )} +
+ {backup.backupType === "compose" && ( +
+

+ {backup.serviceName} +

+ + {backup.databaseType} + +
+ )} +
+
+ + {backup.enabled ? "Active" : "Inactive"}
+
+
-
- - Database Type - - - {backup.databaseType} - -
- - )} -
- Destination - - {backup.destination.name} - -
-
- Database - - {backup.database} - -
-
- Scheduled - - {backup.schedule} - -
-
- Prefix Storage - - {backup.prefix} - -
-
- Enabled - - {backup.enabled ? "Yes" : "No"} - -
-
- Keep Latest - - {backup.keepLatestCount || "All"} - +
+
+ + Destination + +

+ {backup.destination.name} +

+
+ +
+ + Database + +

+ {backup.database} +

+
+ +
+ + Schedule + +

+ {backup.schedule} +

+
+ +
+ + Prefix Storage + +

+ {backup.prefix} +

+
+ +
+ + Keep Latest + +

+ {backup.keepLatestCount || "All"} +

+
-
+ +
Run Manual Backup @@ -295,7 +332,7 @@ export const ShowBackups = ({ From 77b1ec473343bb4606577a631a2c1b9f766c70fa Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:08:08 -0600 Subject: [PATCH 13/23] Add DnsHelperModal component for DNS configuration guidance and integrate it into ShowDomainsCompose. Enhance domain validation functionality with server IP checks and improve UI with tooltips for better user experience. --- .../compose/domains/dns-helper-modal.tsx | 109 ++++++ .../compose/domains/show-domains.tsx | 358 ++++++++++++++---- apps/dokploy/server/api/routers/domain.ts | 12 + packages/server/src/services/domain.ts | 84 ++++ 4 files changed, 492 insertions(+), 71 deletions(-) create mode 100644 apps/dokploy/components/dashboard/compose/domains/dns-helper-modal.tsx diff --git a/apps/dokploy/components/dashboard/compose/domains/dns-helper-modal.tsx b/apps/dokploy/components/dashboard/compose/domains/dns-helper-modal.tsx new file mode 100644 index 00000000..82c25d0f --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/domains/dns-helper-modal.tsx @@ -0,0 +1,109 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Copy, HelpCircle, Server } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + domain: { + host: string; + https: boolean; + path?: string; + }; + serverIp?: string; +} + +export const DnsHelperModal = ({ domain, serverIp }: Props) => { + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success("Copied to clipboard!"); + }; + + return ( + + + + + + + + + DNS Configuration Guide + + + Follow these steps to configure your DNS records for {domain.host} + + + +
+ + To make your domain accessible, you need to configure your DNS + records with your domain provider (e.g., Cloudflare, GoDaddy, + NameCheap). + + +
+
+

1. Add A Record

+
+

+ Create an A record that points your domain to the server's IP + address: +

+
+
+
+

Type: A

+

+ Name: @ or {domain.host.split(".")[0]} +

+

+ Value: {serverIp || "Your server IP"} +

+
+ +
+
+
+
+ +
+

2. Verify Configuration

+
+

+ After configuring your DNS records: +

+
    +
  • Wait for DNS propagation (usually 15-30 minutes)
  • +
  • + Test your domain by visiting:{" "} + {domain.https ? "https://" : "http://"} + {domain.host} + {domain.path || "/"} +
  • +
  • Use a DNS lookup tool to verify your records
  • +
+
+
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx index e6468d6f..d6c0c332 100644 --- a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx @@ -7,17 +7,54 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react"; +import { + ExternalLink, + GlobeIcon, + PenBoxIcon, + Trash2, + InfoIcon, + Server, + CheckCircle2, + XCircle, + Loader2, + RefreshCw, +} from "lucide-react"; import Link from "next/link"; import { toast } from "sonner"; import { AddDomainCompose } from "./add-domain"; +import { Badge } from "@/components/ui/badge"; +import { DnsHelperModal } from "./dns-helper-modal"; +import { useState } from "react"; interface Props { composeId: string; } +type ValidationState = { + isLoading: boolean; + isValid?: boolean; + error?: string; + resolvedIp?: string; +}; + +type ValidationStates = { + [key: string]: ValidationState; +}; + export const ShowDomainsCompose = ({ composeId }: Props) => { + const [validationStates, setValidationStates] = useState( + {}, + ); + + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.domain.byComposeId.useQuery( { composeId, @@ -27,11 +64,57 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { }, ); + const { data: compose } = api.compose.one.useQuery( + { + composeId, + }, + { + enabled: !!composeId, + }, + ); + + const { mutateAsync: validateDomain } = + api.domain.validateDomain.useMutation(); const { mutateAsync: deleteDomain, isLoading: isRemoving } = api.domain.delete.useMutation(); + const handleValidateDomain = async (host: string) => { + setValidationStates((prev) => ({ + ...prev, + [host]: { isLoading: true }, + })); + + try { + const result = await validateDomain({ + domain: host, + serverIp: + compose?.server?.ipAddress?.toString() || ip?.toString() || "", + }); + + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: result.isValid, + error: result.error, + resolvedIp: result.resolvedIp, + }, + })); + } catch (err) { + const error = err as Error; + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: false, + error: error.message || "Failed to validate domain", + }, + })); + } + }; + return ( -
+
@@ -45,100 +128,233 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { {data && data?.length > 0 && ( )}
- + {data?.length === 0 ? ( -
+
- + To access to the application it is required to set at least 1 domain
) : ( -
+
{data?.map((item) => { + const validationState = validationStates[item.host]; return ( -
-
- - {item.serviceName} - + +
+ {/* Service & Domain Info */} +
+
+ + + {item.serviceName} + + + {item.host} + + +
+
+ {!item.host.includes("traefik.me") && ( + + )} + + + + { + await deleteDomain({ + domainId: item.domainId, + }) + .then((_data) => { + refetch(); + toast.success( + "Domain deleted successfully", + ); + }) + .catch(() => { + toast.error("Error deleting domain"); + }); + }} + > + + +
+
- - {item.host} - - -
+ {/* Domain Details */} +
+ + + + + + Path: {item.path || "/"} + + + +

URL path for this service

+
+
+
-
-
- {item.path} - {item.port} - {item.https ? "HTTPS" : "HTTP"} + + + + + + Port: {item.port} + + + +

Container port exposed

+
+
+
+ + + + + + {item.https ? "HTTPS" : "HTTP"} + + + +

+ {item.https + ? "Secure HTTPS connection" + : "Standard HTTP connection"} +

+
+
+
+ + {item.certificateType && ( + + + + + Cert: {item.certificateType} + + + +

SSL Certificate Provider

+
+
+
+ )} + + + + + + handleValidateDomain(item.host) + } + > + {validationState?.isLoading ? ( + <> + + Checking DNS... + + ) : validationState?.isValid ? ( + <> + + {"DNS Valid"} + + ) : validationState?.error ? ( + <> + + {validationState.error} + + ) : ( + <> + + Validate DNS + + )} + + + + {validationState?.error ? ( +
+

+ Error: +

+

{validationState.error}

+
+ ) : ( + "Click to validate DNS configuration" + )} +
+
+
+
- -
- - - - { - await deleteDomain({ - domainId: item.domainId, - }) - .then((_data) => { - refetch(); - toast.success("Domain deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting domain"); - }); - }} - > - - -
-
-
+ + ); })}
diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 9e81bee1..2ade32ae 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -21,6 +21,7 @@ import { removeDomain, removeDomainById, updateDomainById, + validateDomain, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -224,4 +225,15 @@ export const domainRouter = createTRPCRouter({ return result; }), + + validateDomain: protectedProcedure + .input( + z.object({ + domain: z.string(), + serverIp: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + return validateDomain(input.domain, input.serverIp); + }), }); diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index da2d6bc4..4035b567 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -7,6 +7,8 @@ import { type apiCreateDomain, domains } from "../db/schema"; import { findUserById } from "./admin"; import { findApplicationById } from "./application"; import { findServerById } from "./server"; +import dns from "node:dns"; +import { promisify } from "node:util"; export type Domain = typeof domains.$inferSelect; @@ -137,3 +139,85 @@ export const removeDomainById = async (domainId: string) => { export const getDomainHost = (domain: Domain) => { return `${domain.https ? "https" : "http"}://${domain.host}`; }; + +const resolveDns = promisify(dns.resolve4); + +// Cloudflare IP ranges (simplified - these are some common ones) +const CLOUDFLARE_IPS = [ + "172.67.", + "104.21.", + "104.16.", + "104.17.", + "104.18.", + "104.19.", + "104.20.", + "104.22.", + "104.23.", + "104.24.", + "104.25.", + "104.26.", + "104.27.", + "104.28.", +]; + +const isCloudflareIp = (ip: string) => { + return CLOUDFLARE_IPS.some((range) => ip.startsWith(range)); +}; + +export const validateDomain = async ( + domain: string, + expectedIp?: string, +): Promise<{ + isValid: boolean; + resolvedIp?: string; + error?: string; + isCloudflare?: boolean; +}> => { + try { + // Remove protocol and path if present + const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0]; + + // Resolve the domain to get its IP + const ips = await resolveDns(cleanDomain || ""); + + const resolvedIp = ips[0]; + + // Check if it's a Cloudflare IP + const behindCloudflare = ips.some((ip) => isCloudflareIp(ip)); + + // If behind Cloudflare, we consider it valid but inform the user + if (behindCloudflare) { + return { + isValid: true, + resolvedIp, + isCloudflare: true, + error: + "Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy", + }; + } + + // If we have an expected IP, validate against it + if (expectedIp) { + return { + isValid: resolvedIp === expectedIp, + resolvedIp, + error: + resolvedIp !== expectedIp + ? `Domain resolves to ${resolvedIp} but should point to ${expectedIp}` + : undefined, + }; + } + + // If no expected IP, just return the resolved IP + return { + isValid: true, + resolvedIp, + }; + } catch (error) { + return { + isValid: false, + error: + error instanceof Error ? error.message : "Failed to resolve domain", + }; + } +}; From bcebcfdfdf4c4ffc0699eee2b6aeb74a339b62af Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:13:30 -0600 Subject: [PATCH 14/23] Refactor ShowDomains component to enhance domain validation functionality with real-time feedback and improved UI. Integrate tooltips for domain details and validation status, and update API queries for better data handling. --- .../application/domains/show-domains.tsx | 326 ++++++++++++++---- .../compose/domains/show-domains.tsx | 4 +- 2 files changed, 266 insertions(+), 64 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 17dbc91f..c62f1c74 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -8,16 +8,49 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react"; +import { + CheckCircle2, + ExternalLink, + GlobeIcon, + InfoIcon, + Loader2, + PenBoxIcon, + RefreshCw, + Trash2, + XCircle, +} from "lucide-react"; import Link from "next/link"; import { toast } from "sonner"; import { AddDomain } from "./add-domain"; +import { useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { ValidationStates } from "../../compose/domains/show-domains"; +import { DnsHelperModal } from "../../compose/domains/dns-helper-modal"; +import { Badge } from "@/components/ui/badge"; interface Props { applicationId: string; } export const ShowDomains = ({ applicationId }: Props) => { + const { data: application } = api.application.one.useQuery( + { + applicationId, + }, + { + enabled: !!applicationId, + }, + ); + const [validationStates, setValidationStates] = useState( + {}, + ); + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.domain.byApplicationId.useQuery( { applicationId, @@ -26,10 +59,46 @@ export const ShowDomains = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); - + const { mutateAsync: validateDomain } = + api.domain.validateDomain.useMutation(); const { mutateAsync: deleteDomain, isLoading: isRemoving } = api.domain.delete.useMutation(); + const handleValidateDomain = async (host: string) => { + setValidationStates((prev) => ({ + ...prev, + [host]: { isLoading: true }, + })); + + try { + const result = await validateDomain({ + domain: host, + serverIp: + application?.server?.ipAddress?.toString() || ip?.toString() || "", + }); + + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: result.isValid, + error: result.error, + resolvedIp: result.resolvedIp, + }, + })); + } catch (err) { + const error = err as Error; + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: false, + error: error.message || "Failed to validate domain", + }, + })); + } + }; + return (
@@ -68,73 +137,206 @@ export const ShowDomains = ({ applicationId }: Props) => {
) : ( -
+
{data?.map((item) => { + const validationState = validationStates[item.host]; return ( -
- - - {item.host} - - - + +
+ {/* Service & Domain Info */} +
+
+ + {item.host} + + +
+
+ {!item.host.includes("traefik.me") && ( + + )} + + + + { + await deleteDomain({ + domainId: item.domainId, + }) + .then((_data) => { + refetch(); + toast.success( + "Domain deleted successfully", + ); + }) + .catch(() => { + toast.error("Error deleting domain"); + }); + }} + > + + +
+
-
-
- {item.path} - {item.port} - {item.https ? "HTTPS" : "HTTP"} -
+ {/* Domain Details */} +
+ + + + + + Path: {item.path || "/"} + + + +

URL path for this service

+
+
+
-
- - - - { - await deleteDomain({ - domainId: item.domainId, - }) - .then(() => { - refetch(); - toast.success("Domain deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting domain"); - }); - }} - > - - + + + + + + Port: {item.port} + + + +

Container port exposed

+
+
+
+ + + + + + {item.https ? "HTTPS" : "HTTP"} + + + +

+ {item.https + ? "Secure HTTPS connection" + : "Standard HTTP connection"} +

+
+
+
+ + {item.certificateType && ( + + + + + Cert: {item.certificateType} + + + +

SSL Certificate Provider

+
+
+
+ )} + + + + + + handleValidateDomain(item.host) + } + > + {validationState?.isLoading ? ( + <> + + Checking DNS... + + ) : validationState?.isValid ? ( + <> + + {"DNS Valid"} + + ) : validationState?.error ? ( + <> + + {validationState.error} + + ) : ( + <> + + Validate DNS + + )} + + + + {validationState?.error ? ( +
+

+ Error: +

+

{validationState.error}

+
+ ) : ( + "Click to validate DNS configuration" + )} +
+
+
+
-
-
+
+ ); })}
diff --git a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx index d6c0c332..85ae3d9a 100644 --- a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx @@ -37,14 +37,14 @@ interface Props { composeId: string; } -type ValidationState = { +export type ValidationState = { isLoading: boolean; isValid?: boolean; error?: string; resolvedIp?: string; }; -type ValidationStates = { +export type ValidationStates = { [key: string]: ValidationState; }; From 8ba4ac22cca7eec0a1ff2654a9d2714b0204b37f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:23:54 -0600 Subject: [PATCH 15/23] Enhance domain validation feedback in ShowDomains and AddDomain components. Add descriptive tooltips for container port input and improve validation state messaging to indicate Cloudflare status. Remove unnecessary console logs for cleaner code. --- .../application/domains/add-domain.tsx | 7 ++++-- .../application/domains/show-domains.tsx | 5 +++- .../dashboard/compose/domains/add-domain.tsx | 5 ++++ .../compose/domains/show-domains.tsx | 23 ++++++++++++++++--- packages/server/src/services/domain.ts | 17 +++++++------- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index 8da85a87..e5e4d799 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -89,8 +89,6 @@ export const AddDomain = ({ serverId: application?.serverId || "", }); - console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains); - const form = useForm({ resolver: zodResolver(domain), defaultValues: { @@ -276,6 +274,11 @@ export const AddDomain = ({ return ( Container Port + + The port where your application is running inside the + container (e.g., 3000 for Node.js, 80 for Nginx, 8080 + for Java) + diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index c62f1c74..5e29f145 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -84,6 +84,7 @@ export const ShowDomains = ({ applicationId }: Props) => { isValid: result.isValid, error: result.error, resolvedIp: result.resolvedIp, + message: result.error && result.isValid ? result.error : undefined, }, })); } catch (err) { @@ -304,7 +305,9 @@ export const ShowDomains = ({ applicationId }: Props) => { ) : validationState?.isValid ? ( <> - {"DNS Valid"} + {validationState.message + ? "Behind Cloudflare" + : "DNS Valid"} ) : validationState?.error ? ( <> diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 6089c99f..9f08296e 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -401,6 +401,11 @@ export const AddDomainCompose = ({ return ( Container Port + + The port where your application is running inside the + container (e.g., 3000 for Node.js, 80 for Nginx, 8080 + for Java) + diff --git a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx index 85ae3d9a..d5b876aa 100644 --- a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx @@ -42,6 +42,7 @@ export type ValidationState = { isValid?: boolean; error?: string; resolvedIp?: string; + message?: string; }; export type ValidationStates = { @@ -98,6 +99,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { isValid: result.isValid, error: result.error, resolvedIp: result.resolvedIp, + message: result.error && result.isValid ? result.error : undefined, }, })); } catch (err) { @@ -322,12 +324,14 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { ) : validationState?.isValid ? ( <> - {"DNS Valid"} + {validationState.message + ? "Behind Cloudflare" + : "DNS Valid"} ) : validationState?.error ? ( <> - {validationState.error} + DNS Invalid ) : ( <> @@ -338,13 +342,26 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { - {validationState?.error ? ( + {validationState?.error && + !validationState.isValid ? (

Error:

{validationState.error}

+ ) : validationState?.isValid ? ( +
+

+ {validationState.message + ? "Info:" + : "Valid Configuration:"} +

+

+ {validationState.message || + `Domain points to ${validationState.resolvedIp}`} +

+
) : ( "Click to validate DNS configuration" )} diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index 4035b567..1ce8f199 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -180,7 +180,7 @@ export const validateDomain = async ( // Resolve the domain to get its IP const ips = await resolveDns(cleanDomain || ""); - const resolvedIp = ips[0]; + const resolvedIps = ips.map((ip) => ip.toString()); // Check if it's a Cloudflare IP const behindCloudflare = ips.some((ip) => isCloudflareIp(ip)); @@ -189,7 +189,7 @@ export const validateDomain = async ( if (behindCloudflare) { return { isValid: true, - resolvedIp, + resolvedIp: resolvedIps.join(", "), isCloudflare: true, error: "Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy", @@ -199,19 +199,18 @@ export const validateDomain = async ( // If we have an expected IP, validate against it if (expectedIp) { return { - isValid: resolvedIp === expectedIp, - resolvedIp, - error: - resolvedIp !== expectedIp - ? `Domain resolves to ${resolvedIp} but should point to ${expectedIp}` - : undefined, + isValid: resolvedIps.includes(expectedIp), + resolvedIp: resolvedIps.join(", "), + error: !resolvedIps.includes(expectedIp) + ? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}` + : undefined, }; } // If no expected IP, just return the resolved IP return { isValid: true, - resolvedIp, + resolvedIp: resolvedIps.join(", "), }; } catch (error) { return { From 3ad5982f397ab5e4cd14a777fd921d8ca1c43d1d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:28:56 -0600 Subject: [PATCH 16/23] Add removeInvitation mutation and UI integration in ShowInvitations component --- .../settings/users/show-invitations.tsx | 19 +++++++++++++ .../server/api/routers/organization.ts | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx b/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx index 1bf7aa08..8c9813fc 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx @@ -36,6 +36,9 @@ export const ShowInvitations = () => { const { data, isLoading, refetch } = api.organization.allInvitations.useQuery(); + const { mutateAsync: removeInvitation } = + api.organization.removeInvitation.useMutation(); + return (
@@ -182,6 +185,22 @@ export const ShowInvitations = () => { Cancel Invitation )} + + { + await removeInvitation({ + invitationId: invitation.id, + }).then(() => { + refetch(); + toast.success( + "Invitation removed", + ); + }); + }} + > + Remove Invitation + )} diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 3d7753de..25498143 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -157,4 +157,31 @@ export const organizationRouter = createTRPCRouter({ orderBy: [desc(invitation.status), desc(invitation.expiresAt)], }); }), + removeInvitation: adminProcedure + .input(z.object({ invitationId: z.string() })) + .mutation(async ({ ctx, input }) => { + const invitationResult = await db.query.invitation.findFirst({ + where: eq(invitation.id, input.invitationId), + }); + + if (!invitationResult) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Invitation not found", + }); + } + + if ( + invitationResult?.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You are not allowed to remove this invitation", + }); + } + + return await db + .delete(invitation) + .where(eq(invitation.id, input.invitationId)); + }), }); From 52a660add37aedf8856955c606790572ed1ad401 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:32:38 -0600 Subject: [PATCH 17/23] Update validation error messaging in ShowDomainsCompose and refine button styling in ShowBackups for improved UI consistency. --- .../dashboard/compose/domains/show-domains.tsx | 2 +- .../dashboard/database/backups/show-backups.tsx | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx index d5b876aa..014f82a5 100644 --- a/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/show-domains.tsx @@ -331,7 +331,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => { ) : validationState?.error ? ( <> - DNS Invalid + {validationState.error} ) : ( <> diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index ff31da75..a50d2057 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -20,7 +20,6 @@ import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { RestoreBackup } from "./restore-backup"; -import { AlertBlock } from "@/components/shared/alert-block"; import { HandleBackup } from "./handle-backup"; import { cn } from "@/lib/utils"; import { @@ -164,14 +163,6 @@ export const ShowBackups = ({
) : (
-
- {backupType === "compose" && ( - - Deploy is required to apply changes after creating or - updating the service name in the backup. - - )} -
{postgres?.backups.map((backup) => (
@@ -300,7 +291,7 @@ export const ShowBackups = ({ setActiveManualBackup(undefined); }} > - + Run Manual Backup From 4afc6ac2501bf19af8333e5b82b53b04a4389fd3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:42:51 -0600 Subject: [PATCH 18/23] Update HandleBackup component to increase dialog content width for improved user experience during backup creation and updates. --- .../components/dashboard/database/backups/handle-backup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index 3c9da72e..032616b1 100644 --- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -313,7 +313,7 @@ export const HandleBackup = ({ )} - + {backupId ? "Update Backup" : "Create Backup"} From c8e2f4bfdcdfb1194fb8d2dbe53a44b5b8e445a2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 1 May 2025 19:48:47 -0600 Subject: [PATCH 19/23] Refactor RestoreBackup component to enhance validation schema and improve database type handling. Update metadata requirements for different database types and streamline form initialization. Add alert for compose backups in ShowBackups to inform users about running services. --- .../database/backups/restore-backup.tsx | 213 +++++++++++------- .../database/backups/show-backups.tsx | 6 + 2 files changed, 137 insertions(+), 82 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index d7323e5b..2d266ad2 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -65,74 +65,130 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; +type DatabaseType = + | Exclude + | "web-server"; + interface Props { id: string; - databaseType: Exclude | "web-server"; + databaseType: DatabaseType; serverId?: string | null; backupType?: "database" | "compose"; } -const getMetadataSchema = ( - backupType: "database" | "compose", - databaseType: string, -) => { - if (backupType !== "compose") return z.object({}).optional(); - - const schemas = { - postgres: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - }), - mariadb: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mongo: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mysql: z.object({ - databaseRootPassword: z.string().min(1, "Root password is required"), - }), - "web-server": z.object({}), - }; - - return z.object({ - [databaseType]: schemas[databaseType as keyof typeof schemas], - serviceName: z.string().min(1, "Service name is required"), +const RestoreBackupSchema = z + .object({ + destinationId: z + .string({ + required_error: "Please select a destination", + }) + .min(1, { + message: "Destination is required", + }), + backupFile: z + .string({ + required_error: "Please select a backup file", + }) + .min(1, { + message: "Backup file is required", + }), + databaseName: z + .string({ + required_error: "Please enter a database name", + }) + .min(1, { + message: "Database name is required", + }), + databaseType: z + .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]) + .optional(), + backupType: z.enum(["database", "compose"]).default("database"), + serviceName: z.string().nullable().optional(), + metadata: z + .object({ + postgres: z + .object({ + databaseUser: z.string(), + }) + .optional(), + mariadb: z + .object({ + databaseUser: z.string(), + databasePassword: z.string(), + }) + .optional(), + mongo: z + .object({ + databaseUser: z.string(), + databasePassword: z.string(), + }) + .optional(), + mysql: z + .object({ + databaseRootPassword: z.string(), + }) + .optional(), + }) + .optional(), + }) + .superRefine((data, ctx) => { + if (data.backupType === "compose" && !data.databaseType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database type is required for compose backups", + path: ["databaseType"], + }); + } + if (data.backupType === "compose" && data.databaseType) { + if (data.databaseType === "postgres") { + if (!data.metadata?.postgres?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for PostgreSQL", + path: ["metadata", "postgres", "databaseUser"], + }); + } + } else if (data.databaseType === "mariadb") { + if (!data.metadata?.mariadb?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for MariaDB", + path: ["metadata", "mariadb", "databaseUser"], + }); + } + if (!data.metadata?.mariadb?.databasePassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database password is required for MariaDB", + path: ["metadata", "mariadb", "databasePassword"], + }); + } + } else if (data.databaseType === "mongo") { + if (!data.metadata?.mongo?.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for MongoDB", + path: ["metadata", "mongo", "databaseUser"], + }); + } + if (!data.metadata?.mongo?.databasePassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database password is required for MongoDB", + path: ["metadata", "mongo", "databasePassword"], + }); + } + } else if (data.databaseType === "mysql") { + if (!data.metadata?.mysql?.databaseRootPassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Root password is required for MySQL", + path: ["metadata", "mysql", "databaseRootPassword"], + }); + } + } + } }); -}; - -const RestoreBackupSchema = z.object({ - destinationId: z - .string({ - required_error: "Please select a destination", - }) - .min(1, { - message: "Destination is required", - }), - backupFile: z - .string({ - required_error: "Please select a backup file", - }) - .min(1, { - message: "Backup file is required", - }), - databaseName: z - .string({ - required_error: "Please enter a database name", - }) - .min(1, { - message: "Database name is required", - }), - databaseType: z - .string({ - required_error: "Please select a database type", - }) - .min(1, { - message: "Database type is required", - }), - metadata: z.object({}).optional(), -}); const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; @@ -151,31 +207,24 @@ export const RestoreBackup = ({ const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); - const [selectedDatabaseType, setSelectedDatabaseType] = useState( - backupType === "compose" ? "" : databaseType, - ); const { data: destinations = [] } = api.destination.all.useQuery(); - const schema = RestoreBackupSchema.extend({ - metadata: getMetadataSchema(backupType, selectedDatabaseType), - }); - - const form = useForm>({ + const form = useForm>({ defaultValues: { destinationId: "", backupFile: "", databaseName: databaseType === "web-server" ? "dokploy" : "", - databaseType: backupType === "compose" ? "" : databaseType, + databaseType: + backupType === "compose" ? ("postgres" as DatabaseType) : databaseType, metadata: {}, }, - resolver: zodResolver(schema), + resolver: zodResolver(RestoreBackupSchema), }); const destionationId = form.watch("destinationId"); - + const currentDatabaseType = form.watch("databaseType"); const metadata = form.watch("metadata"); - // console.log({ metadata }); const debouncedSetSearch = debounce((value: string) => { setDebouncedSearchTerm(value); @@ -204,7 +253,7 @@ export const RestoreBackup = ({ api.backup.restoreBackupWithLogs.useSubscription( { databaseId: id, - databaseType: form.watch("databaseType"), + databaseType: currentDatabaseType as DatabaseType, databaseName: form.watch("databaseName"), backupFile: form.watch("backupFile"), destinationId: form.watch("destinationId"), @@ -231,7 +280,7 @@ export const RestoreBackup = ({ }, ); - const onSubmit = async (data: z.infer) => { + const onSubmit = async (data: z.infer) => { if (backupType === "compose" && !data.databaseType) { toast.error("Please select a database type"); return; @@ -488,9 +537,9 @@ export const RestoreBackup = ({ Database Type