From ba8a334fbee53e1f6833715dd8f5207132ef19ad Mon Sep 17 00:00:00 2001 From: mhbdev Date: Sat, 24 Jan 2026 21:29:19 +0330 Subject: [PATCH] feat: add resend notification functionality - Introduced a new notification type "resend" to the system. - Added database schema for resend notifications including fields for apiKey, fromAddress, and toAddress. - Implemented functions to create, update, and send resend notifications. - Updated notification router to handle resend notifications with appropriate API endpoints. - Enhanced existing notification services to support sending notifications via the Resend service. - Modified various notification utilities to accommodate the new resend functionality. --- .../notifications/handle-notifications.tsx | 3624 +++++---- .../notifications/show-notifications.tsx | 8 +- .../components/icons/notification-icons.tsx | 488 +- apps/dokploy/drizzle/0137_worried_shriek.sql | 10 + apps/dokploy/drizzle/meta/0137_snapshot.json | 7107 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + .../server/api/routers/notification.ts | 1359 ++-- apps/dokploy/server/api/routers/user.ts | 39 +- packages/server/package.json | 1 + packages/server/schema.dbml | 13 +- packages/server/src/db/schema/notification.ts | 49 + packages/server/src/services/notification.ts | 1884 ++--- .../src/utils/notifications/build-error.ts | 726 +- .../src/utils/notifications/build-success.ts | 754 +- .../utils/notifications/database-backup.ts | 782 +- .../src/utils/notifications/docker-cleanup.ts | 487 +- .../utils/notifications/dokploy-restart.ts | 465 +- .../server/src/utils/notifications/utils.ts | 528 +- .../src/utils/notifications/volume-backup.ts | 571 +- pnpm-lock.yaml | 50 + schema.dbml | 13 +- 21 files changed, 13341 insertions(+), 5624 deletions(-) create mode 100644 apps/dokploy/drizzle/0137_worried_shriek.sql create mode 100644 apps/dokploy/drizzle/meta/0137_snapshot.json diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index a8c8a543d..b328e3c8f 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1,1742 +1,1898 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { - AlertTriangle, - Mail, - PenBoxIcon, - PlusIcon, - Trash2, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { +import { zodResolver } from "@hookform/resolvers/zod"; +import { + AlertTriangle, + Mail, + PenBoxIcon, + PlusIcon, + Trash2, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { DiscordIcon, GotifyIcon, LarkIcon, NtfyIcon, PushoverIcon, + ResendIcon, SlackIcon, TelegramIcon, } from "@/components/icons/notification-icons"; -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 } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; - -const notificationBaseSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), - appDeploy: z.boolean().default(false), - appBuildError: z.boolean().default(false), - databaseBackup: z.boolean().default(false), - volumeBackup: z.boolean().default(false), - dokployRestart: z.boolean().default(false), - dockerCleanup: z.boolean().default(false), - serverThreshold: z.boolean().default(false), -}); - -export const notificationSchema = z.discriminatedUnion("type", [ - z - .object({ - type: z.literal("slack"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - channel: z.string(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("telegram"), - botToken: z.string().min(1, { message: "Bot Token is required" }), - chatId: z.string().min(1, { message: "Chat ID is required" }), - messageThreadId: z.string().optional(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("discord"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - decoration: z.boolean().default(true), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("email"), - smtpServer: z.string().min(1, { message: "SMTP Server is required" }), - smtpPort: z.number().min(1, { message: "SMTP Port is required" }), - username: z.string().min(1, { message: "Username is required" }), - password: z.string().min(1, { message: "Password is required" }), - fromAddress: z.string().min(1, { message: "From Address is required" }), - toAddresses: z - .array( - z.string().min(1, { message: "Email is required" }).email({ - message: "Email is invalid", - }), - ) - .min(1, { message: "At least one email is required" }), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("gotify"), - serverUrl: z.string().min(1, { message: "Server URL is required" }), - appToken: z.string().min(1, { message: "App Token is required" }), - priority: z.number().min(1).max(10).default(5), - decoration: z.boolean().default(true), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("ntfy"), - serverUrl: z.string().min(1, { message: "Server URL is required" }), - topic: z.string().min(1, { message: "Topic is required" }), - accessToken: z.string().optional(), - priority: z.number().min(1).max(5).default(3), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("pushover"), - userKey: z.string().min(1, { message: "User Key is required" }), - apiToken: z.string().min(1, { message: "API Token is required" }), - priority: z.number().min(-2).max(2).default(0), - retry: z.number().min(30).nullish(), - expire: z.number().min(1).max(10800).nullish(), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("custom"), - endpoint: z.string().min(1, { message: "Endpoint URL is required" }), - headers: z - .array( - z.object({ - key: z.string(), - value: z.string(), - }), - ) - .optional() - .default([]), - }) - .merge(notificationBaseSchema), - z - .object({ - type: z.literal("lark"), - webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), - }) - .merge(notificationBaseSchema), -]); - -export const notificationsMap = { - slack: { - icon: , - label: "Slack", - }, - telegram: { - icon: , - label: "Telegram", - }, - discord: { - icon: , - label: "Discord", - }, - lark: { - icon: , - label: "Lark", - }, - email: { - icon: , - label: "Email", - }, - gotify: { - icon: , - label: "Gotify", - }, - ntfy: { - icon: , - label: "ntfy", - }, - pushover: { - icon: , - label: "Pushover", - }, - custom: { - icon: , - label: "Custom", - }, -}; - -export type NotificationSchema = z.infer; - -interface Props { - notificationId?: string; -} - -export const HandleNotifications = ({ notificationId }: Props) => { - const utils = api.useUtils(); - const [visible, setVisible] = useState(false); - const { data: isCloud } = api.settings.isCloud.useQuery(); - - const { data: notification } = api.notification.one.useQuery( - { - notificationId: notificationId || "", - }, - { - enabled: !!notificationId, - }, - ); - const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = - api.notification.testSlackConnection.useMutation(); - const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = - api.notification.testTelegramConnection.useMutation(); - const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = - api.notification.testDiscordConnection.useMutation(); - const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = - api.notification.testEmailConnection.useMutation(); - const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = - api.notification.testGotifyConnection.useMutation(); - const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = - api.notification.testNtfyConnection.useMutation(); - const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } = - api.notification.testLarkConnection.useMutation(); - - const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = - api.notification.testCustomConnection.useMutation(); - - const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } = - api.notification.testPushoverConnection.useMutation(); - - const customMutation = notificationId - ? api.notification.updateCustom.useMutation() - : api.notification.createCustom.useMutation(); - const slackMutation = notificationId - ? api.notification.updateSlack.useMutation() - : api.notification.createSlack.useMutation(); - const telegramMutation = notificationId - ? api.notification.updateTelegram.useMutation() - : api.notification.createTelegram.useMutation(); - const discordMutation = notificationId - ? api.notification.updateDiscord.useMutation() - : api.notification.createDiscord.useMutation(); - const emailMutation = notificationId - ? api.notification.updateEmail.useMutation() - : api.notification.createEmail.useMutation(); - const gotifyMutation = notificationId - ? api.notification.updateGotify.useMutation() - : api.notification.createGotify.useMutation(); - const ntfyMutation = notificationId - ? api.notification.updateNtfy.useMutation() - : api.notification.createNtfy.useMutation(); - const larkMutation = notificationId - ? api.notification.updateLark.useMutation() - : api.notification.createLark.useMutation(); - const pushoverMutation = notificationId - ? api.notification.updatePushover.useMutation() - : api.notification.createPushover.useMutation(); - - const form = useForm({ - defaultValues: { - type: "slack", - webhookUrl: "", - channel: "", - name: "", - }, - resolver: zodResolver(notificationSchema), - }); - const type = form.watch("type"); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "toAddresses" as never, - }); - - const { - fields: headerFields, - append: appendHeader, - remove: removeHeader, - } = useFieldArray({ - control: form.control, - name: "headers" as never, - }); - - useEffect(() => { - if (type === "email" && fields.length === 0) { - append(""); - } - }, [type, append, fields.length]); - - useEffect(() => { - if (notification) { - if (notification.notificationType === "slack") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - dockerCleanup: notification.dockerCleanup, - webhookUrl: notification.slack?.webhookUrl, - channel: notification.slack?.channel || "", - name: notification.name, - type: notification.notificationType, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "telegram") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - botToken: notification.telegram?.botToken, - messageThreadId: notification.telegram?.messageThreadId || "", - chatId: notification.telegram?.chatId, - type: notification.notificationType, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "discord") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - webhookUrl: notification.discord?.webhookUrl, - decoration: notification.discord?.decoration || undefined, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "email") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - smtpServer: notification.email?.smtpServer, - smtpPort: notification.email?.smtpPort, - username: notification.email?.username, - password: notification.email?.password, - toAddresses: notification.email?.toAddresses, - fromAddress: notification.email?.fromAddress, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "gotify") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - appToken: notification.gotify?.appToken, - decoration: notification.gotify?.decoration || undefined, - priority: notification.gotify?.priority, - serverUrl: notification.gotify?.serverUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - }); - } else if (notification.notificationType === "ntfy") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - accessToken: notification.ntfy?.accessToken || "", - topic: notification.ntfy?.topic, - priority: notification.ntfy?.priority, - serverUrl: notification.ntfy?.serverUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "lark") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - type: notification.notificationType, - webhookUrl: notification.lark?.webhookUrl, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - volumeBackup: notification.volumeBackup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "custom") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - type: notification.notificationType, - endpoint: notification.custom?.endpoint || "", - headers: notification.custom?.headers - ? Object.entries(notification.custom.headers).map( - ([key, value]) => ({ - key, - value, - }), - ) - : [], - name: notification.name, - volumeBackup: notification.volumeBackup, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } else if (notification.notificationType === "pushover") { - form.reset({ - appBuildError: notification.appBuildError, - appDeploy: notification.appDeploy, - dokployRestart: notification.dokployRestart, - databaseBackup: notification.databaseBackup, - volumeBackup: notification.volumeBackup, - type: notification.notificationType, - userKey: notification.pushover?.userKey, - apiToken: notification.pushover?.apiToken, - priority: notification.pushover?.priority, - retry: notification.pushover?.retry ?? undefined, - expire: notification.pushover?.expire ?? undefined, - name: notification.name, - dockerCleanup: notification.dockerCleanup, - serverThreshold: notification.serverThreshold, - }); - } - } else { - form.reset(); - } - }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); - - const activeMutation = { - slack: slackMutation, - telegram: telegramMutation, - discord: discordMutation, - email: emailMutation, - gotify: gotifyMutation, - ntfy: ntfyMutation, - lark: larkMutation, - custom: customMutation, - pushover: pushoverMutation, - }; - - const onSubmit = async (data: NotificationSchema) => { - const { - appBuildError, - appDeploy, - dokployRestart, - databaseBackup, - volumeBackup, - dockerCleanup, - serverThreshold, - } = data; - let promise: Promise | null = null; - if (data.type === "slack") { - promise = slackMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - channel: data.channel, - name: data.name, - dockerCleanup: dockerCleanup, - slackId: notification?.slackId || "", - notificationId: notificationId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "telegram") { - promise = telegramMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - botToken: data.botToken, - messageThreadId: data.messageThreadId || "", - chatId: data.chatId, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - telegramId: notification?.telegramId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "discord") { - promise = discordMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - decoration: data.decoration, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - discordId: notification?.discordId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "email") { - promise = emailMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - smtpServer: data.smtpServer, - smtpPort: data.smtpPort, - username: data.username, - password: data.password, - fromAddress: data.fromAddress, - toAddresses: data.toAddresses, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - emailId: notification?.emailId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "gotify") { - promise = gotifyMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - serverUrl: data.serverUrl, - appToken: data.appToken, - priority: data.priority, - name: data.name, - dockerCleanup: dockerCleanup, - decoration: data.decoration, - notificationId: notificationId || "", - gotifyId: notification?.gotifyId || "", - }); - } else if (data.type === "ntfy") { - promise = ntfyMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - serverUrl: data.serverUrl, - accessToken: data.accessToken || "", - topic: data.topic, - priority: data.priority, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - ntfyId: notification?.ntfyId || "", - }); - } else if (data.type === "lark") { - promise = larkMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - webhookUrl: data.webhookUrl, - name: data.name, - dockerCleanup: dockerCleanup, - notificationId: notificationId || "", - larkId: notification?.larkId || "", - serverThreshold: serverThreshold, - }); - } else if (data.type === "custom") { - // Convert headers array to object - const headersRecord = - data.headers && data.headers.length > 0 - ? data.headers.reduce( - (acc, { key, value }) => { - if (key.trim()) acc[key] = value; - return acc; - }, - {} as Record, - ) - : undefined; - - promise = customMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - endpoint: data.endpoint, - headers: headersRecord, - name: data.name, - dockerCleanup: dockerCleanup, - serverThreshold: serverThreshold, - notificationId: notificationId || "", - customId: notification?.customId || "", - }); - } else if (data.type === "pushover") { - if (data.priority === 2 && (data.retry == null || data.expire == null)) { - toast.error("Retry and expire are required for emergency priority (2)"); - return; - } - promise = pushoverMutation.mutateAsync({ - appBuildError: appBuildError, - appDeploy: appDeploy, - dokployRestart: dokployRestart, - databaseBackup: databaseBackup, - volumeBackup: volumeBackup, - userKey: data.userKey, - apiToken: data.apiToken, - priority: data.priority, - retry: data.priority === 2 ? data.retry : undefined, - expire: data.priority === 2 ? data.expire : undefined, - name: data.name, - dockerCleanup: dockerCleanup, - serverThreshold: serverThreshold, - notificationId: notificationId || "", - pushoverId: notification?.pushoverId || "", - }); - } - - if (promise) { - await promise - .then(async () => { - toast.success( - notificationId ? "Notification Updated" : "Notification Created", - ); - form.reset({ - type: "slack", - webhookUrl: "", - }); - setVisible(false); - await utils.notification.all.invalidate(); - }) - .catch(() => { - toast.error( - notificationId - ? "Error updating a notification" - : "Error creating a notification", - ); - }); - } - }; - return ( - - - {notificationId ? ( - - ) : ( - - )} - - - - - {notificationId ? "Update" : "Add"} Notification - - - {notificationId - ? "Update your notification providers for multiple channels." - : "Create new notification providers for multiple channels."} - - -
- - ( - - - Select a provider - - - - {Object.entries(notificationsMap).map(([key, value]) => ( - - -
- - -
-
-
- ))} -
-
- - {activeMutation[field.value].isError && ( -
- - - {activeMutation[field.value].error?.message} - -
- )} -
- )} - /> - -
- - Fill the next fields. - -
- ( - - Name - - - - - - - )} - /> - - {type === "slack" && ( - <> - ( - - Webhook URL - - - - - - - )} - /> - - ( - - Channel - - - - - - - )} - /> - - )} - - {type === "telegram" && ( - <> - ( - - Bot Token - - - - - - - )} - /> - - ( - - Chat ID - - - - - - )} - /> - - ( - - Message Thread ID - - - - - - - Optional. Use it when you want to send notifications - to a specific topic in a group. - - - )} - /> - - )} - - {type === "discord" && ( - <> - ( - - Webhook URL - - - - - - - )} - /> - - ( - -
- Decoration - - Decorate the notification with emojis. - -
- - - -
- )} - /> - - )} - - {type === "email" && ( - <> -
- ( - - SMTP Server - - - - - - - )} - /> - ( - - SMTP Port - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const port = Number.parseInt(value); - if (port > 0 && port < 65536) { - field.onChange(port); - } - } - }} - value={field.value || ""} - type="number" - /> - - - - - )} - /> -
- -
- ( - - Username - - - - - - - )} - /> - - ( - - Password - - - - - - - )} - /> -
- - ( - - From Address - - - - - - )} - /> -
- To Addresses - - {fields.map((field, index) => ( -
- ( - - - - - - - - )} - /> - -
- ))} - {type === "email" && - "toAddresses" in form.formState.errors && ( -
- {form.formState?.errors?.toAddresses?.root?.message} -
- )} -
- - - - )} - - {type === "gotify" && ( - <> - ( - - Server URL - - - - - - )} - /> - ( - - App Token - - - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value) { - const port = Number.parseInt(value); - if (port > 0 && port < 10) { - field.onChange(port); - } - } - }} - type="number" - /> - - - Message priority (1-10, default: 5) - - - - )} - /> - ( - -
- Decoration - - Decorate the notification with emojis. - -
- - - -
- )} - /> - - )} - - {type === "ntfy" && ( - <> - ( - - Server URL - - - - - - )} - /> - ( - - Topic - - - - - - )} - /> - ( - - Access Token - - - - - Optional. Leave blank for public topics. - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value) { - const port = Number.parseInt(value); - if (port > 0 && port <= 5) { - field.onChange(port); - } - } - }} - type="number" - /> - - - Message priority (1-5, default: 3) - - - - )} - /> - - )} - {type === "custom" && ( -
- ( - - Webhook URL - - - - - The URL where POST requests will be sent with - notification data. - - - - )} - /> - -
-
- Headers - - Optional. Custom headers for your POST request (e.g., - Authorization, Content-Type). - -
- -
- {headerFields.map((field, index) => ( -
- ( - - - - - - )} - /> - ( - - - - - - )} - /> - -
- ))} -
- - -
-
- )} - {type === "lark" && ( - <> - ( - - Webhook URL - - - - - - )} - /> - - )} - {type === "pushover" && ( - <> - ( - - User Key - - - - - - )} - /> - ( - - API Token - - - - - - )} - /> - ( - - Priority - - { - const value = e.target.value; - if (value === "" || value === "-") { - field.onChange(0); - } else { - const priority = Number.parseInt(value); - if ( - !Number.isNaN(priority) && - priority >= -2 && - priority <= 2 - ) { - field.onChange(priority); - } - } - }} - type="number" - min={-2} - max={2} - /> - - - Message priority (-2 to 2, default: 0, emergency: 2) - - - - )} - /> - {form.watch("priority") === 2 && ( - <> - ( - - Retry (seconds) - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const retry = Number.parseInt(value); - if (!Number.isNaN(retry)) { - field.onChange(retry); - } - } - }} - type="number" - min={30} - /> - - - How often (in seconds) to retry. Minimum 30 - seconds. - - - - )} - /> - ( - - Expire (seconds) - - { - const value = e.target.value; - if (value === "") { - field.onChange(undefined); - } else { - const expire = Number.parseInt(value); - if (!Number.isNaN(expire)) { - field.onChange(expire); - } - } - }} - type="number" - min={1} - max={10800} - /> - - - How long to keep retrying (max 10800 seconds / 3 - hours). - - - - )} - /> - - )} - - )} -
-
-
- - Select the actions. - - -
- ( - -
- App Deploy - - Trigger the action when a app is deployed. - -
- - - -
- )} - /> - ( - -
- App Build Error - - Trigger the action when the build fails. - -
- - - -
- )} - /> - - ( - -
- Database Backup - - Trigger the action when a database backup is created. - -
- - - -
- )} - /> - - ( - -
- Volume Backup - - Trigger the action when a volume backup is created. - -
- - - -
- )} - /> - - ( - -
- Docker Cleanup - - Trigger the action when the docker cleanup is - performed. - -
- - - -
- )} - /> - - {!isCloud && ( - ( - -
- Dokploy Restart - - Trigger the action when dokploy is restarted. - -
- - - -
- )} - /> - )} - - {isCloud && ( - ( - -
- Server Threshold - - Trigger the action when the server threshold is - reached. - -
- - - -
- )} - /> - )} -
-
- - - - - - - -
-
- ); -}; +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 } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +const notificationBaseSchema = z.object({ + name: z.string().min(1, { + message: "Name is required", + }), + appDeploy: z.boolean().default(false), + appBuildError: z.boolean().default(false), + databaseBackup: z.boolean().default(false), + volumeBackup: z.boolean().default(false), + dokployRestart: z.boolean().default(false), + dockerCleanup: z.boolean().default(false), + serverThreshold: z.boolean().default(false), +}); + +export const notificationSchema = z.discriminatedUnion("type", [ + z + .object({ + type: z.literal("slack"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + channel: z.string(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("telegram"), + botToken: z.string().min(1, { message: "Bot Token is required" }), + chatId: z.string().min(1, { message: "Chat ID is required" }), + messageThreadId: z.string().optional(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("discord"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + decoration: z.boolean().default(true), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("email"), + smtpServer: z.string().min(1, { message: "SMTP Server is required" }), + smtpPort: z.number().min(1, { message: "SMTP Port is required" }), + username: z.string().min(1, { message: "Username is required" }), + password: z.string().min(1, { message: "Password is required" }), + fromAddress: z.string().min(1, { message: "From Address is required" }), + toAddresses: z + .array( + z.string().min(1, { message: "Email is required" }).email({ + message: "Email is invalid", + }), + ) + .min(1, { message: "At least one email is required" }), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("resend"), + apiKey: z.string().min(1, { message: "API Key is required" }), + fromAddress: z + .string() + .min(1, { message: "From Address is required" }) + .email({ message: "Email is invalid" }), + toAddresses: z + .array( + z.string().min(1, { message: "Email is required" }).email({ + message: "Email is invalid", + }), + ) + .min(1, { message: "At least one email is required" }), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("gotify"), + serverUrl: z.string().min(1, { message: "Server URL is required" }), + appToken: z.string().min(1, { message: "App Token is required" }), + priority: z.number().min(1).max(10).default(5), + decoration: z.boolean().default(true), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("ntfy"), + serverUrl: z.string().min(1, { message: "Server URL is required" }), + topic: z.string().min(1, { message: "Topic is required" }), + accessToken: z.string().optional(), + priority: z.number().min(1).max(5).default(3), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("pushover"), + userKey: z.string().min(1, { message: "User Key is required" }), + apiToken: z.string().min(1, { message: "API Token is required" }), + priority: z.number().min(-2).max(2).default(0), + retry: z.number().min(30).nullish(), + expire: z.number().min(1).max(10800).nullish(), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("custom"), + endpoint: z.string().min(1, { message: "Endpoint URL is required" }), + headers: z + .array( + z.object({ + key: z.string(), + value: z.string(), + }), + ) + .optional() + .default([]), + }) + .merge(notificationBaseSchema), + z + .object({ + type: z.literal("lark"), + webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + }) + .merge(notificationBaseSchema), +]); + +export const notificationsMap = { + slack: { + icon: , + label: "Slack", + }, + telegram: { + icon: , + label: "Telegram", + }, + discord: { + icon: , + label: "Discord", + }, + lark: { + icon: , + label: "Lark", + }, + email: { + icon: , + label: "Email", + }, + resend: { + icon: , + label: "Resend", + }, + gotify: { + icon: , + label: "Gotify", + }, + ntfy: { + icon: , + label: "ntfy", + }, + pushover: { + icon: , + label: "Pushover", + }, + custom: { + icon: , + label: "Custom", + }, +}; + +export type NotificationSchema = z.infer; + +interface Props { + notificationId?: string; +} + +export const HandleNotifications = ({ notificationId }: Props) => { + const utils = api.useUtils(); + const [visible, setVisible] = useState(false); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + const { data: notification } = api.notification.one.useQuery( + { + notificationId: notificationId || "", + }, + { + enabled: !!notificationId, + }, + ); + const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = + api.notification.testSlackConnection.useMutation(); + const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = + api.notification.testTelegramConnection.useMutation(); + const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = + api.notification.testDiscordConnection.useMutation(); + const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = + api.notification.testEmailConnection.useMutation(); + const { mutateAsync: testResendConnection, isLoading: isLoadingResend } = + api.notification.testResendConnection.useMutation(); + const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = + api.notification.testGotifyConnection.useMutation(); + const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = + api.notification.testNtfyConnection.useMutation(); + const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } = + api.notification.testLarkConnection.useMutation(); + + const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = + api.notification.testCustomConnection.useMutation(); + + const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } = + api.notification.testPushoverConnection.useMutation(); + + const customMutation = notificationId + ? api.notification.updateCustom.useMutation() + : api.notification.createCustom.useMutation(); + const slackMutation = notificationId + ? api.notification.updateSlack.useMutation() + : api.notification.createSlack.useMutation(); + const telegramMutation = notificationId + ? api.notification.updateTelegram.useMutation() + : api.notification.createTelegram.useMutation(); + const discordMutation = notificationId + ? api.notification.updateDiscord.useMutation() + : api.notification.createDiscord.useMutation(); + const emailMutation = notificationId + ? api.notification.updateEmail.useMutation() + : api.notification.createEmail.useMutation(); + const resendMutation = notificationId + ? api.notification.updateResend.useMutation() + : api.notification.createResend.useMutation(); + const gotifyMutation = notificationId + ? api.notification.updateGotify.useMutation() + : api.notification.createGotify.useMutation(); + const ntfyMutation = notificationId + ? api.notification.updateNtfy.useMutation() + : api.notification.createNtfy.useMutation(); + const larkMutation = notificationId + ? api.notification.updateLark.useMutation() + : api.notification.createLark.useMutation(); + const pushoverMutation = notificationId + ? api.notification.updatePushover.useMutation() + : api.notification.createPushover.useMutation(); + + const form = useForm({ + defaultValues: { + type: "slack", + webhookUrl: "", + channel: "", + name: "", + }, + resolver: zodResolver(notificationSchema), + }); + const type = form.watch("type"); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "toAddresses" as never, + }); + + const { + fields: headerFields, + append: appendHeader, + remove: removeHeader, + } = useFieldArray({ + control: form.control, + name: "headers" as never, + }); + + useEffect(() => { + if ((type === "email" || type === "resend") && fields.length === 0) { + append(""); + } + }, [type, append, fields.length]); + + useEffect(() => { + if (notification) { + if (notification.notificationType === "slack") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + dockerCleanup: notification.dockerCleanup, + webhookUrl: notification.slack?.webhookUrl, + channel: notification.slack?.channel || "", + name: notification.name, + type: notification.notificationType, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "telegram") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + botToken: notification.telegram?.botToken, + messageThreadId: notification.telegram?.messageThreadId || "", + chatId: notification.telegram?.chatId, + type: notification.notificationType, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "discord") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + webhookUrl: notification.discord?.webhookUrl, + decoration: notification.discord?.decoration || undefined, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "email") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + smtpServer: notification.email?.smtpServer, + smtpPort: notification.email?.smtpPort, + username: notification.email?.username, + password: notification.email?.password, + toAddresses: notification.email?.toAddresses, + fromAddress: notification.email?.fromAddress, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "resend") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + apiKey: notification.resend?.apiKey, + toAddresses: notification.resend?.toAddresses, + fromAddress: notification.resend?.fromAddress, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "gotify") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + appToken: notification.gotify?.appToken, + decoration: notification.gotify?.decoration || undefined, + priority: notification.gotify?.priority, + serverUrl: notification.gotify?.serverUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + }); + } else if (notification.notificationType === "ntfy") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + accessToken: notification.ntfy?.accessToken || "", + topic: notification.ntfy?.topic, + priority: notification.ntfy?.priority, + serverUrl: notification.ntfy?.serverUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "lark") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + webhookUrl: notification.lark?.webhookUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + volumeBackup: notification.volumeBackup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "custom") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + endpoint: notification.custom?.endpoint || "", + headers: notification.custom?.headers + ? Object.entries(notification.custom.headers).map( + ([key, value]) => ({ + key, + value, + }), + ) + : [], + name: notification.name, + volumeBackup: notification.volumeBackup, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } else if (notification.notificationType === "pushover") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + volumeBackup: notification.volumeBackup, + type: notification.notificationType, + userKey: notification.pushover?.userKey, + apiToken: notification.pushover?.apiToken, + priority: notification.pushover?.priority, + retry: notification.pushover?.retry ?? undefined, + expire: notification.pushover?.expire ?? undefined, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); + } + } else { + form.reset(); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); + + const activeMutation = { + slack: slackMutation, + telegram: telegramMutation, + discord: discordMutation, + email: emailMutation, + resend: resendMutation, + gotify: gotifyMutation, + ntfy: ntfyMutation, + lark: larkMutation, + custom: customMutation, + pushover: pushoverMutation, + }; + + const onSubmit = async (data: NotificationSchema) => { + const { + appBuildError, + appDeploy, + dokployRestart, + databaseBackup, + volumeBackup, + dockerCleanup, + serverThreshold, + } = data; + let promise: Promise | null = null; + if (data.type === "slack") { + promise = slackMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + channel: data.channel, + name: data.name, + dockerCleanup: dockerCleanup, + slackId: notification?.slackId || "", + notificationId: notificationId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "telegram") { + promise = telegramMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + botToken: data.botToken, + messageThreadId: data.messageThreadId || "", + chatId: data.chatId, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + telegramId: notification?.telegramId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "discord") { + promise = discordMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + decoration: data.decoration, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + discordId: notification?.discordId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "email") { + promise = emailMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + smtpServer: data.smtpServer, + smtpPort: data.smtpPort, + username: data.username, + password: data.password, + fromAddress: data.fromAddress, + toAddresses: data.toAddresses, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + emailId: notification?.emailId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "resend") { + promise = resendMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + apiKey: data.apiKey, + fromAddress: data.fromAddress, + toAddresses: data.toAddresses, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + resendId: notification?.resendId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "gotify") { + promise = gotifyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + serverUrl: data.serverUrl, + appToken: data.appToken, + priority: data.priority, + name: data.name, + dockerCleanup: dockerCleanup, + decoration: data.decoration, + notificationId: notificationId || "", + gotifyId: notification?.gotifyId || "", + }); + } else if (data.type === "ntfy") { + promise = ntfyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + serverUrl: data.serverUrl, + accessToken: data.accessToken || "", + topic: data.topic, + priority: data.priority, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + ntfyId: notification?.ntfyId || "", + }); + } else if (data.type === "lark") { + promise = larkMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + webhookUrl: data.webhookUrl, + name: data.name, + dockerCleanup: dockerCleanup, + notificationId: notificationId || "", + larkId: notification?.larkId || "", + serverThreshold: serverThreshold, + }); + } else if (data.type === "custom") { + // Convert headers array to object + const headersRecord = + data.headers && data.headers.length > 0 + ? data.headers.reduce( + (acc, { key, value }) => { + if (key.trim()) acc[key] = value; + return acc; + }, + {} as Record, + ) + : undefined; + + promise = customMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + endpoint: data.endpoint, + headers: headersRecord, + name: data.name, + dockerCleanup: dockerCleanup, + serverThreshold: serverThreshold, + notificationId: notificationId || "", + customId: notification?.customId || "", + }); + } else if (data.type === "pushover") { + if (data.priority === 2 && (data.retry == null || data.expire == null)) { + toast.error("Retry and expire are required for emergency priority (2)"); + return; + } + promise = pushoverMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + volumeBackup: volumeBackup, + userKey: data.userKey, + apiToken: data.apiToken, + priority: data.priority, + retry: data.priority === 2 ? data.retry : undefined, + expire: data.priority === 2 ? data.expire : undefined, + name: data.name, + dockerCleanup: dockerCleanup, + serverThreshold: serverThreshold, + notificationId: notificationId || "", + pushoverId: notification?.pushoverId || "", + }); + } + + if (promise) { + await promise + .then(async () => { + toast.success( + notificationId ? "Notification Updated" : "Notification Created", + ); + form.reset({ + type: "slack", + webhookUrl: "", + }); + setVisible(false); + await utils.notification.all.invalidate(); + }) + .catch(() => { + toast.error( + notificationId + ? "Error updating a notification" + : "Error creating a notification", + ); + }); + } + }; + return ( + + + {notificationId ? ( + + ) : ( + + )} + + + + + {notificationId ? "Update" : "Add"} Notification + + + {notificationId + ? "Update your notification providers for multiple channels." + : "Create new notification providers for multiple channels."} + + +
+ + ( + + + Select a provider + + + + {Object.entries(notificationsMap).map(([key, value]) => ( + + +
+ + +
+
+
+ ))} +
+
+ + {activeMutation[field.value].isError && ( +
+ + + {activeMutation[field.value].error?.message} + +
+ )} +
+ )} + /> + +
+ + Fill the next fields. + +
+ ( + + Name + + + + + + + )} + /> + + {type === "slack" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + + Channel + + + + + + + )} + /> + + )} + + {type === "telegram" && ( + <> + ( + + Bot Token + + + + + + + )} + /> + + ( + + Chat ID + + + + + + )} + /> + + ( + + Message Thread ID + + + + + + + Optional. Use it when you want to send notifications + to a specific topic in a group. + + + )} + /> + + )} + + {type === "discord" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} + + {type === "email" && ( + <> +
+ ( + + SMTP Server + + + + + + + )} + /> + ( + + SMTP Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const port = Number.parseInt(value); + if (port > 0 && port < 65536) { + field.onChange(port); + } + } + }} + value={field.value || ""} + type="number" + /> + + + + + )} + /> +
+ +
+ ( + + Username + + + + + + + )} + /> + + ( + + Password + + + + + + + )} + /> +
+ + ( + + From Address + + + + + + )} + /> +
+ To Addresses + + {fields.map((field, index) => ( +
+ ( + + + + + + + + )} + /> + +
+ ))} + {type === "email" && + "toAddresses" in form.formState.errors && ( +
+ {form.formState?.errors?.toAddresses?.root?.message} +
+ )} +
+ + + + )} + + {type === "resend" && ( + <> + ( + + API Key + + + + + + )} + /> + + ( + + From Address + + + + + + )} + /> + +
+ To Addresses + + {fields.map((field, index) => ( +
+ ( + + + + + + + + )} + /> + +
+ ))} + {type === "resend" && + "toAddresses" in form.formState.errors && ( +
+ {form.formState?.errors?.toAddresses?.root?.message} +
+ )} +
+ + + + )} + + {type === "gotify" && ( + <> + ( + + Server URL + + + + + + )} + /> + ( + + App Token + + + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port < 10) { + field.onChange(port); + } + } + }} + type="number" + /> + + + Message priority (1-10, default: 5) + + + + )} + /> + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} + + {type === "ntfy" && ( + <> + ( + + Server URL + + + + + + )} + /> + ( + + Topic + + + + + + )} + /> + ( + + Access Token + + + + + Optional. Leave blank for public topics. + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port <= 5) { + field.onChange(port); + } + } + }} + type="number" + /> + + + Message priority (1-5, default: 3) + + + + )} + /> + + )} + {type === "custom" && ( +
+ ( + + Webhook URL + + + + + The URL where POST requests will be sent with + notification data. + + + + )} + /> + +
+
+ Headers + + Optional. Custom headers for your POST request (e.g., + Authorization, Content-Type). + +
+ +
+ {headerFields.map((field, index) => ( +
+ ( + + + + + + )} + /> + ( + + + + + + )} + /> + +
+ ))} +
+ + +
+
+ )} + {type === "lark" && ( + <> + ( + + Webhook URL + + + + + + )} + /> + + )} + {type === "pushover" && ( + <> + ( + + User Key + + + + + + )} + /> + ( + + API Token + + + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value === "" || value === "-") { + field.onChange(0); + } else { + const priority = Number.parseInt(value); + if ( + !Number.isNaN(priority) && + priority >= -2 && + priority <= 2 + ) { + field.onChange(priority); + } + } + }} + type="number" + min={-2} + max={2} + /> + + + Message priority (-2 to 2, default: 0, emergency: 2) + + + + )} + /> + {form.watch("priority") === 2 && ( + <> + ( + + Retry (seconds) + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const retry = Number.parseInt(value); + if (!Number.isNaN(retry)) { + field.onChange(retry); + } + } + }} + type="number" + min={30} + /> + + + How often (in seconds) to retry. Minimum 30 + seconds. + + + + )} + /> + ( + + Expire (seconds) + + { + const value = e.target.value; + if (value === "") { + field.onChange(undefined); + } else { + const expire = Number.parseInt(value); + if (!Number.isNaN(expire)) { + field.onChange(expire); + } + } + }} + type="number" + min={1} + max={10800} + /> + + + How long to keep retrying (max 10800 seconds / 3 + hours). + + + + )} + /> + + )} + + )} +
+
+
+ + Select the actions. + + +
+ ( + +
+ App Deploy + + Trigger the action when a app is deployed. + +
+ + + +
+ )} + /> + ( + +
+ App Build Error + + Trigger the action when the build fails. + +
+ + + +
+ )} + /> + + ( + +
+ Database Backup + + Trigger the action when a database backup is created. + +
+ + + +
+ )} + /> + + ( + +
+ Volume Backup + + Trigger the action when a volume backup is created. + +
+ + + +
+ )} + /> + + ( + +
+ Docker Cleanup + + Trigger the action when the docker cleanup is + performed. + +
+ + + +
+ )} + /> + + {!isCloud && ( + ( + +
+ Dokploy Restart + + Trigger the action when dokploy is restarted. + +
+ + + +
+ )} + /> + )} + + {isCloud && ( + ( + +
+ Server Threshold + + Trigger the action when the server threshold is + reached. + +
+ + + +
+ )} + /> + )} +
+
+ + + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 06ffd91e4..a3c1377ae 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -5,6 +5,7 @@ import { GotifyIcon, LarkIcon, NtfyIcon, + ResendIcon, SlackIcon, TelegramIcon, } from "@/components/icons/notification-icons"; @@ -36,7 +37,7 @@ export const ShowNotifications = () => { Add your providers to receive notifications, like Discord, Slack, - Telegram, Email, Lark. + Telegram, Email, Resend, Lark. @@ -86,6 +87,11 @@ export const ShowNotifications = () => { )} + {notification.notificationType === "resend" && ( +
+ +
+ )} {notification.notificationType === "gotify" && (
diff --git a/apps/dokploy/components/icons/notification-icons.tsx b/apps/dokploy/components/icons/notification-icons.tsx index 87bb6c0ae..0f3a37729 100644 --- a/apps/dokploy/components/icons/notification-icons.tsx +++ b/apps/dokploy/components/icons/notification-icons.tsx @@ -1,237 +1,237 @@ -import { cn } from "@/lib/utils"; - -interface Props { - className?: string; -} -export const SlackIcon = ({ className }: Props) => { - return ( - - - - - - - - - ); -}; - -export const TelegramIcon = ({ className }: Props) => { - return ( - - - - - - - - - - - ); -}; - -export const DiscordIcon = ({ className }: Props) => { - return ( - - - - ); -}; -export const LarkIcon = ({ className }: Props) => { - return ( - - - - - - ); -}; -export const GotifyIcon = ({ className }: Props) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export const NtfyIcon = ({ className }: Props) => { - return ( - - - - ); -}; - +import { cn } from "@/lib/utils"; + +interface Props { + className?: string; +} +export const SlackIcon = ({ className }: Props) => { + return ( + + + + + + + + + ); +}; + +export const TelegramIcon = ({ className }: Props) => { + return ( + + + + + + + + + + + ); +}; + +export const DiscordIcon = ({ className }: Props) => { + return ( + + + + ); +}; +export const LarkIcon = ({ className }: Props) => { + return ( + + + + + + ); +}; +export const GotifyIcon = ({ className }: Props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const NtfyIcon = ({ className }: Props) => { + return ( + + + + ); +}; + export const PushoverIcon = ({ className }: Props) => { return ( { ); }; + +export const ResendIcon = ({ className }: Props) => { + return ( + + + + + ); +}; diff --git a/apps/dokploy/drizzle/0137_worried_shriek.sql b/apps/dokploy/drizzle/0137_worried_shriek.sql new file mode 100644 index 000000000..ae3629c6e --- /dev/null +++ b/apps/dokploy/drizzle/0137_worried_shriek.sql @@ -0,0 +1,10 @@ +ALTER TYPE "public"."notificationType" ADD VALUE 'resend' BEFORE 'gotify';--> statement-breakpoint +CREATE TABLE "resend" ( + "resendId" text PRIMARY KEY NOT NULL, + "apiKey" text NOT NULL, + "fromAddress" text NOT NULL, + "toAddress" text[] NOT NULL +); +--> statement-breakpoint +ALTER TABLE "notification" ADD COLUMN "resendId" text;--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_resendId_resend_resendId_fk" FOREIGN KEY ("resendId") REFERENCES "public"."resend"("resendId") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0137_snapshot.json b/apps/dokploy/drizzle/meta/0137_snapshot.json new file mode 100644 index 000000000..5698c3339 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0137_snapshot.json @@ -0,0 +1,7107 @@ +{ + "id": "3f56c8d0-1d0d-4791-9c4a-06be622dc244", + "prevId": "5958b029-1fb9-4a44-be24-c96b4e899b84", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_userId_user_id_fk": { + "name": "schedule_userId_user_id_fk", + "tableFrom": "schedule", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_id_fk": { + "name": "session_temp_user_id_user_id_fk", + "tableFrom": "session_temp", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "pushover", + "custom", + "lark" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index d442758b3..a066d7bf7 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -960,6 +960,13 @@ "when": 1769580434296, "tag": "0136_tidy_puff_adder", "breakpoints": true + }, + { + "idx": 137, + "version": "7", + "when": 1769595469101, + "tag": "0137_worried_shriek", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index c22ce7aa5..a5097288b 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -1,4 +1,4 @@ -import { +import { createCustomNotification, createDiscordNotification, createEmailNotification, @@ -6,12 +6,13 @@ import { createLarkNotification, createNtfyNotification, createPushoverNotification, + createResendNotification, createSlackNotification, createTelegramNotification, findNotificationById, getWebServerSettings, IS_CLOUD, - removeNotificationById, + removeNotificationById, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -19,6 +20,7 @@ import { sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendServerThresholdNotifications, sendSlackNotification, sendTelegramNotification, @@ -29,27 +31,29 @@ import { updateLarkNotification, updateNtfyNotification, updatePushoverNotification, + updateResendNotification, updateSlackNotification, updateTelegramNotification, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; -import { desc, eq, sql } from "drizzle-orm"; -import { z } from "zod"; -import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "@/server/api/trpc"; -import { db } from "@/server/db"; -import { - apiCreateCustom, +import { TRPCError } from "@trpc/server"; +import { desc, eq, sql } from "drizzle-orm"; +import { z } from "zod"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateCustom, apiCreateDiscord, apiCreateEmail, apiCreateGotify, apiCreateLark, apiCreateNtfy, apiCreatePushover, + apiCreateResend, apiCreateSlack, apiCreateTelegram, apiFindOneNotification, @@ -60,6 +64,7 @@ import { apiTestLarkConnection, apiTestNtfyConnection, apiTestPushoverConnection, + apiTestResendConnection, apiTestSlackConnection, apiTestTelegramConnection, apiUpdateCustom, @@ -69,640 +74,700 @@ import { apiUpdateLark, apiUpdateNtfy, apiUpdatePushover, + apiUpdateResend, apiUpdateSlack, apiUpdateTelegram, notifications, server, -} from "@/server/db/schema"; - -export const notificationRouter = createTRPCRouter({ - createSlack: adminProcedure - .input(apiCreateSlack) - .mutation(async ({ input, ctx }) => { - try { - return await createSlackNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateSlack: adminProcedure - .input(apiUpdateSlack) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateSlackNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testSlackConnection: adminProcedure - .input(apiTestSlackConnection) - .mutation(async ({ input }) => { - try { - await sendSlackNotification(input, { - channel: input.channel, - text: "Hi, From Dokploy ๐Ÿ‘‹", - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createTelegram: adminProcedure - .input(apiCreateTelegram) - .mutation(async ({ input, ctx }) => { - try { - return await createTelegramNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - - updateTelegram: adminProcedure - .input(apiUpdateTelegram) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateTelegramNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - testTelegramConnection: adminProcedure - .input(apiTestTelegramConnection) - .mutation(async ({ input }) => { - try { - await sendTelegramNotification(input, "Hi, From Dokploy ๐Ÿ‘‹"); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createDiscord: adminProcedure - .input(apiCreateDiscord) - .mutation(async ({ input, ctx }) => { - try { - return await createDiscordNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - - updateDiscord: adminProcedure - .input(apiUpdateDiscord) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateDiscordNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - - testDiscordConnection: adminProcedure - .input(apiTestDiscordConnection) - .mutation(async ({ input }) => { - try { - const decorate = (decoration: string, text: string) => - `${input.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(input, { - title: decorate(">", "`๐Ÿคš` - Test Notification"), - description: decorate(">", "Hi, From Dokploy ๐Ÿ‘‹"), - color: 0xf3f7f4, - }); - - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createEmail: adminProcedure - .input(apiCreateEmail) - .mutation(async ({ input, ctx }) => { - try { - return await createEmailNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateEmail: adminProcedure - .input(apiUpdateEmail) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateEmailNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error updating the notification", - cause: error, - }); - } - }), - testEmailConnection: adminProcedure - .input(apiTestEmailConnection) - .mutation(async ({ input }) => { - try { - await sendEmailNotification( - input, - "Test Email", - "

Hi, From Dokploy ๐Ÿ‘‹

", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - remove: adminProcedure - .input(apiFindOneNotification) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this notification", - }); - } - return await removeNotificationById(input.notificationId); - } catch (error) { - const message = - error instanceof Error - ? error.message - : "Error deleting this notification"; - throw new TRPCError({ - code: "BAD_REQUEST", - message, - }); - } - }), - one: protectedProcedure - .input(apiFindOneNotification) - .query(async ({ input, ctx }) => { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this notification", - }); - } - return notification; - }), - all: adminProcedure.query(async ({ ctx }) => { - return await db.query.notifications.findMany({ - with: { - slack: true, - telegram: true, - discord: true, - email: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - orderBy: desc(notifications.createdAt), - where: eq(notifications.organizationId, ctx.session.activeOrganizationId), - }); - }), - receiveNotification: publicProcedure - .input( - z.object({ - ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"), - Type: z.enum(["Memory", "CPU"]), - Value: z.number(), - Threshold: z.number(), - Message: z.string(), - Timestamp: z.string(), - Token: z.string(), - }), - ) - .mutation(async ({ input }) => { - try { - let organizationId = ""; - let ServerName = ""; - if (input.ServerType === "Dokploy") { - const settings = await getWebServerSettings(); - if ( - !settings?.metricsConfig?.server?.token || - settings.metricsConfig.server.token !== input.Token - ) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Token not found", - }); - } - - // For Dokploy server type, we don't have a specific organizationId - // This might need to be adjusted based on your business logic - organizationId = ""; - ServerName = "Dokploy"; - } else { - const result = await db - .select() - .from(server) - .where( - sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, - ); - - if (!result?.[0]?.organizationId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Token not found", - }); - } - - organizationId = result?.[0]?.organizationId; - ServerName = "Remote"; - } - - await sendServerThresholdNotifications(organizationId, { - ...input, - ServerName, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error sending the notification", - cause: error, - }); - } - }), - createGotify: adminProcedure - .input(apiCreateGotify) - .mutation(async ({ input, ctx }) => { - try { - return await createGotifyNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateGotify: adminProcedure - .input(apiUpdateGotify) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateGotifyNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testGotifyConnection: adminProcedure - .input(apiTestGotifyConnection) - .mutation(async ({ input }) => { - try { - await sendGotifyNotification( - input, - "Test Notification", - "Hi, From Dokploy ๐Ÿ‘‹", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createNtfy: adminProcedure - .input(apiCreateNtfy) - .mutation(async ({ input, ctx }) => { - try { - return await createNtfyNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateNtfy: adminProcedure - .input(apiUpdateNtfy) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateNtfyNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testNtfyConnection: adminProcedure - .input(apiTestNtfyConnection) - .mutation(async ({ input }) => { - try { - await sendNtfyNotification( - input, - "Test Notification", - "", - "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;", - "Hi, From Dokploy ๐Ÿ‘‹", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createCustom: adminProcedure - .input(apiCreateCustom) - .mutation(async ({ input, ctx }) => { - try { - return await createCustomNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateCustom: adminProcedure - .input(apiUpdateCustom) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if (notification.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateCustomNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testCustomConnection: adminProcedure - .input(apiTestCustomConnection) - .mutation(async ({ input }) => { - try { - await sendCustomNotification(input, { - title: "Test Notification", - message: "Hi, From Dokploy ๐Ÿ‘‹", - timestamp: new Date().toISOString(), - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `${error instanceof Error ? error.message : "Unknown error"}`, - cause: error, - }); - } - }), - createLark: adminProcedure - .input(apiCreateLark) - .mutation(async ({ input, ctx }) => { - try { - return await createLarkNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updateLark: adminProcedure - .input(apiUpdateLark) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updateLarkNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testLarkConnection: adminProcedure - .input(apiTestLarkConnection) - .mutation(async ({ input }) => { - try { - await sendLarkNotification(input, { - msg_type: "text", - content: { - text: "Hi, From Dokploy ๐Ÿ‘‹", - }, - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - createPushover: adminProcedure - .input(apiCreatePushover) - .mutation(async ({ input, ctx }) => { - try { - return await createPushoverNotification( - input, - ctx.session.activeOrganizationId, - ); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the notification", - cause: error, - }); - } - }), - updatePushover: adminProcedure - .input(apiUpdatePushover) - .mutation(async ({ input, ctx }) => { - try { - const notification = await findNotificationById(input.notificationId); - if ( - IS_CLOUD && - notification.organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this notification", - }); - } - return await updatePushoverNotification({ - ...input, - organizationId: ctx.session.activeOrganizationId, - }); - } catch (error) { - throw error; - } - }), - testPushoverConnection: adminProcedure - .input(apiTestPushoverConnection) - .mutation(async ({ input }) => { - try { - await sendPushoverNotification( - input, - "Test Notification", - "Hi, From Dokploy ๐Ÿ‘‹", - ); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error testing the notification", - cause: error, - }); - } - }), - getEmailProviders: adminProcedure.query(async ({ ctx }) => { - return await db.query.notifications.findMany({ - where: eq(notifications.organizationId, ctx.session.activeOrganizationId), - with: { - email: true, - }, - }); - }), -}); +} from "@/server/db/schema"; + +export const notificationRouter = createTRPCRouter({ + createSlack: adminProcedure + .input(apiCreateSlack) + .mutation(async ({ input, ctx }) => { + try { + return await createSlackNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateSlack: adminProcedure + .input(apiUpdateSlack) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateSlackNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testSlackConnection: adminProcedure + .input(apiTestSlackConnection) + .mutation(async ({ input }) => { + try { + await sendSlackNotification(input, { + channel: input.channel, + text: "Hi, From Dokploy ๐Ÿ‘‹", + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createTelegram: adminProcedure + .input(apiCreateTelegram) + .mutation(async ({ input, ctx }) => { + try { + return await createTelegramNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + + updateTelegram: adminProcedure + .input(apiUpdateTelegram) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateTelegramNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testTelegramConnection: adminProcedure + .input(apiTestTelegramConnection) + .mutation(async ({ input }) => { + try { + await sendTelegramNotification(input, "Hi, From Dokploy ๐Ÿ‘‹"); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createDiscord: adminProcedure + .input(apiCreateDiscord) + .mutation(async ({ input, ctx }) => { + try { + return await createDiscordNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + + updateDiscord: adminProcedure + .input(apiUpdateDiscord) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateDiscordNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + + testDiscordConnection: adminProcedure + .input(apiTestDiscordConnection) + .mutation(async ({ input }) => { + try { + const decorate = (decoration: string, text: string) => + `${input.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(input, { + title: decorate(">", "`๐Ÿคš` - Test Notification"), + description: decorate(">", "Hi, From Dokploy ๐Ÿ‘‹"), + color: 0xf3f7f4, + }); + + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createEmail: adminProcedure + .input(apiCreateEmail) + .mutation(async ({ input, ctx }) => { + try { + return await createEmailNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateEmail: adminProcedure + .input(apiUpdateEmail) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateEmailNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testEmailConnection: adminProcedure + .input(apiTestEmailConnection) + .mutation(async ({ input }) => { + try { + await sendEmailNotification( + input, + "Test Email", + "

Hi, From Dokploy ๐Ÿ‘‹

", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createResend: adminProcedure + .input(apiCreateResend) + .mutation(async ({ input, ctx }) => { + try { + return await createResendNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateResend: adminProcedure + .input(apiUpdateResend) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateResendNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error updating the notification", + cause: error, + }); + } + }), + testResendConnection: adminProcedure + .input(apiTestResendConnection) + .mutation(async ({ input }) => { + try { + await sendResendNotification( + input, + "Test Email", + "

Hi, From Dokploy ๐Ÿ‘‹

", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + remove: adminProcedure + .input(apiFindOneNotification) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this notification", + }); + } + return await removeNotificationById(input.notificationId); + } catch (error) { + const message = + error instanceof Error + ? error.message + : "Error deleting this notification"; + throw new TRPCError({ + code: "BAD_REQUEST", + message, + }); + } + }), + one: protectedProcedure + .input(apiFindOneNotification) + .query(async ({ input, ctx }) => { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this notification", + }); + } + return notification; + }), + all: adminProcedure.query(async ({ ctx }) => { + return await db.query.notifications.findMany({ + with: { + slack: true, + telegram: true, + discord: true, + email: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + orderBy: desc(notifications.createdAt), + where: eq(notifications.organizationId, ctx.session.activeOrganizationId), + }); + }), + receiveNotification: publicProcedure + .input( + z.object({ + ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"), + Type: z.enum(["Memory", "CPU"]), + Value: z.number(), + Threshold: z.number(), + Message: z.string(), + Timestamp: z.string(), + Token: z.string(), + }), + ) + .mutation(async ({ input }) => { + try { + let organizationId = ""; + let ServerName = ""; + if (input.ServerType === "Dokploy") { + const settings = await getWebServerSettings(); + if ( + !settings?.metricsConfig?.server?.token || + settings.metricsConfig.server.token !== input.Token + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Token not found", + }); + } + + // For Dokploy server type, we don't have a specific organizationId + // This might need to be adjusted based on your business logic + organizationId = ""; + ServerName = "Dokploy"; + } else { + const result = await db + .select() + .from(server) + .where( + sql`${server.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, + ); + + if (!result?.[0]?.organizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Token not found", + }); + } + + organizationId = result?.[0]?.organizationId; + ServerName = "Remote"; + } + + await sendServerThresholdNotifications(organizationId, { + ...input, + ServerName, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error sending the notification", + cause: error, + }); + } + }), + createGotify: adminProcedure + .input(apiCreateGotify) + .mutation(async ({ input, ctx }) => { + try { + return await createGotifyNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateGotify: adminProcedure + .input(apiUpdateGotify) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateGotifyNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testGotifyConnection: adminProcedure + .input(apiTestGotifyConnection) + .mutation(async ({ input }) => { + try { + await sendGotifyNotification( + input, + "Test Notification", + "Hi, From Dokploy ๐Ÿ‘‹", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createNtfy: adminProcedure + .input(apiCreateNtfy) + .mutation(async ({ input, ctx }) => { + try { + return await createNtfyNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateNtfy: adminProcedure + .input(apiUpdateNtfy) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateNtfyNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testNtfyConnection: adminProcedure + .input(apiTestNtfyConnection) + .mutation(async ({ input }) => { + try { + await sendNtfyNotification( + input, + "Test Notification", + "", + "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;", + "Hi, From Dokploy ๐Ÿ‘‹", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createCustom: adminProcedure + .input(apiCreateCustom) + .mutation(async ({ input, ctx }) => { + try { + return await createCustomNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateCustom: adminProcedure + .input(apiUpdateCustom) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (notification.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateCustomNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testCustomConnection: adminProcedure + .input(apiTestCustomConnection) + .mutation(async ({ input }) => { + try { + await sendCustomNotification(input, { + title: "Test Notification", + message: "Hi, From Dokploy ๐Ÿ‘‹", + timestamp: new Date().toISOString(), + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${error instanceof Error ? error.message : "Unknown error"}`, + cause: error, + }); + } + }), + createLark: adminProcedure + .input(apiCreateLark) + .mutation(async ({ input, ctx }) => { + try { + return await createLarkNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateLark: adminProcedure + .input(apiUpdateLark) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateLarkNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testLarkConnection: adminProcedure + .input(apiTestLarkConnection) + .mutation(async ({ input }) => { + try { + await sendLarkNotification(input, { + msg_type: "text", + content: { + text: "Hi, From Dokploy ๐Ÿ‘‹", + }, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + createPushover: adminProcedure + .input(apiCreatePushover) + .mutation(async ({ input, ctx }) => { + try { + return await createPushoverNotification( + input, + ctx.session.activeOrganizationId, + ); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updatePushover: adminProcedure + .input(apiUpdatePushover) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if ( + IS_CLOUD && + notification.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updatePushoverNotification({ + ...input, + organizationId: ctx.session.activeOrganizationId, + }); + } catch (error) { + throw error; + } + }), + testPushoverConnection: adminProcedure + .input(apiTestPushoverConnection) + .mutation(async ({ input }) => { + try { + await sendPushoverNotification( + input, + "Test Notification", + "Hi, From Dokploy ๐Ÿ‘‹", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), + getEmailProviders: adminProcedure.query(async ({ ctx }) => { + return await db.query.notifications.findMany({ + where: eq(notifications.organizationId, ctx.session.activeOrganizationId), + with: { + email: true, + resend: true, + }, + }); + }), +}); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index e801a5adb..3f217ceed 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -9,6 +9,7 @@ import { IS_CLOUD, removeUserById, sendEmailNotification, + sendResendNotification, updateUser, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; @@ -509,15 +510,16 @@ export const userRouter = createTRPCRouter({ const notification = await findNotificationById(input.notificationId); const email = notification.email; + const resend = notification.resend; const currentInvitation = await db.query.invitation.findFirst({ where: eq(invitation.id, input.invitationId), }); - if (!email) { + if (!email && !resend) { throw new TRPCError({ code: "NOT_FOUND", - message: "Email notification not found", + message: "Email provider not found", }); } @@ -532,16 +534,29 @@ export const userRouter = createTRPCRouter({ ); try { - await sendEmailNotification( - { - ...email, - toAddresses: [currentInvitation?.email || ""], - }, - "Invitation to join organization", - ` -

You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: Accept Invitation

- `, - ); + const htmlContent = ` +\t\t\t\t

You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: Accept Invitation

+\t\t\t\t`; + + if (email) { + await sendEmailNotification( + { + ...email, + toAddresses: [currentInvitation?.email || ""], + }, + "Invitation to join organization", + htmlContent, + ); + } else if (resend) { + await sendResendNotification( + { + ...resend, + toAddresses: [currentInvitation?.email || ""], + }, + "Invitation to join organization", + htmlContent, + ); + } } catch (error) { console.log(error); throw error; diff --git a/packages/server/package.json b/packages/server/package.json index 820300b15..e424bf00c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -73,6 +73,7 @@ "qrcode": "^1.5.4", "react": "18.2.0", "react-dom": "18.2.0", + "resend": "^6.0.2", "shell-quote": "^1.8.1", "slugify": "^1.6.6", "ssh2": "1.15.0", diff --git a/packages/server/schema.dbml b/packages/server/schema.dbml index 0fe7c05e8..0bb22d80d 100644 --- a/packages/server/schema.dbml +++ b/packages/server/schema.dbml @@ -69,6 +69,7 @@ enum notificationType { telegram discord email + resend gotify ntfy custom @@ -456,6 +457,13 @@ table email { toAddress text[] [not null] } +table resend { + resendId text [pk, not null] + apiKey text [not null] + fromAddress text [not null] + toAddress text[] [not null] +} + table environment { environmentId text [pk, not null] name text [not null] @@ -695,6 +703,7 @@ table notification { telegramId text discordId text emailId text + resendId text gotifyId text ntfyId text customId text @@ -1139,6 +1148,8 @@ ref: notification.discordId - discord.discordId ref: notification.emailId - email.emailId +ref: notification.resendId - resend.resendId + ref: notification.gotifyId - gotify.gotifyId ref: notification.ntfyId - ntfy.ntfyId @@ -1197,4 +1208,4 @@ ref: volume_backup.redisId - redis.redisId ref: volume_backup.composeId - compose.composeId -ref: volume_backup.destinationId - destination.destinationId \ No newline at end of file +ref: volume_backup.destinationId - destination.destinationId diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 3075459ba..73af358d0 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -17,6 +17,7 @@ export const notificationType = pgEnum("notificationType", [ "telegram", "discord", "email", + "resend", "gotify", "ntfy", "pushover", @@ -53,6 +54,9 @@ export const notifications = pgTable("notification", { emailId: text("emailId").references(() => email.emailId, { onDelete: "cascade", }), + resendId: text("resendId").references(() => resend.resendId, { + onDelete: "cascade", + }), gotifyId: text("gotifyId").references(() => gotify.gotifyId, { onDelete: "cascade", }), @@ -114,6 +118,16 @@ export const email = pgTable("email", { toAddresses: text("toAddress").array().notNull(), }); +export const resend = pgTable("resend", { + resendId: text("resendId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + apiKey: text("apiKey").notNull(), + fromAddress: text("fromAddress").notNull(), + toAddresses: text("toAddress").array().notNull(), +}); + export const gotify = pgTable("gotify", { gotifyId: text("gotifyId") .notNull() @@ -182,6 +196,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ fields: [notifications.emailId], references: [email.emailId], }), + resend: one(resend, { + fields: [notifications.resendId], + references: [resend.resendId], + }), gotify: one(gotify, { fields: [notifications.gotifyId], references: [gotify.gotifyId], @@ -335,6 +353,36 @@ export const apiTestEmailConnection = apiCreateEmail.pick({ fromAddress: true, }); +export const apiCreateResend = notificationsSchema + .pick({ + appBuildError: true, + databaseBackup: true, + volumeBackup: true, + dokployRestart: true, + name: true, + appDeploy: true, + dockerCleanup: true, + serverThreshold: true, + }) + .extend({ + apiKey: z.string().min(1), + fromAddress: z.string().min(1), + toAddresses: z.array(z.string()).min(1), + }) + .required(); + +export const apiUpdateResend = apiCreateResend.partial().extend({ + notificationId: z.string().min(1), + resendId: z.string().min(1), + organizationId: z.string().optional(), +}); + +export const apiTestResendConnection = apiCreateResend.pick({ + apiKey: true, + fromAddress: true, + toAddresses: true, +}); + export const apiCreateGotify = notificationsSchema .pick({ appBuildError: true, @@ -534,6 +582,7 @@ export const apiSendTest = notificationsSchema username: z.string(), password: z.string(), toAddresses: z.array(z.string()), + apiKey: z.string(), serverUrl: z.string(), topic: z.string(), appToken: z.string(), diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index 453a61ca0..f11619aac 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -1,12 +1,13 @@ -import { db } from "@dokploy/server/db"; -import { - type apiCreateCustom, - type apiCreateDiscord, - type apiCreateEmail, +import { db } from "@dokploy/server/db"; +import { + type apiCreateCustom, + type apiCreateDiscord, + type apiCreateEmail, type apiCreateGotify, type apiCreateLark, type apiCreateNtfy, type apiCreatePushover, + type apiCreateResend, type apiCreateSlack, type apiCreateTelegram, type apiUpdateCustom, @@ -16,6 +17,7 @@ import { type apiUpdateLark, type apiUpdateNtfy, type apiUpdatePushover, + type apiUpdateResend, type apiUpdateSlack, type apiUpdateTelegram, custom, @@ -26,894 +28,990 @@ import { notifications, ntfy, pushover, + resend, slack, telegram, } from "@dokploy/server/db/schema"; -import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; - -export type Notification = typeof notifications.$inferSelect; - -export const createSlackNotification = async ( - input: typeof apiCreateSlack._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newSlack = await tx - .insert(slack) - .values({ - channel: input.channel, - webhookUrl: input.webhookUrl, - }) - .returning() - .then((value) => value[0]); - - if (!newSlack) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting slack", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - slackId: newSlack.slackId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "slack", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateSlackNotification = async ( - input: typeof apiUpdateSlack._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(slack) - .set({ - channel: input.channel, - webhookUrl: input.webhookUrl, - }) - .where(eq(slack.slackId, input.slackId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createTelegramNotification = async ( - input: typeof apiCreateTelegram._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newTelegram = await tx - .insert(telegram) - .values({ - botToken: input.botToken, - chatId: input.chatId, - messageThreadId: input.messageThreadId, - }) - .returning() - .then((value) => value[0]); - - if (!newTelegram) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting telegram", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - telegramId: newTelegram.telegramId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "telegram", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateTelegramNotification = async ( - input: typeof apiUpdateTelegram._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(telegram) - .set({ - botToken: input.botToken, - chatId: input.chatId, - messageThreadId: input.messageThreadId, - }) - .where(eq(telegram.telegramId, input.telegramId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createDiscordNotification = async ( - input: typeof apiCreateDiscord._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newDiscord = await tx - .insert(discord) - .values({ - webhookUrl: input.webhookUrl, - decoration: input.decoration, - }) - .returning() - .then((value) => value[0]); - - if (!newDiscord) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting discord", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - discordId: newDiscord.discordId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "discord", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateDiscordNotification = async ( - input: typeof apiUpdateDiscord._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(discord) - .set({ - webhookUrl: input.webhookUrl, - decoration: input.decoration, - }) - .where(eq(discord.discordId, input.discordId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createEmailNotification = async ( - input: typeof apiCreateEmail._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newEmail = await tx - .insert(email) - .values({ - smtpServer: input.smtpServer, - smtpPort: input.smtpPort, - username: input.username, - password: input.password, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .returning() - .then((value) => value[0]); - - if (!newEmail) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting email", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - emailId: newEmail.emailId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "email", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateEmailNotification = async ( - input: typeof apiUpdateEmail._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(email) - .set({ - smtpServer: input.smtpServer, - smtpPort: input.smtpPort, - username: input.username, - password: input.password, - fromAddress: input.fromAddress, - toAddresses: input.toAddresses, - }) - .where(eq(email.emailId, input.emailId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const createGotifyNotification = async ( - input: typeof apiCreateGotify._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newGotify = await tx - .insert(gotify) - .values({ - serverUrl: input.serverUrl, - appToken: input.appToken, - priority: input.priority, - decoration: input.decoration, - }) - .returning() - .then((value) => value[0]); - - if (!newGotify) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting gotify", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - gotifyId: newGotify.gotifyId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "gotify", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateGotifyNotification = async ( - input: typeof apiUpdateGotify._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(gotify) - .set({ - serverUrl: input.serverUrl, - appToken: input.appToken, - priority: input.priority, - decoration: input.decoration, - }) - .where(eq(gotify.gotifyId, input.gotifyId)); - - return newDestination; - }); -}; - -export const createNtfyNotification = async ( - input: typeof apiCreateNtfy._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newNtfy = await tx - .insert(ntfy) - .values({ - serverUrl: input.serverUrl, - topic: input.topic, - accessToken: input.accessToken ?? null, - priority: input.priority, - }) - .returning() - .then((value) => value[0]); - - if (!newNtfy) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting ntfy", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - ntfyId: newNtfy.ntfyId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "ntfy", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateNtfyNotification = async ( - input: typeof apiUpdateNtfy._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(ntfy) - .set({ - serverUrl: input.serverUrl, - topic: input.topic, - accessToken: input.accessToken ?? null, - priority: input.priority, - }) - .where(eq(ntfy.ntfyId, input.ntfyId)); - - return newDestination; - }); -}; - -export const createCustomNotification = async ( - input: typeof apiCreateCustom._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newCustom = await tx - .insert(custom) - .values({ - endpoint: input.endpoint, - headers: input.headers, - }) - .returning() - .then((value) => value[0]); - - if (!newCustom) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting custom", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - customId: newCustom.customId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "custom", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateCustomNotification = async ( - input: typeof apiUpdateCustom._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(custom) - .set({ - endpoint: input.endpoint, - headers: input.headers, - }) - .where(eq(custom.customId, input.customId)); - - return newDestination; - }); -}; - -export const findNotificationById = async (notificationId: string) => { - const notification = await db.query.notifications.findFirst({ - where: eq(notifications.notificationId, notificationId), - with: { - slack: true, - telegram: true, - discord: true, - email: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - if (!notification) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Notification not found", - }); - } - return notification; -}; - -export const removeNotificationById = async (notificationId: string) => { - const result = await db - .delete(notifications) - .where(eq(notifications.notificationId, notificationId)) - .returning(); - - return result[0]; -}; - -export const createLarkNotification = async ( - input: typeof apiCreateLark._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newLark = await tx - .insert(lark) - .values({ - webhookUrl: input.webhookUrl, - }) - .returning() - .then((value) => value[0]); - - if (!newLark) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting lark", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - larkId: newLark.larkId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - notificationType: "lark", - organizationId: organizationId, - serverThreshold: input.serverThreshold, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updateLarkNotification = async ( - input: typeof apiUpdateLark._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(lark) - .set({ - webhookUrl: input.webhookUrl, - }) - .where(eq(lark.larkId, input.larkId)) - .returning() - .then((value) => value[0]); - - return newDestination; - }); -}; - -export const updateNotificationById = async ( - notificationId: string, - notificationData: Partial, -) => { - const result = await db - .update(notifications) - .set({ - ...notificationData, - }) - .where(eq(notifications.notificationId, notificationId)) - .returning(); - - return result[0]; -}; - -export const createPushoverNotification = async ( - input: typeof apiCreatePushover._type, - organizationId: string, -) => { - await db.transaction(async (tx) => { - const newPushover = await tx - .insert(pushover) - .values({ - userKey: input.userKey, - apiToken: input.apiToken, - priority: input.priority, - retry: input.retry, - expire: input.expire, - }) - .returning() - .then((value) => value[0]); - - if (!newPushover) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting pushover", - }); - } - - const newDestination = await tx - .insert(notifications) - .values({ - pushoverId: newPushover.pushoverId, - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - serverThreshold: input.serverThreshold, - notificationType: "pushover", - organizationId: organizationId, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting notification", - }); - } - - return newDestination; - }); -}; - -export const updatePushoverNotification = async ( - input: typeof apiUpdatePushover._type, -) => { - await db.transaction(async (tx) => { - const newDestination = await tx - .update(notifications) - .set({ - name: input.name, - appDeploy: input.appDeploy, - appBuildError: input.appBuildError, - databaseBackup: input.databaseBackup, - volumeBackup: input.volumeBackup, - dokployRestart: input.dokployRestart, - dockerCleanup: input.dockerCleanup, - organizationId: input.organizationId, - serverThreshold: input.serverThreshold, - }) - .where(eq(notifications.notificationId, input.notificationId)) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error Updating notification", - }); - } - - await tx - .update(pushover) - .set({ - userKey: input.userKey, - apiToken: input.apiToken, - priority: input.priority, - retry: input.retry, - expire: input.expire, - }) - .where(eq(pushover.pushoverId, input.pushoverId)); - - return newDestination; - }); -}; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Notification = typeof notifications.$inferSelect; + +export const createSlackNotification = async ( + input: typeof apiCreateSlack._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newSlack = await tx + .insert(slack) + .values({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newSlack) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting slack", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + slackId: newSlack.slackId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "slack", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateSlackNotification = async ( + input: typeof apiUpdateSlack._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(slack) + .set({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .where(eq(slack.slackId, input.slackId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createTelegramNotification = async ( + input: typeof apiCreateTelegram._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newTelegram = await tx + .insert(telegram) + .values({ + botToken: input.botToken, + chatId: input.chatId, + messageThreadId: input.messageThreadId, + }) + .returning() + .then((value) => value[0]); + + if (!newTelegram) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting telegram", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + telegramId: newTelegram.telegramId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "telegram", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateTelegramNotification = async ( + input: typeof apiUpdateTelegram._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(telegram) + .set({ + botToken: input.botToken, + chatId: input.chatId, + messageThreadId: input.messageThreadId, + }) + .where(eq(telegram.telegramId, input.telegramId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createDiscordNotification = async ( + input: typeof apiCreateDiscord._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newDiscord = await tx + .insert(discord) + .values({ + webhookUrl: input.webhookUrl, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newDiscord) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting discord", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + discordId: newDiscord.discordId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "discord", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateDiscordNotification = async ( + input: typeof apiUpdateDiscord._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(discord) + .set({ + webhookUrl: input.webhookUrl, + decoration: input.decoration, + }) + .where(eq(discord.discordId, input.discordId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createEmailNotification = async ( + input: typeof apiCreateEmail._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newEmail = await tx + .insert(email) + .values({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .returning() + .then((value) => value[0]); + + if (!newEmail) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting email", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + emailId: newEmail.emailId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "email", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateEmailNotification = async ( + input: typeof apiUpdateEmail._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(email) + .set({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .where(eq(email.emailId, input.emailId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createResendNotification = async ( + input: typeof apiCreateResend._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newResend = await tx + .insert(resend) + .values({ + apiKey: input.apiKey, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .returning() + .then((value) => value[0]); + + if (!newResend) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting resend", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + resendId: newResend.resendId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "resend", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateResendNotification = async ( + input: typeof apiUpdateResend._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(resend) + .set({ + apiKey: input.apiKey, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .where(eq(resend.resendId, input.resendId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createGotifyNotification = async ( + input: typeof apiCreateGotify._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newGotify = await tx + .insert(gotify) + .values({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newGotify) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting gotify", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + gotifyId: newGotify.gotifyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "gotify", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateGotifyNotification = async ( + input: typeof apiUpdateGotify._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(gotify) + .set({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .where(eq(gotify.gotifyId, input.gotifyId)); + + return newDestination; + }); +}; + +export const createNtfyNotification = async ( + input: typeof apiCreateNtfy._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newNtfy = await tx + .insert(ntfy) + .values({ + serverUrl: input.serverUrl, + topic: input.topic, + accessToken: input.accessToken ?? null, + priority: input.priority, + }) + .returning() + .then((value) => value[0]); + + if (!newNtfy) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting ntfy", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + ntfyId: newNtfy.ntfyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "ntfy", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateNtfyNotification = async ( + input: typeof apiUpdateNtfy._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(ntfy) + .set({ + serverUrl: input.serverUrl, + topic: input.topic, + accessToken: input.accessToken ?? null, + priority: input.priority, + }) + .where(eq(ntfy.ntfyId, input.ntfyId)); + + return newDestination; + }); +}; + +export const createCustomNotification = async ( + input: typeof apiCreateCustom._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newCustom = await tx + .insert(custom) + .values({ + endpoint: input.endpoint, + headers: input.headers, + }) + .returning() + .then((value) => value[0]); + + if (!newCustom) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting custom", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + customId: newCustom.customId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "custom", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateCustomNotification = async ( + input: typeof apiUpdateCustom._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(custom) + .set({ + endpoint: input.endpoint, + headers: input.headers, + }) + .where(eq(custom.customId, input.customId)); + + return newDestination; + }); +}; + +export const findNotificationById = async (notificationId: string) => { + const notification = await db.query.notifications.findFirst({ + where: eq(notifications.notificationId, notificationId), + with: { + slack: true, + telegram: true, + discord: true, + email: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + if (!notification) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Notification not found", + }); + } + return notification; +}; + +export const removeNotificationById = async (notificationId: string) => { + const result = await db + .delete(notifications) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; + +export const createLarkNotification = async ( + input: typeof apiCreateLark._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newLark = await tx + .insert(lark) + .values({ + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newLark) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting lark", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + larkId: newLark.larkId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "lark", + organizationId: organizationId, + serverThreshold: input.serverThreshold, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateLarkNotification = async ( + input: typeof apiUpdateLark._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(lark) + .set({ + webhookUrl: input.webhookUrl, + }) + .where(eq(lark.larkId, input.larkId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const updateNotificationById = async ( + notificationId: string, + notificationData: Partial, +) => { + const result = await db + .update(notifications) + .set({ + ...notificationData, + }) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; + +export const createPushoverNotification = async ( + input: typeof apiCreatePushover._type, + organizationId: string, +) => { + await db.transaction(async (tx) => { + const newPushover = await tx + .insert(pushover) + .values({ + userKey: input.userKey, + apiToken: input.apiToken, + priority: input.priority, + retry: input.retry, + expire: input.expire, + }) + .returning() + .then((value) => value[0]); + + if (!newPushover) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting pushover", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + pushoverId: newPushover.pushoverId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + serverThreshold: input.serverThreshold, + notificationType: "pushover", + organizationId: organizationId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updatePushoverNotification = async ( + input: typeof apiUpdatePushover._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + volumeBackup: input.volumeBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + organizationId: input.organizationId, + serverThreshold: input.serverThreshold, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(pushover) + .set({ + userKey: input.userKey, + apiToken: input.apiToken, + priority: input.priority, + retry: input.retry, + expire: input.expire, + }) + .where(eq(pushover.pushoverId, input.pushoverId)); + + return newDestination; + }); +}; diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 3c2497324..021ccd05a 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -1,61 +1,64 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendCustomNotification, - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendCustomNotification, + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -interface Props { - projectName: string; - applicationName: string; - applicationType: string; - errorMessage: string; - buildLink: string; - organizationId: string; -} - -export const sendBuildErrorNotifications = async ({ - projectName, - applicationName, - applicationType, - errorMessage, - buildLink, - organizationId, -}: Props) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.appBuildError, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { + +interface Props { + projectName: string; + applicationName: string; + applicationType: string; + errorMessage: string; + buildLink: string; + organizationId: string; +} + +export const sendBuildErrorNotifications = async ({ + projectName, + applicationName, + applicationType, + errorMessage, + buildLink, + organizationId, +}: Props) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.appBuildError, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { const { email, + resend, discord, telegram, slack, @@ -65,311 +68,322 @@ export const sendBuildErrorNotifications = async ({ lark, pushover, } = notification; - try { - if (email) { - const template = await renderAsync( - BuildFailedEmail({ - projectName, - applicationName, - applicationType, - errorMessage: errorMessage, - buildLink, - date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification( - email, - "Build failed for dokploy", - template, - ); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - await sendDiscordNotification(discord, { - title: decorate(">", "`โš ๏ธ` Build Failed"), - color: 0xed4245, - fields: [ - { - name: decorate("`๐Ÿ› ๏ธ`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`โš™๏ธ`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`โ”`", "Type"), - value: applicationType, - inline: true, - }, - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: "Failed", - inline: true, - }, - { - name: decorate("`โš ๏ธ`", "Error Message"), - value: `\`\`\`${truncatedErrorMessage}\`\`\``, - }, - { - name: decorate("`๐Ÿงท`", "Build Link"), - value: `[Click here to access build link](${buildLink})`, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Build Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("โš ๏ธ", "Build Failed"), - `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + - `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + - `${decorate("โ”", `Type: ${applicationType}`)}` + - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + - `${decorate("โš ๏ธ", `Error:\n${errorMessage}`)}` + - `${decorate("๐Ÿ”—", `Build details:\n${buildLink}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Build Failed", - "warning", - `view, Build details, ${buildLink}, clear=true;`, - `๐Ÿ› ๏ธProject: ${projectName}\n` + - `โš™๏ธApplication: ${applicationName}\n` + - `โ”Type: ${applicationType}\n` + - `๐Ÿ•’Date: ${date.toLocaleString()}\n` + - `โš ๏ธError:\n${errorMessage}`, - ); - } - - if (telegram) { - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, - }, - ], - ]; - - await sendTelegramNotification( - telegram, - `โš ๏ธ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, - inlineButton, - ); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#FF0000", - pretext: ":warning: *Build Failed*", - fields: [ - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Error", - value: `\`\`\`${errorMessage}\`\`\``, - short: false, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } - - if (custom) { - await sendCustomNotification(custom, { - title: "Build Error", - message: "Build failed with errors", - projectName, - applicationName, - applicationType, - errorMessage, - buildLink, - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "error", - type: "build", - }); - } - - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", - }, - }, - }, - }, - header: { - title: { - tag: "plain_text", - content: "โš ๏ธ Build Failed", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "red", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Type:**\n${applicationType}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", - }, - type: "danger", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", - }, - ], - margin: "0px 0px 0px 0px", - }, - ], - }, - }, - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - "Build Failed", - `Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`, - ); - } - } catch (error) { - console.log(error); - } - } -}; + try { + if (email || resend) { + const template = await renderAsync( + BuildFailedEmail({ + projectName, + applicationName, + applicationType, + errorMessage: errorMessage, + buildLink, + date: date.toLocaleString(), + }), + ).catch(); + + if (email) { + await sendEmailNotification( + email, + "Build failed for dokploy", + template, + ); + } + + if (resend) { + await sendResendNotification( + resend, + "Build failed for dokploy", + template, + ); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + await sendDiscordNotification(discord, { + title: decorate(">", "`โš ๏ธ` Build Failed"), + color: 0xed4245, + fields: [ + { + name: decorate("`๐Ÿ› ๏ธ`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`โš™๏ธ`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`โ”`", "Type"), + value: applicationType, + inline: true, + }, + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: "Failed", + inline: true, + }, + { + name: decorate("`โš ๏ธ`", "Error Message"), + value: `\`\`\`${truncatedErrorMessage}\`\`\``, + }, + { + name: decorate("`๐Ÿงท`", "Build Link"), + value: `[Click here to access build link](${buildLink})`, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Build Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("โš ๏ธ", "Build Failed"), + `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + + `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + + `${decorate("โ”", `Type: ${applicationType}`)}` + + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("โš ๏ธ", `Error:\n${errorMessage}`)}` + + `${decorate("๐Ÿ”—", `Build details:\n${buildLink}`)}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Build Failed", + "warning", + `view, Build details, ${buildLink}, clear=true;`, + `๐Ÿ› ๏ธProject: ${projectName}\n` + + `โš™๏ธApplication: ${applicationName}\n` + + `โ”Type: ${applicationType}\n` + + `๐Ÿ•’Date: ${date.toLocaleString()}\n` + + `โš ๏ธError:\n${errorMessage}`, + ); + } + + if (telegram) { + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ]; + + await sendTelegramNotification( + telegram, + `โš ๏ธ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + inlineButton, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#FF0000", + pretext: ":warning: *Build Failed*", + fields: [ + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Type", + value: applicationType, + short: true, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Error", + value: `\`\`\`${errorMessage}\`\`\``, + short: false, + }, + ], + actions: [ + { + type: "button", + text: "View Build Details", + url: buildLink, + }, + ], + }, + ], + }); + } + + if (custom) { + await sendCustomNotification(custom, { + title: "Build Error", + message: "Build failed with errors", + projectName, + applicationName, + applicationType, + errorMessage, + buildLink, + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: "error", + type: "build", + }); + } + + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, + }, + }, + }, + header: { + title: { + tag: "plain_text", + content: "โš ๏ธ Build Failed", + }, + subtitle: { + tag: "plain_text", + content: "", + }, + template: "red", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Type:**\n${applicationType}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", + }, + type: "danger", + width: "default", + size: "medium", + behaviors: [ + { + type: "open_url", + default_url: buildLink, + pc_url: "", + ios_url: "", + android_url: "", + }, + ], + margin: "0px 0px 0px 0px", + }, + ], + }, + }, + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + "Build Failed", + `Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`, + ); + } + } catch (error) { + console.log(error); + } + } +}; diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index d1bc04796..e5a735b4d 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -1,64 +1,67 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success"; -import type { Domain } from "@dokploy/server/services/domain"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendCustomNotification, - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success"; +import type { Domain } from "@dokploy/server/services/domain"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendCustomNotification, + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -interface Props { - projectName: string; - applicationName: string; - applicationType: string; - buildLink: string; - organizationId: string; - domains: Domain[]; - environmentName: string; -} - -export const sendBuildSuccessNotifications = async ({ - projectName, - applicationName, - applicationType, - buildLink, - organizationId, - domains, - environmentName, -}: Props) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.appDeploy, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { + +interface Props { + projectName: string; + applicationName: string; + applicationType: string; + buildLink: string; + organizationId: string; + domains: Domain[]; + environmentName: string; +} + +export const sendBuildSuccessNotifications = async ({ + projectName, + applicationName, + applicationType, + buildLink, + organizationId, + domains, + environmentName, +}: Props) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.appDeploy, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { const { email, + resend, discord, telegram, slack, @@ -68,322 +71,333 @@ export const sendBuildSuccessNotifications = async ({ lark, pushover, } = notification; - try { - if (email) { - const template = await renderAsync( - BuildSuccessEmail({ - projectName, - applicationName, - applicationType, - buildLink, - date: date.toLocaleString(), - environmentName, - }), - ).catch(); - await sendEmailNotification( - email, - "Build success for dokploy", - template, - ); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`โœ…` Build Successes"), - color: 0x57f287, - fields: [ - { - name: decorate("`๐Ÿ› ๏ธ`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`โš™๏ธ`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`๐ŸŒ`", "Environment"), - value: environmentName, - inline: true, - }, - { - name: decorate("`โ”`", "Type"), - value: applicationType, - inline: true, - }, - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: "Successful", - inline: true, - }, - { - name: decorate("`๐Ÿงท`", "Build Link"), - value: `[Click here to access build link](${buildLink})`, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Build Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("โœ…", "Build Success"), - `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + - `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + - `${decorate("๐ŸŒ", `Environment: ${environmentName}`)}` + - `${decorate("โ”", `Type: ${applicationType}`)}` + - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + - `${decorate("๐Ÿ”—", `Build details:\n${buildLink}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Build Success", - "white_check_mark", - `view, Build details, ${buildLink}, clear=true;`, - `๐Ÿ› Project: ${projectName}\n` + - `โš™๏ธApplication: ${applicationName}\n` + - `๐ŸŒEnvironment: ${environmentName}\n` + - `โ”Type: ${applicationType}\n` + - `๐Ÿ•’Date: ${date.toLocaleString()}`, - ); - } - - if (telegram) { - const chunkArray = (array: T[], chunkSize: number): T[][] => - Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => - array.slice(i * chunkSize, i * chunkSize + chunkSize), - ); - - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, - }, - ], - ...chunkArray(domains, 2).map((chunk) => - chunk.map((data) => ({ - text: data.host, - url: `${data.https ? "https" : "http"}://${data.host}`, - })), - ), - ]; - - await sendTelegramNotification( - telegram, - `โœ… Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format( - date, - "PP", - )}\nTime: ${format(date, "pp")}`, - inlineButton, - ); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Build Success*", - fields: [ - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Environment", - value: environmentName, - short: true, - }, - { - title: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } - - if (custom) { - await sendCustomNotification(custom, { - title: "Build Success", - message: "Build completed successfully", - projectName, - applicationName, - applicationType, - buildLink, - timestamp: date.toISOString(), - date: date.toLocaleString(), - domains: domains.map((domain) => domain.host).join(", "), - status: "success", - type: "build", - }); - } - - if (lark) { - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", - }, - }, - }, - }, - header: { - title: { - tag: "plain_text", - content: "โœ… Build Success", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "green", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Environment:**\n${environmentName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Type:**\n${applicationType}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", - }, - type: "primary", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", - }, - ], - margin: "0px 0px 0px 0px", - }, - ], - }, - }, - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - "Build Success", - `Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`, - ); - } - } catch (error) { - console.log(error); - } - } -}; + try { + if (email || resend) { + const template = await renderAsync( + BuildSuccessEmail({ + projectName, + applicationName, + applicationType, + buildLink, + date: date.toLocaleString(), + environmentName, + }), + ).catch(); + + if (email) { + await sendEmailNotification( + email, + "Build success for dokploy", + template, + ); + } + + if (resend) { + await sendResendNotification( + resend, + "Build success for dokploy", + template, + ); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: decorate(">", "`โœ…` Build Successes"), + color: 0x57f287, + fields: [ + { + name: decorate("`๐Ÿ› ๏ธ`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`โš™๏ธ`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`๐ŸŒ`", "Environment"), + value: environmentName, + inline: true, + }, + { + name: decorate("`โ”`", "Type"), + value: applicationType, + inline: true, + }, + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: "Successful", + inline: true, + }, + { + name: decorate("`๐Ÿงท`", "Build Link"), + value: `[Click here to access build link](${buildLink})`, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Build Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("โœ…", "Build Success"), + `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + + `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + + `${decorate("๐ŸŒ", `Environment: ${environmentName}`)}` + + `${decorate("โ”", `Type: ${applicationType}`)}` + + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("๐Ÿ”—", `Build details:\n${buildLink}`)}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Build Success", + "white_check_mark", + `view, Build details, ${buildLink}, clear=true;`, + `๐Ÿ› Project: ${projectName}\n` + + `โš™๏ธApplication: ${applicationName}\n` + + `๐ŸŒEnvironment: ${environmentName}\n` + + `โ”Type: ${applicationType}\n` + + `๐Ÿ•’Date: ${date.toLocaleString()}`, + ); + } + + if (telegram) { + const chunkArray = (array: T[], chunkSize: number): T[][] => + Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => + array.slice(i * chunkSize, i * chunkSize + chunkSize), + ); + + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ...chunkArray(domains, 2).map((chunk) => + chunk.map((data) => ({ + text: data.host, + url: `${data.https ? "https" : "http"}://${data.host}`, + })), + ), + ]; + + await sendTelegramNotification( + telegram, + `โœ… Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format( + date, + "PP", + )}\nTime: ${format(date, "pp")}`, + inlineButton, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Build Success*", + fields: [ + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Environment", + value: environmentName, + short: true, + }, + { + title: "Type", + value: applicationType, + short: true, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + actions: [ + { + type: "button", + text: "View Build Details", + url: buildLink, + }, + ], + }, + ], + }); + } + + if (custom) { + await sendCustomNotification(custom, { + title: "Build Success", + message: "Build completed successfully", + projectName, + applicationName, + applicationType, + buildLink, + timestamp: date.toISOString(), + date: date.toLocaleString(), + domains: domains.map((domain) => domain.host).join(", "), + status: "success", + type: "build", + }); + } + + if (lark) { + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, + }, + }, + }, + header: { + title: { + tag: "plain_text", + content: "โœ… Build Success", + }, + subtitle: { + tag: "plain_text", + content: "", + }, + template: "green", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Environment:**\n${environmentName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Type:**\n${applicationType}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", + }, + type: "primary", + width: "default", + size: "medium", + behaviors: [ + { + type: "open_url", + default_url: buildLink, + pc_url: "", + ios_url: "", + android_url: "", + }, + ], + margin: "0px 0px 0px 0px", + }, + ], + }, + }, + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + "Build Success", + `Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`, + ); + } + } catch (error) { + console.log(error); + } + } +}; diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 1b2b49bf1..2c9e5b78e 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -1,61 +1,64 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendCustomNotification, - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendCustomNotification, + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -export const sendDatabaseBackupNotifications = async ({ - projectName, - applicationName, - databaseType, - type, - errorMessage, - organizationId, - databaseName, -}: { - projectName: string; - applicationName: string; - databaseType: "postgres" | "mysql" | "mongodb" | "mariadb"; - type: "error" | "success"; - organizationId: string; - errorMessage?: string; - databaseName: string; -}) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.databaseBackup, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { + +export const sendDatabaseBackupNotifications = async ({ + projectName, + applicationName, + databaseType, + type, + errorMessage, + organizationId, + databaseName, +}: { + projectName: string; + applicationName: string; + databaseType: "postgres" | "mysql" | "mongodb" | "mariadb"; + type: "error" | "success"; + organizationId: string; + errorMessage?: string; + databaseName: string; +}) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.databaseBackup, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { const { email, + resend, discord, telegram, slack, @@ -65,339 +68,350 @@ export const sendDatabaseBackupNotifications = async ({ lark, pushover, } = notification; - try { - if (email) { - const template = await renderAsync( - DatabaseBackupEmail({ - projectName, - applicationName, - databaseType, - type, - errorMessage, - date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification( - email, - "Database backup for dokploy", - template, - ); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: - type === "success" - ? decorate(">", "`โœ…` Database Backup Successful") - : decorate(">", "`โŒ` Database Backup Failed"), - color: type === "success" ? 0x57f287 : 0xed4245, - fields: [ - { - name: decorate("`๐Ÿ› ๏ธ`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`โš™๏ธ`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`โ”`", "Database"), - value: databaseType, - inline: true, - }, - { - name: decorate("`๐Ÿ“‚`", "Database Name"), - value: databaseName, - inline: true, - }, - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: type - .replace("error", "Failed") - .replace("success", "Successful"), - inline: true, - }, - ...(type === "error" && errorMessage - ? [ - { - name: decorate("`โš ๏ธ`", "Error Message"), - value: `\`\`\`${errorMessage}\`\`\``, - }, - ] - : []), - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Database Backup Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - - await sendGotifyNotification( - gotify, - decorate( - type === "success" ? "โœ…" : "โŒ", - `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - ), - `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + - `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + - `${decorate("โ”", `Type: ${databaseType}`)}` + - `${decorate("๐Ÿ“‚", `Database Name: ${databaseName}`)}` + - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + - `${type === "error" && errorMessage ? decorate("โŒ", `Error:\n${errorMessage}`) : ""}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - `${type === "success" ? "white_check_mark" : "x"}`, - "", - `๐Ÿ› Project: ${projectName}\n` + - `โš™๏ธApplication: ${applicationName}\n` + - `โ”Type: ${databaseType}\n` + - `๐Ÿ“‚Database Name: ${databaseName}` + - `๐Ÿ•’Date: ${date.toLocaleString()}\n` + - `${type === "error" && errorMessage ? `โŒError:\n${errorMessage}` : ""}`, - ); - } - - if (telegram) { - const isError = type === "error" && errorMessage; - - const statusEmoji = type === "success" ? "โœ…" : "โŒ"; - const typeStatus = type === "success" ? "Successful" : "Failed"; - const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` - : ""; - - const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - - await sendTelegramNotification(telegram, messageText); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: type === "success" ? "#00FF00" : "#FF0000", - pretext: - type === "success" - ? ":white_check_mark: *Database Backup Successful*" - : ":x: *Database Backup Failed*", - fields: [ - ...(type === "error" && errorMessage - ? [ - { - title: "Error Message", - value: errorMessage, - short: false, - }, - ] - : []), - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Type", - value: databaseType, - short: true, - }, - { - title: "Database Name", - value: databaseName, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Type", - value: type, - }, - { - title: "Status", - value: type === "success" ? "Successful" : "Failed", - }, - ], - }, - ], - }); - } - - if (custom) { - await sendCustomNotification(custom, { - title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - message: - type === "success" - ? "Database backup completed successfully" - : "Database backup failed", - projectName, - applicationName, - databaseType, - databaseName, - type, - errorMessage: errorMessage || "", - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: type, - }); - } - - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = - errorMessage && errorMessage.length > limitCharacter - ? errorMessage.substring(0, limitCharacter) - : errorMessage; - - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", - }, - }, - }, - }, - header: { - title: { - tag: "plain_text", - content: - type === "success" - ? "โœ… Database Backup Successful" - : "โŒ Database Backup Failed", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: type === "success" ? "green" : "red", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Database Type:**\n${databaseType}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Database Name:**\n${databaseName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - ...(type === "error" && truncatedErrorMessage - ? [ - { - tag: "markdown", - content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, - text_align: "left", - text_size: "normal_v2", - }, - ] - : []), - ], - }, - }, - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - `Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, - ); - } - } catch (error) { - console.log(error); - } - } -}; + try { + if (email || resend) { + const template = await renderAsync( + DatabaseBackupEmail({ + projectName, + applicationName, + databaseType, + type, + errorMessage, + date: date.toLocaleString(), + }), + ).catch(); + + if (email) { + await sendEmailNotification( + email, + "Database backup for dokploy", + template, + ); + } + + if (resend) { + await sendResendNotification( + resend, + "Database backup for dokploy", + template, + ); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: + type === "success" + ? decorate(">", "`โœ…` Database Backup Successful") + : decorate(">", "`โŒ` Database Backup Failed"), + color: type === "success" ? 0x57f287 : 0xed4245, + fields: [ + { + name: decorate("`๐Ÿ› ๏ธ`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`โš™๏ธ`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`โ”`", "Database"), + value: databaseType, + inline: true, + }, + { + name: decorate("`๐Ÿ“‚`", "Database Name"), + value: databaseName, + inline: true, + }, + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: type + .replace("error", "Failed") + .replace("success", "Successful"), + inline: true, + }, + ...(type === "error" && errorMessage + ? [ + { + name: decorate("`โš ๏ธ`", "Error Message"), + value: `\`\`\`${errorMessage}\`\`\``, + }, + ] + : []), + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Database Backup Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + + await sendGotifyNotification( + gotify, + decorate( + type === "success" ? "โœ…" : "โŒ", + `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + ), + `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + + `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + + `${decorate("โ”", `Type: ${databaseType}`)}` + + `${decorate("๐Ÿ“‚", `Database Name: ${databaseName}`)}` + + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + + `${type === "error" && errorMessage ? decorate("โŒ", `Error:\n${errorMessage}`) : ""}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + `${type === "success" ? "white_check_mark" : "x"}`, + "", + `๐Ÿ› Project: ${projectName}\n` + + `โš™๏ธApplication: ${applicationName}\n` + + `โ”Type: ${databaseType}\n` + + `๐Ÿ“‚Database Name: ${databaseName}` + + `๐Ÿ•’Date: ${date.toLocaleString()}\n` + + `${type === "error" && errorMessage ? `โŒError:\n${errorMessage}` : ""}`, + ); + } + + if (telegram) { + const isError = type === "error" && errorMessage; + + const statusEmoji = type === "success" ? "โœ…" : "โŒ"; + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; + + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + + await sendTelegramNotification(telegram, messageText); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: type === "success" ? "#00FF00" : "#FF0000", + pretext: + type === "success" + ? ":white_check_mark: *Database Backup Successful*" + : ":x: *Database Backup Failed*", + fields: [ + ...(type === "error" && errorMessage + ? [ + { + title: "Error Message", + value: errorMessage, + short: false, + }, + ] + : []), + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Type", + value: databaseType, + short: true, + }, + { + title: "Database Name", + value: databaseName, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Type", + value: type, + }, + { + title: "Status", + value: type === "success" ? "Successful" : "Failed", + }, + ], + }, + ], + }); + } + + if (custom) { + await sendCustomNotification(custom, { + title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + message: + type === "success" + ? "Database backup completed successfully" + : "Database backup failed", + projectName, + applicationName, + databaseType, + databaseName, + type, + errorMessage: errorMessage || "", + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: type, + }); + } + + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = + errorMessage && errorMessage.length > limitCharacter + ? errorMessage.substring(0, limitCharacter) + : errorMessage; + + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, + }, + }, + }, + header: { + title: { + tag: "plain_text", + content: + type === "success" + ? "โœ… Database Backup Successful" + : "โŒ Database Backup Failed", + }, + subtitle: { + tag: "plain_text", + content: "", + }, + template: type === "success" ? "green" : "red", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Database Type:**\n${databaseType}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Database Name:**\n${databaseName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ...(type === "error" && truncatedErrorMessage + ? [ + { + tag: "markdown", + content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, + text_align: "left", + text_size: "normal_v2", + }, + ] + : []), + ], + }, + }, + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + `Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, + ); + } + } catch (error) { + console.log(error); + } + } +}; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 834ff489c..0646a2b64 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -1,48 +1,51 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendCustomNotification, - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendCustomNotification, + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -export const sendDockerCleanupNotifications = async ( - organizationId: string, - message = "Docker cleanup for dokploy", -) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.dockerCleanup, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { + +export const sendDockerCleanupNotifications = async ( + organizationId: string, + message = "Docker cleanup for dokploy", +) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.dockerCleanup, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { const { email, + resend, discord, telegram, slack, @@ -52,205 +55,215 @@ export const sendDockerCleanupNotifications = async ( lark, pushover, } = notification; - try { - if (email) { - const template = await renderAsync( - DockerCleanupEmail({ message, date: date.toLocaleString() }), - ).catch(); - - await sendEmailNotification( - email, - "Docker cleanup for dokploy", - template, - ); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`โœ…` Docker Cleanup"), - color: 0x57f287, - fields: [ - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: "Successful", - inline: true, - }, - { - name: decorate("`๐Ÿ“œ`", "Message"), - value: `\`\`\`${message}\`\`\``, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Docker Cleanup Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("โœ…", "Docker Cleanup"), - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + - `${decorate("๐Ÿ“œ", `Message:\n${message}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Docker Cleanup", - "white_check_mark", - "", - `๐Ÿ•’Date: ${date.toLocaleString()}\n` + `๐Ÿ“œMessage:\n${message}`, - ); - } - - if (telegram) { - await sendTelegramNotification( - telegram, - `โœ… Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - ); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Docker Cleanup*", - fields: [ - { - title: "Message", - value: message, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - }, - ], - }); - } - - if (custom) { - await sendCustomNotification(custom, { - title: "Docker Cleanup", - message: "Docker cleanup completed successfully", - cleanupMessage: message, - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "success", - type: "docker-cleanup", - }); - } - - if (lark) { - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", - }, - }, - }, - }, - header: { - title: { - tag: "plain_text", - content: "โœ… Docker Cleanup", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "green", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: "**Status:**\nSuccessful", - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Cleanup Details:**\n${message}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - ], - }, - }, - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - "Docker Cleanup", - `Date: ${date.toLocaleString()}\nMessage: ${message}`, - ); - } - } catch (error) { - console.log(error); - } - } -}; + try { + if (email || resend) { + const template = await renderAsync( + DockerCleanupEmail({ message, date: date.toLocaleString() }), + ).catch(); + + if (email) { + await sendEmailNotification( + email, + "Docker cleanup for dokploy", + template, + ); + } + + if (resend) { + await sendResendNotification( + resend, + "Docker cleanup for dokploy", + template, + ); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: decorate(">", "`โœ…` Docker Cleanup"), + color: 0x57f287, + fields: [ + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: "Successful", + inline: true, + }, + { + name: decorate("`๐Ÿ“œ`", "Message"), + value: `\`\`\`${message}\`\`\``, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Docker Cleanup Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("โœ…", "Docker Cleanup"), + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("๐Ÿ“œ", `Message:\n${message}`)}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Docker Cleanup", + "white_check_mark", + "", + `๐Ÿ•’Date: ${date.toLocaleString()}\n` + `๐Ÿ“œMessage:\n${message}`, + ); + } + + if (telegram) { + await sendTelegramNotification( + telegram, + `โœ… Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Docker Cleanup*", + fields: [ + { + title: "Message", + value: message, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + }, + ], + }); + } + + if (custom) { + await sendCustomNotification(custom, { + title: "Docker Cleanup", + message: "Docker cleanup completed successfully", + cleanupMessage: message, + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: "success", + type: "docker-cleanup", + }); + } + + if (lark) { + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, + }, + }, + }, + header: { + title: { + tag: "plain_text", + content: "โœ… Docker Cleanup", + }, + subtitle: { + tag: "plain_text", + content: "", + }, + template: "green", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: "**Status:**\nSuccessful", + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Cleanup Details:**\n${message}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ], + }, + }, + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + "Docker Cleanup", + `Date: ${date.toLocaleString()}\nMessage: ${message}`, + ); + } + } catch (error) { + console.log(error); + } + } +}; diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index f93f31ac5..6526fb222 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -1,42 +1,45 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { eq } from "drizzle-orm"; -import { - sendCustomNotification, - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { eq } from "drizzle-orm"; +import { + sendCustomNotification, + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendLarkNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -export const sendDokployRestartNotifications = async () => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: eq(notifications.dokployRestart, true), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { + +export const sendDokployRestartNotifications = async () => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: eq(notifications.dokployRestart, true), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { const { email, + resend, discord, telegram, slack, @@ -46,200 +49,210 @@ export const sendDokployRestartNotifications = async () => { lark, pushover, } = notification; - - try { - if (email) { - const template = await renderAsync( - DokployRestartEmail({ date: date.toLocaleString() }), - ).catch(); - - await sendEmailNotification( - email, - "Dokploy Server Restarted", - template, - ); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`โœ…` Dokploy Server Restarted"), - color: 0x57f287, - fields: [ - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: "Successful", - inline: true, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Restart Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("โœ…", "Dokploy Server Restarted"), - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Dokploy Server Restarted", - "white_check_mark", - "", - `๐Ÿ•’Date: ${date.toLocaleString()}`, - ); - } - - if (telegram) { - await sendTelegramNotification( - telegram, - `โœ… Dokploy Server Restarted\n\nDate: ${format( - date, - "PP", - )}\nTime: ${format(date, "pp")}`, - ); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Dokploy Server Restarted*", - fields: [ - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - }, - ], - }); - } - - if (custom) { - try { - await sendCustomNotification(custom, { - title: "Dokploy Server Restarted", - message: "Dokploy server has been restarted successfully", - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "success", - type: "dokploy-restart", - }); - } catch (error) { - console.log(error); - } - } - - if (lark) { - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", - }, - }, - }, - }, - header: { - title: { - tag: "plain_text", - content: "โœ… Dokploy Server Restarted", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "green", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: "**Status:**\nSuccessful", - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Restart Time:**\n${format( - date, - "PP pp", - )}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - ], - }, - }, - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - "Dokploy Server Restarted", - `Date: ${date.toLocaleString()}`, - ); - } - } catch (error) { - console.log(error); - } - } -}; + + try { + if (email || resend) { + const template = await renderAsync( + DokployRestartEmail({ date: date.toLocaleString() }), + ).catch(); + + if (email) { + await sendEmailNotification( + email, + "Dokploy Server Restarted", + template, + ); + } + + if (resend) { + await sendResendNotification( + resend, + "Dokploy Server Restarted", + template, + ); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: decorate(">", "`โœ…` Dokploy Server Restarted"), + color: 0x57f287, + fields: [ + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: "Successful", + inline: true, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Restart Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("โœ…", "Dokploy Server Restarted"), + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Dokploy Server Restarted", + "white_check_mark", + "", + `๐Ÿ•’Date: ${date.toLocaleString()}`, + ); + } + + if (telegram) { + await sendTelegramNotification( + telegram, + `โœ… Dokploy Server Restarted\n\nDate: ${format( + date, + "PP", + )}\nTime: ${format(date, "pp")}`, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Dokploy Server Restarted*", + fields: [ + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + }, + ], + }); + } + + if (custom) { + try { + await sendCustomNotification(custom, { + title: "Dokploy Server Restarted", + message: "Dokploy server has been restarted successfully", + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: "success", + type: "dokploy-restart", + }); + } catch (error) { + console.log(error); + } + } + + if (lark) { + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, + }, + }, + }, + header: { + title: { + tag: "plain_text", + content: "โœ… Dokploy Server Restarted", + }, + subtitle: { + tag: "plain_text", + content: "", + }, + template: "green", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: "**Status:**\nSuccessful", + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Restart Time:**\n${format( + date, + "PP pp", + )}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ], + }, + }, + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + "Dokploy Server Restarted", + `Date: ${date.toLocaleString()}`, + ); + } + } catch (error) { + console.log(error); + } + } +}; diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 170b90e8a..72a6b04be 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -1,256 +1,284 @@ -import type { - custom, - discord, - email, - gotify, +import type { + custom, + discord, + email, + gotify, lark, ntfy, pushover, + resend, slack, telegram, } from "@dokploy/server/db/schema"; -import nodemailer from "nodemailer"; - -export const sendEmailNotification = async ( - connection: typeof email.$inferInsert, - subject: string, - htmlContent: string, -) => { - try { - const { - smtpServer, - smtpPort, - username, - password, - fromAddress, - toAddresses, - } = connection; - const transporter = nodemailer.createTransport({ - host: smtpServer, - port: smtpPort, - auth: { user: username, pass: password }, - }); - - await transporter.sendMail({ - from: fromAddress, - to: toAddresses.join(", "), - subject, - html: htmlContent, - textEncoding: "base64", - }); - } catch (err) { - console.log(err); - throw new Error( - `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendDiscordNotification = async ( - connection: typeof discord.$inferInsert, - embed: any, -) => { - try { - const response = await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ embeds: [embed] }), - }); - if (!response.ok) { - throw new Error( - `Failed to send discord notification ${response.statusText}`, - ); - } - } catch (err) { - console.log("error", err); - throw new Error( - `Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendTelegramNotification = async ( - connection: typeof telegram.$inferInsert, - messageText: string, - inlineButton?: { - text: string; - url: string; - }[][], -) => { - try { - const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; - await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: connection.chatId, - message_thread_id: connection.messageThreadId, - text: messageText, - parse_mode: "HTML", - disable_web_page_preview: true, - reply_markup: { - inline_keyboard: inlineButton, - }, - }), - }); - } catch (err) { - console.log(err); - } -}; - -export const sendSlackNotification = async ( - connection: typeof slack.$inferInsert, - message: any, -) => { - try { - const response = await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(message), - }); - if (!response.ok) { - throw new Error( - `Failed to send slack notification ${response.statusText}`, - ); - } - } catch (err) { - console.log("error", err); - throw new Error( - `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, - ); - } -}; - -export const sendGotifyNotification = async ( - connection: typeof gotify.$inferInsert, - title: string, - message: string, -) => { - const response = await fetch(`${connection.serverUrl}/message`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Gotify-Key": connection.appToken, - }, - body: JSON.stringify({ - title: title, - message: message, - priority: connection.priority, - extras: { - "client::display": { - contentType: "text/plain", - }, - }, - }), - }); - - if (!response.ok) { - throw new Error( - `Failed to send Gotify notification: ${response.statusText}`, - ); - } -}; - -export const sendNtfyNotification = async ( - connection: typeof ntfy.$inferInsert, - title: string, - tags: string, - actions: string, - message: string, -) => { - const response = await fetch(`${connection.serverUrl}/${connection.topic}`, { - method: "POST", - headers: { - ...(connection.accessToken && { - Authorization: `Bearer ${connection.accessToken}`, - }), - "X-Priority": connection.priority?.toString() || "3", - "X-Title": title, - "X-Tags": tags, - "X-Actions": actions, - }, - body: message, - }); - - if (!response.ok) { - throw new Error(`Failed to send ntfy notification: ${response.statusText}`); - } -}; - -export const sendCustomNotification = async ( - connection: typeof custom.$inferInsert, - payload: Record, -) => { - try { - // Merge default headers with custom headers (now already an object from jsonb) - const headers: Record = { - "Content-Type": "application/json", - ...(connection.headers || {}), - }; - - // Default body with payload - const body = JSON.stringify(payload); - - const response = await fetch(connection.endpoint, { - method: "POST", - headers, - body, - }); - - if (!response.ok) { - throw new Error( - `Failed to send custom notification: ${response.statusText}`, - ); - } - - return response; - } catch (error) { - console.error("Error sending custom notification:", error); - throw error; - } -}; - -export const sendLarkNotification = async ( - connection: typeof lark.$inferInsert, - message: any, -) => { - try { - await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(message), - }); - } catch (err) { - console.log(err); - } -}; - -export const sendPushoverNotification = async ( - connection: typeof pushover.$inferInsert, - title: string, - message: string, -) => { - const formData = new URLSearchParams(); - formData.append("token", connection.apiToken); - formData.append("user", connection.userKey); - formData.append("title", title); - formData.append("message", message); - formData.append("priority", connection.priority?.toString() || "0"); - - // For emergency priority (2), retry and expire are required - if (connection.priority === 2) { - formData.append("retry", connection.retry?.toString() || "30"); - formData.append("expire", connection.expire?.toString() || "3600"); - } - - const response = await fetch("https://api.pushover.net/1/messages.json", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - throw new Error( - `Failed to send Pushover notification: ${response.statusText}`, - ); - } -}; +import nodemailer from "nodemailer"; +import { Resend } from "resend"; + +export const sendEmailNotification = async ( + connection: typeof email.$inferInsert, + subject: string, + htmlContent: string, +) => { + try { + const { + smtpServer, + smtpPort, + username, + password, + fromAddress, + toAddresses, + } = connection; + const transporter = nodemailer.createTransport({ + host: smtpServer, + port: smtpPort, + auth: { user: username, pass: password }, + }); + + await transporter.sendMail({ + from: fromAddress, + to: toAddresses.join(", "), + subject, + html: htmlContent, + textEncoding: "base64", + }); + } catch (err) { + console.log(err); + throw new Error( + `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendResendNotification = async ( + connection: typeof resend.$inferInsert, + subject: string, + htmlContent: string, +) => { + try { + const client = new Resend(connection.apiKey); + + const result = await client.emails.send({ + from: connection.fromAddress, + to: connection.toAddresses, + subject, + html: htmlContent, + }); + + if (result.error) { + throw new Error(result.error.message); + } + } catch (err) { + console.log(err); + throw new Error( + `Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendDiscordNotification = async ( + connection: typeof discord.$inferInsert, + embed: any, +) => { + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + if (!response.ok) { + throw new Error( + `Failed to send discord notification ${response.statusText}`, + ); + } + } catch (err) { + console.log("error", err); + throw new Error( + `Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendTelegramNotification = async ( + connection: typeof telegram.$inferInsert, + messageText: string, + inlineButton?: { + text: string; + url: string; + }[][], +) => { + try { + const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; + await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: connection.chatId, + message_thread_id: connection.messageThreadId, + text: messageText, + parse_mode: "HTML", + disable_web_page_preview: true, + reply_markup: { + inline_keyboard: inlineButton, + }, + }), + }); + } catch (err) { + console.log(err); + } +}; + +export const sendSlackNotification = async ( + connection: typeof slack.$inferInsert, + message: any, +) => { + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + if (!response.ok) { + throw new Error( + `Failed to send slack notification ${response.statusText}`, + ); + } + } catch (err) { + console.log("error", err); + throw new Error( + `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } +}; + +export const sendGotifyNotification = async ( + connection: typeof gotify.$inferInsert, + title: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/message`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Gotify-Key": connection.appToken, + }, + body: JSON.stringify({ + title: title, + message: message, + priority: connection.priority, + extras: { + "client::display": { + contentType: "text/plain", + }, + }, + }), + }); + + if (!response.ok) { + throw new Error( + `Failed to send Gotify notification: ${response.statusText}`, + ); + } +}; + +export const sendNtfyNotification = async ( + connection: typeof ntfy.$inferInsert, + title: string, + tags: string, + actions: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/${connection.topic}`, { + method: "POST", + headers: { + ...(connection.accessToken && { + Authorization: `Bearer ${connection.accessToken}`, + }), + "X-Priority": connection.priority?.toString() || "3", + "X-Title": title, + "X-Tags": tags, + "X-Actions": actions, + }, + body: message, + }); + + if (!response.ok) { + throw new Error(`Failed to send ntfy notification: ${response.statusText}`); + } +}; + +export const sendCustomNotification = async ( + connection: typeof custom.$inferInsert, + payload: Record, +) => { + try { + // Merge default headers with custom headers (now already an object from jsonb) + const headers: Record = { + "Content-Type": "application/json", + ...(connection.headers || {}), + }; + + // Default body with payload + const body = JSON.stringify(payload); + + const response = await fetch(connection.endpoint, { + method: "POST", + headers, + body, + }); + + if (!response.ok) { + throw new Error( + `Failed to send custom notification: ${response.statusText}`, + ); + } + + return response; + } catch (error) { + console.error("Error sending custom notification:", error); + throw error; + } +}; + +export const sendLarkNotification = async ( + connection: typeof lark.$inferInsert, + message: any, +) => { + try { + await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + } catch (err) { + console.log(err); + } +}; + +export const sendPushoverNotification = async ( + connection: typeof pushover.$inferInsert, + title: string, + message: string, +) => { + const formData = new URLSearchParams(); + formData.append("token", connection.apiToken); + formData.append("user", connection.userKey); + formData.append("title", title); + formData.append("message", message); + formData.append("priority", connection.priority?.toString() || "0"); + + // For emergency priority (2), retry and expire are required + if (connection.priority === 2) { + formData.append("retry", connection.retry?.toString() || "30"); + formData.append("expire", connection.expire?.toString() || "3600"); + } + + const response = await fetch("https://api.pushover.net/1/messages.json", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error( + `Failed to send Pushover notification: ${response.statusText}`, + ); + } +}; diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index 44e2b5fb3..611ccaf85 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -1,285 +1,300 @@ -import { db } from "@dokploy/server/db"; -import { notifications } from "@dokploy/server/db/schema"; -import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup"; -import { renderAsync } from "@react-email/components"; -import { format } from "date-fns"; -import { and, eq } from "drizzle-orm"; -import { - sendDiscordNotification, +import { db } from "@dokploy/server/db"; +import { notifications } from "@dokploy/server/db/schema"; +import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup"; +import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; +import { + sendDiscordNotification, sendEmailNotification, sendGotifyNotification, sendNtfyNotification, sendPushoverNotification, + sendResendNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; - -export const sendVolumeBackupNotifications = async ({ - projectName, - applicationName, - volumeName, - serviceType, - type, - errorMessage, - organizationId, - backupSize, -}: { - projectName: string; - applicationName: string; - volumeName: string; - serviceType: - | "application" - | "postgres" - | "mysql" - | "mongodb" - | "mariadb" - | "redis" - | "compose"; - type: "error" | "success"; - organizationId: string; - errorMessage?: string; - backupSize?: string; -}) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.volumeBackup, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - pushover: true, - }, - }); - - for (const notification of notificationList) { - const { email, discord, telegram, slack, gotify, ntfy, pushover } = - notification; - - if (email) { - const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`; - const htmlContent = await renderAsync( - VolumeBackupEmail({ - projectName, - applicationName, - volumeName, - serviceType, - type, - errorMessage, - backupSize, - date: date.toISOString(), - }), - ); - await sendEmailNotification(email, subject, htmlContent); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: - type === "success" - ? decorate(">", "`โœ…` Volume Backup Successful") - : decorate(">", "`โŒ` Volume Backup Failed"), - color: type === "success" ? 0x57f287 : 0xed4245, - fields: [ - { - name: decorate("`๐Ÿ› ๏ธ`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`โš™๏ธ`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`๐Ÿ’พ`", "Volume Name"), - value: volumeName, - inline: true, - }, - { - name: decorate("`๐Ÿ”ง`", "Service Type"), - value: serviceType, - inline: true, - }, - ...(backupSize - ? [ - { - name: decorate("`๐Ÿ“Š`", "Backup Size"), - value: backupSize, - inline: true, - }, - ] - : []), - { - name: decorate("`๐Ÿ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`โŒš`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`โ“`", "Type"), - value: type - .replace("error", "Failed") - .replace("success", "Successful"), - inline: true, - }, - ...(type === "error" && errorMessage - ? [ - { - name: decorate("`โš ๏ธ`", "Error Message"), - value: `\`\`\`${errorMessage}\`\`\``, - }, - ] - : []), - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Volume Backup Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - - await sendGotifyNotification( - gotify, - decorate( - type === "success" ? "โœ…" : "โŒ", - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - ), - `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + - `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + - `${decorate("๐Ÿ’พ", `Volume Name: ${volumeName}`)}` + - `${decorate("๐Ÿ”ง", `Service Type: ${serviceType}`)}` + - `${backupSize ? decorate("๐Ÿ“Š", `Backup Size: ${backupSize}`) : ""}` + - `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + - `${type === "error" && errorMessage ? decorate("โŒ", `Error:\n${errorMessage}`) : ""}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - `${type === "success" ? "white_check_mark" : "x"}`, - "", - `๐Ÿ› ๏ธProject: ${projectName}\n` + - `โš™๏ธApplication: ${applicationName}\n` + - `๐Ÿ’พVolume Name: ${volumeName}\n` + - `๐Ÿ”งService Type: ${serviceType}\n` + - `${backupSize ? `๐Ÿ“ŠBackup Size: ${backupSize}\n` : ""}` + - `๐Ÿ•’Date: ${date.toLocaleString()}\n` + - `${type === "error" && errorMessage ? `โŒError:\n${errorMessage}` : ""}`, - ); - } - - if (telegram) { - const isError = type === "error" && errorMessage; - - const statusEmoji = type === "success" ? "โœ…" : "โŒ"; - const typeStatus = type === "success" ? "Successful" : "Failed"; - const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` - : ""; - const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` : ""; - - const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - - await sendTelegramNotification(telegram, messageText); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: type === "success" ? "#00FF00" : "#FF0000", - pretext: - type === "success" - ? ":white_check_mark: *Volume Backup Successful*" - : ":x: *Volume Backup Failed*", - fields: [ - ...(type === "error" && errorMessage - ? [ - { - title: "Error Message", - value: errorMessage, - short: false, - }, - ] - : []), - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Volume Name", - value: volumeName, - short: true, - }, - { - title: "Service Type", - value: serviceType, - short: true, - }, - ...(backupSize - ? [ - { - title: "Backup Size", - value: backupSize, - short: true, - }, - ] - : []), - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Type", - value: type, - short: true, - }, - { - title: "Status", - value: type === "success" ? "Successful" : "Failed", - short: true, - }, - ], - }, - ], - }); - } - - if (pushover) { - await sendPushoverNotification( - pushover, - `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, - `Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, - ); - } - } -}; + +export const sendVolumeBackupNotifications = async ({ + projectName, + applicationName, + volumeName, + serviceType, + type, + errorMessage, + organizationId, + backupSize, +}: { + projectName: string; + applicationName: string; + volumeName: string; + serviceType: + | "application" + | "postgres" + | "mysql" + | "mongodb" + | "mariadb" + | "redis" + | "compose"; + type: "error" | "success"; + organizationId: string; + errorMessage?: string; + backupSize?: string; +}) => { + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.volumeBackup, true), + eq(notifications.organizationId, organizationId), + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + resend: true, + gotify: true, + ntfy: true, + pushover: true, + }, + }); + + for (const notification of notificationList) { + const { + email, + resend, + discord, + telegram, + slack, + gotify, + ntfy, + pushover, + } = notification; + + if (email || resend) { + const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`; + const htmlContent = await renderAsync( + VolumeBackupEmail({ + projectName, + applicationName, + volumeName, + serviceType, + type, + errorMessage, + backupSize, + date: date.toISOString(), + }), + ); + if (email) { + await sendEmailNotification(email, subject, htmlContent); + } + if (resend) { + await sendResendNotification(resend, subject, htmlContent); + } + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: + type === "success" + ? decorate(">", "`โœ…` Volume Backup Successful") + : decorate(">", "`โŒ` Volume Backup Failed"), + color: type === "success" ? 0x57f287 : 0xed4245, + fields: [ + { + name: decorate("`๐Ÿ› ๏ธ`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`โš™๏ธ`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`๐Ÿ’พ`", "Volume Name"), + value: volumeName, + inline: true, + }, + { + name: decorate("`๐Ÿ”ง`", "Service Type"), + value: serviceType, + inline: true, + }, + ...(backupSize + ? [ + { + name: decorate("`๐Ÿ“Š`", "Backup Size"), + value: backupSize, + inline: true, + }, + ] + : []), + { + name: decorate("`๐Ÿ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`โŒš`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`โ“`", "Type"), + value: type + .replace("error", "Failed") + .replace("success", "Successful"), + inline: true, + }, + ...(type === "error" && errorMessage + ? [ + { + name: decorate("`โš ๏ธ`", "Error Message"), + value: `\`\`\`${errorMessage}\`\`\``, + }, + ] + : []), + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Volume Backup Notification", + }, + }); + } + + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + + await sendGotifyNotification( + gotify, + decorate( + type === "success" ? "โœ…" : "โŒ", + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + ), + `${decorate("๐Ÿ› ๏ธ", `Project: ${projectName}`)}` + + `${decorate("โš™๏ธ", `Application: ${applicationName}`)}` + + `${decorate("๐Ÿ’พ", `Volume Name: ${volumeName}`)}` + + `${decorate("๐Ÿ”ง", `Service Type: ${serviceType}`)}` + + `${backupSize ? decorate("๐Ÿ“Š", `Backup Size: ${backupSize}`) : ""}` + + `${decorate("๐Ÿ•’", `Date: ${date.toLocaleString()}`)}` + + `${type === "error" && errorMessage ? decorate("โŒ", `Error:\n${errorMessage}`) : ""}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + `${type === "success" ? "white_check_mark" : "x"}`, + "", + `๐Ÿ› ๏ธProject: ${projectName}\n` + + `โš™๏ธApplication: ${applicationName}\n` + + `๐Ÿ’พVolume Name: ${volumeName}\n` + + `๐Ÿ”งService Type: ${serviceType}\n` + + `${backupSize ? `๐Ÿ“ŠBackup Size: ${backupSize}\n` : ""}` + + `๐Ÿ•’Date: ${date.toLocaleString()}\n` + + `${type === "error" && errorMessage ? `โŒError:\n${errorMessage}` : ""}`, + ); + } + + if (telegram) { + const isError = type === "error" && errorMessage; + + const statusEmoji = type === "success" ? "โœ…" : "โŒ"; + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; + const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` : ""; + + const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + + await sendTelegramNotification(telegram, messageText); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: type === "success" ? "#00FF00" : "#FF0000", + pretext: + type === "success" + ? ":white_check_mark: *Volume Backup Successful*" + : ":x: *Volume Backup Failed*", + fields: [ + ...(type === "error" && errorMessage + ? [ + { + title: "Error Message", + value: errorMessage, + short: false, + }, + ] + : []), + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Volume Name", + value: volumeName, + short: true, + }, + { + title: "Service Type", + value: serviceType, + short: true, + }, + ...(backupSize + ? [ + { + title: "Backup Size", + value: backupSize, + short: true, + }, + ] + : []), + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Type", + value: type, + short: true, + }, + { + title: "Status", + value: type === "success" ? "Successful" : "Failed", + short: true, + }, + ], + }, + ], + }); + } + + if (pushover) { + await sendPushoverNotification( + pushover, + `Volume Backup ${type === "success" ? "Successful" : "Failed"}`, + `Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`, + ); + } + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5fc7f074..ac0450e5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -711,6 +711,9 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + resend: + specifier: ^6.0.2 + version: 6.8.0(@react-email/render@0.0.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) semver: specifier: 7.7.3 version: 7.7.3 @@ -3637,6 +3640,9 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -5013,6 +5019,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6686,6 +6695,15 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resend@6.8.0: + resolution: {integrity: sha512-fDOXGqafQfQXl8nXe93wr93pus8tW7YPpowenE3SmG7dJJf0hH3xUWm3xqacnPvhqjCQTJH9xETg07rmUeSuqQ==} + engines: {node: '>=20'} + peerDependencies: + '@react-email/render': '*' + peerDependenciesMeta: + '@react-email/render': + optional: true + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -6898,6 +6916,9 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -6993,6 +7014,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix@1.84.1: + resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} + swagger-client@3.35.3: resolution: {integrity: sha512-4bO+dhBbasP485Ak67o46cWNVUnV0/92ypb2997bhvxTO2M+IuQZM1ilkN/7nSaiGuxDKJhkuL54I35PVI3AAw==} @@ -7273,6 +7297,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -10346,6 +10374,8 @@ snapshots: '@sindresorhus/is@5.6.0': {} + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.0.0': {} '@stepperize/react@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -12007,6 +12037,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -13914,6 +13946,12 @@ snapshots: reselect@5.1.1: {} + resend@6.8.0(@react-email/render@0.0.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)): + dependencies: + svix: 1.84.1 + optionalDependencies: + '@react-email/render': 0.0.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + resolve-alpn@1.2.1: {} resolve-pkg-maps@1.0.0: {} @@ -14148,6 +14186,11 @@ snapshots: standard-as-callback@2.1.0: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.1: {} std-env@3.9.0: {} @@ -14241,6 +14284,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix@1.84.1: + dependencies: + standardwebhooks: 1.0.0 + uuid: 10.0.0 + swagger-client@3.35.3: dependencies: '@babel/runtime-corejs3': 7.27.3 @@ -14585,6 +14633,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@10.0.0: {} + uuid@9.0.1: {} vfile-message@4.0.2: diff --git a/schema.dbml b/schema.dbml index d0845f3ed..3ee3e763a 100644 --- a/schema.dbml +++ b/schema.dbml @@ -69,6 +69,7 @@ enum notificationType { telegram discord email + resend gotify ntfy custom @@ -455,6 +456,13 @@ table email { toAddress text[] [not null] } +table resend { + resendId text [pk, not null] + apiKey text [not null] + fromAddress text [not null] + toAddress text[] [not null] +} + table environment { environmentId text [pk, not null] name text [not null] @@ -691,6 +699,7 @@ table notification { telegramId text discordId text emailId text + resendId text gotifyId text ntfyId text customId text @@ -1133,6 +1142,8 @@ ref: notification.discordId - discord.discordId ref: notification.emailId - email.emailId +ref: notification.resendId - resend.resendId + ref: notification.gotifyId - gotify.gotifyId ref: notification.ntfyId - ntfy.ntfyId @@ -1191,4 +1202,4 @@ ref: volume_backup.redisId - redis.redisId ref: volume_backup.composeId - compose.composeId -ref: volume_backup.destinationId - destination.destinationId \ No newline at end of file +ref: volume_backup.destinationId - destination.destinationId