diff --git a/.circleci/config.yml b/.circleci/config.yml index 25982d59..dd91309d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -99,14 +99,14 @@ workflows: only: - main - canary - - refactor/enhancement-languages + - 379-preview-deployment - build-arm64: filters: branches: only: - main - canary - - refactor/enhancement-languages + - 379-preview-deployment - combine-manifests: requires: - build-amd64 @@ -116,4 +116,4 @@ workflows: only: - main - canary - - refactor/enhancement-languages + - 379-preview-deployment diff --git a/LICENSE.MD b/LICENSE.MD index 59e9d822..8a508efb 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations ## Additional Terms for Specific Features -The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: +The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/apps/dokploy/LICENSE.MD b/apps/dokploy/LICENSE.MD index 59e9d822..8a508efb 100644 --- a/apps/dokploy/LICENSE.MD +++ b/apps/dokploy/LICENSE.MD @@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations ## Additional Terms for Specific Features -The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: +The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index 8b5c2f9c..8bc9fbcc 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -17,6 +17,7 @@ describe("createDomainLabels", () => { domainId: "", path: "/", createdAt: "", + previewDeploymentId: "", }; it("should create basic labels for web entrypoint", async () => { diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 9a6473af..c4b2ba8d 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -34,6 +34,15 @@ const baseApp: ApplicationNested = { registryUrl: "", branch: null, dockerBuildStage: "", + isPreviewDeploymentsActive: false, + previewBuildArgs: null, + previewCertificateType: "none", + previewEnv: null, + previewHttps: false, + previewPath: "/", + previewPort: 3000, + previewLimit: 0, + previewWildcard: "", project: { env: "", adminId: "", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index d7ad29ab..d05dda81 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -15,6 +15,15 @@ const baseApp: ApplicationNested = { dockerBuildStage: "", registryUrl: "", buildArgs: null, + isPreviewDeploymentsActive: false, + previewBuildArgs: null, + previewCertificateType: "none", + previewEnv: null, + previewHttps: false, + previewPath: "/", + previewPort: 3000, + previewLimit: 0, + previewWildcard: "", project: { env: "", adminId: "", @@ -96,6 +105,7 @@ const baseDomain: Domain = { composeId: "", domainType: "application", uniqueConfigKey: 1, + previewDeploymentId: "", }; const baseRedirect: Redirect = { diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index c2288bb8..a767350f 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -28,6 +28,7 @@ export const ShowDeployments = ({ applicationId }: Props) => { refetchInterval: 1000, }, ); + const [url, setUrl] = React.useState(""); useEffect(() => { setUrl(document.location.origin); diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index 9b38f1ce..c7b8d4bb 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -61,7 +61,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => { onSubmit={form.handleSubmit(onSubmit)} className="flex w-full flex-col gap-5 " > - + ; + +interface Props { + previewDeploymentId: string; + domainId?: string; + children: React.ReactNode; +} + +export const AddPreviewDomain = ({ + previewDeploymentId, + domainId = "", + children, +}: Props) => { + const [isOpen, setIsOpen] = useState(false); + const utils = api.useUtils(); + const { data, refetch } = api.domain.one.useQuery( + { + domainId, + }, + { + enabled: !!domainId, + }, + ); + + const { data: previewDeployment } = api.previewDeployment.one.useQuery( + { + previewDeploymentId, + }, + { + enabled: !!previewDeploymentId, + }, + ); + + const { mutateAsync, isError, error, isLoading } = domainId + ? api.domain.update.useMutation() + : api.domain.create.useMutation(); + + const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } = + api.domain.generateDomain.useMutation(); + + const form = useForm({ + resolver: zodResolver(domain), + }); + + useEffect(() => { + if (data) { + form.reset({ + ...data, + /* Convert null to undefined */ + path: data?.path || undefined, + port: data?.port || undefined, + }); + } + + if (!domainId) { + form.reset({}); + } + }, [form, form.reset, data, isLoading]); + + const dictionary = { + success: domainId ? "Domain Updated" : "Domain Created", + error: domainId + ? "Error to update the domain" + : "Error to create the domain", + submit: domainId ? "Update" : "Create", + dialogDescription: domainId + ? "In this section you can edit a domain" + : "In this section you can add domains", + }; + + const onSubmit = async (data: Domain) => { + await mutateAsync({ + domainId, + previewDeploymentId, + ...data, + }) + .then(async () => { + toast.success(dictionary.success); + await utils.previewDeployment.all.invalidate({ + applicationId: previewDeployment?.applicationId, + }); + + if (domainId) { + refetch(); + } + setIsOpen(false); + }) + .catch(() => { + toast.error(dictionary.error); + }); + }; + return ( + + + {children} + + + + Domain + {dictionary.dialogDescription} + + {isError && {error?.message}} + +
+ +
+
+ ( + + Host +
+ + + + + + + + + +

Generate traefik.me domain

+
+
+
+
+ + +
+ )} + /> + + { + return ( + + Path + + + + + + ); + }} + /> + + { + return ( + + Container Port + + + + + + ); + }} + /> + + ( + +
+ HTTPS + + Automatically provision SSL Certificate. + + +
+ + + +
+ )} + /> + + {form.getValues().https && ( + ( + + Certificate + + + + )} + /> + )} +
+
+
+ + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx new file mode 100644 index 00000000..4eb2107f --- /dev/null +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -0,0 +1,87 @@ +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +import type { RouterOutputs } from "@/utils/api"; +import { useState } from "react"; +import { ShowDeployment } from "../deployments/show-deployment"; + +interface Props { + deployments: RouterOutputs["deployment"]["all"]; + serverId?: string; +} + +export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { + const [activeLog, setActiveLog] = useState(null); + const [isOpen, setIsOpen] = useState(false); + return ( + + + + + + + Preview Builds + + See all the preview builds for this application on this Pull Request + + +
+ {deployments?.map((deployment) => ( +
+
+ + {deployment.status} + + + + + {deployment.title} + + {deployment.description && ( + + {deployment.description} + + )} +
+
+
+ +
+ + +
+
+ ))} +
+
+ setActiveLog(null)} + logPath={activeLog} + /> +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx new file mode 100644 index 00000000..45451e78 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -0,0 +1,212 @@ +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { Pencil, RocketIcon } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { ShowDeployment } from "../deployments/show-deployment"; +import Link from "next/link"; +import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { AddPreviewDomain } from "./add-preview-domain"; +import { GithubIcon } from "@/components/icons/data-tools-icons"; +import { ShowPreviewSettings } from "./show-preview-settings"; +import { ShowPreviewBuilds } from "./show-preview-builds"; + +interface Props { + applicationId: string; +} + +export const ShowPreviewDeployments = ({ applicationId }: Props) => { + const [activeLog, setActiveLog] = useState(null); + const { data } = api.application.one.useQuery({ applicationId }); + + const { mutateAsync: deletePreviewDeployment, isLoading } = + api.previewDeployment.delete.useMutation(); + const { data: previewDeployments, refetch: refetchPreviewDeployments } = + api.previewDeployment.all.useQuery( + { applicationId }, + { + enabled: !!applicationId, + }, + ); + // const [url, setUrl] = React.useState(""); + // useEffect(() => { + // setUrl(document.location.origin); + // }, []); + + return ( + + +
+ Preview Deployments + See all the preview deployments +
+ {data?.isPreviewDeploymentsActive && ( + + )} +
+ + {data?.isPreviewDeploymentsActive ? ( + <> +
+ + Preview deployments are a way to test your application before it + is deployed to production. It will create a new deployment for + each pull request you create. + +
+ {data?.previewDeployments?.length === 0 ? ( +
+ + + No preview deployments found + +
+ ) : ( +
+ {previewDeployments?.map((previewDeployment) => { + const { deployments, domain } = previewDeployment; + + return ( +
+
+
+ {deployments?.length === 0 ? ( +
+ + No deployments found + +
+ ) : ( +
+ + {previewDeployment?.pullRequestTitle} + + +
+ )} +
+ {previewDeployment?.pullRequestTitle && ( +
+ + Title: {previewDeployment?.pullRequestTitle} + +
+ )} + + {previewDeployment?.pullRequestURL && ( +
+ + + Pull Request URL + +
+ )} +
+
+ Domain +
+ + {domain?.host} + + + + +
+
+
+ +
+ {previewDeployment?.createdAt && ( +
+ +
+ )} + + + + + + + { + deletePreviewDeployment({ + previewDeploymentId: + previewDeployment.previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }} + > + + +
+
+
+ ); + })} +
+ )} + + ) : ( +
+ + + Preview deployments are disabled for this application, please + enable it + + +
+ )} +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx new file mode 100644 index 00000000..6e56bbdd --- /dev/null +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -0,0 +1,351 @@ +import { api } from "@/utils/api"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input, NumberInput } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Secrets } from "@/components/ui/secrets"; +import { toast } from "sonner"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const schema = z.object({ + env: z.string(), + buildArgs: z.string(), + wildcardDomain: z.string(), + port: z.number(), + previewLimit: z.number(), + previewHttps: z.boolean(), + previewPath: z.string(), + previewCertificateType: z.enum(["letsencrypt", "none"]), +}); + +type Schema = z.infer; + +interface Props { + applicationId: string; +} + +export const ShowPreviewSettings = ({ applicationId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [isEnabled, setIsEnabled] = useState(false); + const { mutateAsync: updateApplication, isLoading } = + api.application.update.useMutation(); + + const { data, refetch } = api.application.one.useQuery({ applicationId }); + + const form = useForm({ + defaultValues: { + env: "", + wildcardDomain: "*.traefik.me", + port: 3000, + previewLimit: 3, + previewHttps: false, + previewPath: "/", + previewCertificateType: "none", + }, + resolver: zodResolver(schema), + }); + + const previewHttps = form.watch("previewHttps"); + + useEffect(() => { + setIsEnabled(data?.isPreviewDeploymentsActive || false); + }, [data?.isPreviewDeploymentsActive]); + + useEffect(() => { + if (data) { + form.reset({ + env: data.previewEnv || "", + buildArgs: data.previewBuildArgs || "", + wildcardDomain: data.previewWildcard || "*.traefik.me", + port: data.previewPort || 3000, + previewLimit: data.previewLimit || 3, + previewHttps: data.previewHttps || false, + previewPath: data.previewPath || "/", + previewCertificateType: data.previewCertificateType || "none", + }); + } + }, [data]); + + const onSubmit = async (formData: Schema) => { + updateApplication({ + previewEnv: formData.env, + previewBuildArgs: formData.buildArgs, + previewWildcard: formData.wildcardDomain, + previewPort: formData.port, + applicationId, + previewLimit: formData.previewLimit, + previewHttps: formData.previewHttps, + previewPath: formData.previewPath, + previewCertificateType: formData.previewCertificateType, + }) + .then(() => { + toast.success("Preview Deployments settings updated"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; + return ( +
+ + + + + + + Preview Deployment Settings + + Adjust the settings for preview deployments of this application, + including environment variables, build options, and deployment + rules. + + +
+
+ +
+ ( + + Wildcard Domain + + + + + + )} + /> + ( + + Preview Path + + + + + + )} + /> + ( + + Port + + + + + + )} + /> + ( + + Preview Limit + {/* + Set the limit of preview deployments that can be + created for this app. + */} + + + + + + )} + /> + ( + +
+ HTTPS + + Automatically provision SSL Certificate. + + +
+ + + +
+ )} + /> + {previewHttps && ( + ( + + Certificate + + + + )} + /> + )} +
+
+
+
+ + Enable preview deployments + + + Enable or disable preview deployments for this + application. + +
+ { + updateApplication({ + isPreviewDeploymentsActive: checked, + applicationId, + }) + .then(() => { + refetch(); + toast.success("Preview deployments enabled"); + }) + .catch((error) => { + toast.error(error.message); + }); + }} + /> +
+
+ + ( + + + + {/* */} + + + + )} + /> + {data?.buildType === "dockerfile" && ( + + Available only at build-time. See documentation  + + here + + . + + } + placeholder="NPM_TOKEN=xyz" + /> + )} + + +
+ + + + +
+
+ {/* */} +
+ ); +}; diff --git a/apps/dokploy/components/ui/secrets.tsx b/apps/dokploy/components/ui/secrets.tsx index a596ef1b..5669b051 100644 --- a/apps/dokploy/components/ui/secrets.tsx +++ b/apps/dokploy/components/ui/secrets.tsx @@ -29,7 +29,7 @@ export const Secrets = (props: Props) => { return ( <> - +
{props.title} {props.description} @@ -47,7 +47,7 @@ export const Secrets = (props: Props) => { )} - + statement-breakpoint +CREATE TABLE IF NOT EXISTS "preview_deployments" ( + "previewDeploymentId" text PRIMARY KEY NOT NULL, + "branch" text NOT NULL, + "pullRequestId" text NOT NULL, + "pullRequestNumber" text NOT NULL, + "pullRequestURL" text NOT NULL, + "pullRequestTitle" text NOT NULL, + "pullRequestCommentId" text NOT NULL, + "previewStatus" "applicationStatus" DEFAULT 'idle' NOT NULL, + "appName" text NOT NULL, + "applicationId" text NOT NULL, + "domainId" text, + "createdAt" text NOT NULL, + "expiresAt" text, + CONSTRAINT "preview_deployments_appName_unique" UNIQUE("appName") +); +--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewEnv" text;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewBuildArgs" text;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewWildcard" text;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewPort" integer DEFAULT 3000;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewHttps" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewPath" text DEFAULT '/';--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "certificateType" "certificateType" DEFAULT 'none' NOT NULL;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "previewLimit" integer DEFAULT 3;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "isPreviewDeploymentsActive" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "domain" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint +ALTER TABLE "deployment" ADD COLUMN "isPreviewDeployment" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "deployment" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_domainId_domain_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domain"("domainId") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "domain" ADD CONSTRAINT "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "deployment" ADD CONSTRAINT "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/dokploy/drizzle/meta/0049_snapshot.json b/apps/dokploy/drizzle/meta/0049_snapshot.json new file mode 100644 index 00000000..7ef4e679 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0049_snapshot.json @@ -0,0 +1,4226 @@ +{ + "id": "db518175-259d-4f4a-b6d0-fe95067bba61", + "prevId": "928417c8-2e7b-43ba-bc19-44b4d70107f1", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "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": "'/'" + }, + "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 + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_adminId_admin_adminId_fk": { + "name": "user_adminId_admin_adminId_fk", + "tableFrom": "user", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_authId_auth_id_fk": { + "name": "user_authId_auth_id_fk", + "tableFrom": "user", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.admin": { + "name": "admin", + "schema": "", + "columns": { + "adminId": { + "name": "adminId", + "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'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableLogRotation": { + "name": "enableLogRotation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "admin_authId_auth_id_fk": { + "name": "admin_authId_auth_id_fk", + "tableFrom": "admin", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.auth": { + "name": "auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rol": { + "name": "rol", + "type": "Roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_email_unique": { + "name": "auth_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "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 + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_adminId_admin_adminId_fk": { + "name": "project_adminId_admin_adminId_fk", + "tableFrom": "project", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "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 + } + }, + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "destination_adminId_admin_adminId_fk": { + "name": "destination_adminId_admin_adminId_fk", + "tableFrom": "destination", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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 + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_adminId_admin_adminId_fk": { + "name": "certificate_adminId_admin_adminId_fk", + "tableFrom": "certificate", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "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" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_auth_id_fk": { + "name": "session_user_id_auth_id_fk", + "tableFrom": "session", + "tableTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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": {} + }, + "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" + ] + } + } + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "''" + }, + "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 + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_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": {} + }, + "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'" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_adminId_admin_adminId_fk": { + "name": "registry_adminId_admin_adminId_fk", + "tableFrom": "registry", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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": {} + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "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_adminId_admin_adminId_fk": { + "name": "notification_adminId_admin_adminId_fk", + "tableFrom": "notification", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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": {} + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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 + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_adminId_admin_adminId_fk": { + "name": "ssh-key_adminId_admin_adminId_fk", + "tableFrom": "ssh-key", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "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 + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_adminId_admin_adminId_fk": { + "name": "git_provider_adminId_admin_adminId_fk", + "tableFrom": "git_provider", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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": {} + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "server_adminId_admin_adminId_fk": { + "name": "server_adminId_admin_adminId_fk", + "tableFrom": "server", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "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": {} + }, + "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" + ] + } + } + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "drop" + ] + }, + "public.Roles": { + "name": "Roles", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + } + }, + "schemas": {}, + "_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 385ee369..d952323d 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -344,6 +344,13 @@ "when": 1733599163710, "tag": "0048_flat_expediter", "breakpoints": true + }, + { + "idx": 49, + "version": "6", + "when": 1733628762978, + "tag": "0049_dark_leopardon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index f1e9505f..11439f58 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -11,7 +11,8 @@ "build-next": "next build", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", "reset-password": "node -r dotenv/config dist/reset-password.mjs", - "dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", + "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", + "dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts", "migration:run": "tsx -r dotenv/config migration.ts", diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index e5ed154f..0e0a6c82 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -3,11 +3,19 @@ import { applications, compose, github } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; -import { IS_CLOUD } from "@dokploy/server"; +import { + createPreviewDeployment, + type Domain, + findPreviewDeploymentByApplicationId, + findPreviewDeploymentsByPullRequestId, + IS_CLOUD, + removePreviewDeployment, +} from "@dokploy/server"; import { Webhooks } from "@octokit/webhooks"; import { and, eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; import { extractCommitMessage, extractHash } from "./[refreshToken]"; +import { generateRandomDomain } from "@/templates/utils"; export default async function handler( req: NextApiRequest, @@ -53,36 +61,183 @@ export default async function handler( return; } - if (req.headers["x-github-event"] !== "push") { - res.status(400).json({ message: "We only accept push events" }); + if ( + req.headers["x-github-event"] !== "push" && + req.headers["x-github-event"] !== "pull_request" + ) { + res + .status(400) + .json({ message: "We only accept push events or pull_request events" }); return; } - try { - const branchName = githubBody?.ref?.replace("refs/heads/", ""); + if (req.headers["x-github-event"] === "push") { + try { + const branchName = githubBody?.ref?.replace("refs/heads/", ""); + const repository = githubBody?.repository?.name; + const deploymentTitle = extractCommitMessage(req.headers, req.body); + const deploymentHash = extractHash(req.headers, req.body); + const owner = githubBody?.repository?.owner?.name; + + const apps = await db.query.applications.findMany({ + where: and( + eq(applications.sourceType, "github"), + eq(applications.autoDeploy, true), + eq(applications.branch, branchName), + eq(applications.repository, repository), + eq(applications.owner, owner), + ), + }); + + for (const app of apps) { + const jobData: DeploymentJob = { + applicationId: app.applicationId as string, + titleLog: deploymentTitle, + descriptionLog: `Hash: ${deploymentHash}`, + type: "deploy", + applicationType: "application", + server: !!app.serverId, + }; + + if (IS_CLOUD && app.serverId) { + jobData.serverId = app.serverId; + await deploy(jobData); + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + } + + const composeApps = await db.query.compose.findMany({ + where: and( + eq(compose.sourceType, "github"), + eq(compose.autoDeploy, true), + eq(compose.branch, branchName), + eq(compose.repository, repository), + eq(compose.owner, owner), + ), + }); + + for (const composeApp of composeApps) { + const jobData: DeploymentJob = { + composeId: composeApp.composeId as string, + titleLog: deploymentTitle, + type: "deploy", + applicationType: "compose", + descriptionLog: `Hash: ${deploymentHash}`, + server: !!composeApp.serverId, + }; + + if (IS_CLOUD && composeApp.serverId) { + jobData.serverId = composeApp.serverId; + await deploy(jobData); + return true; + } + + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + } + + const totalApps = apps.length + composeApps.length; + const emptyApps = totalApps === 0; + + if (emptyApps) { + res.status(200).json({ message: "No apps to deploy" }); + return; + } + res.status(200).json({ message: `Deployed ${totalApps} apps` }); + } catch (error) { + res.status(400).json({ message: "Error To Deploy Application", error }); + } + } else if (req.headers["x-github-event"] === "pull_request") { + const prId = githubBody?.pull_request?.id; + + if (githubBody?.action === "closed") { + const previewDeploymentResult = + await findPreviewDeploymentsByPullRequestId(prId); + + if (previewDeploymentResult.length > 0) { + for (const previewDeployment of previewDeploymentResult) { + try { + await removePreviewDeployment( + previewDeployment.previewDeploymentId, + ); + } catch (error) { + console.log(error); + } + } + } + res.status(200).json({ message: "Preview Deployment Closed" }); + return; + } + // opened or synchronize or reopened const repository = githubBody?.repository?.name; - const deploymentTitle = extractCommitMessage(req.headers, req.body); - const deploymentHash = extractHash(req.headers, req.body); - const owner = githubBody?.repository?.owner?.name; + const deploymentHash = githubBody?.pull_request?.head?.sha; + const branch = githubBody?.pull_request?.base?.ref; + const owner = githubBody?.repository?.owner?.login; const apps = await db.query.applications.findMany({ where: and( eq(applications.sourceType, "github"), - eq(applications.autoDeploy, true), - eq(applications.branch, branchName), eq(applications.repository, repository), + eq(applications.branch, branch), + eq(applications.isPreviewDeploymentsActive, true), eq(applications.owner, owner), ), + with: { + previewDeployments: true, + }, }); + const prBranch = githubBody?.pull_request?.head?.ref; + + const prNumber = githubBody?.pull_request?.number; + const prTitle = githubBody?.pull_request?.title; + const prURL = githubBody?.pull_request?.html_url; + for (const app of apps) { + const previewLimit = app?.previewLimit || 0; + if (app?.previewDeployments?.length > previewLimit) { + continue; + } + const previewDeploymentResult = + await findPreviewDeploymentByApplicationId(app.applicationId, prId); + + let previewDeploymentId = + previewDeploymentResult?.previewDeploymentId || ""; + + if (!previewDeploymentResult) { + const previewDeployment = await createPreviewDeployment({ + applicationId: app.applicationId as string, + branch: prBranch, + pullRequestId: prId, + pullRequestNumber: prNumber, + pullRequestTitle: prTitle, + pullRequestURL: prURL, + }); + previewDeploymentId = previewDeployment.previewDeploymentId; + } + const jobData: DeploymentJob = { applicationId: app.applicationId as string, - titleLog: deploymentTitle, + titleLog: "Preview Deployment", descriptionLog: `Hash: ${deploymentHash}`, type: "deploy", - applicationType: "application", + applicationType: "application-preview", server: !!app.serverId, + previewDeploymentId, }; if (IS_CLOUD && app.serverId) { @@ -99,52 +254,8 @@ export default async function handler( }, ); } - - const composeApps = await db.query.compose.findMany({ - where: and( - eq(compose.sourceType, "github"), - eq(compose.autoDeploy, true), - eq(compose.branch, branchName), - eq(compose.repository, repository), - eq(compose.owner, owner), - ), - }); - - for (const composeApp of composeApps) { - const jobData: DeploymentJob = { - composeId: composeApp.composeId as string, - titleLog: deploymentTitle, - type: "deploy", - applicationType: "compose", - descriptionLog: `Hash: ${deploymentHash}`, - server: !!composeApp.serverId, - }; - - if (IS_CLOUD && composeApp.serverId) { - jobData.serverId = composeApp.serverId; - await deploy(jobData); - return true; - } - - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - }, - ); - } - - const totalApps = apps.length + composeApps.length; - const emptyApps = totalApps === 0; - - if (emptyApps) { - res.status(200).json({ message: "No apps to deploy" }); - return; - } - res.status(200).json({ message: `Deployed ${totalApps} apps` }); - } catch (error) { - res.status(400).json({ message: "Error To Deploy Application", error }); + return res.status(200).json({ message: "Apps Deployed" }); } + + return res.status(400).json({ message: "No Actions matched" }); } diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index 156e1973..bcbd4b78 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -12,6 +12,7 @@ import { ShowDomains } from "@/components/dashboard/application/domains/show-dom import { ShowEnvironment } from "@/components/dashboard/application/environment/show"; import { ShowGeneralApplication } from "@/components/dashboard/application/general/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments"; import { UpdateApplication } from "@/components/dashboard/application/update-application"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ProjectLayout } from "@/components/layouts/project-layout"; @@ -51,7 +52,8 @@ type TabState = | "advanced" | "deployments" | "domains" - | "monitoring"; + | "monitoring" + | "preview-deployments"; const Service = ( props: InferGetServerSidePropsType, @@ -191,8 +193,8 @@ const Service = (
General @@ -202,6 +204,9 @@ const Service = ( )} Logs Deployments + + Preview Deployments + Domains Advanced @@ -244,6 +249,11 @@ const Service = (
+ +
+ +
+
diff --git a/apps/dokploy/public/templates/budibase.svg b/apps/dokploy/public/templates/budibase.svg new file mode 100644 index 00000000..26d09cc9 --- /dev/null +++ b/apps/dokploy/public/templates/budibase.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/dokploy/public/templates/drawio.svg b/apps/dokploy/public/templates/drawio.svg new file mode 100644 index 00000000..07909528 --- /dev/null +++ b/apps/dokploy/public/templates/drawio.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/dokploy/public/templates/kimai.svg b/apps/dokploy/public/templates/kimai.svg new file mode 100644 index 00000000..8b2a6c1e --- /dev/null +++ b/apps/dokploy/public/templates/kimai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 1b67d350..85eb9763 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -31,6 +31,7 @@ import { settingsRouter } from "./routers/settings"; import { sshRouter } from "./routers/ssh-key"; import { stripeRouter } from "./routers/stripe"; import { userRouter } from "./routers/user"; +import { previewDeploymentRouter } from "./routers/preview-deployment"; /** * This is the primary router for your server. @@ -55,6 +56,7 @@ export const appRouter = createTRPCRouter({ destination: destinationRouter, backup: backupRouter, deployment: deploymentRouter, + previewDeployment: previewDeploymentRouter, mounts: mountRouter, certificates: certificateRouter, settings: settingsRouter, diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 3c94fc93..b0a1000c 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -13,6 +13,7 @@ import { findDomainById, findDomainsByApplicationId, findDomainsByComposeId, + findPreviewDeploymentById, generateTraefikMeDomain, manageDomain, removeDomain, @@ -108,12 +109,33 @@ export const domainRouter = createTRPCRouter({ message: "You are not authorized to access this compose", }); } + } else if (currentDomain.previewDeploymentId) { + const newPreviewDeployment = await findPreviewDeploymentById( + currentDomain.previewDeploymentId, + ); + if ( + newPreviewDeployment.application.project.adminId !== ctx.user.adminId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this preview deployment", + }); + } } const result = await updateDomainById(input.domainId, input); const domain = await findDomainById(input.domainId); if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); await manageDomain(application, domain); + } else if (domain.previewDeploymentId) { + const previewDeployment = await findPreviewDeploymentById( + domain.previewDeploymentId, + ); + const application = await findApplicationById( + previewDeployment.applicationId, + ); + application.appName = previewDeployment.appName; + await manageDomain(application, domain); } return result; }), diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts new file mode 100644 index 00000000..74b8461a --- /dev/null +++ b/apps/dokploy/server/api/routers/preview-deployment.ts @@ -0,0 +1,54 @@ +import { apiFindAllByApplication } from "@/server/db/schema"; +import { + findApplicationById, + findPreviewDeploymentById, + findPreviewDeploymentsByApplicationId, + removePreviewDeployment, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const previewDeploymentRouter = createTRPCRouter({ + all: protectedProcedure + .input(apiFindAllByApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await findPreviewDeploymentsByApplicationId(input.applicationId); + }), + delete: protectedProcedure + .input(z.object({ previewDeploymentId: z.string() })) + .mutation(async ({ input, ctx }) => { + const previewDeployment = await findPreviewDeploymentById( + input.previewDeploymentId, + ); + if (previewDeployment.application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this preview deployment", + }); + } + await removePreviewDeployment(input.previewDeploymentId); + return true; + }), + one: protectedProcedure + .input(z.object({ previewDeploymentId: z.string() })) + .query(async ({ input, ctx }) => { + const previewDeployment = await findPreviewDeploymentById( + input.previewDeploymentId, + ); + if (previewDeployment.application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this preview deployment", + }); + } + return previewDeployment; + }), +}); diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts index 08e0c9a1..9ff8a157 100644 --- a/apps/dokploy/server/queues/deployments-queue.ts +++ b/apps/dokploy/server/queues/deployments-queue.ts @@ -1,14 +1,17 @@ import { deployApplication, deployCompose, + deployPreviewApplication, deployRemoteApplication, deployRemoteCompose, + deployRemotePreviewApplication, rebuildApplication, rebuildCompose, rebuildRemoteApplication, rebuildRemoteCompose, updateApplicationStatus, updateCompose, + updatePreviewDeployment, } from "@dokploy/server"; import { type Job, Worker } from "bullmq"; import type { DeploymentJob } from "./queue-types"; @@ -18,8 +21,11 @@ export const deploymentWorker = new Worker( "deployments", async (job: Job) => { try { + console.log(job.data); + if (job.data.applicationType === "application") { await updateApplicationStatus(job.data.applicationId, "running"); + if (job.data.server) { if (job.data.type === "redeploy") { await rebuildRemoteApplication({ @@ -83,6 +89,29 @@ export const deploymentWorker = new Worker( }); } } + } else if (job.data.applicationType === "application-preview") { + await updatePreviewDeployment(job.data.previewDeploymentId, { + previewStatus: "running", + }); + if (job.data.server) { + if (job.data.type === "deploy") { + await deployRemotePreviewApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + previewDeploymentId: job.data.previewDeploymentId, + }); + } + } else { + if (job.data.type === "deploy") { + await deployPreviewApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + previewDeploymentId: job.data.previewDeploymentId, + }); + } + } } } catch (error) { console.log("Error", error); diff --git a/apps/dokploy/server/queues/queue-types.ts b/apps/dokploy/server/queues/queue-types.ts index f467836a..ef8df694 100644 --- a/apps/dokploy/server/queues/queue-types.ts +++ b/apps/dokploy/server/queues/queue-types.ts @@ -16,6 +16,16 @@ type DeployJob = type: "deploy" | "redeploy"; applicationType: "compose"; serverId?: string; + } + | { + applicationId: string; + titleLog: string; + descriptionLog: string; + server?: boolean; + type: "deploy"; + applicationType: "application-preview"; + previewDeploymentId: string; + serverId?: string; }; export type DeploymentJob = DeployJob; diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index b65446f8..b13e5df5 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -24,7 +24,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal"; config({ path: ".env" }); const PORT = Number.parseInt(process.env.PORT || "3000", 10); const dev = process.env.NODE_ENV !== "production"; -const app = next({ dev, turbopack: dev }); +const app = next({ dev, turbopack: process.env.TURBOPACK === "1" }); const handle = app.getRequestHandler(); void app.prepare().then(async () => { try { diff --git a/apps/dokploy/templates/budibase/docker-compose.yml b/apps/dokploy/templates/budibase/docker-compose.yml new file mode 100644 index 00000000..3f82de3e --- /dev/null +++ b/apps/dokploy/templates/budibase/docker-compose.yml @@ -0,0 +1,199 @@ +services: + apps: + image: budibase.docker.scarf.sh/budibase/apps:3.2.25 + restart: unless-stopped + networks: + - dokploy-network + environment: + SELF_HOSTED: 1 + LOG_LEVEL: info + PORT: 4002 + INTERNAL_API_KEY: ${BB_INTERNAL_API_KEY} + API_ENCRYPTION_KEY: ${BB_API_ENCRYPTION_KEY} + JWT_SECRET: ${BB_JWT_SECRET} + MINIO_ACCESS_KEY: ${BB_MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${BB_MINIO_SECRET_KEY} + MINIO_URL: http://minio:9000 + REDIS_URL: redis:6379 + REDIS_PASSWORD: ${BB_REDIS_PASSWORD} + WORKER_URL: http://worker:4003 + COUCH_DB_USERNAME: budibase + COUCH_DB_PASSWORD: ${BB_COUCHDB_PASSWORD} + COUCH_DB_URL: http://budibase:${BB_COUCHDB_PASSWORD}@couchdb:5984 + BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT:-PRODUCTION} + ENABLE_ANALYTICS: ${ENABLE_ANALYTICS:-true} + BB_ADMIN_USER_EMAIL: '' + BB_ADMIN_USER_PASSWORD: '' + depends_on: + worker: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - '--spider' + - '-qO-' + - 'http://localhost:4002/health' + interval: 15s + timeout: 15s + retries: 5 + start_period: 10s + worker: + image: budibase.docker.scarf.sh/budibase/worker:3.2.25 + restart: unless-stopped + networks: + - dokploy-network + environment: + SELF_HOSTED: 1 + LOG_LEVEL: info + PORT: 4003 + CLUSTER_PORT: 10000 + INTERNAL_API_KEY: ${BB_INTERNAL_API_KEY} + API_ENCRYPTION_KEY: ${BB_API_ENCRYPTION_KEY} + JWT_SECRET: ${BB_JWT_SECRET} + MINIO_ACCESS_KEY: ${BB_MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${BB_MINIO_SECRET_KEY} + APPS_URL: http://apps:4002 + MINIO_URL: http://minio:9000 + REDIS_URL: redis:6379 + REDIS_PASSWORD: ${BB_REDIS_PASSWORD} + COUCH_DB_USERNAME: budibase + COUCH_DB_PASSWORD: ${BB_COUCHDB_PASSWORD} + COUCH_DB_URL: http://budibase:${BB_COUCHDB_PASSWORD}@couchdb:5984 + BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT:-PRODUCTION} + ENABLE_ANALYTICS: ${ENABLE_ANALYTICS:-true} + depends_on: + redis: + condition: service_healthy + minio: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - '--spider' + - '-qO-' + - 'http://localhost:4003/health' + interval: 15s + timeout: 15s + retries: 5 + start_period: 10s + minio: + image: minio/minio:RELEASE.2024-11-07T00-52-20Z + restart: unless-stopped + networks: + - dokploy-network + volumes: + - 'minio_data:/data' + environment: + MINIO_ROOT_USER: ${BB_MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${BB_MINIO_SECRET_KEY} + MINIO_BROWSER: off + command: 'server /data --console-address ":9001"' + healthcheck: + test: + - CMD + - curl + - '-f' + - 'http://localhost:9000/minio/health/live' + interval: 30s + timeout: 20s + retries: 3 + proxy: + image: budibase/proxy:3.2.25 + restart: unless-stopped + networks: + - dokploy-network + environment: + PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND: 10 + PROXY_RATE_LIMIT_API_PER_SECOND: 20 + APPS_UPSTREAM_URL: http://apps:4002 + WORKER_UPSTREAM_URL: http://worker:4003 + MINIO_UPSTREAM_URL: http://minio:9000 + COUCHDB_UPSTREAM_URL: http://couchdb:5984 + WATCHTOWER_UPSTREAM_URL: http://watchtower:8080 + RESOLVER: 127.0.0.11 + depends_on: + minio: + condition: service_healthy + worker: + condition: service_healthy + apps: + condition: service_healthy + couchdb: + condition: service_healthy + healthcheck: + test: + - CMD + - curl + - '-f' + - 'http://localhost:10000/' + interval: 15s + timeout: 15s + retries: 5 + start_period: 10s + couchdb: + image: budibase/couchdb:v3.3.3 + restart: unless-stopped + networks: + - dokploy-network + environment: + COUCHDB_USER: budibase + COUCHDB_PASSWORD: ${BB_COUCHDB_PASSWORD} + TARGETBUILD: docker-compose + healthcheck: + test: + - CMD + - curl + - '-f' + - 'http://localhost:5984/' + interval: 15s + timeout: 15s + retries: 5 + start_period: 10s + volumes: + - 'couchdb3_data:/opt/couchdb/data' + redis: + image: redis:7.2-alpine + networks: + - dokploy-network + restart: unless-stopped + command: 'redis-server --requirepass "${BB_REDIS_PASSWORD}"' + volumes: + - 'redis_data:/data' + healthcheck: + test: + - CMD + - redis-cli + - '-a' + - ${BB_REDIS_PASSWORD} + - ping + interval: 15s + timeout: 15s + retries: 5 + start_period: 10s + watchtower: + restart: unless-stopped + networks: + - dokploy-network + image: containrrr/watchtower:1.7.1 + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' + command: '--debug --http-api-update bbapps bbworker bbproxy' + environment: + WATCHTOWER_HTTP_API: true + WATCHTOWER_HTTP_API_TOKEN: ${BB_WATCHTOWER_PASSWORD} + WATCHTOWER_CLEANUP: true + labels: + - com.centurylinklabs.watchtower.enable=false + +networks: + dokploy-network: + external: true + +volumes: + minio_data: + couchdb3_data: + redis_data: \ No newline at end of file diff --git a/apps/dokploy/templates/budibase/index.ts b/apps/dokploy/templates/budibase/index.ts new file mode 100644 index 00000000..50bdfdba --- /dev/null +++ b/apps/dokploy/templates/budibase/index.ts @@ -0,0 +1,45 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const apiKey = generatePassword(32); + const encryptionKey = generatePassword(32); + const jwtSecret = generatePassword(32); + const couchDbPassword = generatePassword(32); + const redisPassword = generatePassword(32); + const minioAccessKey = generatePassword(32); + const minioSecretKey = generatePassword(32); + const watchtowerPassword = generatePassword(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 10000, + serviceName: "proxy", + }, + ]; + + const envs = [ + `BB_HOST=${mainDomain}`, + `BB_INTERNAL_API_KEY=${apiKey}`, + `BB_API_ENCRYPTION_KEY=${encryptionKey}`, + `BB_JWT_SECRET=${jwtSecret}`, + `BB_COUCHDB_PASSWORD=${couchDbPassword}`, + `BB_REDIS_PASSWORD=${redisPassword}`, + `BB_WATCHTOWER_PASSWORD=${watchtowerPassword}`, + `BB_MINIO_ACCESS_KEY=${minioAccessKey}`, + `BB_MINIO_SECRET_KEY=${minioSecretKey}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/drawio/docker-compose.yml b/apps/dokploy/templates/drawio/docker-compose.yml new file mode 100644 index 00000000..1712363f --- /dev/null +++ b/apps/dokploy/templates/drawio/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3' +services: + plantuml-server: + image: plantuml/plantuml-server + ports: + - "8080" + networks: + - dokploy-network + volumes: + - fonts_volume:/usr/share/fonts/drawio + image-export: + image: jgraph/export-server + ports: + - "8000" + networks: + - dokploy-network + volumes: + - fonts_volume:/usr/share/fonts/drawio + environment: + - DRAWIO_BASE_URL=${DRAWIO_BASE_URL} + drawio: + image: jgraph/drawio:24.7.17 + ports: + - "8080" + links: + - plantuml-server:plantuml-server + - image-export:image-export + depends_on: + - plantuml-server + - image-export + networks: + - dokploy-network + environment: + RAWIO_SELF_CONTAINED: 1 + DRAWIO_USE_HTTP: 1 + PLANTUML_URL: http://plantuml-server:8080/ + EXPORT_URL: http://image-export:8000/ + DRAWIO_BASE_URL: ${DRAWIO_BASE_URL} + DRAWIO_SERVER_URL: ${DRAWIO_SERVER_URL} + DRAWIO_CSP_HEADER: ${DRAWIO_CSP_HEADER} + DRAWIO_VIEWER_URL: ${DRAWIO_VIEWER_URL} + DRAWIO_LIGHTBOX_URL: ${DRAWIO_LIGHTBOX_URL} + DRAWIO_CONFIG: ${DRAWIO_CONFIG} + DRAWIO_GOOGLE_CLIENT_ID: ${DRAWIO_GOOGLE_CLIENT_ID} + DRAWIO_GOOGLE_APP_ID: ${DRAWIO_GOOGLE_APP_ID} + DRAWIO_GOOGLE_CLIENT_SECRET: ${DRAWIO_GOOGLE_CLIENT_SECRET} + DRAWIO_GOOGLE_VIEWER_CLIENT_ID: ${DRAWIO_GOOGLE_VIEWER_CLIENT_ID} + DRAWIO_GOOGLE_VIEWER_APP_ID: ${DRAWIO_GOOGLE_VIEWER_APP_ID} + DRAWIO_GOOGLE_VIEWER_CLIENT_SECRET: ${DRAWIO_GOOGLE_VIEWER_CLIENT_SECRET} + DRAWIO_MSGRAPH_CLIENT_ID: ${DRAWIO_MSGRAPH_CLIENT_ID} + DRAWIO_MSGRAPH_CLIENT_SECRET: ${DRAWIO_MSGRAPH_CLIENT_SECRET} + DRAWIO_MSGRAPH_TENANT_ID: ${DRAWIO_MSGRAPH_TENANT_ID} + DRAWIO_GITLAB_ID: ${DRAWIO_GITLAB_ID} + DRAWIO_GITLAB_SECRET: ${DRAWIO_GITLAB_SECRET} + DRAWIO_GITLAB_URL: ${DRAWIO_GITLAB_URL} + DRAWIO_CLOUD_CONVERT_APIKEY: ${DRAWIO_CLOUD_CONVERT_APIKEY} +networks: + dokploy-network: + external: true + +volumes: + fonts_volume: \ No newline at end of file diff --git a/apps/dokploy/templates/drawio/index.ts b/apps/dokploy/templates/drawio/index.ts new file mode 100644 index 00000000..e3c57c5a --- /dev/null +++ b/apps/dokploy/templates/drawio/index.ts @@ -0,0 +1,31 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretKeyBase = generateBase64(64); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "drawio", + }, + ]; + + const envs = [ + `DRAWIO_HOST=${mainDomain}`, + `DRAWIO_BASE_URL=https://${mainDomain}`, + `DRAWIO_SERVER_URL=https://${mainDomain}/`, + ]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/kimai/docker-compose.yml b/apps/dokploy/templates/kimai/docker-compose.yml new file mode 100644 index 00000000..6a04b3b9 --- /dev/null +++ b/apps/dokploy/templates/kimai/docker-compose.yml @@ -0,0 +1,51 @@ +services: + app: + image: kimai/kimai2:apache-2.26.0 + restart: unless-stopped + environment: + APP_ENV: prod + DATABASE_URL: mysql://kimai:${KI_MYSQL_PASSWORD:-kimai}@db/kimai + TRUSTED_PROXIES: localhost + APP_SECRET: ${KI_APP_SECRET} + MAILER_FROM: ${KI_MAILER_FROM:-admin@kimai.local} + MAILER_URL: ${KI_MAILER_URL:-null://null} + ADMINMAIL: ${KI_ADMINMAIL:-admin@kimai.local} + ADMINPASS: ${KI_ADMINPASS} + volumes: + - kimai-data:/opt/kimai/var + depends_on: + db: + condition: service_healthy + networks: + - dokploy-network + db: + image: mariadb:10.11 + restart: unless-stopped + environment: + - MYSQL_DATABASE=kimai + - MYSQL_USER=kimai + - MYSQL_PASSWORD=${KI_MYSQL_PASSWORD} + - MYSQL_ROOT_PASSWORD=${KI_MYSQL_ROOT_PASSWORD} + volumes: + - mysql-data:/var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --innodb-buffer-pool-size=256M + - --innodb-flush-log-at-trx-commit=2 + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "kimai", "-p${KI_MYSQL_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - dokploy-network + +networks: + dokploy-network: + external: true + +volumes: + kimai-data: + mysql-data: \ No newline at end of file diff --git a/apps/dokploy/templates/kimai/index.ts b/apps/dokploy/templates/kimai/index.ts new file mode 100644 index 00000000..5569905e --- /dev/null +++ b/apps/dokploy/templates/kimai/index.ts @@ -0,0 +1,37 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domain = generateRandomDomain(schema); + const domains: DomainSchema[] = [ + { + host: domain, + port: 8001, + serviceName: "app", + }, + ]; + + const adminPassword = generatePassword(32); + const mysqlPassword = generatePassword(32); + const mysqlRootPassword = generatePassword(32); + const appSecret = generatePassword(32); + + const envs = [ + `KI_HOST=${domain}`, + "KI_ADMINMAIL=admin@kimai.local", + `KI_ADMINPASS=${adminPassword}`, + `KI_MYSQL_ROOT_PASSWORD=${mysqlRootPassword}`, + `KI_MYSQL_PASSWORD=${mysqlPassword}`, + `KI_APP_SECRET=${appSecret}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 0d97dce1..78f1f522 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -107,6 +107,21 @@ export const templates: TemplateData[] = [ tags: ["database"], load: () => import("./baserow/index").then((m) => m.generate), }, + { + id: "budibase", + name: "Budibase", + version: "3.2.25", + description: + "Budibase is an open-source low-code platform that saves engineers 100s of hours building forms, portals, and approval apps, securely.", + logo: "budibase.svg", + links: { + github: "https://github.com/Budibase/budibase", + website: "https://budibase.com/", + docs: "https://docs.budibase.com/docs/", + }, + tags: ["database", "low-code", "nocode", "applications"], + load: () => import("./budibase/index").then((m) => m.generate), + }, { id: "ghost", name: "Ghost", @@ -1001,5 +1016,35 @@ export const templates: TemplateData[] = [ }, tags: ["browser", "automation"], load: () => import("./browserless/index").then((m) => m.generate), + }, + { + id: "drawio", + name: "draw.io", + version: "24.7.17", + description: + "draw.io is a configurable diagramming/whiteboarding visualization application.", + logo: "drawio.svg", + links: { + github: "https://github.com/jgraph/drawio", + website: "https://draw.io/", + docs: "https://www.drawio.com/doc/", + }, + tags: ["drawing", "diagrams"], + load: () => import("./drawio/index").then((m) => m.generate), + }, + { + id: "kimai", + name: "Kimai", + version: "2.26.0", + description: + "Kimai is a web-based multi-user time-tracking application. Works great for everyone: freelancers, companies, organizations - everyone can track their times, generate reports, create invoices and do so much more.", + logo: "kimai.svg", + links: { + github: "https://github.com/kimai/kimai", + website: "https://www.kimai.org", + docs: "https://www.kimai.org/documentation", + }, + tags: ["invoice", "business", "finance"], + load: () => import("./kimai/index").then((m) => m.generate), }, ]; diff --git a/package.json b/package.json index 94d176a6..f520707d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dokploy:setup": "pnpm --filter=dokploy run setup", "dokploy:dev": "pnpm --filter=dokploy run dev", + "dokploy:dev:turbopack": "pnpm --filter=dokploy run dev-turbopack", "dokploy:build": "pnpm --filter=dokploy run build", "dokploy:start": "pnpm --filter=dokploy run start", "test": "pnpm --filter=dokploy run test", diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 818a922d..d9b1a5df 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -22,9 +22,10 @@ import { redirects } from "./redirects"; import { registry } from "./registry"; import { security } from "./security"; import { server } from "./server"; -import { applicationStatus } from "./shared"; +import { applicationStatus, certificateType } from "./shared"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; +import { previewDeployments } from "./preview-deployments"; export const sourceType = pgEnum("sourceType", [ "docker", @@ -114,6 +115,19 @@ export const applications = pgTable("application", { .unique(), description: text("description"), env: text("env"), + previewEnv: text("previewEnv"), + previewBuildArgs: text("previewBuildArgs"), + previewWildcard: text("previewWildcard"), + previewPort: integer("previewPort").default(3000), + previewHttps: boolean("previewHttps").notNull().default(false), + previewPath: text("previewPath").default("/"), + previewCertificateType: certificateType("certificateType") + .notNull() + .default("none"), + previewLimit: integer("previewLimit").default(3), + isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( + false, + ), buildArgs: text("buildArgs"), memoryReservation: integer("memoryReservation"), memoryLimit: integer("memoryLimit"), @@ -240,6 +254,7 @@ export const applicationsRelations = relations( fields: [applications.serverId], references: [server.serverId], }), + previewDeployments: many(previewDeployments), }), ); @@ -349,6 +364,7 @@ const createSchema = createInsertSchema(applications, { subtitle: z.string().optional(), dockerImage: z.string().optional(), username: z.string().optional(), + isPreviewDeploymentsActive: z.boolean().optional(), password: z.string().optional(), registryUrl: z.string().optional(), customGitSSHKeyId: z.string().optional(), @@ -380,6 +396,14 @@ const createSchema = createInsertSchema(applications, { modeSwarm: ServiceModeSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(), + previewPort: z.number().optional(), + previewEnv: z.string().optional(), + previewBuildArgs: z.string().optional(), + previewWildcard: z.string().optional(), + previewLimit: z.number().optional(), + previewHttps: z.boolean().optional(), + previewPath: z.string().optional(), + previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), }); export const apiCreateApplication = createSchema.pick({ diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index db9838f0..f79b48ee 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -1,11 +1,18 @@ -import { relations } from "drizzle-orm"; -import { pgEnum, pgTable, text } from "drizzle-orm/pg-core"; +import { is, relations } from "drizzle-orm"; +import { + type AnyPgColumn, + boolean, + pgEnum, + pgTable, + text, +} from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; import { applications } from "./application"; import { compose } from "./compose"; import { server } from "./server"; +import { previewDeployments } from "./preview-deployments"; export const deploymentStatus = pgEnum("deploymentStatus", [ "running", @@ -32,6 +39,11 @@ export const deployments = pgTable("deployment", { serverId: text("serverId").references(() => server.serverId, { onDelete: "cascade", }), + isPreviewDeployment: boolean("isPreviewDeployment").default(false), + previewDeploymentId: text("previewDeploymentId").references( + (): AnyPgColumn => previewDeployments.previewDeploymentId, + { onDelete: "cascade" }, + ), createdAt: text("createdAt") .notNull() .$defaultFn(() => new Date().toISOString()), @@ -50,6 +62,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({ fields: [deployments.serverId], references: [server.serverId], }), + previewDeployment: one(previewDeployments, { + fields: [deployments.previewDeploymentId], + references: [previewDeployments.previewDeploymentId], + }), })); const schema = createInsertSchema(deployments, { @@ -59,6 +75,7 @@ const schema = createInsertSchema(deployments, { applicationId: z.string(), composeId: z.string(), description: z.string().optional(), + previewDeploymentId: z.string(), }); export const apiCreateDeployment = schema @@ -68,11 +85,24 @@ export const apiCreateDeployment = schema logPath: true, applicationId: true, description: true, + previewDeploymentId: true, }) .extend({ applicationId: z.string().min(1), }); +export const apiCreateDeploymentPreview = schema + .pick({ + title: true, + status: true, + logPath: true, + description: true, + previewDeploymentId: true, + }) + .extend({ + previewDeploymentId: z.string().min(1), + }); + export const apiCreateDeploymentCompose = schema .pick({ title: true, diff --git a/packages/server/src/db/schema/domain.ts b/packages/server/src/db/schema/domain.ts index 28829118..f115ce66 100644 --- a/packages/server/src/db/schema/domain.ts +++ b/packages/server/src/db/schema/domain.ts @@ -1,5 +1,6 @@ import { relations } from "drizzle-orm"; import { + type AnyPgColumn, boolean, integer, pgEnum, @@ -14,8 +15,13 @@ import { domain } from "../validations/domain"; import { applications } from "./application"; import { compose } from "./compose"; import { certificateType } from "./shared"; +import { previewDeployments } from "./preview-deployments"; -export const domainType = pgEnum("domainType", ["compose", "application"]); +export const domainType = pgEnum("domainType", [ + "compose", + "application", + "preview", +]); export const domains = pgTable("domain", { domainId: text("domainId") @@ -39,6 +45,10 @@ export const domains = pgTable("domain", { () => applications.applicationId, { onDelete: "cascade" }, ), + previewDeploymentId: text("previewDeploymentId").references( + (): AnyPgColumn => previewDeployments.previewDeploymentId, + { onDelete: "cascade" }, + ), certificateType: certificateType("certificateType").notNull().default("none"), }); @@ -51,6 +61,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({ fields: [domains.composeId], references: [compose.composeId], }), + previewDeployment: one(previewDeployments, { + fields: [domains.previewDeploymentId], + references: [previewDeployments.previewDeploymentId], + }), })); const createSchema = createInsertSchema(domains, domain._def.schema.shape); @@ -65,6 +79,7 @@ export const apiCreateDomain = createSchema.pick({ composeId: true, serviceName: true, domainType: true, + previewDeploymentId: true, }); export const apiFindDomain = createSchema diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index 4a610368..f07a1870 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -29,3 +29,4 @@ export * from "./github"; export * from "./gitlab"; export * from "./server"; export * from "./utils"; +export * from "./preview-deployments"; \ No newline at end of file diff --git a/packages/server/src/db/schema/preview-deployments.ts b/packages/server/src/db/schema/preview-deployments.ts new file mode 100644 index 00000000..5d0671e8 --- /dev/null +++ b/packages/server/src/db/schema/preview-deployments.ts @@ -0,0 +1,74 @@ +import { relations } from "drizzle-orm"; +import { pgTable, text } from "drizzle-orm/pg-core"; +import { nanoid } from "nanoid"; +import { applications } from "./application"; +import { domains } from "./domain"; +import { deployments } from "./deployment"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; +import { generateAppName } from "./utils"; +import { applicationStatus } from "./shared"; + +export const previewDeployments = pgTable("preview_deployments", { + previewDeploymentId: text("previewDeploymentId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + branch: text("branch").notNull(), + pullRequestId: text("pullRequestId").notNull(), + pullRequestNumber: text("pullRequestNumber").notNull(), + pullRequestURL: text("pullRequestURL").notNull(), + pullRequestTitle: text("pullRequestTitle").notNull(), + pullRequestCommentId: text("pullRequestCommentId").notNull(), + previewStatus: applicationStatus("previewStatus").notNull().default("idle"), + appName: text("appName") + .notNull() + .$defaultFn(() => generateAppName("preview")) + .unique(), + applicationId: text("applicationId") + .notNull() + .references(() => applications.applicationId, { + onDelete: "cascade", + }), + domainId: text("domainId").references(() => domains.domainId, { + onDelete: "cascade", + }), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + expiresAt: text("expiresAt"), +}); + +export const previewDeploymentsRelations = relations( + previewDeployments, + ({ one, many }) => ({ + deployments: many(deployments), + domain: one(domains, { + fields: [previewDeployments.domainId], + references: [domains.domainId], + }), + application: one(applications, { + fields: [previewDeployments.applicationId], + references: [applications.applicationId], + }), + }), +); + +export const createSchema = createInsertSchema(previewDeployments, { + applicationId: z.string(), +}); + +export const apiCreatePreviewDeployment = createSchema + .pick({ + applicationId: true, + domainId: true, + branch: true, + pullRequestId: true, + pullRequestNumber: true, + pullRequestURL: true, + pullRequestTitle: true, + }) + .extend({ + applicationId: z.string().min(1), + // deploymentId: z.string().min(1), + }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 12f3b64e..b8ec30e2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -20,6 +20,7 @@ export * from "./services/mount"; export * from "./services/certificate"; export * from "./services/redirect"; export * from "./services/security"; +export * from "./services/preview-deployment"; export * from "./services/port"; export * from "./services/redis"; export * from "./services/compose"; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index ae8a9b76..fef1457c 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -28,6 +28,7 @@ import { getCustomGitCloneCommand, } from "@dokploy/server/utils/providers/git"; import { + authGithub, cloneGithubRepository, getGithubCloneCommand, } from "@dokploy/server/utils/providers/github"; @@ -40,8 +41,23 @@ import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; -import { createDeployment, updateDeploymentStatus } from "./deployment"; +import { + createDeployment, + createDeploymentPreview, + updateDeploymentStatus, +} from "./deployment"; import { validUniqueServerAppName } from "./project"; +import { + findPreviewDeploymentById, + updatePreviewDeployment, +} from "./preview-deployment"; +import { + createPreviewDeploymentComment, + getIssueComment, + issueCommentExists, + updateIssueComment, +} from "./github"; +import { type Domain, getDomainHost } from "./domain"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( @@ -100,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => { github: true, bitbucket: true, server: true, + previewDeployments: true, }, }); if (!application) { @@ -168,7 +185,10 @@ export const deployApplication = async ({ try { if (application.sourceType === "github") { - await cloneGithubRepository(application, deployment.logPath); + await cloneGithubRepository({ + ...application, + logPath: deployment.logPath, + }); await buildApplication(application, deployment.logPath); } else if (application.sourceType === "gitlab") { await cloneGitlabRepository(application, deployment.logPath); @@ -276,7 +296,11 @@ export const deployRemoteApplication = async ({ if (application.serverId) { let command = "set -e;"; if (application.sourceType === "github") { - command += await getGithubCloneCommand(application, deployment.logPath); + command += await getGithubCloneCommand({ + ...application, + serverId: application.serverId, + logPath: deployment.logPath, + }); } else if (application.sourceType === "gitlab") { command += await getGitlabCloneCommand(application, deployment.logPath); } else if (application.sourceType === "bitbucket") { @@ -348,6 +372,225 @@ export const deployRemoteApplication = async ({ return true; }; +export const deployPreviewApplication = async ({ + applicationId, + titleLog = "Preview Deployment", + descriptionLog = "", + previewDeploymentId, +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; + previewDeploymentId: string; +}) => { + const application = await findApplicationById(applicationId); + const deployment = await createDeploymentPreview({ + title: titleLog, + description: descriptionLog, + previewDeploymentId: previewDeploymentId, + }); + + const previewDeployment = + await findPreviewDeploymentById(previewDeploymentId); + + await updatePreviewDeployment(previewDeploymentId, { + createdAt: new Date().toISOString(), + }); + + const previewDomain = getDomainHost(previewDeployment?.domain as Domain); + const issueParams = { + owner: application?.owner || "", + repository: application?.repository || "", + issue_number: previewDeployment.pullRequestNumber, + comment_id: Number.parseInt(previewDeployment.pullRequestCommentId), + githubId: application?.githubId || "", + }; + try { + const commentExists = await issueCommentExists({ + ...issueParams, + }); + if (!commentExists) { + const result = await createPreviewDeploymentComment({ + ...issueParams, + previewDomain, + appName: previewDeployment.appName, + githubId: application?.githubId || "", + previewDeploymentId, + }); + + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Pull request comment not found", + }); + } + + issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); + } + const buildingComment = getIssueComment( + application.name, + "running", + previewDomain, + ); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${buildingComment}`, + }); + application.appName = previewDeployment.appName; + application.env = application.previewEnv; + application.buildArgs = application.previewBuildArgs; + + if (application.sourceType === "github") { + await cloneGithubRepository({ + ...application, + appName: previewDeployment.appName, + branch: previewDeployment.branch, + logPath: deployment.logPath, + }); + await buildApplication(application, deployment.logPath); + } + // 4eef09efc46009187d668cf1c25f768d0bde4f91 + const successComment = getIssueComment( + application.name, + "success", + previewDomain, + ); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${successComment}`, + }); + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updatePreviewDeployment(previewDeploymentId, { + previewStatus: "done", + }); + } catch (error) { + const comment = getIssueComment(application.name, "error", previewDomain); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${comment}`, + }); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updatePreviewDeployment(previewDeploymentId, { + previewStatus: "error", + }); + throw error; + } + + return true; +}; + +export const deployRemotePreviewApplication = async ({ + applicationId, + titleLog = "Preview Deployment", + descriptionLog = "", + previewDeploymentId, +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; + previewDeploymentId: string; +}) => { + const application = await findApplicationById(applicationId); + const deployment = await createDeploymentPreview({ + title: titleLog, + description: descriptionLog, + previewDeploymentId: previewDeploymentId, + }); + + const previewDeployment = + await findPreviewDeploymentById(previewDeploymentId); + + await updatePreviewDeployment(previewDeploymentId, { + createdAt: new Date().toISOString(), + }); + + const previewDomain = getDomainHost(previewDeployment?.domain as Domain); + const issueParams = { + owner: application?.owner || "", + repository: application?.repository || "", + issue_number: previewDeployment.pullRequestNumber, + comment_id: Number.parseInt(previewDeployment.pullRequestCommentId), + githubId: application?.githubId || "", + }; + try { + const commentExists = await issueCommentExists({ + ...issueParams, + }); + if (!commentExists) { + const result = await createPreviewDeploymentComment({ + ...issueParams, + previewDomain, + appName: previewDeployment.appName, + githubId: application?.githubId || "", + previewDeploymentId, + }); + + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Pull request comment not found", + }); + } + + issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); + } + const buildingComment = getIssueComment( + application.name, + "running", + previewDomain, + ); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${buildingComment}`, + }); + application.appName = previewDeployment.appName; + application.env = application.previewEnv; + application.buildArgs = application.previewBuildArgs; + + if (application.serverId) { + let command = "set -e;"; + if (application.sourceType === "github") { + command += await getGithubCloneCommand({ + ...application, + serverId: application.serverId, + logPath: deployment.logPath, + }); + } + + command += getBuildCommand(application, deployment.logPath); + await execAsyncRemote(application.serverId, command); + await mechanizeDockerContainer(application); + } + + const successComment = getIssueComment( + application.name, + "success", + previewDomain, + ); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${successComment}`, + }); + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updatePreviewDeployment(previewDeploymentId, { + previewStatus: "done", + }); + } catch (error) { + const comment = getIssueComment(application.name, "error", previewDomain); + await updateIssueComment({ + ...issueParams, + body: `### Dokploy Preview Deployment\n\n${comment}`, + }); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updatePreviewDeployment(previewDeploymentId, { + previewStatus: "error", + }); + throw error; + } + + return true; +}; + export const rebuildRemoteApplication = async ({ applicationId, titleLog = "Rebuild deployment", diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 604c2150..5ae0d774 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -214,7 +214,11 @@ export const deployCompose = async ({ try { if (compose.sourceType === "github") { - await cloneGithubRepository(compose, deployment.logPath, true); + await cloneGithubRepository({ + ...compose, + logPath: deployment.logPath, + type: "compose", + }); } else if (compose.sourceType === "gitlab") { await cloneGitlabRepository(compose, deployment.logPath, true); } else if (compose.sourceType === "bitbucket") { @@ -314,11 +318,12 @@ export const deployRemoteCompose = async ({ let command = "set -e;"; if (compose.sourceType === "github") { - command += await getGithubCloneCommand( - compose, - deployment.logPath, - true, - ); + command += await getGithubCloneCommand({ + ...compose, + logPath: deployment.logPath, + type: "compose", + serverId: compose.serverId, + }); } else if (compose.sourceType === "gitlab") { command += await getGitlabCloneCommand( compose, diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 63f3cf23..b18b132d 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -5,13 +5,14 @@ import { db } from "@dokploy/server/db"; import { type apiCreateDeployment, type apiCreateDeploymentCompose, + type apiCreateDeploymentPreview, type apiCreateDeploymentServer, deployments, } from "@dokploy/server/db/schema"; import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; import { TRPCError } from "@trpc/server"; import { format } from "date-fns"; -import { desc, eq } from "drizzle-orm"; +import { and, desc, eq, isNull } from "drizzle-orm"; import { type Application, findApplicationById, @@ -21,6 +22,11 @@ import { type Compose, findComposeById, updateCompose } from "./compose"; import { type Server, findServerById } from "./server"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; +import { + findPreviewDeploymentById, + type PreviewDeployment, + updatePreviewDeployment, +} from "./preview-deployment"; export type Deployment = typeof deployments.$inferSelect; @@ -101,6 +107,74 @@ export const createDeployment = async ( } }; +export const createDeploymentPreview = async ( + deployment: Omit< + typeof apiCreateDeploymentPreview._type, + "deploymentId" | "createdAt" | "status" | "logPath" + >, +) => { + const previewDeployment = await findPreviewDeploymentById( + deployment.previewDeploymentId, + ); + try { + await removeLastTenPreviewDeploymenById( + deployment.previewDeploymentId, + previewDeployment?.application?.serverId, + ); + + const appName = `${previewDeployment.appName}`; + const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId); + const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); + const fileName = `${appName}-${formattedDateTime}.log`; + const logFilePath = path.join(LOGS_PATH, appName, fileName); + + if (previewDeployment?.application?.serverId) { + const server = await findServerById( + previewDeployment?.application?.serverId, + ); + + const command = ` + mkdir -p ${LOGS_PATH}/${appName}; + echo "Initializing deployment" >> ${logFilePath}; + `; + + await execAsyncRemote(server.serverId, command); + } else { + await fsPromises.mkdir(path.join(LOGS_PATH, appName), { + recursive: true, + }); + await fsPromises.writeFile(logFilePath, "Initializing deployment"); + } + + const deploymentCreate = await db + .insert(deployments) + .values({ + title: deployment.title || "Deployment", + status: "running", + logPath: logFilePath, + description: deployment.description || "", + previewDeploymentId: deployment.previewDeploymentId, + }) + .returning(); + if (deploymentCreate.length === 0 || !deploymentCreate[0]) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } + return deploymentCreate[0]; + } catch (error) { + await updatePreviewDeployment(deployment.previewDeploymentId, { + previewStatus: "error", + }); + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } +}; + export const createDeploymentCompose = async ( deployment: Omit< typeof apiCreateDeploymentCompose._type, @@ -257,6 +331,41 @@ const removeLastTenComposeDeployments = async ( } }; +export const removeLastTenPreviewDeploymenById = async ( + previewDeploymentId: string, + serverId: string | null, +) => { + const deploymentList = await db.query.deployments.findMany({ + where: eq(deployments.previewDeploymentId, previewDeploymentId), + orderBy: desc(deployments.createdAt), + }); + + if (deploymentList.length > 10) { + const deploymentsToDelete = deploymentList.slice(10); + if (serverId) { + let command = ""; + for (const oldDeployment of deploymentsToDelete) { + const logPath = path.join(oldDeployment.logPath); + + command += ` + rm -rf ${logPath}; + `; + await removeDeployment(oldDeployment.deploymentId); + } + + await execAsyncRemote(serverId, command); + } else { + for (const oldDeployment of deploymentsToDelete) { + const logPath = path.join(oldDeployment.logPath); + if (existsSync(logPath)) { + await fsPromises.unlink(logPath); + } + await removeDeployment(oldDeployment.deploymentId); + } + } + } +}; + export const removeDeployments = async (application: Application) => { const { appName, applicationId } = application; const { LOGS_PATH } = paths(!!application.serverId); @@ -269,6 +378,30 @@ export const removeDeployments = async (application: Application) => { await removeDeploymentsByApplicationId(applicationId); }; +export const removeDeploymentsByPreviewDeploymentId = async ( + previewDeployment: PreviewDeployment, + serverId: string | null, +) => { + const { appName } = previewDeployment; + const { LOGS_PATH } = paths(!!serverId); + const logsPath = path.join(LOGS_PATH, appName); + if (serverId) { + await execAsyncRemote(serverId, `rm -rf ${logsPath}`); + } else { + await removeDirectoryIfExistsContent(logsPath); + } + + await db + .delete(deployments) + .where( + eq( + deployments.previewDeploymentId, + previewDeployment.previewDeploymentId, + ), + ) + .returning(); +}; + export const removeDeploymentsByComposeId = async (compose: Compose) => { const { appName } = compose; const { LOGS_PATH } = paths(!!compose.serverId); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index d5a40fe0..6ac61354 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -110,7 +110,7 @@ export const getContainersByAppNameMatch = async ( const command = appType === "docker-compose" ? `${cmd} --filter='label=com.docker.compose.project=${appName}'` - : `${cmd} | grep ${appName}`; + : `${cmd} | grep '^.*Name: ${appName}'`; if (serverId) { const { stdout, stderr } = await execAsyncRemote(serverId, command); diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index 28dd3ba2..b99c4869 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -134,3 +134,7 @@ export const removeDomainById = async (domainId: string) => { return result[0]; }; + +export const getDomainHost = (domain: Domain) => { + return `${domain.https ? "https" : "http"}://${domain.host}`; +}; diff --git a/packages/server/src/services/github.ts b/packages/server/src/services/github.ts index a7317bc7..d60c8353 100644 --- a/packages/server/src/services/github.ts +++ b/packages/server/src/services/github.ts @@ -6,6 +6,8 @@ import { } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { authGithub } from "../utils/providers/github"; +import { updatePreviewDeployment } from "./preview-deployment"; export type Github = typeof github.$inferSelect; export const createGithub = async ( @@ -72,3 +74,119 @@ export const updateGithub = async ( .returning() .then((response) => response[0]); }; + +export const getIssueComment = ( + appName: string, + status: "success" | "error" | "running" | "initializing", + previewDomain: string, +) => { + let statusMessage = ""; + if (status === "success") { + statusMessage = "✅ Done"; + } else if (status === "error") { + statusMessage = "❌ Failed"; + } else if (status === "initializing") { + statusMessage = "🔄 Building"; + } else { + statusMessage = "🔄 Building"; + } + const finished = ` +| Name | Status | Preview | Updated (UTC) | +|------------|--------------|-------------------------------------|-----------------------| +| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} | +`; + + return finished; +}; +interface CommentExists { + owner: string; + repository: string; + comment_id: number; + githubId: string; +} +export const issueCommentExists = async ({ + owner, + repository, + comment_id, + githubId, +}: CommentExists) => { + const github = await findGithubById(githubId); + const octokit = authGithub(github); + try { + await octokit.rest.issues.getComment({ + owner: owner || "", + repo: repository || "", + comment_id: comment_id, + }); + return true; + } catch (error) { + return false; + } +}; +interface Comment { + owner: string; + repository: string; + issue_number: string; + body: string; + comment_id: number; + githubId: string; +} +export const updateIssueComment = async ({ + owner, + repository, + issue_number, + body, + comment_id, + githubId, +}: Comment) => { + const github = await findGithubById(githubId); + const octokit = authGithub(github); + + await octokit.rest.issues.updateComment({ + owner: owner || "", + repo: repository || "", + issue_number: issue_number, + body, + comment_id: comment_id, + }); +}; + +interface CommentCreate { + appName: string; + owner: string; + repository: string; + issue_number: string; + previewDomain: string; + githubId: string; + previewDeploymentId: string; +} + +export const createPreviewDeploymentComment = async ({ + owner, + repository, + issue_number, + previewDomain, + appName, + githubId, + previewDeploymentId, +}: CommentCreate) => { + const github = await findGithubById(githubId); + const octokit = authGithub(github); + + const runningComment = getIssueComment( + appName, + "initializing", + previewDomain, + ); + + const issue = await octokit.rest.issues.createComment({ + owner: owner || "", + repo: repository || "", + issue_number: Number.parseInt(issue_number), + body: `### Dokploy Preview Deployment\n\n${runningComment}`, + }); + + return await updatePreviewDeployment(previewDeploymentId, { + pullRequestCommentId: `${issue.data.id}`, + }).then((response) => response[0]); +}; diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts new file mode 100644 index 00000000..e52f4553 --- /dev/null +++ b/packages/server/src/services/preview-deployment.ts @@ -0,0 +1,283 @@ +import { db } from "@dokploy/server/db"; +import { + type apiCreatePreviewDeployment, + deployments, + previewDeployments, +} from "@dokploy/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { and, desc, eq } from "drizzle-orm"; +import { slugify } from "../setup/server-setup"; +import { findApplicationById } from "./application"; +import { createDomain } from "./domain"; +import { generatePassword, generateRandomDomain } from "../templates/utils"; +import { manageDomain } from "../utils/traefik/domain"; +import { + removeDeployments, + removeDeploymentsByPreviewDeploymentId, +} from "./deployment"; +import { removeDirectoryCode } from "../utils/filesystem/directory"; +import { removeTraefikConfig } from "../utils/traefik/application"; +import { removeService } from "../utils/docker/utils"; +import { authGithub } from "../utils/providers/github"; +import { getIssueComment, type Github } from "./github"; +import { findAdminById } from "./admin"; + +export type PreviewDeployment = typeof previewDeployments.$inferSelect; + +export const findPreviewDeploymentById = async ( + previewDeploymentId: string, +) => { + const application = await db.query.previewDeployments.findFirst({ + where: eq(previewDeployments.previewDeploymentId, previewDeploymentId), + with: { + domain: true, + application: { + with: { + server: true, + project: true, + }, + }, + }, + }); + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Preview Deployment not found", + }); + } + return application; +}; + +export const findApplicationByPreview = async (applicationId: string) => { + const application = await db.query.applications.findFirst({ + with: { + previewDeployments: { + where: eq(previewDeployments.applicationId, applicationId), + }, + project: true, + domains: true, + deployments: true, + mounts: true, + redirects: true, + security: true, + ports: true, + registry: true, + gitlab: true, + github: true, + bitbucket: true, + server: true, + }, + }); + + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Applicationnot found", + }); + } + return application; +}; + +export const removePreviewDeployment = async (previewDeploymentId: string) => { + try { + const application = await findApplicationByPreview(previewDeploymentId); + const previewDeployment = + await findPreviewDeploymentById(previewDeploymentId); + + const deployment = await db + .delete(previewDeployments) + .where(eq(previewDeployments.previewDeploymentId, previewDeploymentId)) + .returning(); + + application.appName = previewDeployment.appName; + const cleanupOperations = [ + async () => + await removeDeploymentsByPreviewDeploymentId( + previewDeployment, + application.serverId, + ), + async () => + await removeDirectoryCode(application.appName, application.serverId), + async () => + await removeTraefikConfig(application.appName, application.serverId), + async () => + await removeService(application?.appName, application.serverId), + ]; + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + return deployment[0]; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this preview deployment", + }); + } +}; +// testing-tesoitnmg-ddq0ul-preview-ihl44o +export const updatePreviewDeployment = async ( + previewDeploymentId: string, + previewDeploymentData: Partial, +) => { + const application = await db + .update(previewDeployments) + .set({ + ...previewDeploymentData, + }) + .where(eq(previewDeployments.previewDeploymentId, previewDeploymentId)) + .returning(); + + return application; +}; + +export const findPreviewDeploymentsByApplicationId = async ( + applicationId: string, +) => { + const deploymentsList = await db.query.previewDeployments.findMany({ + where: eq(previewDeployments.applicationId, applicationId), + orderBy: desc(previewDeployments.createdAt), + with: { + deployments: { + orderBy: desc(deployments.createdAt), + }, + domain: true, + }, + }); + return deploymentsList; +}; + +export const createPreviewDeployment = async ( + schema: typeof apiCreatePreviewDeployment._type, +) => { + const application = await findApplicationById(schema.applicationId); + const appName = `preview-${application.appName}-${generatePassword(6)}`; + + const generateDomain = await generateWildcardDomain( + application.previewWildcard || "*.traefik.me", + appName, + application.server?.ipAddress || "", + application.project.adminId, + ); + + const octokit = authGithub(application?.github as Github); + + const runningComment = getIssueComment( + application.name, + "initializing", + generateDomain, + ); + + const issue = await octokit.rest.issues.createComment({ + owner: application?.owner || "", + repo: application?.repository || "", + issue_number: Number.parseInt(schema.pullRequestNumber), + body: `### Dokploy Preview Deployment\n\n${runningComment}`, + }); + + const previewDeployment = await db + .insert(previewDeployments) + .values({ + ...schema, + appName: appName, + pullRequestCommentId: `${issue.data.id}`, + }) + .returning() + .then((value) => value[0]); + + if (!previewDeployment) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the preview deployment", + }); + } + + const newDomain = await createDomain({ + host: generateDomain, + path: application.previewPath, + port: application.previewPort, + https: application.previewHttps, + certificateType: application.previewCertificateType, + domainType: "preview", + previewDeploymentId: previewDeployment.previewDeploymentId, + }); + + application.appName = appName; + + await manageDomain(application, newDomain); + + await db + .update(previewDeployments) + .set({ + domainId: newDomain.domainId, + }) + .where( + eq( + previewDeployments.previewDeploymentId, + previewDeployment.previewDeploymentId, + ), + ); + + return previewDeployment; +}; + +export const findPreviewDeploymentsByPullRequestId = async ( + pullRequestId: string, +) => { + const previewDeploymentResult = await db.query.previewDeployments.findMany({ + where: eq(previewDeployments.pullRequestId, pullRequestId), + }); + + return previewDeploymentResult; +}; + +export const findPreviewDeploymentByApplicationId = async ( + applicationId: string, + pullRequestId: string, +) => { + const previewDeploymentResult = await db.query.previewDeployments.findFirst({ + where: and( + eq(previewDeployments.applicationId, applicationId), + eq(previewDeployments.pullRequestId, pullRequestId), + ), + }); + + return previewDeploymentResult; +}; + +const generateWildcardDomain = async ( + baseDomain: string, + appName: string, + serverIp: string, + adminId: string, +): Promise => { + if (!baseDomain.startsWith("*.")) { + throw new Error('The base domain must start with "*."'); + } + const hash = `${appName}`; + if (baseDomain.includes("traefik.me")) { + let ip = ""; + + if (process.env.NODE_ENV === "development") { + ip = "127.0.0.1"; + } + + if (serverIp) { + ip = serverIp; + } + + if (!ip) { + const admin = await findAdminById(adminId); + ip = admin?.serverIp || ""; + } + + const slugIp = ip.replaceAll(".", "-"); + return baseDomain.replace( + "*", + `${hash}${slugIp === "" ? "" : `-${slugIp}`}`, + ); + } + + return baseDomain.replace("*", hash); +}; diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 702121d2..1cdc9787 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -17,6 +17,7 @@ import { buildHeroku, getHerokuCommand } from "./heroku"; import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; import { buildPaketo, getPaketoCommand } from "./paketo"; import { buildStatic, getStaticCommand } from "./static"; +import { nanoid } from "nanoid"; // NIXPACKS codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory @@ -33,6 +34,7 @@ export type ApplicationNested = InferResultType< project: true; } >; + export const buildApplication = async ( application: ApplicationNested, logPath: string, diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 7c10e4c0..56560e4e 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -14,7 +14,7 @@ export const buildNixpacks = async ( application: ApplicationNested, writeStream: WriteStream, ) => { - const { env, appName, publishDirectory, serverId } = application; + const { env, appName, publishDirectory } = application; const buildAppDirectory = getBuildAppDirectory(application); const buildContainerId = `${appName}-${nanoid(10)}`; diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index a63a822d..c366eeba 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -74,11 +74,22 @@ export type ApplicationWithGithub = InferResultType< >; export type ComposeWithGithub = InferResultType<"compose", { github: true }>; -export const cloneGithubRepository = async ( - entity: ApplicationWithGithub | ComposeWithGithub, - logPath: string, - isCompose = false, -) => { + +interface CloneGithubRepository { + appName: string; + owner: string | null; + branch: string | null; + githubId: string | null; + repository: string | null; + logPath: string; + type?: "application" | "compose"; +} +export const cloneGithubRepository = async ({ + logPath, + type = "application", + ...entity +}: CloneGithubRepository) => { + const isCompose = type === "compose"; const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const writeStream = createWriteStream(logPath, { flags: "a" }); const { appName, repository, owner, branch, githubId } = entity; @@ -145,13 +156,13 @@ export const cloneGithubRepository = async ( } }; -export const getGithubCloneCommand = async ( - entity: ApplicationWithGithub | ComposeWithGithub, - logPath: string, - isCompose = false, -) => { +export const getGithubCloneCommand = async ({ + logPath, + type = "application", + ...entity +}: CloneGithubRepository & { serverId: string }) => { const { appName, repository, owner, branch, githubId, serverId } = entity; - + const isCompose = type === "compose"; if (!serverId) { throw new TRPCError({ code: "NOT_FOUND",