From 0ecee7718091a18c9185d47f84cfb28543d3edcd Mon Sep 17 00:00:00 2001 From: Riad Date: Tue, 8 Sep 2026 17:18:20 -0400 Subject: [PATCH 1/4] feat(destinations): add Azure Blob Storage backup destination support - Support Azure Blob Storage (AZ_BS) backups for databases, compose stacks, web server settings, and volumes - Support Shared Key (Account Name + Key) and Shared Access Signature (SAS URL) - Add connection string auto-parser helper in destination modal - Redesign dashboard/settings/destinations UI to Backup Destinations with provider badges and in-row connection test - Add Rclone credential redaction for Azure keys and SAS tokens - Generate Drizzle migration 0196_windy_jack_power and update migration journal - Add 45 automated unit and integration tests --- .../backups/azure-connection-string.test.ts | 70 + .../db-backup-restore-injection.test.ts | 2 +- .../destination-rclone-builder.test.ts | 164 + .../destination-schema-validation.test.ts | 144 + .../backups/redact-credentials.test.ts | 17 + .../database/backups/show-backups.tsx | 2 +- .../destination/handle-destinations.tsx | 732 +- .../destination/show-destinations.tsx | 240 +- apps/dokploy/components/layouts/side.tsx | 2 +- .../dokploy/drizzle/0196_windy_jack_power.sql | 4 + apps/dokploy/drizzle/meta/0196_snapshot.json | 9170 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + .../pages/dashboard/settings/destinations.tsx | 4 +- apps/dokploy/server/api/routers/backup.ts | 6 +- .../dokploy/server/api/routers/destination.ts | 32 +- packages/server/src/db/schema/destination.ts | 116 +- .../server/src/db/validations/destination.ts | 76 + packages/server/src/index.ts | 1 + packages/server/src/utils/backups/azure.ts | 9 + packages/server/src/utils/backups/compose.ts | 6 +- packages/server/src/utils/backups/index.ts | 8 +- packages/server/src/utils/backups/libsql.ts | 6 +- packages/server/src/utils/backups/mariadb.ts | 6 +- packages/server/src/utils/backups/mongo.ts | 6 +- packages/server/src/utils/backups/mysql.ts | 6 +- packages/server/src/utils/backups/postgres.ts | 6 +- packages/server/src/utils/backups/redact.ts | 11 +- packages/server/src/utils/backups/utils.ts | 94 +- .../server/src/utils/backups/web-server.ts | 16 +- packages/server/src/utils/restore/compose.ts | 7 +- packages/server/src/utils/restore/libsql.ts | 11 +- packages/server/src/utils/restore/mariadb.ts | 7 +- packages/server/src/utils/restore/mongo.ts | 7 +- packages/server/src/utils/restore/mysql.ts | 7 +- packages/server/src/utils/restore/postgres.ts | 8 +- .../server/src/utils/restore/web-server.ts | 7 +- .../server/src/utils/volume-backups/backup.ts | 6 +- .../src/utils/volume-backups/restore.ts | 9 +- .../server/src/utils/volume-backups/utils.ts | 8 +- 39 files changed, 10615 insertions(+), 425 deletions(-) create mode 100644 apps/dokploy/__test__/backups/azure-connection-string.test.ts create mode 100644 apps/dokploy/__test__/backups/destination-rclone-builder.test.ts create mode 100644 apps/dokploy/__test__/backups/destination-schema-validation.test.ts create mode 100644 apps/dokploy/drizzle/0196_windy_jack_power.sql create mode 100644 apps/dokploy/drizzle/meta/0196_snapshot.json create mode 100644 packages/server/src/utils/backups/azure.ts diff --git a/apps/dokploy/__test__/backups/azure-connection-string.test.ts b/apps/dokploy/__test__/backups/azure-connection-string.test.ts new file mode 100644 index 0000000000..1d55229eaa --- /dev/null +++ b/apps/dokploy/__test__/backups/azure-connection-string.test.ts @@ -0,0 +1,70 @@ +import { + parseAzureConnectionString, + parseAzureSasUrl, +} from "@dokploy/server/utils/backups/azure"; +import { describe, expect, it } from "vitest"; + +describe("Azure Connection String & SAS URL Parser", () => { + it("parses standard Azure Portal connection string", () => { + const connStr = + "DefaultEndpointsProtocol=https;AccountName=dokploystorage;AccountKey=VGVzdEFjY291bnRLZXkxMjM0NTY3ODkwMTI=;EndpointSuffix=core.windows.net"; + + const parsed = parseAzureConnectionString(connStr); + expect(parsed.accountName).toBe("dokploystorage"); + expect(parsed.accountKey).toBe("VGVzdEFjY291bnRLZXkxMjM0NTY3ODkwMTI="); + expect(parsed.endpoint).toBeUndefined(); // Standard core.windows.net does not need custom endpoint + }); + + it("parses Azurite local emulator connection string with BlobEndpoint", () => { + const connStr = + "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; + + const parsed = parseAzureConnectionString(connStr); + expect(parsed.accountName).toBe("devstoreaccount1"); + expect(parsed.accountKey).toBe( + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", + ); + expect(parsed.endpoint).toBe("http://127.0.0.1:10000/devstoreaccount1"); + }); + + it("parses sovereign / custom cloud endpoint suffix", () => { + const connStr = + "DefaultEndpointsProtocol=https;AccountName=govstorage;AccountKey=MyGovKey==;EndpointSuffix=core.usgovcloudapi.net"; + + const parsed = parseAzureConnectionString(connStr); + expect(parsed.accountName).toBe("govstorage"); + expect(parsed.accountKey).toBe("MyGovKey=="); + expect(parsed.endpoint).toBe( + "https://govstorage.blob.core.usgovcloudapi.net", + ); + }); + + it("handles empty or whitespace connection strings gracefully", () => { + expect(parseAzureConnectionString("")).toEqual({ + accountName: "", + accountKey: "", + }); + expect(parseAzureConnectionString(" ")).toEqual({ + accountName: "", + accountKey: "", + }); + }); + + it("extracts account name and container from SAS URL", () => { + const sasUrl = + "https://mystorageaccount.blob.core.windows.net/mybackups?sp=r&st=2026-09-08T00:00:00Z&se=2026-09-09T00:00:00Z&spr=https&sv=2022-11-02&sr=c&sig=sig123"; + + const { accountName, containerName } = parseAzureSasUrl(sasUrl); + expect(accountName).toBe("mystorageaccount"); + expect(containerName).toBe("mybackups"); + }); + + it("handles root SAS URL without container", () => { + const sasUrl = + "https://mystorageaccount.blob.core.windows.net/?sv=2022-11-02&ss=b&srt=sco&sp=rwlacup"; + + const { accountName, containerName } = parseAzureSasUrl(sasUrl); + expect(accountName).toBe("mystorageaccount"); + expect(containerName).toBeUndefined(); + }); +}); diff --git a/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts index d48644c95c..e0e7699a40 100644 --- a/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts +++ b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts @@ -63,7 +63,7 @@ const runsSafely = (command: string) => { // Payloads that try to break out of every quoting style used in the builders. const p = (mark: string) => [ `$(touch ${mark})`, - "`touch " + mark + "`", + `\`touch ${mark}\``, `x'; touch ${mark}; '`, `x"; touch ${mark}; echo "`, `x; touch ${mark}`, diff --git a/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts b/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts new file mode 100644 index 0000000000..5354674f4f --- /dev/null +++ b/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts @@ -0,0 +1,164 @@ +import { getDestinationRemote } from "@dokploy/server/utils/backups/utils"; +import { describe, expect, it } from "vitest"; + +describe("getDestinationRemote", () => { + describe("S3 destinations", () => { + it("generates correct rclone flags and remote paths for AWS S3", () => { + const destination = { + destinationType: "s3" as const, + name: "AWS S3 Production", + provider: "AWS", + accessKey: "AKIAIOSFODNN7EXAMPLE", + secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + bucket: "my-production-backups", + region: "us-east-1", + endpoint: "https://s3.amazonaws.com", + additionalFlags: ["--s3-upload-concurrency=4"], + }; + + const { rcloneFlags, remoteBase, getRemotePath } = + getDestinationRemote(destination); + + expect(remoteBase).toBe(":s3:my-production-backups"); + expect(getRemotePath("mydb/dump.sql.gz")).toBe( + ":s3:my-production-backups/mydb/dump.sql.gz", + ); + expect(getRemotePath()).toBe(":s3:my-production-backups"); + + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--s3-access-key-id=AKIAIOSFODNN7EXAMPLE"); + expect(flagsStr).toContain( + "--s3-secret-access-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + ); + expect(flagsStr).toContain("--s3-region=us-east-1"); + expect(flagsStr).toContain("--s3-endpoint="); + expect(flagsStr).toContain("--s3-provider=AWS"); + expect(flagsStr).toContain("--s3-no-check-bucket"); + expect(flagsStr).toContain("--s3-force-path-style"); + expect(flagsStr).toContain("--s3-upload-concurrency=4"); + }); + + it("defaults to s3 when destinationType is omitted (backward compatibility)", () => { + const destination = { + name: "Legacy Destination", + provider: "Cloudflare", + accessKey: "cf_key_123", + secretAccessKey: "cf_secret_456", + bucket: "cf-r2-backups", + region: "auto", + endpoint: "https://myaccount.r2.cloudflarestorage.com", + additionalFlags: [], + }; + + const { rcloneFlags, remoteBase, getRemotePath } = + getDestinationRemote(destination); + + expect(remoteBase).toBe(":s3:cf-r2-backups"); + expect(getRemotePath("path/to/file")).toBe( + ":s3:cf-r2-backups/path/to/file", + ); + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--s3-access-key-id=cf_key_123"); + expect(flagsStr).toContain("--s3-provider=Cloudflare"); + }); + }); + + describe("Azure Blob Storage destinations", () => { + it("generates correct rclone flags for Account Name and Account Key authentication", () => { + const destination = { + destinationType: "azure_blob" as const, + name: "Azure Production Blob", + provider: "account_key", + accessKey: "myazureaccount", + secretAccessKey: "dGhpcyBpcyBhIHZhbGlkIGJhc2U2NCBrZXk=", + bucket: "dokploy-backup-container", + region: "", + endpoint: "", + additionalFlags: ["--azureblob-access-tier=cool"], + }; + + const { rcloneFlags, remoteBase, getRemotePath } = + getDestinationRemote(destination); + + expect(remoteBase).toBe(":azureblob:dokploy-backup-container"); + expect(getRemotePath("postgres/dump.sql.gz")).toBe( + ":azureblob:dokploy-backup-container/postgres/dump.sql.gz", + ); + + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--azureblob-account=myazureaccount"); + expect(flagsStr).toContain("--azureblob-key="); + expect(flagsStr).toContain("--azureblob-access-tier=cool"); + expect(flagsStr).not.toContain("--azureblob-endpoint"); + expect(flagsStr).not.toContain("--s3-access-key-id"); + }); + + it("generates correct rclone flags for SAS URL authentication", () => { + const sasUrl = + "https://myaccount.blob.core.windows.net/mycontainer?sp=racwdl&st=2026-09-08T00:00:00Z&se=2026-09-09T00:00:00Z&spr=https&sv=2022-11-02&sr=c&sig=testsignature"; + const destination = { + destinationType: "azure_blob" as const, + name: "Azure SAS Destination", + provider: "sas_url", + accessKey: "", + secretAccessKey: sasUrl, + bucket: "mycontainer", + region: "", + endpoint: "", + additionalFlags: [], + }; + + const { rcloneFlags, remoteBase, getRemotePath } = + getDestinationRemote(destination); + + expect(remoteBase).toBe(":azureblob:mycontainer"); + expect(getRemotePath("volumes/app-data.tar")).toBe( + ":azureblob:mycontainer/volumes/app-data.tar", + ); + + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--azureblob-sas-url="); + expect(flagsStr).not.toContain("--azureblob-account"); + expect(flagsStr).not.toContain("--azureblob-key"); + }); + + it("handles custom endpoint for Azurite local emulator or sovereign clouds", () => { + const destination = { + destinationType: "azure_blob" as const, + name: "Azurite Local Emulator", + provider: "account_key", + accessKey: "devstoreaccount1", + secretAccessKey: + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", + bucket: "local-container", + region: "", + endpoint: "http://127.0.0.1:10000/devstoreaccount1", + additionalFlags: [], + }; + + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + + expect(remoteBase).toBe(":azureblob:local-container"); + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--azureblob-account=devstoreaccount1"); + expect(flagsStr).toContain("--azureblob-endpoint="); + }); + + it("accepts az_bs alias for destinationType", () => { + const destination = { + destinationType: "az_bs" as const, + name: "AZ_BS Destination", + provider: "account_key", + accessKey: "myaccount", + secretAccessKey: "secretkey123", + bucket: "test-container", + region: "", + endpoint: "", + additionalFlags: [], + }; + + const { remoteBase } = getDestinationRemote(destination); + expect(remoteBase).toBe(":azureblob:test-container"); + }); + }); +}); diff --git a/apps/dokploy/__test__/backups/destination-schema-validation.test.ts b/apps/dokploy/__test__/backups/destination-schema-validation.test.ts new file mode 100644 index 0000000000..46974f1d85 --- /dev/null +++ b/apps/dokploy/__test__/backups/destination-schema-validation.test.ts @@ -0,0 +1,144 @@ +import { + apiCreateDestination, + apiUpdateDestination, + azureBlobDestinationSchema, + s3DestinationSchema, +} from "@dokploy/server/db/schema/destination"; +import { describe, expect, it } from "vitest"; + +describe("Destination Zod Schema Validation", () => { + describe("S3 Destination Validation", () => { + it("validates valid S3 destination input", () => { + const input = { + destinationType: "s3" as const, + name: "AWS S3 Prod", + provider: "AWS", + accessKey: "AKIA1234567890", + secretAccessKey: "secret1234567890", + bucket: "my-bucket", + region: "us-east-1", + endpoint: "https://s3.amazonaws.com", + }; + + const result = s3DestinationSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("rejects S3 destination missing required fields", () => { + const input = { + destinationType: "s3" as const, + name: "Incomplete S3", + provider: "AWS", + // missing accessKey, secretAccessKey, bucket, endpoint + }; + + const result = s3DestinationSchema.safeParse(input); + expect(result.success).toBe(false); + }); + }); + + describe("Azure Blob Storage Validation", () => { + it("validates Azure Blob with Account Key", () => { + const input = { + destinationType: "azure_blob" as const, + name: "Azure Backup", + provider: "account_key" as const, + accessKey: "myazurestorageacc", + secretAccessKey: "dGVzdGtleQ==", + bucket: "mycontainer", + }; + + const result = azureBlobDestinationSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("rejects Azure Blob with Account Key if storage account name is missing", () => { + const input = { + destinationType: "azure_blob" as const, + name: "Azure Backup", + provider: "account_key" as const, + accessKey: "", // missing storage account + secretAccessKey: "dGVzdGtleQ==", + bucket: "mycontainer", + }; + + const result = azureBlobDestinationSchema.safeParse(input); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toContain( + "Storage Account Name is required", + ); + } + }); + + it("validates Azure Blob with SAS URL", () => { + const input = { + destinationType: "azure_blob" as const, + name: "Azure Backup via SAS", + provider: "sas_url" as const, + secretAccessKey: + "https://mystorage.blob.core.windows.net/mycontainer?sv=2022-11-02&sig=test", + bucket: "mycontainer", + }; + + const result = azureBlobDestinationSchema.safeParse(input); + expect(result.success).toBe(true); + }); + }); + + describe("apiCreateDestination & apiUpdateDestination Discriminated Union", () => { + it("routes payload without destinationType to S3 (backward compatibility)", () => { + const legacyInput = { + name: "Legacy Destination", + provider: "Minio", + accessKey: "minioadmin", + secretAccessKey: "miniopassword", + bucket: "dokploy", + region: "us-east-1", + endpoint: "http://minio:9000", + }; + + const result = apiCreateDestination.safeParse(legacyInput); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.destinationType).toBe("s3"); + } + }); + + it("normalizes az_bs destinationType to azure_blob", () => { + const azBsInput = { + destinationType: "az_bs", + name: "Azure Prompt Notation", + provider: "account_key", + accessKey: "myacc", + secretAccessKey: "mysecretkey", + bucket: "mycontainer", + }; + + const result = apiCreateDestination.safeParse(azBsInput); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.destinationType).toBe("azure_blob"); + } + }); + + it("validates apiUpdateDestination with destinationId", () => { + const updateInput = { + destinationId: "dest-12345", + destinationType: "azure_blob", + name: "Updated Azure Backup", + provider: "account_key", + accessKey: "myacc", + secretAccessKey: "newsecretkey", + bucket: "mycontainer", + }; + + const result = apiUpdateDestination.safeParse(updateInput); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.destinationId).toBe("dest-12345"); + expect(result.data.name).toBe("Updated Azure Backup"); + } + }); + }); +}); diff --git a/apps/dokploy/__test__/backups/redact-credentials.test.ts b/apps/dokploy/__test__/backups/redact-credentials.test.ts index 5fff508cc8..939afe0389 100644 --- a/apps/dokploy/__test__/backups/redact-credentials.test.ts +++ b/apps/dokploy/__test__/backups/redact-credentials.test.ts @@ -47,4 +47,21 @@ describe("redactRcloneCredentials (#4621)", () => { expect(redacted).not.toContain("MYSECRET"); expect(redacted).toContain("[REDACTED]"); }); + + it("should redact Azure Blob Storage account key in rclone command", () => { + const cmd = + 'rclone rcat --azureblob-account="myazurestorage" --azureblob-key="dGhpcy1pcy1hbi1henVyZS1rZXk=" :azureblob:container/file.sql.gz'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("dGhpcy1pcy1hbi1henVyZS1rZXk="); + expect(redacted).toContain('--azureblob-key="[REDACTED]"'); + expect(redacted).toContain('--azureblob-account="myazurestorage"'); + }); + + it("should redact Azure Blob Storage SAS URL in rclone command", () => { + const cmd = + 'rclone lsf --azureblob-sas-url="https://myaccount.blob.core.windows.net/?sv=2022-11-02&sig=supersensitive" :azureblob:container/'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("supersensitive"); + expect(redacted).toContain('--azureblob-sas-url="[REDACTED]"'); + }); }); diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index ebffaccb37..a8848a6e4c 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -143,7 +143,7 @@ export const ShowBackups = ({ href="/dashboard/settings/destinations" className="text-foreground" > - S3 Destinations + Backup Destinations {" "} to do so. diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index d1542dc803..9c4365907b 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -1,9 +1,18 @@ import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, + parseAzureConnectionString, } from "@dokploy/server/db/validations/destination"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react"; +import { + Cloud, + Database, + Eye, + EyeOff, + PenBoxIcon, + PlusIcon, + Trash2, +} from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -22,6 +31,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -41,28 +51,65 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { S3_PROVIDERS } from "./constants"; -const addDestination = z.object({ - name: z.string().min(1, "Name is required"), - provider: z.string().min(1, "Provider is required"), - accessKeyId: z.string().min(1, "Access Key Id is required"), - secretAccessKey: z.string().min(1, "Secret Access Key is required"), - bucket: z.string().min(1, "Bucket is required"), - region: z.string(), - endpoint: z.string().min(1, "Endpoint is required"), - serverId: z.string().optional(), - additionalFlags: z - .array( - z.object({ - value: z - .string() - .min(1, "Flag cannot be empty") - .regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR), - }), - ) - .optional(), -}); - -type AddDestination = z.infer; +const destinationFormSchema = z + .object({ + destinationType: z.enum(["s3", "azure_blob"]), + name: z.string().min(1, "Name is required"), + provider: z.string(), + accessKeyId: z.string(), + secretAccessKey: z.string().min(1, "Secret key / credential is required"), + bucket: z.string().min(1, "Bucket or Container name is required"), + region: z.string().optional(), + endpoint: z.string().optional(), + serverId: z.string().optional(), + additionalFlags: z + .array( + z.object({ + value: z + .string() + .min(1, "Flag cannot be empty") + .regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR), + }), + ) + .optional(), + }) + .superRefine((data, ctx) => { + if (data.destinationType === "s3") { + if (!data.provider || data.provider.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provider is required for S3", + path: ["provider"], + }); + } + if (!data.accessKeyId || data.accessKeyId.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Access Key ID is required for S3", + path: ["accessKeyId"], + }); + } + if (!data.endpoint || data.endpoint.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Endpoint is required for S3", + path: ["endpoint"], + }); + } + } else if (data.destinationType === "azure_blob") { + if (data.provider === "account_key") { + if (!data.accessKeyId || data.accessKeyId.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Storage Account Name is required when using Account Key", + path: ["accessKeyId"], + }); + } + } + } + }); + +type DestinationFormData = z.infer; interface Props { destinationId?: string; @@ -70,6 +117,8 @@ interface Props { export const HandleDestinations = ({ destinationId }: Props) => { const [open, setOpen] = useState(false); + const [showSecret, setShowSecret] = useState(false); + const [rawConnectionString, setRawConnectionString] = useState(""); const utils = api.useUtils(); const { data: servers } = api.server.withSSHKey.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -87,6 +136,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { refetchOnWindowFocus: false, }, ); + const { mutateAsync: testConnection, isPending: isPendingConnection, @@ -94,20 +144,24 @@ export const HandleDestinations = ({ destinationId }: Props) => { isError: isErrorConnection, } = api.destination.testConnection.useMutation(); - const form = useForm({ + const form = useForm({ defaultValues: { - provider: "", - accessKeyId: "", - bucket: "", + destinationType: "s3", name: "", - region: "", + provider: "AWS", + accessKeyId: "", secretAccessKey: "", + bucket: "", + region: "us-east-1", endpoint: "", additionalFlags: [], }, - resolver: zodResolver(addDestination), + resolver: zodResolver(destinationFormSchema), }); + const destinationType = form.watch("destinationType"); + const isAzure = destinationType === "azure_blob"; + const { fields, append, remove } = useFieldArray({ control: form.control, name: "additionalFlags", @@ -115,31 +169,72 @@ export const HandleDestinations = ({ destinationId }: Props) => { useEffect(() => { if (destination) { + const isDestAzure = + destination.destinationType === "azure_blob" || + destination.destinationType === "az_bs"; + form.reset({ + destinationType: isDestAzure ? "azure_blob" : "s3", name: destination.name, - provider: destination.provider || "", - accessKeyId: destination.accessKey, + provider: destination.provider || (isDestAzure ? "account_key" : "AWS"), + accessKeyId: destination.accessKey || "", secretAccessKey: destination.secretAccessKey, bucket: destination.bucket, - region: destination.region, - endpoint: destination.endpoint, + region: destination.region || "", + endpoint: destination.endpoint || "", additionalFlags: destination.additionalFlags?.map((f) => ({ value: f })) ?? [], }); } else { - form.reset(); + form.reset({ + destinationType: "s3", + name: "", + provider: "AWS", + accessKeyId: "", + secretAccessKey: "", + bucket: "", + region: "us-east-1", + endpoint: "", + additionalFlags: [], + }); + } + setRawConnectionString(""); + setShowSecret(false); + }, [form, destination, open]); + + const handlePasteConnectionString = (raw: string) => { + setRawConnectionString(raw); + const parsed = parseAzureConnectionString(raw); + if (parsed.accountName) { + form.setValue("accessKeyId", parsed.accountName, { + shouldValidate: true, + }); + } + if (parsed.accountKey) { + form.setValue("secretAccessKey", parsed.accountKey, { + shouldValidate: true, + }); } - }, [form, form.reset, form.formState.isSubmitSuccessful, destination]); + if (parsed.endpoint) { + form.setValue("endpoint", parsed.endpoint, { shouldValidate: true }); + } + if (parsed.accountName || parsed.accountKey) { + toast.success("Azure Connection String parsed successfully!"); + } + }; - const onSubmit = async (data: AddDestination) => { + const onSubmit = async (data: DestinationFormData) => { await mutateAsync({ - provider: data.provider || "", - accessKey: data.accessKeyId, - bucket: data.bucket, - endpoint: data.endpoint, + destinationType: data.destinationType, name: data.name, - region: data.region, + provider: + data.provider || + (data.destinationType === "azure_blob" ? "account_key" : "AWS"), + accessKey: data.accessKeyId || "", secretAccessKey: data.secretAccessKey, + bucket: data.bucket, + region: data.region || "", + endpoint: data.endpoint || "", destinationId: destinationId || "", additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [], }) @@ -162,14 +257,15 @@ export const HandleDestinations = ({ destinationId }: Props) => { }; const handleTestConnection = async (serverId?: string) => { - const result = await form.trigger([ - "provider", - "accessKeyId", - "secretAccessKey", - "bucket", - "endpoint", - "additionalFlags", - ]); + const triggerFields: Array = isAzure + ? ["name", "bucket", "secretAccessKey"] + : ["provider", "accessKeyId", "secretAccessKey", "bucket", "endpoint"]; + + if (isAzure && form.getValues("provider") === "account_key") { + triggerFields.push("accessKeyId"); + } + + const result = await form.trigger(triggerFields); if (!result) { const errors = form.formState.errors; @@ -189,66 +285,59 @@ export const HandleDestinations = ({ destinationId }: Props) => { return; } - const provider = form.getValues("provider"); - const accessKey = form.getValues("accessKeyId"); - const secretKey = form.getValues("secretAccessKey"); - const bucket = form.getValues("bucket"); - const endpoint = form.getValues("endpoint"); - const region = form.getValues("region"); - - const connectionString = `:s3,provider=${provider},access_key_id=${accessKey},secret_access_key=${secretKey},endpoint=${endpoint}${region ? `,region=${region}` : ""}:${bucket}`; + const currentVals = form.getValues(); await testConnection({ - provider, - accessKey, - bucket, - endpoint, - name: "Test", - region, - secretAccessKey: secretKey, + destinationType: currentVals.destinationType, + provider: currentVals.provider || (isAzure ? "account_key" : "AWS"), + accessKey: currentVals.accessKeyId || "", + secretAccessKey: currentVals.secretAccessKey, + bucket: currentVals.bucket, + endpoint: currentVals.endpoint || "", + name: currentVals.name || "Test", + region: currentVals.region || "", serverId, - additionalFlags: - form.getValues("additionalFlags")?.map((f) => f.value) ?? [], + additionalFlags: currentVals.additionalFlags?.map((f) => f.value) ?? [], }) .then(() => { - toast.success("Connection Success"); + toast.success("Connection test successful! Target storage verified."); }) .catch((e) => { - toast.error("Error connecting to provider", { - description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`, + toast.error("Error connecting to storage destination", { + description: e.message, }); }); }; return ( - + {destinationId ? ( ) : ( - )} - + - {destinationId ? "Update" : "Add"} Destination + {destinationId ? "Update" : "Add"} Backup Destination - In this section, you can configure and add new destinations for your - backups. Please ensure that you provide the correct information to - guarantee secure and efficient storage. + Configure cloud storage for your backups. Supports Amazon S3 (and + S3-compatible providers) and Microsoft Azure Blob Storage. + {(isError || isErrorConnection) && ( {connectionError?.message || error?.message} @@ -259,136 +348,382 @@ export const HandleDestinations = ({ destinationId }: Props) => {
+ {/* Provider Type Segmented Selector */} +
+ Storage Type +
+ + +
+
+ + {/* Common: Destination Name */} { - return ( - - Name - - - - - - ); - }} + render={({ field }) => ( + + Destination Name + + + + + + )} /> - { - return ( - - Provider - + + {/* AZURE BLOB STORAGE FIELDS */} + {isAzure ? ( + <> + ( + + Authentication Method - - - - ); - }} - /> + + + )} + /> - { - return ( - - Access Key Id - - - - - - ); - }} - /> - ( - -
- Secret Access Key -
- - - - -
- )} - /> - ( - -
- Bucket -
- - - - -
- )} - /> - ( - -
- Region -
- - - - -
- )} - /> - ( - - Endpoint - + {form.watch("provider") === "account_key" && ( +
+
+ + Quick Paste: Azure Connection String + + + Auto-populates Account & Key + +
+ handlePasteConnectionString(e.target.value) + } + className="text-xs h-8 font-mono" /> - - - - )} - /> -
+
+ )} + + {form.watch("provider") === "account_key" ? ( + <> + ( + + Storage Account Name + + + + + + )} + /> + + ( + + Storage Account Key +
+ + + + +
+ +
+ )} + /> + + ) : ( + ( + + SAS URL + + + + + A Shared Access Signature URL with read, write, and + list permissions. + + + + )} + /> + )} + + ( + + Container Name + + + + + The Azure Blob container where backup archives are + stored. + + + + )} + /> + + ( + + Custom Endpoint (Optional) + + + + + Only needed for Sovereign Clouds (Gov/China) or local + Azurite emulator. + + + + )} + /> + + ) : ( + /* S3-COMPATIBLE STORAGE FIELDS */ + <> + ( + + S3 Provider + + + + + + )} + /> + + ( + + Access Key ID + + + + + + )} + /> + + ( + + Secret Access Key +
+ + + + +
+ +
+ )} + /> + + ( + + Bucket Name + + + + + + )} + /> + + ( + + Region + + + + + + )} + /> + + ( + + Endpoint URL + + + + + + )} + /> + + )} + + {/* COMMON: Additional Flags */} +
Additional Flags (Optional) )} @@ -508,7 +846,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { form="hook-form-destination-add" type="submit" > - {destinationId ? "Update" : "Create"} + {destinationId ? "Update Destination" : "Create Destination"} diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 39741b932d..1bb524b1fe 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -1,6 +1,16 @@ -import { Database, FolderUp, Loader2, Trash2 } from "lucide-react"; +import { + Cloud, + Database, + FolderUp, + HardDrive, + Loader2, + PlugZap, + Trash2, +} from "lucide-react"; +import { useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, @@ -14,100 +24,192 @@ import { HandleDestinations } from "./handle-destinations"; export const ShowDestinations = () => { const { data, isPending, refetch } = api.destination.all.useQuery(); - const { mutateAsync, isPending: isRemoving } = + const { mutateAsync: removeDestination, isPending: isRemoving } = api.destination.remove.useMutation(); + const { mutateAsync: testConnection } = + api.destination.testConnection.useMutation(); const { data: permissions } = api.user.getPermissions.useQuery(); + const [testingDestinationId, setTestingDestinationId] = useState< + string | null + >(null); + + const handleQuickTest = async ( + destination: NonNullable[number], + ) => { + setTestingDestinationId(destination.destinationId); + try { + const isAzure = + destination.destinationType === "azure_blob" || + destination.destinationType === "az_bs"; + + await testConnection({ + destinationType: isAzure ? "azure_blob" : "s3", + name: destination.name, + provider: + (destination.provider as any) || (isAzure ? "account_key" : "AWS"), + accessKey: destination.accessKey || "", + secretAccessKey: destination.secretAccessKey, + bucket: destination.bucket, + region: destination.region || "", + endpoint: destination.endpoint || "", + additionalFlags: destination.additionalFlags || [], + }); + toast.success(`Connection to "${destination.name}" succeeded!`); + } catch (error) { + toast.error(`Connection to "${destination.name}" failed`, { + description: + error instanceof Error + ? error.message + : "Error connecting to storage", + }); + } finally { + setTestingDestinationId(null); + } + }; + return (
- -
- - - - S3 Destinations + +
+ + + + Backup Destinations - Add your providers like AWS S3, Cloudflare R2, Wasabi, - DigitalOcean Spaces etc. + Configure storage destinations for your backups, including Amazon + S3 (and S3-compatible providers) and Azure Blob Storage. {isPending ? (
- Loading... + Loading destinations...
) : ( <> {data?.length === 0 ? ( -
+
- To create a backup it is required to set at least 1 - provider. + To create backups, set up at least one storage destination + (S3 or Azure Blob Storage). {permissions?.destination.create && }
) : ( -
-
- {data?.map((destination, index) => ( -
-
-
- - {index + 1}. {destination.name} - - - Created at:{" "} - {new Date( - destination.createdAt, - ).toLocaleDateString()} - -
-
- - {permissions?.destination.delete && ( - { - await mutateAsync({ - destinationId: destination.destinationId, - }) - .then(() => { - toast.success( - "Destination deleted successfully", - ); - refetch(); - }) - .catch(() => { - toast.error( - "Error deleting destination", - ); - }); - }} +
+
+ {data?.map((destination, index) => { + const isAzure = + destination.destinationType === "azure_blob" || + destination.destinationType === "az_bs"; + const isTesting = + testingDestinationId === destination.destinationId; + + return ( +
+
+
+
+ + {index + 1}. {destination.name} + + {isAzure ? ( + + + Azure Blob Storage + + ) : ( + + + S3 • {destination.provider || "Standard"} + + )} +
+
+ + + {isAzure ? "Container" : "Bucket"}: + {" "} + {destination.bucket} + + + Created at:{" "} + {new Date( + destination.createdAt, + ).toLocaleDateString()} + +
+
+ +
+ + + + + {permissions?.destination.delete && ( + { + await removeDestination({ + destinationId: + destination.destinationId, + }) + .then(() => { + toast.success( + "Destination deleted successfully", + ); + refetch(); + }) + .catch(() => { + toast.error( + "Error deleting destination", + ); + }); + }} > - - - - )} + + + )} +
-
- ))} + ); + })}
{permissions?.destination.create && ( diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 1e20a0fbe8..fc5780fdfa 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -390,7 +390,7 @@ const MENU: Menu = { }, { isSingle: true, - title: "S3 Destinations", + title: "Backup Destinations", url: "/dashboard/settings/destinations", icon: HardDrive, isEnabled: ({ permissions }) => !!permissions?.destination.read, diff --git a/apps/dokploy/drizzle/0196_windy_jack_power.sql b/apps/dokploy/drizzle/0196_windy_jack_power.sql new file mode 100644 index 0000000000..8f8d787686 --- /dev/null +++ b/apps/dokploy/drizzle/0196_windy_jack_power.sql @@ -0,0 +1,4 @@ +ALTER TABLE "destination" ALTER COLUMN "accessKey" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "region" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "endpoint" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "destinationType" text DEFAULT 's3' NOT NULL; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0196_snapshot.json b/apps/dokploy/drizzle/meta/0196_snapshot.json new file mode 100644 index 0000000000..cb7c732863 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0196_snapshot.json @@ -0,0 +1,9170 @@ +{ + "id": "afaa48ea-5d8a-4b04-9e2d-7201ab1c4994", + "prevId": "719d3b27-c42a-41e4-8b5d-b76b7d8de983", + "version": "7", + "dialect": "postgresql", + "tables": { + "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_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "reference_id": { + "name": "reference_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_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedGitProviders": { + "name": "accessedGitProviders", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedServers": { + "name": "accessedServers", + "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_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "default_role": { + "name": "default_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "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.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizationRole_organizationId_idx": { + "name": "organizationRole_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizationRole_role_idx": { + "name": "organizationRole_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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.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 + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "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 + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "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 + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "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 + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "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, + "default": "'Dockerfile'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "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'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "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 + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auditLog_organizationId_idx": { + "name": "auditLog_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_userId_idx": { + "name": "auditLog_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_createdAt_idx": { + "name": "auditLog_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_organization_id_organization_id_fk": { + "name": "audit_log_organization_id_organization_id_fk", + "tableFrom": "audit_log", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "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 + }, + "includeEncryptionKey": { + "name": "includeEncryptionKey", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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_libsqlId_libsql_libsqlId_fk": { + "name": "backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "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 + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "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.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.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 + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "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": "''" + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "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'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "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 + }, + "serviceNetworks": { + "name": "serviceNetworks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "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_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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.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 + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "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" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "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 + }, + "destinationType": { + "name": "destinationType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'s3'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additionalFlags": { + "name": "additionalFlags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "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.dns_provider": { + "name": "dns_provider", + "schema": "", + "columns": { + "dnsProviderId": { + "name": "dnsProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "DnsProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dns_provider_org_name_idx": { + "name": "dns_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dns_provider_organizationId_organization_id_fk": { + "name": "dns_provider_organizationId_organization_id_fk", + "tableFrom": "dns_provider", + "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 + }, + "customEntrypoint": { + "name": "customEntrypoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "middlewares": { + "name": "middlewares", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "forwardAuthEnabled": { + "name": "forwardAuthEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "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.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "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 + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forward_auth_settings": { + "name": "forward_auth_settings", + "schema": "", + "columns": { + "forwardAuthSettingsId": { + "name": "forwardAuthSettingsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "authDomain": { + "name": "authDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "baseDomain": { + "name": "baseDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'letsencrypt'" + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "forward_auth_settings_providerId_sso_provider_provider_id_fk": { + "name": "forward_auth_settings_providerId_sso_provider_provider_id_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "sso_provider", + "columnsFrom": [ + "providerId" + ], + "columnsTo": [ + "provider_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forward_auth_settings_serverId_server_serverId_fk": { + "name": "forward_auth_settings_serverId_server_serverId_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "forward_auth_settings_serverId_unique": { + "name": "forward_auth_settings_serverId_unique", + "nullsNotDistinct": false, + "columns": [ + "serverId" + ] + } + }, + "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 + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharedWithOrganization": { + "name": "sharedWithOrganization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "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" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "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'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.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 + }, + "githubUrl": { + "name": "githubUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://github.com'" + }, + "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'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.libsql": { + "name": "libsql", + "schema": "", + "columns": { + "libsqlId": { + "name": "libsqlId", + "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 + }, + "sqldNode": { + "name": "sqldNode", + "type": "sqldNode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "sqldPrimaryUrl": { + "name": "sqldPrimaryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableNamespaces": { + "name": "enableNamespaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "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 + }, + "externalGRPCPort": { + "name": "externalGRPCPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalAdminPort": { + "name": "externalAdminPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "libsql_environmentId_environment_environmentId_fk": { + "name": "libsql_environmentId_environment_environmentId_fk", + "tableFrom": "libsql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "libsql_serverId_server_serverId_fk": { + "name": "libsql_serverId_server_serverId_fk", + "tableFrom": "libsql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "libsql_appName_unique": { + "name": "libsql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "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 + }, + "args": { + "name": "args", + "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'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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, + "default": "'mongo:8'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "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'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "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 + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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.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 + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "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 + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "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_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_libsqlId_libsql_libsqlId_fk": { + "name": "mount_libsqlId_libsql_libsqlId_fk", + "tableFrom": "mount", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "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_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "args": { + "name": "args", + "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'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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.network": { + "name": "network", + "schema": "", + "columns": { + "networkId": { + "name": "networkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerId": { + "name": "dockerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "networkDriver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bridge'" + }, + "internal": { + "name": "internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attachable": { + "name": "attachable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableIPv4": { + "name": "enableIPv4", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableIPv6": { + "name": "enableIPv6", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ipam": { + "name": "ipam", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "network_organizationId_organization_id_fk": { + "name": "network_organizationId_organization_id_fk", + "tableFrom": "network", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_serverId_server_serverId_fk": { + "name": "network_serverId_server_serverId_fk", + "tableFrom": "network", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "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.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mattermost": { + "name": "mattermost", + "schema": "", + "columns": { + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "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 + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployBackup": { + "name": "dokployBackup", + "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 + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "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_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "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_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_mattermostId_mattermost_mattermostId_fk": { + "name": "notification_mattermostId_mattermost_mattermostId_fk", + "tableFrom": "notification", + "tableTo": "mattermost", + "columnsFrom": [ + "mattermostId" + ], + "columnsTo": [ + "mattermostId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "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.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "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.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.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "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.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "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 + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "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.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 + }, + "args": { + "name": "args", + "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'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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.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.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.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.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 + }, + "args": { + "name": "args", + "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'" + }, + "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 + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "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.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.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_organizationId_organization_id_fk": { + "name": "schedule_organizationId_organization_id_fk", + "tableFrom": "schedule", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_provider": { + "name": "scim_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_token": { + "name": "scim_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scim_provider_organization_id_organization_id_fk": { + "name": "scim_provider_organization_id_organization_id_fk", + "tableFrom": "scim_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_provider_provider_id_unique": { + "name": "scim_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + }, + "scim_provider_scim_token_unique": { + "name": "scim_provider_scim_token_unique", + "nullsNotDistinct": false, + "columns": [ + "scim_token" + ] + } + }, + "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.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 + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "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.session": { + "name": "session", + "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_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tag": { + "name": "project_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_projectId_project_projectId_fk": { + "name": "project_tag_projectId_project_projectId_fk", + "tableFrom": "project_tag", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tag_tagId_tag_tagId_fk": { + "name": "project_tag_tagId_tag_tagId_fk", + "tableFrom": "project_tag", + "tableTo": "tag", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "tagId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_project_tag": { + "name": "unique_project_tag", + "nullsNotDistinct": false, + "columns": [ + "projectId", + "tagId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tag_organizationId_organization_id_fk": { + "name": "tag_organizationId_organization_id_fk", + "tableFrom": "tag", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_org_tag_name": { + "name": "unique_org_tag_name", + "nullsNotDistinct": false, + "columns": [ + "organizationId", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "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 + }, + "sendInvoiceNotifications": { + "name": "sendInvoiceNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isEnterpriseCloud": { + "name": "isEnterpriseCloud", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "bookmarkedTemplates": { + "name": "bookmarkedTemplates", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "onboardingCompletedAt": { + "name": "onboardingCompletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_provider": { + "name": "vault_provider", + "schema": "", + "columns": { + "vaultProviderId": { + "name": "vaultProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "VaultProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "assignments": { + "name": "assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vault_provider_org_name_idx": { + "name": "vault_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_provider_organizationId_organization_id_fk": { + "name": "vault_provider_organizationId_organization_id_fk", + "tableFrom": "vault_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_libsqlId_libsql_libsqlId_fk": { + "name": "volume_backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "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" + }, + "whitelabelingConfig": { + "name": "whitelabelingConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"footerText\":null,\"ogImageUrl\":null}'::jsonb" + }, + "remoteServersOnly": { + "name": "remoteServersOnly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "enforceSSO": { + "name": "enforceSSO", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "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.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server", + "libsql" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.DnsProviderType": { + "name": "DnsProviderType", + "schema": "public", + "values": [ + "cloudflare", + "route53", + "porkbun", + "infomaniak", + "ovh" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose", + "libsql" + ] + }, + "public.networkDriver": { + "name": "networkDriver", + "schema": "public", + "values": [ + "bridge", + "overlay" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "mattermost", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.sqldNode": { + "name": "sqldNode", + "schema": "public", + "values": [ + "primary", + "replica" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.VaultProviderType": { + "name": "VaultProviderType", + "schema": "public", + "values": [ + "hashicorp", + "infisical", + "aws", + "aws-parameter-store", + "doppler", + "azure", + "scaleway", + "phase" + ] + } + }, + "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 a0a6d1f576..b939f2b57e 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1373,6 +1373,13 @@ "when": 1788867370653, "tag": "0195_classy_whirlwind", "breakpoints": true + }, + { + "idx": 196, + "version": "7", + "when": 1788902124787, + "tag": "0196_windy_jack_power", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/pages/dashboard/settings/destinations.tsx b/apps/dokploy/pages/dashboard/settings/destinations.tsx index ff4637c45d..65dd111e34 100644 --- a/apps/dokploy/pages/dashboard/settings/destinations.tsx +++ b/apps/dokploy/pages/dashboard/settings/destinations.tsx @@ -18,7 +18,9 @@ const Page = () => { export default Page; Page.getLayout = (page: ReactElement) => { - return {page}; + return ( + {page} + ); }; export async function getServerSideProps( ctx: GetServerSidePropsContext<{ serviceId: string }>, diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 9bc73ce77d..7f6cab5cfe 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -32,7 +32,7 @@ import { findDestinationById } from "@dokploy/server/services/destination"; import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { runComposeBackup } from "@dokploy/server/utils/backups/compose"; import { - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "@dokploy/server/utils/backups/utils"; import { @@ -497,8 +497,8 @@ export const backupRouter = createTRPCRouter({ }); } } - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const bucketPath = remoteBase; const lastSlashIndex = input.search.lastIndexOf("/"); const baseDir = diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index 3d2ad60574..445ca9cede 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -3,6 +3,7 @@ import { execAsync, execAsyncRemote, findDestinationById, + getDestinationRemote, IS_CLOUD, removeDestinationById, updateDestinationById, @@ -48,36 +49,16 @@ export const destinationRouter = createTRPCRouter({ testConnection: withPermission("destination", "create") .input(apiCreateDestination) .mutation(async ({ input }) => { - const { - secretAccessKey, - bucket, - region, - endpoint, - accessKey, - provider, - additionalFlags, - } = input; try { - const rcloneFlags = [ - `--s3-access-key-id=${quote([accessKey])}`, - `--s3-secret-access-key=${quote([secretAccessKey])}`, - `--s3-region=${quote([region])}`, - `--s3-endpoint=${quote([endpoint])}`, - "--s3-no-check-bucket", - "--s3-force-path-style", + const { rcloneFlags, remoteBase } = getDestinationRemote(input); + const testFlags = [ + ...rcloneFlags, "--retries 1", "--low-level-retries 1", "--timeout 10s", "--contimeout 5s", ]; - if (provider) { - rcloneFlags.unshift(`--s3-provider=${quote([provider])}`); - } - if (additionalFlags?.length) { - rcloneFlags.push(...additionalFlags); - } - const rcloneDestination = `:s3:${bucket}`; - const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} ${quote([rcloneDestination])}`; + const rcloneCommand = `rclone ls ${testFlags.join(" ")} ${quote([remoteBase])}`; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -92,12 +73,13 @@ export const destinationRouter = createTRPCRouter({ await execAsync(rcloneCommand); } } catch (error) { + const isAzure = input.destinationType === "azure_blob"; throw new TRPCError({ code: "BAD_REQUEST", message: error instanceof Error ? error?.message - : "Error connecting to bucket", + : `Error connecting to ${isAzure ? "container" : "bucket"}`, cause: error, }); } diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index c479679fe3..0e9607b80f 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -1,6 +1,5 @@ import { relations } from "drizzle-orm"; import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; import { @@ -16,12 +15,13 @@ export const destinations = pgTable("destination", { .primaryKey() .$defaultFn(() => nanoid()), name: text("name").notNull(), + destinationType: text("destinationType").notNull().default("s3"), provider: text("provider"), - accessKey: text("accessKey").notNull(), + accessKey: text("accessKey"), secretAccessKey: text("secretAccessKey").notNull(), bucket: text("bucket").notNull(), - region: text("region").notNull(), - endpoint: text("endpoint").notNull(), + region: text("region"), + endpoint: text("endpoint"), additionalFlags: text("additionalFlags").array(), organizationId: text("organizationId") .notNull() @@ -40,59 +40,79 @@ export const destinationsRelations = relations( }), ); -const createSchema = createInsertSchema(destinations, { - destinationId: z.string(), - name: z.string().min(1), - provider: z.string(), - accessKey: z.string(), - bucket: z.string(), - endpoint: z.string(), - secretAccessKey: z.string(), - region: z.string(), +export const s3DestinationSchema = z.object({ + destinationType: z.literal("s3").default("s3"), + name: z.string().min(1, "Name is required"), + provider: z.string().min(1, "Provider is required"), + accessKey: z.string().min(1, "Access Key Id is required"), + secretAccessKey: z.string().min(1, "Secret Access Key is required"), + bucket: z.string().min(1, "Bucket is required"), + region: z.string().default(""), + endpoint: z.string().min(1, "Endpoint is required"), additionalFlags: z .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) .default([]), + serverId: z.string().optional(), }); -export const apiCreateDestination = createSchema - .pick({ - name: true, - provider: true, - accessKey: true, - bucket: true, - region: true, - endpoint: true, - secretAccessKey: true, - additionalFlags: true, - }) - .required() - .extend({ +export const azureBlobDestinationSchema = z + .object({ + destinationType: z.literal("azure_blob").default("azure_blob"), + name: z.string().min(1, "Name is required"), + provider: z.enum(["account_key", "sas_url"]).default("account_key"), + accessKey: z.string().optional().default(""), + secretAccessKey: z.string().min(1, "Account Key or SAS URL is required"), + bucket: z.string().min(1, "Container name is required"), + region: z.string().optional().default(""), + endpoint: z.string().optional().default(""), + additionalFlags: z + .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) + .default([]), serverId: z.string().optional(), - }); + }) + .refine( + (data) => { + if (data.provider === "account_key") { + return !!data.accessKey && data.accessKey.trim().length > 0; + } + return true; + }, + { + message: "Storage Account Name is required when using Account Key", + path: ["accessKey"], + }, + ); + +export const destinationSchema = z.object({ + name: z.string().min(1, "Name is required"), + destinationType: z.preprocess( + (val) => (val === "az_bs" ? "azure_blob" : val), + z.enum(["s3", "azure_blob"]).default("s3"), + ), + provider: z.string().min(1, "Provider is required").default("AWS"), + accessKey: z.string().optional().default(""), + secretAccessKey: z + .string() + .min(1, "Secret Access Key / Account Key / SAS URL is required"), + bucket: z.string().min(1, "Bucket / Container is required"), + region: z.string().optional().default(""), + endpoint: z.string().optional().default(""), + additionalFlags: z + .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) + .default([]), + serverId: z.string().optional(), +}); + +export const apiCreateDestination = destinationSchema; export const apiFindOneDestination = z.object({ destinationId: z.string().min(1), }); -export const apiRemoveDestination = createSchema - .pick({ - destinationId: true, - }) - .required(); +export const apiRemoveDestination = z.object({ + destinationId: z.string().min(1), +}); -export const apiUpdateDestination = createSchema - .pick({ - name: true, - accessKey: true, - bucket: true, - region: true, - endpoint: true, - secretAccessKey: true, - destinationId: true, - provider: true, - additionalFlags: true, - }) - .required() - .extend({ - serverId: z.string().optional(), - }); +export const apiUpdateDestination = destinationSchema.extend({ + destinationId: z.string().min(1), +}); diff --git a/packages/server/src/db/validations/destination.ts b/packages/server/src/db/validations/destination.ts index d342646b4a..d7605060ff 100644 --- a/packages/server/src/db/validations/destination.ts +++ b/packages/server/src/db/validations/destination.ts @@ -1,3 +1,79 @@ export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/; export const ADDITIONAL_FLAG_ERROR = "Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)"; + +export interface ParsedAzureConnectionString { + accountName: string; + accountKey: string; + endpoint?: string; +} + +/** + * Parses an Azure Storage Connection String into account name, key, and optional custom endpoint. + */ +export const parseAzureConnectionString = ( + connectionString: string, +): ParsedAzureConnectionString => { + const trimmed = connectionString.trim(); + if (!trimmed) { + return { accountName: "", accountKey: "" }; + } + + const parts = trimmed.split(";").reduce( + (acc, part) => { + const index = part.indexOf("="); + if (index !== -1) { + const key = part.slice(0, index).trim(); + const val = part.slice(index + 1).trim(); + if (key) { + acc[key] = val; + } + } + return acc; + }, + {} as Record, + ); + + const accountName = parts.AccountName || ""; + const accountKey = parts.AccountKey || ""; + let endpoint: string | undefined; + + if (parts.BlobEndpoint) { + endpoint = parts.BlobEndpoint; + } else if ( + parts.EndpointSuffix && + parts.DefaultEndpointsProtocol && + accountName + ) { + if (parts.EndpointSuffix.toLowerCase() !== "core.windows.net") { + endpoint = `${parts.DefaultEndpointsProtocol}://${accountName}.blob.${parts.EndpointSuffix}`; + } + } + + return { + accountName, + accountKey, + endpoint, + }; +}; + +/** + * Extracts container name and account name from a SAS URL if present. + */ +export const parseAzureSasUrl = ( + sasUrl: string, +): { accountName?: string; containerName?: string } => { + try { + const url = new URL(sasUrl.trim()); + const hostParts = url.hostname.split("."); + const accountName = hostParts.length > 0 ? hostParts[0] : undefined; + + const pathSegments = url.pathname.replace(/^\/+|\/+$/g, "").split("/"); + const containerName = + pathSegments.length > 0 && pathSegments[0] ? pathSegments[0] : undefined; + + return { accountName, containerName }; + } catch { + return {}; + } +}; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 766ccdd6fb..368f0b74d6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -76,6 +76,7 @@ export { } from "./utils/access-log/handler"; export * from "./utils/access-log/types"; export * from "./utils/access-log/utils"; +export * from "./utils/backups/azure"; export * from "./utils/backups/compose"; export * from "./utils/backups/index"; export * from "./utils/backups/libsql"; diff --git a/packages/server/src/utils/backups/azure.ts b/packages/server/src/utils/backups/azure.ts new file mode 100644 index 0000000000..615824e1f9 --- /dev/null +++ b/packages/server/src/utils/backups/azure.ts @@ -0,0 +1,9 @@ +/** + * Utility functions for Azure Blob Storage (AZ_BS) backups. + */ + +export { + type ParsedAzureConnectionString, + parseAzureConnectionString, + parseAzureSasUrl, +} from "../../db/validations/destination"; diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 7a5ba5535d..f450ee8dfc 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -35,8 +35,8 @@ export const runComposeBackup = async ( }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index d0d3045587..be05321b42 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -12,7 +12,7 @@ import { cleanupAll } from "../docker/utils"; import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { redactRcloneCredentials } from "./redact"; -import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils"; +import { getDestinationRemote, normalizeS3Path, scheduleBackup } from "./utils"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); @@ -134,9 +134,11 @@ export const keepLatestNBackups = async ( try { const destination = await findDestinationById(backup.destinationId); - const rcloneFlags = getS3Credentials(destination); + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); const appName = getServiceAppName(backup); - const backupFilesPath = `:s3:${destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`; + const backupFilesPath = getRemotePath( + `${appName}/${normalizeS3Path(backup.prefix)}`, + ); // --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`; diff --git a/packages/server/src/utils/backups/libsql.ts b/packages/server/src/utils/backups/libsql.ts index c4ece6ed63..0928632f25 100644 --- a/packages/server/src/utils/backups/libsql.ts +++ b/packages/server/src/utils/backups/libsql.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -34,8 +34,8 @@ export const runLibsqlBackup = async ( const backupFileName = `${getBackupTimestamp()}.sql.gz`; const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 4a3f7dc262..1f598ae48b 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -33,8 +33,8 @@ export const runMariadbBackup = async ( description: "MariaDB Backup", }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index 6b80ccc862..494832da0b 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -30,8 +30,8 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { description: "MongoDB Backup", }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index 2ac9228bed..5024b79b09 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -31,8 +31,8 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index de7ef55003..739d6757a4 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -12,7 +12,7 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "./utils"; @@ -34,8 +34,8 @@ export const runPostgresBackup = async ( const backupFileName = `${getBackupTimestamp()}.sql.gz`; const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const backupCommand = getBackupCommand( backup, rcloneFlags, diff --git a/packages/server/src/utils/backups/redact.ts b/packages/server/src/utils/backups/redact.ts index 065e76d8cd..b4bc9163e9 100644 --- a/packages/server/src/utils/backups/redact.ts +++ b/packages/server/src/utils/backups/redact.ts @@ -1,12 +1,15 @@ /** - * Redacts S3 credentials from rclone command strings. + * Redacts S3 and Azure Blob Storage credentials from rclone command strings. * * Used to prevent credential leakage in structured logs and error output. - * Matches the flag format produced by `getS3Credentials()`: - * --s3-access-key-id="VALUE" and --s3-secret-access-key="VALUE" + * Matches the flag formats: + * --s3-access-key-id="VALUE", --s3-secret-access-key="VALUE", + * --azureblob-key="VALUE", and --azureblob-sas-url="VALUE" */ export const redactRcloneCredentials = (command: string): string => { return command .replace(/(--s3-access-key-id=)"[^"]*"/g, '$1"[REDACTED]"') - .replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"'); + .replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"') + .replace(/(--azureblob-key=)"[^"]*"/g, '$1"[REDACTED]"') + .replace(/(--azureblob-sas-url=)"[^"]*"/g, '$1"[REDACTED]"'); }; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 4253a38769..2df23f0fc8 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -68,14 +68,81 @@ export const normalizeS3Path = (prefix: string) => { return normalizedPrefix ? `${normalizedPrefix}/` : ""; }; -export const getS3Credentials = (destination: Destination) => { - const { accessKey, secretAccessKey, region, endpoint, provider } = +export interface DestinationRemote { + rcloneFlags: string[]; + remoteBase: string; + getRemotePath: (subPath?: string) => string; +} + +export const getDestinationRemote = ( + destination: Pick< + Destination, + | "accessKey" + | "secretAccessKey" + | "bucket" + | "region" + | "endpoint" + | "provider" + | "additionalFlags" + > & { + destinationType?: string | null; + }, +): DestinationRemote => { + const destinationType = + destination.destinationType === "az_bs" || + destination.destinationType === "azure_blob" + ? "azure_blob" + : "s3"; + + if (destinationType === "azure_blob") { + const rcloneFlags: string[] = []; + const isSas = + destination.provider === "sas_url" || + destination.secretAccessKey.startsWith("http://") || + destination.secretAccessKey.startsWith("https://"); + + if (isSas) { + rcloneFlags.push( + `--azureblob-sas-url=${quote([destination.secretAccessKey])}`, + ); + } else { + if (destination.accessKey) { + rcloneFlags.push( + `--azureblob-account=${quote([destination.accessKey])}`, + ); + } + rcloneFlags.push( + `--azureblob-key=${quote([destination.secretAccessKey])}`, + ); + } + + if (destination.endpoint) { + rcloneFlags.push(`--azureblob-endpoint=${quote([destination.endpoint])}`); + } + + if (destination.additionalFlags?.length) { + rcloneFlags.push(...destination.additionalFlags); + } + + const container = destination.bucket; + const remoteBase = `:azureblob:${container}`; + + return { + rcloneFlags, + remoteBase, + getRemotePath: (subPath?: string) => + subPath ? `${remoteBase}/${subPath.replace(/^\/+/, "")}` : remoteBase, + }; + } + + // Default: S3 + const { accessKey, secretAccessKey, region, endpoint, provider, bucket } = destination; const rcloneFlags = [ - `--s3-access-key-id=${quote([accessKey])}`, + `--s3-access-key-id=${quote([accessKey || ""])}`, `--s3-secret-access-key=${quote([secretAccessKey])}`, - `--s3-region=${quote([region])}`, - `--s3-endpoint=${quote([endpoint])}`, + `--s3-region=${quote([region || ""])}`, + `--s3-endpoint=${quote([endpoint || ""])}`, "--s3-no-check-bucket", "--s3-force-path-style", ]; @@ -88,7 +155,18 @@ export const getS3Credentials = (destination: Destination) => { rcloneFlags.push(...destination.additionalFlags); } - return rcloneFlags; + const remoteBase = `:s3:${bucket}`; + + return { + rcloneFlags, + remoteBase, + getRemotePath: (subPath?: string) => + subPath ? `${remoteBase}/${subPath.replace(/^\/+/, "")}` : remoteBase, + }; +}; + +export const getS3Credentials = (destination: Destination) => { + return getDestinationRemote(destination).rcloneFlags; }; // User-controlled values (database name, user, password) are passed to the @@ -290,7 +368,7 @@ export const getBackupCommand = ( fi; echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath}; - echo "[$(date)] Starting backup and upload to S3..." >> ${logPath}; + echo "[$(date)] Starting backup and upload to destination..." >> ${logPath}; UPLOAD_OUTPUT=$({ ${backupCommand} | ${rcloneCommand}; } 2>&1 >/dev/null) || { echo "[$(date)] ❌ Error: Backup failed" >> ${logPath}; @@ -299,7 +377,7 @@ export const getBackupCommand = ( exit 1; }; - echo "[$(date)] ✅ Backup uploaded to S3 successfully" >> ${logPath}; + echo "[$(date)] ✅ Backup uploaded to destination successfully" >> ${logPath}; echo "Backup done ✅" >> ${logPath}; `; }; diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index aae90a5bf3..e4992248a2 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -16,7 +16,11 @@ import { findDestinationById } from "@dokploy/server/services/destination"; import { sendDokployBackupNotifications } from "../notifications/dokploy-backup"; import { execAsync } from "../process/execAsync"; import { redactRcloneCredentials } from "./redact"; -import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils"; +import { + getBackupTimestamp, + getDestinationRemote, + normalizeS3Path, +} from "./utils"; function formatBytes(bytes?: number) { if (bytes === undefined) return "Unknown size"; @@ -41,12 +45,14 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { let computedBackupSize: number | undefined; try { const destination = await findDestinationById(backup.destinationId); - const rcloneFlags = getS3Credentials(destination); + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); const timestamp = getBackupTimestamp(); const { BASE_PATH } = paths(); const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-")); const backupFileName = `webserver-backup-${timestamp}.zip`; - const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`; + const s3Path = getRemotePath( + `${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`, + ); try { await execAsync(`mkdir -p ${tempDir}/filesystem`); @@ -115,9 +121,9 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { } const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${zipPath}" "${s3Path}"`; - writeStream.write("Running command to upload backup to S3\n"); + writeStream.write("Running command to upload backup to destination\n"); await execAsync(uploadCommand); - writeStream.write("Uploaded backup to S3 ✅\n"); + writeStream.write("Uploaded backup to destination ✅\n"); writeStream.end(); await sendDokployBackupNotifications({ type: "success", diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index 07f23c772b..eb7cbd7de3 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -3,7 +3,7 @@ import type { Compose } from "@dokploy/server/services/compose"; import type { Destination } from "@dokploy/server/services/destination"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -24,9 +24,8 @@ export const restoreComposeBackup = async ( } const { serverId, appName, composeType } = compose; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; if (backupInput.metadata?.mongo) { diff --git a/packages/server/src/utils/restore/libsql.ts b/packages/server/src/utils/restore/libsql.ts index 9d7c7f15c8..4d98deb354 100644 --- a/packages/server/src/utils/restore/libsql.ts +++ b/packages/server/src/utils/restore/libsql.ts @@ -3,7 +3,10 @@ import type { Destination } from "@dokploy/server/services/destination"; import type { Libsql } from "@dokploy/server/services/libsql"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials, getServiceContainerCommand } from "../backups/utils"; +import { + getDestinationRemote, + getServiceContainerCommand, +} from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; export const restoreLibsqlBackup = async ( @@ -15,10 +18,8 @@ export const restoreLibsqlBackup = async ( try { const { appName, serverId } = libsql; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])}`; diff --git a/packages/server/src/utils/restore/mariadb.ts b/packages/server/src/utils/restore/mariadb.ts index e4b301f46b..cf52820305 100644 --- a/packages/server/src/utils/restore/mariadb.ts +++ b/packages/server/src/utils/restore/mariadb.ts @@ -3,7 +3,7 @@ import type { Destination } from "@dokploy/server/services/destination"; import type { Mariadb } from "@dokploy/server/services/mariadb"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -16,9 +16,8 @@ export const restoreMariadbBackup = async ( try { const { appName, serverId, databaseUser, databasePassword } = mariadb; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/mongo.ts b/packages/server/src/utils/restore/mongo.ts index 163340ca8c..8018ecf7e3 100644 --- a/packages/server/src/utils/restore/mongo.ts +++ b/packages/server/src/utils/restore/mongo.ts @@ -3,7 +3,7 @@ import type { Destination } from "@dokploy/server/services/destination"; import type { Mongo } from "@dokploy/server/services/mongo"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -16,9 +16,8 @@ export const restoreMongoBackup = async ( try { const { appName, databasePassword, databaseUser, serverId } = mongo; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} ${quote([backupPath])}`; const command = getRestoreCommand({ diff --git a/packages/server/src/utils/restore/mysql.ts b/packages/server/src/utils/restore/mysql.ts index d237da895d..5c871efc57 100644 --- a/packages/server/src/utils/restore/mysql.ts +++ b/packages/server/src/utils/restore/mysql.ts @@ -3,7 +3,7 @@ import type { Destination } from "@dokploy/server/services/destination"; import type { MySql } from "@dokploy/server/services/mysql"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -16,9 +16,8 @@ export const restoreMySqlBackup = async ( try { const { appName, databaseRootPassword, serverId } = mysql; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 05dcf38a38..e6d947760f 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -3,7 +3,7 @@ import type { Destination } from "@dokploy/server/services/destination"; import type { Postgres } from "@dokploy/server/services/postgres"; import { quote } from "shell-quote"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -16,10 +16,8 @@ export const restorePostgresBackup = async ( try { const { appName, databaseUser, serverId } = postgres; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - - const backupPath = `${bucketPath}/${backupInput.backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index afcd81cf6f..b30ab8d924 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Destination } from "@dokploy/server/services/destination"; import { quote } from "shell-quote"; -import { getS3Credentials } from "../backups/utils"; +import { getDestinationRemote } from "../backups/utils"; import { execAsync } from "../process/execAsync"; export const restoreWebServerBackup = async ( @@ -16,9 +16,8 @@ export const restoreWebServerBackup = async ( return; } try { - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupFile}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupFile}`; const { BASE_PATH } = paths(); // Create a temporary directory outside of BASE_PATH diff --git a/packages/server/src/utils/volume-backups/backup.ts b/packages/server/src/utils/volume-backups/backup.ts index 3172c9c211..1a99e65169 100644 --- a/packages/server/src/utils/volume-backups/backup.ts +++ b/packages/server/src/utils/volume-backups/backup.ts @@ -5,7 +5,7 @@ import { findDestinationById } from "@dokploy/server/services/destination"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { getBackupTimestamp, - getS3Credentials, + getDestinationRemote, normalizeS3Path, } from "../backups/utils"; @@ -76,8 +76,8 @@ export const backupVolume = async ( const s3AppName = getVolumeServiceAppName(volumeBackup); const backupFileName = `${volumeName}-${getBackupTimestamp()}.tar`; const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`; - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const rcloneDestination = getRemotePath(bucketDestination); const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`; diff --git a/packages/server/src/utils/volume-backups/restore.ts b/packages/server/src/utils/volume-backups/restore.ts index 812f6cefd4..c20d71cd8c 100644 --- a/packages/server/src/utils/volume-backups/restore.ts +++ b/packages/server/src/utils/volume-backups/restore.ts @@ -4,7 +4,7 @@ import { findApplicationById, findComposeById, findDestinationById, - getS3Credentials, + getDestinationRemote, paths, } from "../.."; @@ -19,9 +19,8 @@ export const restoreVolume = async ( const destination = await findDestinationById(destinationId); const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName); - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupFileName}`; + const { rcloneFlags, remoteBase } = getDestinationRemote(destination); + const backupPath = `${remoteBase}/${backupFileName}`; // Command to download backup file from S3 const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} ${quote([backupPath])} ${quote([`${volumeBackupPath}/${backupFileName}`])}`; @@ -32,7 +31,7 @@ export const restoreVolume = async ( echo "Volume name: ${volumeName}" echo "Backup file name: ${backupFileName}" echo "Volume backup path: ${volumeBackupPath}" - echo "Downloading backup from S3..." + echo "Downloading backup from destination..." mkdir -p ${volumeBackupPath} ${downloadCommand} echo "Download completed ✅" diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 8edca6cf68..240bf1d3af 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -11,7 +11,7 @@ import { execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { scheduledJobs, scheduleJob } from "node-schedule"; -import { getS3Credentials, normalizeS3Path } from "../backups/utils"; +import { getDestinationRemote, normalizeS3Path } from "../backups/utils"; import { sendVolumeBackupNotifications } from "../notifications/volume-backup"; import { backupVolume, getVolumeServiceAppName } from "./backup"; @@ -84,9 +84,11 @@ const cleanupOldVolumeBackups = async ( if (!keepLatestCount) return; try { - const rcloneFlags = getS3Credentials(destination); + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); const s3AppName = getVolumeServiceAppName(volumeBackup); - const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`; + const backupFilesPath = getRemotePath( + `${s3AppName}/${normalizeS3Path(prefix || "")}`, + ); const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; From f8cf4f897f81ab9f477114445b8ac87c442b5910 Mon Sep 17 00:00:00 2001 From: Riad Date: Tue, 8 Sep 2026 19:00:20 -0400 Subject: [PATCH 2/4] style: provide specific error messages when storage buckets or containers are not found --- .../dokploy/server/api/routers/destination.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index 445ca9cede..f6b29112f2 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -74,12 +74,23 @@ export const destinationRouter = createTRPCRouter({ } } catch (error) { const isAzure = input.destinationType === "azure_blob"; + let message = + error instanceof Error + ? error?.message + : `Error connecting to ${isAzure ? "container" : "bucket"}`; + + if ( + error instanceof Error && + error.message.includes("directory not found") + ) { + message = isAzure + ? `Container "${input.bucket}" was not found in storage account "${input.accessKey || input.name}". Please make sure the container exists in Azure.` + : `Bucket "${input.bucket}" was not found. Please make sure the bucket exists in your storage provider.`; + } + throw new TRPCError({ code: "BAD_REQUEST", - message: - error instanceof Error - ? error?.message - : `Error connecting to ${isAzure ? "container" : "bucket"}`, + message, cause: error, }); } From 71dd7de8577cc12bdd6c8cabf7b4cd6bffc680b4 Mon Sep 17 00:00:00 2001 From: Riad Date: Tue, 8 Sep 2026 23:02:25 -0400 Subject: [PATCH 3/4] feat: enable server selection for destination connection tests in cloud mode and improve error message redaction --- .../destination-schema-validation.test.ts | 75 +++++++++++++++ .../backups/redact-credentials.test.ts | 32 +++++++ .../destination/handle-destinations.tsx | 31 +++++-- .../destination/show-destinations.tsx | 93 ++++++++++++++++--- .../dokploy/server/api/routers/destination.ts | 3 +- packages/server/src/db/schema/destination.ts | 75 +++++++++++---- packages/server/src/index.ts | 1 + packages/server/src/utils/backups/redact.ts | 17 +++- .../utils/notifications/database-backup.ts | 6 +- .../src/utils/notifications/volume-backup.ts | 6 +- 10 files changed, 293 insertions(+), 46 deletions(-) diff --git a/apps/dokploy/__test__/backups/destination-schema-validation.test.ts b/apps/dokploy/__test__/backups/destination-schema-validation.test.ts index 46974f1d85..507640a7cd 100644 --- a/apps/dokploy/__test__/backups/destination-schema-validation.test.ts +++ b/apps/dokploy/__test__/backups/destination-schema-validation.test.ts @@ -140,5 +140,80 @@ describe("Destination Zod Schema Validation", () => { expect(result.data.name).toBe("Updated Azure Backup"); } }); + + it("rejects apiCreateDestination for S3 when accessKey is missing or empty", () => { + const invalidS3 = { + name: "S3 without key", + destinationType: "s3", + provider: "AWS", + secretAccessKey: "mysecret", + bucket: "mybucket", + endpoint: "https://s3.amazonaws.com", + }; + + const result = apiCreateDestination.safeParse(invalidS3); + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some((i) => i.path.includes("accessKey")), + ).toBe(true); + } + }); + + it("rejects apiCreateDestination for S3 when endpoint is missing or empty", () => { + const invalidS3 = { + name: "S3 without endpoint", + destinationType: "s3", + provider: "AWS", + accessKey: "mykey", + secretAccessKey: "mysecret", + bucket: "mybucket", + }; + + const result = apiCreateDestination.safeParse(invalidS3); + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some((i) => i.path.includes("endpoint")), + ).toBe(true); + } + }); + + it("rejects apiCreateDestination for Azure account_key when storage account name is missing", () => { + const invalidAzure = { + name: "Azure without account name", + destinationType: "azure_blob", + provider: "account_key", + secretAccessKey: "mysecretkey", + bucket: "mycontainer", + }; + + const result = apiCreateDestination.safeParse(invalidAzure); + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some((i) => i.path.includes("accessKey")), + ).toBe(true); + } + }); + + it("rejects apiCreateDestination for Azure when provider is invalid", () => { + const invalidAzure = { + name: "Azure with invalid provider", + destinationType: "azure_blob", + provider: "Minio", + accessKey: "acc", + secretAccessKey: "key", + bucket: "mycontainer", + }; + + const result = apiCreateDestination.safeParse(invalidAzure); + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some((i) => i.path.includes("provider")), + ).toBe(true); + } + }); }); }); diff --git a/apps/dokploy/__test__/backups/redact-credentials.test.ts b/apps/dokploy/__test__/backups/redact-credentials.test.ts index 939afe0389..e0968a8ef7 100644 --- a/apps/dokploy/__test__/backups/redact-credentials.test.ts +++ b/apps/dokploy/__test__/backups/redact-credentials.test.ts @@ -64,4 +64,36 @@ describe("redactRcloneCredentials (#4621)", () => { expect(redacted).not.toContain("supersensitive"); expect(redacted).toContain('--azureblob-sas-url="[REDACTED]"'); }); + + it("should redact unquoted and shell-escaped Azure credentials produced by shell-quote", () => { + const cmdKey = + "rclone rcat --azureblob-account=myazurestorage --azureblob-key=dGhpcy1pcy1hbi1henVyZS1rZXk\\=\\= :azureblob:container/file.sql.gz"; + const redactedKey = redactRcloneCredentials(cmdKey); + expect(redactedKey).not.toContain("dGhpcy1pcy1hbi1henVyZS1rZXk"); + expect(redactedKey).toContain('--azureblob-key="[REDACTED]"'); + + const cmdSas = + "rclone lsf --azureblob-sas-url=https\\://myaccount.blob.core.windows.net/\\?sv\\=2022-11-02\\&sig\\=supersensitive :azureblob:container/"; + const redactedSas = redactRcloneCredentials(cmdSas); + expect(redactedSas).not.toContain("supersensitive"); + expect(redactedSas).toContain('--azureblob-sas-url="[REDACTED]"'); + }); + + it("should redact single-quoted Azure credentials", () => { + const cmd = + "rclone rcat --azureblob-account='myazurestorage' --azureblob-key='singlequotedkey' :azureblob:container/"; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("singlequotedkey"); + expect(redacted).toContain('--azureblob-key="[REDACTED]"'); + }); + + it("should redact unquoted S3 credentials", () => { + const cmd = + "rclone rcat --s3-access-key-id=AKIAIOSFODNN7EXAMPLE --s3-secret-access-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY :s3:bucket/file.gz"; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("AKIAIOSFODNN7EXAMPLE"); + expect(redacted).not.toContain("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); + expect(redacted).toContain('--s3-access-key-id="[REDACTED]"'); + expect(redacted).toContain('--s3-secret-access-key="[REDACTED]"'); + }); }); diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index 9c4365907b..f9cdcbf77c 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -97,6 +97,13 @@ const destinationFormSchema = z }); } } else if (data.destinationType === "azure_blob") { + if (data.provider !== "account_key" && data.provider !== "sas_url") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provider must be either 'account_key' or 'sas_url'", + path: ["provider"], + }); + } if (data.provider === "account_key") { if (!data.accessKeyId || data.accessKeyId.trim().length === 0) { ctx.addIssue({ @@ -228,8 +235,11 @@ export const HandleDestinations = ({ destinationId }: Props) => { destinationType: data.destinationType, name: data.name, provider: - data.provider || - (data.destinationType === "azure_blob" ? "account_key" : "AWS"), + data.destinationType === "azure_blob" + ? data.provider === "sas_url" + ? "sas_url" + : "account_key" + : data.provider || "AWS", accessKey: data.accessKeyId || "", secretAccessKey: data.secretAccessKey, bucket: data.bucket, @@ -289,7 +299,12 @@ export const HandleDestinations = ({ destinationId }: Props) => { await testConnection({ destinationType: currentVals.destinationType, - provider: currentVals.provider || (isAzure ? "account_key" : "AWS"), + provider: + currentVals.destinationType === "azure_blob" + ? currentVals.provider === "sas_url" + ? "sas_url" + : "account_key" + : currentVals.provider || "AWS", accessKey: currentVals.accessKeyId || "", secretAccessKey: currentVals.secretAccessKey, bucket: currentVals.bucket, @@ -358,9 +373,10 @@ export const HandleDestinations = ({ destinationId }: Props) => { type="button" onClick={() => { form.setValue("destinationType", "s3"); + const currentProvider = form.getValues("provider"); if ( - form.getValues("provider") === "account_key" || - form.getValues("provider") === "sas_url" + currentProvider === "account_key" || + currentProvider === "sas_url" ) { form.setValue("provider", "AWS"); } @@ -379,9 +395,10 @@ export const HandleDestinations = ({ destinationId }: Props) => { type="button" onClick={() => { form.setValue("destinationType", "azure_blob"); + const currentProvider = form.getValues("provider"); if ( - !form.getValues("provider") || - form.getValues("provider") === "AWS" + currentProvider !== "account_key" && + currentProvider !== "sas_url" ) { form.setValue("provider", "account_key"); } diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 1bb524b1fe..0801a456e4 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -19,6 +19,13 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { HandleDestinations } from "./handle-destinations"; @@ -29,13 +36,28 @@ export const ShowDestinations = () => { const { mutateAsync: testConnection } = api.destination.testConnection.useMutation(); const { data: permissions } = api.user.getPermissions.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: servers } = api.server.withSSHKey.useQuery(undefined, { + enabled: !!isCloud, + }); const [testingDestinationId, setTestingDestinationId] = useState< string | null >(null); const handleQuickTest = async ( destination: NonNullable[number], + serverId?: string, ) => { + const targetServerId = + serverId || (isCloud ? servers?.[0]?.serverId : undefined); + + if (isCloud && !targetServerId) { + toast.error( + "A server with SSH keys is required to test destinations in cloud mode. Please add a server first.", + ); + return; + } + setTestingDestinationId(destination.destinationId); try { const isAzure = @@ -53,6 +75,7 @@ export const ShowDestinations = () => { region: destination.region || "", endpoint: destination.endpoint || "", additionalFlags: destination.additionalFlags || [], + serverId: targetServerId, }); toast.success(`Connection to "${destination.name}" succeeded!`); } catch (error) { @@ -154,19 +177,63 @@ export const ShowDestinations = () => {
- + {isCloud && servers && servers.length > 1 ? ( + + + + + + + Run Test on Server + + {servers.map((server) => ( + + handleQuickTest( + destination, + server.serverId, + ) + } + > + {server.name} + + ))} + + + ) : ( + + )} (val === "az_bs" ? "azure_blob" : val), - z.enum(["s3", "azure_blob"]).default("s3"), - ), - provider: z.string().min(1, "Provider is required").default("AWS"), - accessKey: z.string().optional().default(""), - secretAccessKey: z - .string() - .min(1, "Secret Access Key / Account Key / SAS URL is required"), - bucket: z.string().min(1, "Bucket / Container is required"), - region: z.string().optional().default(""), - endpoint: z.string().optional().default(""), - additionalFlags: z - .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) - .default([]), - serverId: z.string().optional(), -}); +export const destinationSchema = z + .object({ + name: z.string().min(1, "Name is required"), + destinationType: z.preprocess( + (val) => (val === "az_bs" ? "azure_blob" : val), + z.enum(["s3", "azure_blob"]).default("s3"), + ), + provider: z.string().min(1, "Provider is required").default("AWS"), + accessKey: z.string().optional().default(""), + secretAccessKey: z + .string() + .min(1, "Secret Access Key / Account Key / SAS URL is required"), + bucket: z.string().min(1, "Bucket / Container is required"), + region: z.string().optional().default(""), + endpoint: z.string().optional().default(""), + additionalFlags: z + .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) + .default([]), + serverId: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.destinationType === "s3") { + if (!data.accessKey || data.accessKey.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Access Key Id is required for S3", + path: ["accessKey"], + }); + } + if (!data.endpoint || data.endpoint.trim().length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Endpoint is required for S3", + path: ["endpoint"], + }); + } + } else if (data.destinationType === "azure_blob") { + if (data.provider !== "account_key" && data.provider !== "sas_url") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provider must be either 'account_key' or 'sas_url'", + path: ["provider"], + }); + } + if ( + data.provider === "account_key" && + (!data.accessKey || data.accessKey.trim().length === 0) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Storage Account Name is required when using Account Key", + path: ["accessKey"], + }); + } + } + }); export const apiCreateDestination = destinationSchema; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 368f0b74d6..67443b49c2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -84,6 +84,7 @@ export * from "./utils/backups/mariadb"; export * from "./utils/backups/mongo"; export * from "./utils/backups/mysql"; export * from "./utils/backups/postgres"; +export * from "./utils/backups/redact"; export * from "./utils/backups/utils"; export * from "./utils/backups/web-server"; export * from "./utils/builders/compose"; diff --git a/packages/server/src/utils/backups/redact.ts b/packages/server/src/utils/backups/redact.ts index b4bc9163e9..32966c2b8f 100644 --- a/packages/server/src/utils/backups/redact.ts +++ b/packages/server/src/utils/backups/redact.ts @@ -8,8 +8,17 @@ */ export const redactRcloneCredentials = (command: string): string => { return command - .replace(/(--s3-access-key-id=)"[^"]*"/g, '$1"[REDACTED]"') - .replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"') - .replace(/(--azureblob-key=)"[^"]*"/g, '$1"[REDACTED]"') - .replace(/(--azureblob-sas-url=)"[^"]*"/g, '$1"[REDACTED]"'); + .replace( + /(--s3-access-key-id=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, + '$1"[REDACTED]"', + ) + .replace( + /(--s3-secret-access-key=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, + '$1"[REDACTED]"', + ) + .replace(/(--azureblob-key=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, '$1"[REDACTED]"') + .replace( + /(--azureblob-sas-url=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, + '$1"[REDACTED]"', + ); }; diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index fb96dee48e..39f2e5c4ed 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -4,6 +4,7 @@ import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; +import { redactRcloneCredentials } from "../backups/redact"; import { sendCustomNotification, sendDiscordNotification, @@ -24,7 +25,7 @@ export const sendDatabaseBackupNotifications = async ({ applicationName, databaseType, type, - errorMessage, + errorMessage: rawErrorMessage, organizationId, databaseName, }: { @@ -36,6 +37,9 @@ export const sendDatabaseBackupNotifications = async ({ errorMessage?: string; databaseName: string; }) => { + const errorMessage = rawErrorMessage + ? redactRcloneCredentials(rawErrorMessage) + : undefined; const date = new Date(); const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index cdb1e3a965..1fe237b4cd 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -4,6 +4,7 @@ import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup"; import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; +import { redactRcloneCredentials } from "../backups/redact"; import { sendCustomNotification, sendDiscordNotification, @@ -25,7 +26,7 @@ export const sendVolumeBackupNotifications = async ({ volumeName, serviceType, type, - errorMessage, + errorMessage: rawErrorMessage, organizationId, backupSize, }: { @@ -46,6 +47,9 @@ export const sendVolumeBackupNotifications = async ({ errorMessage?: string; backupSize?: string; }) => { + const errorMessage = rawErrorMessage + ? redactRcloneCredentials(rawErrorMessage) + : undefined; const date = new Date(); const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ From 4545c69403d1e8ed5238a6f3d0fef04b1ea66afb Mon Sep 17 00:00:00 2001 From: Riad Date: Sun, 20 Sep 2026 22:36:57 -0400 Subject: [PATCH 4/4] fix(destinations): address Azure Blob Storage review comments Redact shell-quoted secrets that contain mixed quotes, auto-fill container names from pasted SAS URLs, and build rclone remotes from the stored provider instead of sniffing the secret. Missing-container errors now name the storage account from the account key or SAS host. Compose the destination API schemas from the S3 and Azure schemas, reuse getRemotePath on restore, and share Azure type/provider helpers. --- .../backups/azure-connection-string.test.ts | 35 +++- .../destination-rclone-builder.test.ts | 39 +++++ .../backups/redact-credentials.test.ts | 17 ++ .../destination/handle-destinations.tsx | 94 ++++++----- .../destination/show-destinations.tsx | 11 +- .../dokploy/server/api/routers/destination.ts | 11 +- packages/server/src/db/schema/destination.ts | 151 ++++++++---------- .../server/src/db/validations/destination.ts | 48 ++++++ packages/server/src/utils/backups/redact.ts | 26 +-- packages/server/src/utils/backups/utils.ts | 26 ++- packages/server/src/utils/restore/compose.ts | 4 +- packages/server/src/utils/restore/libsql.ts | 4 +- packages/server/src/utils/restore/mariadb.ts | 4 +- packages/server/src/utils/restore/mongo.ts | 4 +- packages/server/src/utils/restore/mysql.ts | 4 +- packages/server/src/utils/restore/postgres.ts | 4 +- .../server/src/utils/restore/web-server.ts | 4 +- .../src/utils/volume-backups/restore.ts | 4 +- 18 files changed, 305 insertions(+), 185 deletions(-) diff --git a/apps/dokploy/__test__/backups/azure-connection-string.test.ts b/apps/dokploy/__test__/backups/azure-connection-string.test.ts index 1d55229eaa..e8bf72e4e0 100644 --- a/apps/dokploy/__test__/backups/azure-connection-string.test.ts +++ b/apps/dokploy/__test__/backups/azure-connection-string.test.ts @@ -1,7 +1,9 @@ import { + azureStorageAccountLabel, + looksLikeMissingStorageTarget, parseAzureConnectionString, parseAzureSasUrl, -} from "@dokploy/server/utils/backups/azure"; +} from "@dokploy/server/db/validations/destination"; import { describe, expect, it } from "vitest"; describe("Azure Connection String & SAS URL Parser", () => { @@ -68,3 +70,34 @@ describe("Azure Connection String & SAS URL Parser", () => { expect(containerName).toBeUndefined(); }); }); + +describe("Azure destination helpers", () => { + it("labels SAS destinations from the URL hostname when accessKey is empty", () => { + const sasUrl = + "https://mystorageaccount.blob.core.windows.net/mybackups?sv=2022-11-02&sig=sig123"; + expect( + azureStorageAccountLabel({ accessKey: "", secretAccessKey: sasUrl }), + ).toBe("mystorageaccount"); + }); + + it("falls back to a generic label when the account cannot be inferred", () => { + expect( + azureStorageAccountLabel({ + accessKey: "", + secretAccessKey: "not-a-url", + }), + ).toBe("this storage account"); + }); + + it("matches rclone directory-not-found and Azure ContainerNotFound errors", () => { + expect( + looksLikeMissingStorageTarget("Failed to ls: directory not found"), + ).toBe(true); + expect( + looksLikeMissingStorageTarget( + "RESPONSE 404: ERROR CODE: ContainerNotFound", + ), + ).toBe(true); + expect(looksLikeMissingStorageTarget("authentication failed")).toBe(false); + }); +}); diff --git a/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts b/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts index 5354674f4f..4f0b8ee4ac 100644 --- a/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts +++ b/apps/dokploy/__test__/backups/destination-rclone-builder.test.ts @@ -160,5 +160,44 @@ describe("getDestinationRemote", () => { const { remoteBase } = getDestinationRemote(destination); expect(remoteBase).toBe(":azureblob:test-container"); }); + + it("throws when account_key mode is missing the storage account name", () => { + const destination = { + destinationType: "azure_blob" as const, + name: "Azure Incomplete", + provider: "account_key", + accessKey: "", + secretAccessKey: "dGVzdGtleQ==", + bucket: "mycontainer", + region: "", + endpoint: "", + additionalFlags: [], + }; + + expect(() => getDestinationRemote(destination)).toThrow( + "Storage Account Name is required when using Account Key", + ); + }); + + it("does not treat an https secret as SAS when provider is account_key", () => { + const destination = { + destinationType: "azure_blob" as const, + name: "Azure Account Key", + provider: "account_key", + accessKey: "myazureaccount", + secretAccessKey: + "https://myaccount.blob.core.windows.net/mycontainer?sv=2022-11-02&sig=test", + bucket: "mycontainer", + region: "", + endpoint: "", + additionalFlags: [], + }; + + const { rcloneFlags } = getDestinationRemote(destination); + const flagsStr = rcloneFlags.join(" "); + expect(flagsStr).toContain("--azureblob-account=myazureaccount"); + expect(flagsStr).toContain("--azureblob-key="); + expect(flagsStr).not.toContain("--azureblob-sas-url="); + }); }); }); diff --git a/apps/dokploy/__test__/backups/redact-credentials.test.ts b/apps/dokploy/__test__/backups/redact-credentials.test.ts index e0968a8ef7..77167b2d4b 100644 --- a/apps/dokploy/__test__/backups/redact-credentials.test.ts +++ b/apps/dokploy/__test__/backups/redact-credentials.test.ts @@ -1,4 +1,5 @@ import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact"; +import { quote } from "shell-quote"; import { describe, expect, it } from "vitest"; describe("redactRcloneCredentials (#4621)", () => { @@ -96,4 +97,20 @@ describe("redactRcloneCredentials (#4621)", () => { expect(redacted).toContain('--s3-access-key-id="[REDACTED]"'); expect(redacted).toContain('--s3-secret-access-key="[REDACTED]"'); }); + + it("should redact secrets that contain both single and double quotes (shell-quote)", () => { + const mixedSecret = `abc'def"ghiLEAKED`; + const quoted = quote([mixedSecret]); + const azureCmd = `rclone rcat --azureblob-key=${quoted} :azureblob:container/file.sql.gz`; + const azureRedacted = redactRcloneCredentials(azureCmd); + expect(azureRedacted).not.toContain("ghiLEAKED"); + expect(azureRedacted).not.toContain(mixedSecret); + expect(azureRedacted).toContain('--azureblob-key="[REDACTED]"'); + + const s3Cmd = `rclone rcat --s3-secret-access-key=${quoted} :s3:bucket/file.gz`; + const s3Redacted = redactRcloneCredentials(s3Cmd); + expect(s3Redacted).not.toContain("ghiLEAKED"); + expect(s3Redacted).not.toContain(mixedSecret); + expect(s3Redacted).toContain('--s3-secret-access-key="[REDACTED]"'); + }); }); diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index f9cdcbf77c..9602ce2474 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -1,7 +1,12 @@ import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, + isAzureAccountKeyProvider, + isAzureDestinationType, + isAzureSasProvider, parseAzureConnectionString, + parseAzureSasUrl, + STORAGE_ACCOUNT_NAME_REQUIRED, } from "@dokploy/server/db/validations/destination"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { @@ -97,21 +102,25 @@ const destinationFormSchema = z }); } } else if (data.destinationType === "azure_blob") { - if (data.provider !== "account_key" && data.provider !== "sas_url") { + if ( + !isAzureAccountKeyProvider(data.provider) && + !isAzureSasProvider(data.provider) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Provider must be either 'account_key' or 'sas_url'", path: ["provider"], }); } - if (data.provider === "account_key") { - if (!data.accessKeyId || data.accessKeyId.trim().length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Storage Account Name is required when using Account Key", - path: ["accessKeyId"], - }); - } + if ( + isAzureAccountKeyProvider(data.provider) && + (!data.accessKeyId || data.accessKeyId.trim().length === 0) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: STORAGE_ACCOUNT_NAME_REQUIRED, + path: ["accessKeyId"], + }); } } }); @@ -167,7 +176,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { }); const destinationType = form.watch("destinationType"); - const isAzure = destinationType === "azure_blob"; + const isAzure = isAzureDestinationType(destinationType); const { fields, append, remove } = useFieldArray({ control: form.control, @@ -176,9 +185,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { useEffect(() => { if (destination) { - const isDestAzure = - destination.destinationType === "azure_blob" || - destination.destinationType === "az_bs"; + const isDestAzure = isAzureDestinationType(destination.destinationType); form.reset({ destinationType: isDestAzure ? "azure_blob" : "s3", @@ -234,12 +241,11 @@ export const HandleDestinations = ({ destinationId }: Props) => { await mutateAsync({ destinationType: data.destinationType, name: data.name, - provider: - data.destinationType === "azure_blob" - ? data.provider === "sas_url" - ? "sas_url" - : "account_key" - : data.provider || "AWS", + provider: isAzureDestinationType(data.destinationType) + ? isAzureSasProvider(data.provider) + ? "sas_url" + : "account_key" + : data.provider || "AWS", accessKey: data.accessKeyId || "", secretAccessKey: data.secretAccessKey, bucket: data.bucket, @@ -268,10 +274,17 @@ export const HandleDestinations = ({ destinationId }: Props) => { const handleTestConnection = async (serverId?: string) => { const triggerFields: Array = isAzure - ? ["name", "bucket", "secretAccessKey"] - : ["provider", "accessKeyId", "secretAccessKey", "bucket", "endpoint"]; - - if (isAzure && form.getValues("provider") === "account_key") { + ? ["name", "bucket", "secretAccessKey", "additionalFlags"] + : [ + "provider", + "accessKeyId", + "secretAccessKey", + "bucket", + "endpoint", + "additionalFlags", + ]; + + if (isAzure && isAzureAccountKeyProvider(form.getValues("provider"))) { triggerFields.push("accessKeyId"); } @@ -299,12 +312,11 @@ export const HandleDestinations = ({ destinationId }: Props) => { await testConnection({ destinationType: currentVals.destinationType, - provider: - currentVals.destinationType === "azure_blob" - ? currentVals.provider === "sas_url" - ? "sas_url" - : "account_key" - : currentVals.provider || "AWS", + provider: isAzureDestinationType(currentVals.destinationType) + ? isAzureSasProvider(currentVals.provider) + ? "sas_url" + : "account_key" + : currentVals.provider || "AWS", accessKey: currentVals.accessKeyId || "", secretAccessKey: currentVals.secretAccessKey, bucket: currentVals.bucket, @@ -375,8 +387,8 @@ export const HandleDestinations = ({ destinationId }: Props) => { form.setValue("destinationType", "s3"); const currentProvider = form.getValues("provider"); if ( - currentProvider === "account_key" || - currentProvider === "sas_url" + isAzureAccountKeyProvider(currentProvider) || + isAzureSasProvider(currentProvider) ) { form.setValue("provider", "AWS"); } @@ -397,8 +409,8 @@ export const HandleDestinations = ({ destinationId }: Props) => { form.setValue("destinationType", "azure_blob"); const currentProvider = form.getValues("provider"); if ( - currentProvider !== "account_key" && - currentProvider !== "sas_url" + !isAzureAccountKeyProvider(currentProvider) && + !isAzureSasProvider(currentProvider) ) { form.setValue("provider", "account_key"); } @@ -416,7 +428,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
- {/* Common: Destination Name */} { )} /> - {/* AZURE BLOB STORAGE FIELDS */} {isAzure ? ( <> { )} /> - {form.watch("provider") === "account_key" && ( + {isAzureAccountKeyProvider(form.watch("provider")) && (
@@ -492,7 +502,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
)} - {form.watch("provider") === "account_key" ? ( + {isAzureAccountKeyProvider(form.watch("provider")) ? ( <> { { + field.onChange(e); + const parsed = parseAzureSasUrl(e.target.value); + if (parsed.containerName) { + form.setValue("bucket", parsed.containerName, { + shouldValidate: true, + }); + } + }} /> @@ -608,7 +627,6 @@ export const HandleDestinations = ({ destinationId }: Props) => { /> ) : ( - /* S3-COMPATIBLE STORAGE FIELDS */ <> { setTestingDestinationId(destination.destinationId); try { - const isAzure = - destination.destinationType === "azure_blob" || - destination.destinationType === "az_bs"; + const isAzure = isAzureDestinationType(destination.destinationType); await testConnection({ destinationType: isAzure ? "azure_blob" : "s3", @@ -125,9 +124,9 @@ export const ShowDestinations = () => {
{data?.map((destination, index) => { - const isAzure = - destination.destinationType === "azure_blob" || - destination.destinationType === "az_bs"; + const isAzure = isAzureDestinationType( + destination.destinationType, + ); const isTesting = testingDestinationId === destination.destinationId; diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index a7a355144f..b175e93f3f 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -10,6 +10,11 @@ import { updateDestinationById, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + azureStorageAccountLabel, + isAzureDestinationType, + looksLikeMissingStorageTarget, +} from "@dokploy/server/db/validations/destination"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { quote } from "shell-quote"; @@ -74,7 +79,7 @@ export const destinationRouter = createTRPCRouter({ await execAsync(rcloneCommand); } } catch (error) { - const isAzure = input.destinationType === "azure_blob"; + const isAzure = isAzureDestinationType(input.destinationType); let message = error instanceof Error ? redactRcloneCredentials(error.message) @@ -82,10 +87,10 @@ export const destinationRouter = createTRPCRouter({ if ( error instanceof Error && - error.message.includes("directory not found") + looksLikeMissingStorageTarget(error.message) ) { message = isAzure - ? `Container "${input.bucket}" was not found in storage account "${input.accessKey || input.name}". Please make sure the container exists in Azure.` + ? `Container "${input.bucket}" was not found in storage account "${azureStorageAccountLabel(input)}". Please make sure the container exists in Azure.` : `Bucket "${input.bucket}" was not found. Please make sure the bucket exists in your storage provider.`; } diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 6630324c36..00ca3120c4 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -5,6 +5,10 @@ import { z } from "zod"; import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, + AZURE_AUTH_PROVIDERS, + isAzureAccountKeyProvider, + normalizeDestinationInput, + STORAGE_ACCOUNT_NAME_REQUIRED, } from "../validations/destination"; import { organization } from "./account"; import { backups } from "./backups"; @@ -41,7 +45,7 @@ export const destinationsRelations = relations( ); export const s3DestinationSchema = z.object({ - destinationType: z.literal("s3").default("s3"), + destinationType: z.literal("s3"), name: z.string().min(1, "Name is required"), provider: z.string().min(1, "Provider is required"), accessKey: z.string().min(1, "Access Key Id is required"), @@ -55,90 +59,53 @@ export const s3DestinationSchema = z.object({ serverId: z.string().optional(), }); -export const azureBlobDestinationSchema = z - .object({ - destinationType: z.literal("azure_blob").default("azure_blob"), - name: z.string().min(1, "Name is required"), - provider: z.enum(["account_key", "sas_url"]).default("account_key"), - accessKey: z.string().optional().default(""), - secretAccessKey: z.string().min(1, "Account Key or SAS URL is required"), - bucket: z.string().min(1, "Container name is required"), - region: z.string().optional().default(""), - endpoint: z.string().optional().default(""), - additionalFlags: z - .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) - .default([]), - serverId: z.string().optional(), - }) - .refine( - (data) => { - if (data.provider === "account_key") { - return !!data.accessKey && data.accessKey.trim().length > 0; - } - return true; - }, - { - message: "Storage Account Name is required when using Account Key", +const azureBlobDestinationObjectSchema = z.object({ + destinationType: z.literal("azure_blob"), + name: z.string().min(1, "Name is required"), + provider: z.enum(AZURE_AUTH_PROVIDERS).default("account_key"), + accessKey: z.string().optional().default(""), + secretAccessKey: z.string().min(1, "Account Key or SAS URL is required"), + bucket: z.string().min(1, "Container name is required"), + region: z.string().optional().default(""), + endpoint: z.string().optional().default(""), + additionalFlags: z + .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) + .default([]), + serverId: z.string().optional(), +}); + +const requireAzureAccountName = ( + data: { provider: string; accessKey?: string }, + ctx: z.RefinementCtx, +) => { + if ( + isAzureAccountKeyProvider(data.provider) && + (!data.accessKey || data.accessKey.trim().length === 0) + ) { + ctx.addIssue({ + code: "custom", + message: STORAGE_ACCOUNT_NAME_REQUIRED, path: ["accessKey"], - }, - ); + }); + } +}; -export const destinationSchema = z - .object({ - name: z.string().min(1, "Name is required"), - destinationType: z.preprocess( - (val) => (val === "az_bs" ? "azure_blob" : val), - z.enum(["s3", "azure_blob"]).default("s3"), - ), - provider: z.string().min(1, "Provider is required").default("AWS"), - accessKey: z.string().optional().default(""), - secretAccessKey: z - .string() - .min(1, "Secret Access Key / Account Key / SAS URL is required"), - bucket: z.string().min(1, "Bucket / Container is required"), - region: z.string().optional().default(""), - endpoint: z.string().optional().default(""), - additionalFlags: z - .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) - .default([]), - serverId: z.string().optional(), - }) - .superRefine((data, ctx) => { - if (data.destinationType === "s3") { - if (!data.accessKey || data.accessKey.trim().length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Access Key Id is required for S3", - path: ["accessKey"], - }); - } - if (!data.endpoint || data.endpoint.trim().length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Endpoint is required for S3", - path: ["endpoint"], - }); - } - } else if (data.destinationType === "azure_blob") { - if (data.provider !== "account_key" && data.provider !== "sas_url") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Provider must be either 'account_key' or 'sas_url'", - path: ["provider"], - }); - } - if ( - data.provider === "account_key" && - (!data.accessKey || data.accessKey.trim().length === 0) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Storage Account Name is required when using Account Key", - path: ["accessKey"], - }); +export const azureBlobDestinationSchema = + azureBlobDestinationObjectSchema.superRefine(requireAzureAccountName); + +export const destinationSchema = z.preprocess( + normalizeDestinationInput, + z + .discriminatedUnion("destinationType", [ + s3DestinationSchema, + azureBlobDestinationObjectSchema, + ]) + .superRefine((data, ctx) => { + if (data.destinationType === "azure_blob") { + requireAzureAccountName(data, ctx); } - } - }); + }), +); export const apiCreateDestination = destinationSchema; @@ -150,6 +117,20 @@ export const apiRemoveDestination = z.object({ destinationId: z.string().min(1), }); -export const apiUpdateDestination = destinationSchema.extend({ - destinationId: z.string().min(1), -}); +export const apiUpdateDestination = z.preprocess( + normalizeDestinationInput, + z + .discriminatedUnion("destinationType", [ + s3DestinationSchema.extend({ + destinationId: z.string().min(1), + }), + azureBlobDestinationObjectSchema.extend({ + destinationId: z.string().min(1), + }), + ]) + .superRefine((data, ctx) => { + if (data.destinationType === "azure_blob") { + requireAzureAccountName(data, ctx); + } + }), +); diff --git a/packages/server/src/db/validations/destination.ts b/packages/server/src/db/validations/destination.ts index d7605060ff..67e36876ae 100644 --- a/packages/server/src/db/validations/destination.ts +++ b/packages/server/src/db/validations/destination.ts @@ -2,6 +2,46 @@ export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/; export const ADDITIONAL_FLAG_ERROR = "Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)"; +export const AZURE_AUTH_PROVIDERS = ["account_key", "sas_url"] as const; +export type AzureAuthProvider = (typeof AZURE_AUTH_PROVIDERS)[number]; + +export const STORAGE_ACCOUNT_NAME_REQUIRED = + "Storage Account Name is required when using Account Key"; + +export const isAzureDestinationType = (value: string | null | undefined) => + value === "azure_blob" || value === "az_bs"; + +export const normalizeDestinationType = (value: unknown) => + value === "az_bs" ? "azure_blob" : value; + +export const isAzureSasProvider = (provider: string | null | undefined) => + provider === "sas_url"; + +export const isAzureAccountKeyProvider = ( + provider: string | null | undefined, +) => provider === "account_key"; + +export const looksLikeMissingStorageTarget = (message: string) => { + const lower = message.toLowerCase(); + return ( + lower.includes("directory not found") || lower.includes("containernotfound") + ); +}; + +export const normalizeDestinationInput = (val: unknown) => { + if (!val || typeof val !== "object" || Array.isArray(val)) { + return val; + } + const obj = { ...(val as Record) }; + if (obj.destinationType === "az_bs") { + obj.destinationType = "azure_blob"; + } + if (obj.destinationType == null || obj.destinationType === "") { + obj.destinationType = "s3"; + } + return obj; +}; + export interface ParsedAzureConnectionString { accountName: string; accountKey: string; @@ -77,3 +117,11 @@ export const parseAzureSasUrl = ( return {}; } }; + +export const azureStorageAccountLabel = (input: { + accessKey?: string | null; + secretAccessKey: string; +}) => + input.accessKey?.trim() || + parseAzureSasUrl(input.secretAccessKey).accountName || + "this storage account"; diff --git a/packages/server/src/utils/backups/redact.ts b/packages/server/src/utils/backups/redact.ts index 32966c2b8f..a007a41cbe 100644 --- a/packages/server/src/utils/backups/redact.ts +++ b/packages/server/src/utils/backups/redact.ts @@ -1,24 +1,6 @@ -/** - * Redacts S3 and Azure Blob Storage credentials from rclone command strings. - * - * Used to prevent credential leakage in structured logs and error output. - * Matches the flag formats: - * --s3-access-key-id="VALUE", --s3-secret-access-key="VALUE", - * --azureblob-key="VALUE", and --azureblob-sas-url="VALUE" - */ +const CREDENTIAL_FLAG_PATTERN = + /(--(?:s3-access-key-id|s3-secret-access-key|azureblob-key|azureblob-sas-url)=)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+)/g; + export const redactRcloneCredentials = (command: string): string => { - return command - .replace( - /(--s3-access-key-id=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, - '$1"[REDACTED]"', - ) - .replace( - /(--s3-secret-access-key=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, - '$1"[REDACTED]"', - ) - .replace(/(--azureblob-key=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, '$1"[REDACTED]"') - .replace( - /(--azureblob-sas-url=)(?:"[^"]*"|'[^']*'|[^\s]+)/g, - '$1"[REDACTED]"', - ); + return command.replace(CREDENTIAL_FLAG_PATTERN, '$1"[REDACTED]"'); }; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 2df23f0fc8..c9302e5774 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -1,3 +1,8 @@ +import { + isAzureDestinationType, + isAzureSasProvider, + STORAGE_ACCOUNT_NAME_REQUIRED, +} from "@dokploy/server/db/validations/destination"; import { logger } from "@dokploy/server/lib/logger"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Destination } from "@dokploy/server/services/destination"; @@ -88,29 +93,22 @@ export const getDestinationRemote = ( destinationType?: string | null; }, ): DestinationRemote => { - const destinationType = - destination.destinationType === "az_bs" || - destination.destinationType === "azure_blob" - ? "azure_blob" - : "s3"; + const destinationType = isAzureDestinationType(destination.destinationType) + ? "azure_blob" + : "s3"; if (destinationType === "azure_blob") { const rcloneFlags: string[] = []; - const isSas = - destination.provider === "sas_url" || - destination.secretAccessKey.startsWith("http://") || - destination.secretAccessKey.startsWith("https://"); - if (isSas) { + if (isAzureSasProvider(destination.provider)) { rcloneFlags.push( `--azureblob-sas-url=${quote([destination.secretAccessKey])}`, ); } else { - if (destination.accessKey) { - rcloneFlags.push( - `--azureblob-account=${quote([destination.accessKey])}`, - ); + if (!destination.accessKey?.trim()) { + throw new Error(STORAGE_ACCOUNT_NAME_REQUIRED); } + rcloneFlags.push(`--azureblob-account=${quote([destination.accessKey])}`); rcloneFlags.push( `--azureblob-key=${quote([destination.secretAccessKey])}`, ); diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index eb7cbd7de3..9e6477990a 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -24,8 +24,8 @@ export const restoreComposeBackup = async ( } const { serverId, appName, composeType } = compose; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; if (backupInput.metadata?.mongo) { diff --git a/packages/server/src/utils/restore/libsql.ts b/packages/server/src/utils/restore/libsql.ts index 4d98deb354..abc7b2c1dc 100644 --- a/packages/server/src/utils/restore/libsql.ts +++ b/packages/server/src/utils/restore/libsql.ts @@ -18,8 +18,8 @@ export const restoreLibsqlBackup = async ( try { const { appName, serverId } = libsql; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])}`; diff --git a/packages/server/src/utils/restore/mariadb.ts b/packages/server/src/utils/restore/mariadb.ts index cf52820305..e985fe5f7f 100644 --- a/packages/server/src/utils/restore/mariadb.ts +++ b/packages/server/src/utils/restore/mariadb.ts @@ -16,8 +16,8 @@ export const restoreMariadbBackup = async ( try { const { appName, serverId, databaseUser, databasePassword } = mariadb; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/mongo.ts b/packages/server/src/utils/restore/mongo.ts index 8018ecf7e3..9969bb7c1e 100644 --- a/packages/server/src/utils/restore/mongo.ts +++ b/packages/server/src/utils/restore/mongo.ts @@ -16,8 +16,8 @@ export const restoreMongoBackup = async ( try { const { appName, databasePassword, databaseUser, serverId } = mongo; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} ${quote([backupPath])}`; const command = getRestoreCommand({ diff --git a/packages/server/src/utils/restore/mysql.ts b/packages/server/src/utils/restore/mysql.ts index 5c871efc57..174bd08bd1 100644 --- a/packages/server/src/utils/restore/mysql.ts +++ b/packages/server/src/utils/restore/mysql.ts @@ -16,8 +16,8 @@ export const restoreMySqlBackup = async ( try { const { appName, databaseRootPassword, serverId } = mysql; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index e6d947760f..7f2a140370 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -16,8 +16,8 @@ export const restorePostgresBackup = async ( try { const { appName, databaseUser, serverId } = postgres; - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupInput.backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupInput.backupFile); const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index b30ab8d924..d0ed95b96e 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -16,8 +16,8 @@ export const restoreWebServerBackup = async ( return; } try { - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupFile}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupFile); const { BASE_PATH } = paths(); // Create a temporary directory outside of BASE_PATH diff --git a/packages/server/src/utils/volume-backups/restore.ts b/packages/server/src/utils/volume-backups/restore.ts index c20d71cd8c..38e1dd82cf 100644 --- a/packages/server/src/utils/volume-backups/restore.ts +++ b/packages/server/src/utils/volume-backups/restore.ts @@ -19,8 +19,8 @@ export const restoreVolume = async ( const destination = await findDestinationById(destinationId); const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName); - const { rcloneFlags, remoteBase } = getDestinationRemote(destination); - const backupPath = `${remoteBase}/${backupFileName}`; + const { rcloneFlags, getRemotePath } = getDestinationRemote(destination); + const backupPath = getRemotePath(backupFileName); // Command to download backup file from S3 const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} ${quote([backupPath])} ${quote([`${volumeBackupPath}/${backupFileName}`])}`;