From d8e9e2dac3bc7340b9ddc27be44bdeb87788068f Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Tue, 13 Jan 2026 22:22:01 +0100 Subject: [PATCH] feat: Working on backend storage providers. --- .../providers/storages/forms/s3.form.tsx | 70 + .../admin/channels/helpers/common.tsx | 5 + .../admin/channels/helpers/storage.tsx | 15 +- .../alert-policy/alert-policy-form.tsx | 6 +- src/db/migrations/0021_soft_blockbuster.sql | 12 + src/db/migrations/meta/0021_snapshot.json | 2145 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/07_database.ts | 9 +- src/db/schema/13_storage-policy.ts | 33 + src/features/storages/dispatch.ts | 118 +- src/features/storages/helpers.ts | 10 + src/features/storages/providers/index.ts | 54 + src/features/storages/providers/local.ts | 69 + src/features/storages/types.ts | 39 + src/utils/init.ts | 22 + 15 files changed, 2506 insertions(+), 108 deletions(-) create mode 100644 src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form.tsx create mode 100644 src/db/migrations/0021_soft_blockbuster.sql create mode 100644 src/db/migrations/meta/0021_snapshot.json create mode 100644 src/db/schema/13_storage-policy.ts create mode 100644 src/features/storages/helpers.ts create mode 100644 src/features/storages/providers/index.ts create mode 100644 src/features/storages/providers/local.ts create mode 100644 src/features/storages/types.ts diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form.tsx new file mode 100644 index 00000000..01406b30 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form.tsx @@ -0,0 +1,70 @@ +import {UseFormReturn} from "react-hook-form"; +import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {Separator} from "@/components/ui/separator"; +import {PasswordInput} from "@/components/ui/password-input"; + + +type StorageS3FormProps = { + form: UseFormReturn +} + +export const StorageS3Form = ({form}: StorageS3FormProps) => { + return ( + <> + + ( + + Endpoind URL + + + + + + )} + /> + ( + + Access Key + + + + + + )} + /> + ( + + Secret Key + + + + + + )} + /> + ( + + Bucket name + + + + + + )} + /> + + ) +} diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx index e1f607a2..966c4ad3 100644 --- a/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx +++ b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx @@ -27,6 +27,9 @@ import {NotificationChannelWith} from "@/db/schema/09_notification-channel"; import {StorageChannelWith} from "@/db/schema/12_storage-channel"; import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react"; import {LucideProps} from "lucide-react"; +import { + StorageS3Form +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form"; export type ChannelKind = "notification" | "storage"; @@ -80,6 +83,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet return ; case "webhook": return ; + case "s3": + return case "local": return <> default: diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx index f1d05f92..220b2310 100644 --- a/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx +++ b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx @@ -1,6 +1,17 @@ import {Server} from "lucide-react"; +import type {SVGProps} from "react"; export const storageTypes = [ {value: "local", label: "Local", icon: Server}, - {value: "s3", label: "s3", icon: Server}, -] \ No newline at end of file + {value: "s3", label: "s3", icon: S3Icon}, +] + +export function S3Icon(props: SVGProps) { + return ( + + + + ); +} + diff --git a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx b/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx index 573e7b1d..8995ac2a 100644 --- a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx +++ b/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx @@ -21,10 +21,10 @@ import { } from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action"; import {useRouter} from "next/navigation"; import {Switch} from "@/components/ui/switch"; -import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers"; import {Card} from "@/components/ui/card"; import Link from "next/link"; import {useIsMobile} from "@/hooks/use-mobile"; +import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; type AlertPolicyFormProps = { onSuccess?: () => void; @@ -239,7 +239,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId, {selectedChannel && (
- {getNotificationChannelIcon(selectedChannel.provider)} + {getChannelIcon(selectedChannel.provider)}
{selectedChannel.name} @@ -257,7 +257,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
- {getNotificationChannelIcon(channel.provider)} + {getChannelIcon(channel.provider)}
{channel.name} diff --git a/src/db/migrations/0021_soft_blockbuster.sql b/src/db/migrations/0021_soft_blockbuster.sql new file mode 100644 index 00000000..4d2605b0 --- /dev/null +++ b/src/db/migrations/0021_soft_blockbuster.sql @@ -0,0 +1,12 @@ +CREATE TABLE "storage_policy" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "storage_channel_id" uuid NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "database_id" uuid NOT NULL, + "updated_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "storage_policy" ADD CONSTRAINT "storage_policy_storage_channel_id_storage_channel_id_fk" FOREIGN KEY ("storage_channel_id") REFERENCES "public"."storage_channel"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "storage_policy" ADD CONSTRAINT "storage_policy_database_id_databases_id_fk" FOREIGN KEY ("database_id") REFERENCES "public"."databases"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0021_snapshot.json b/src/db/migrations/meta/0021_snapshot.json new file mode 100644 index 00000000..cc2d4d5b --- /dev/null +++ b/src/db/migrations/meta/0021_snapshot.json @@ -0,0 +1,2145 @@ +{ + "id": "d5d260b2-43f3-4283-9c20-ba40c79e79cd", + "prevId": "b6099494-2995-4a3a-ba81-2fcf1d82551a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage": { + "name": "storage", + "type": "type_storage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "s3_endpoint_url": { + "name": "s3_endpoint_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_access_key_id": { + "name": "s3_access_key_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_secret_access_key": { + "name": "s3_secret_access_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_bucket_name": { + "name": "s3_bucket_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credentialId": { + "name": "credentialId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_userId_user_id_fk": { + "name": "passkey_userId_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index e47cf140..b51f73ac 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1768248015695, "tag": "0020_thankful_sunspot", "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1768337434929, + "tag": "0021_soft_blockbuster", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/07_database.ts b/src/db/schema/07_database.ts index f8400090..a15e8300 100644 --- a/src/db/schema/07_database.ts +++ b/src/db/schema/07_database.ts @@ -1,4 +1,4 @@ -import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum, uniqueIndex} from "drizzle-orm/pg-core"; +import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum} from "drizzle-orm/pg-core"; import {Agent, agent} from "./08_agent"; import {Project, project} from "./06_project"; import {relations} from "drizzle-orm"; @@ -6,11 +6,8 @@ import {dbmsEnum, statusEnum} from "./types"; import {createSelectSchema} from "drizzle-zod"; import {z} from "zod"; import {timestamps} from "@/db/schema/00_common"; -import {member} from "@/db/schema/04_member"; -import {invitation} from "@/db/schema/05_invitation"; -import {organizationNotificationChannel} from "@/db/schema/09_notification-channel"; -import {organization} from "@/db/schema/03_organization"; import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy"; +import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy"; export const database = pgTable("databases", { id: uuid("id").primaryKey().defaultRandom(), @@ -84,6 +81,7 @@ export const databaseRelations = relations(database, ({one, many}) => ({ backups: many(backup), restorations: many(restoration), alertPolicies: many(alertPolicy), + storagePolicies: many(storagePolicy), })); export const backupRelations = relations(backup, ({one, many}) => ({ @@ -124,5 +122,6 @@ export type DatabaseWith = Database & { restorations?: Restoration[] | null; retentionPolicy?: RetentionPolicy | null; alertPolicies?: AlertPolicy[] | null; + storagePolicies?: StoragePolicy[] | null; }; diff --git a/src/db/schema/13_storage-policy.ts b/src/db/schema/13_storage-policy.ts new file mode 100644 index 00000000..d2fc0f55 --- /dev/null +++ b/src/db/schema/13_storage-policy.ts @@ -0,0 +1,33 @@ +import {boolean, pgTable, uuid} from "drizzle-orm/pg-core"; +import {timestamps} from "@/db/schema/00_common"; +import {relations} from "drizzle-orm"; +import {database} from "@/db/schema/07_database"; +import {createSelectSchema} from "drizzle-zod"; +import {z} from "zod"; +import {storageChannel} from "@/db/schema/12_storage-channel"; + +export const storagePolicy = pgTable('storage_policy', { + id: uuid('id').defaultRandom().primaryKey(), + storageChannelId: uuid('storage_channel_id') + .notNull() + .references(() => storageChannel.id, {onDelete: 'cascade'}), + enabled: boolean('enabled').default(true).notNull(), + databaseId: uuid('database_id') + .notNull() + .references(() => database.id, {onDelete: 'cascade'}), + ...timestamps +}); + +export const storagePolicyRelations = relations(storagePolicy, ({one}) => ({ + storageChannel: one(storageChannel, { + fields: [storagePolicy.storageChannelId], + references: [storageChannel.id], + }), + database: one(database, { + fields: [storagePolicy.databaseId], + references: [database.id], + }), +})); + +export const storagePolicySchema = createSelectSchema(storagePolicy); +export type StoragePolicy = z.infer; diff --git a/src/features/storages/dispatch.ts b/src/features/storages/dispatch.ts index 49e027be..5e58d665 100644 --- a/src/features/storages/dispatch.ts +++ b/src/features/storages/dispatch.ts @@ -1,129 +1,51 @@ "use server"; -import {eq} from "drizzle-orm"; -import {dispatchViaProvider} from "./providers"; -import type {EventPayload, DispatchResult, EventKind} from "./types"; -import * as drizzleDb from "@/db"; -import {db} from "@/db"; -import {notificationLog} from "@/db/schema/11_notification-log"; -import {NotificationChannel} from "@/db/schema/09_notification-channel"; -import {Json} from "drizzle-zod"; + +import {eq} from 'drizzle-orm'; +import * as drizzleDb from '@/db'; +import {db} from '@/db'; +import type {StorageInput, StorageProviderKind, StorageResult,} from './types'; +import {dispatchViaProvider} from "@/features/storages/providers"; export async function dispatchStorage( - payload: EventPayload, + input: StorageInput, policyId?: string, channelId?: string, organizationId?: string -): Promise { +): Promise { try { - let channel: NotificationChannel | null = null; + if (!channelId) { - if (policyId) { - const policyDb = await db.query.alertPolicy.findFirst({ - where: eq(drizzleDb.schemas.alertPolicy.id, policyId), - with: { - notificationChannel: true - }, - }); - if (!policyDb || !policyDb.notificationChannel) { - return { - success: false, - channelId: "", - provider: null, - error: "Policy or associated channel not found", - }; - } - if (!policyDb.enabled || !policyDb.notificationChannel.enabled) { - return { - success: false, - channelId: policyDb.notificationChannel.id, - provider: policyDb.notificationChannel.provider as any, - error: "Policy or channel is disabled", - }; - } - - channel = { - ...policyDb.notificationChannel, - config: policyDb.notificationChannel.config as Json, - }; - } - - if (channelId) { - const fetchedChannel = await db.query.notificationChannel.findFirst({ - where: eq(drizzleDb.schemas.notificationChannel.id, channelId), - }); - - if (!fetchedChannel) { - return { - success: false, - channelId: channelId, - provider: null, - error: "Channel not found", - }; - } - - channel = { - ...fetchedChannel, - config: fetchedChannel.config as Json, - }; - } - - if (!channel) { return { success: false, - channelId: channelId || "", provider: null, - error: "No valid channel to dispatch notification", + error: 'No storage channel provided', }; } + const channel = await db.query.storageChannel.findFirst({ + where: eq(drizzleDb.schemas.storageChannel.id, channelId), + }); - if (!channel.enabled) { + if (!channel || !channel.enabled) { return { success: false, - channelId: channelId || "", - provider: null, - error: "Channel not active", + provider: channel?.provider as StorageProviderKind, + error: 'Storage channel not found or disabled', }; } - const result = await dispatchViaProvider( - channel.provider, + return await dispatchViaProvider( + channel.provider as StorageProviderKind, channel.config, - {...payload, timestamp: payload.timestamp || new Date()}, - channel.id + input ); - - const [log] = await db - .insert(notificationLog) - .values({ - channelId: channel.id, - policyId: policyId || null, - organizationId: organizationId || null, - - provider: channel.provider, - providerName: channel.name, - event: payload.event as EventKind, - - title: payload.title, - message: payload.message, - level: payload.level, - payload: payload.data || null, - success: result.success, - error: result.success ? null : result.error, - providerResponse: result.response || null, - }) - .returning({id: notificationLog.id}); - - return {...result, channelId: channel.id}; - } catch (err: any) { return { success: false, - channelId: channelId || "", provider: null, - error: err?.message || "Unexpected error during dispatch", + error: err.message || 'Unexpected storage dispatch error', }; } } diff --git a/src/features/storages/helpers.ts b/src/features/storages/helpers.ts new file mode 100644 index 00000000..e6d03b6f --- /dev/null +++ b/src/features/storages/helpers.ts @@ -0,0 +1,10 @@ +import {DatabaseWith} from "@/db/schema/07_database"; +import {StorageChannel} from "@/db/schema/12_storage-channel"; +import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; + + +export async function storeFileBackup(database: DatabaseWith, file: Buffer) { + + + +} \ No newline at end of file diff --git a/src/features/storages/providers/index.ts b/src/features/storages/providers/index.ts new file mode 100644 index 00000000..f30a130c --- /dev/null +++ b/src/features/storages/providers/index.ts @@ -0,0 +1,54 @@ +import type { + StorageProviderKind, + StorageInput, + StorageResult, +} from '../types'; + +import {uploadLocal, getLocal, deleteLocal} from './local'; + +type ProviderHandler = { + upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise; + get: (config: any, input: StorageInput & { action: 'get' }) => Promise; + delete: (config: any, input: StorageInput & { action: 'delete' }) => Promise; +}; + +const handlers: Record = { + local: { + upload: uploadLocal, + get: getLocal, + delete: deleteLocal, + }, + // s3: { + // upload: uploadS3, + // get: getS3, + // delete: deleteS3, + // }, + // gcs: null as any, + // azure: null as any, +}; + +export async function dispatchViaProvider( + kind: StorageProviderKind, + config: any, + input: StorageInput +): Promise { + const provider = handlers[kind]; + + if (!provider) { + return { + success: false, + provider: kind, + error: `Unsupported storage provider: ${kind}`, + }; + } + + try { + return await provider[input.action](config, input as any); + } catch (err: any) { + return { + success: false, + provider: kind, + error: err.message || 'Storage provider error', + }; + } +} diff --git a/src/features/storages/providers/local.ts b/src/features/storages/providers/local.ts new file mode 100644 index 00000000..2357c7e2 --- /dev/null +++ b/src/features/storages/providers/local.ts @@ -0,0 +1,69 @@ +"use server" +import {mkdir, writeFile, unlink, readFile} from 'fs/promises'; +import path from 'path'; +import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../types'; +import fs from "node:fs"; +import {getServerUrl} from "@/utils/get-server-url"; + +const BASE_DIR = "/private/uploads/files/"; + +export async function uploadLocal( + config: { baseDir?: string }, + input: { data: StorageUploadInput } +): Promise { + const base = config.baseDir || BASE_DIR; + const fullPath = path.join(process.cwd(), base, input.data.path); + + await mkdir(fullPath, {recursive: true}); + await writeFile(fullPath, input.data.file); + + return { + success: true, + provider: 'local', + url: path.join(fullPath), + }; +} + +export async function getLocal( + config: { baseDir?: string }, + input: { data: StorageGetInput } +): Promise { + const base = config.baseDir || BASE_DIR; + const filePath = path.join(base, input.data.path) + const fileName = path.basename(input.data.path); + const file = await readFile(filePath); + + + if (!fs.existsSync(filePath)) { + console.error("File not found at:", filePath); + return({ + success: false, + provider: 'local', + }); + } + const crypto = require("crypto"); + const baseUrl = getServerUrl(); + + const expiresAt = Date.now() + 60 * 1000; + const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex"); + + return { + success: true, + provider: 'local', + file: file, + url: `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`, + }; +} + +export async function deleteLocal( + config: { baseDir?: string }, + input: { data: StorageDeleteInput } +): Promise { + const base = config.baseDir || BASE_DIR; + const fullPath = path.join(process.cwd(), base, input.data.path); + await unlink(fullPath); + return { + success: true, + provider: 'local', + }; +} diff --git a/src/features/storages/types.ts b/src/features/storages/types.ts new file mode 100644 index 00000000..c0c5ec81 --- /dev/null +++ b/src/features/storages/types.ts @@ -0,0 +1,39 @@ +export type StorageProviderKind = + | 'local' +// | 's3' + ; + +export type StorageAction = + | 'upload' + | 'get' + | 'delete'; + +export interface StorageUploadInput { + path: string; + file: Buffer | Uint8Array; + contentType?: string; +} + +export interface StorageGetInput { + path: string; + signedUrl?: boolean; + expiresInSeconds?: number; +} + +export interface StorageDeleteInput { + path: string; +} + +export type StorageInput = + | { action: 'upload'; data: StorageUploadInput } + | { action: 'get'; data: StorageGetInput } + | { action: 'delete'; data: StorageDeleteInput }; + +export interface StorageResult { + success: boolean; + provider: StorageProviderKind | null; + url?: string; + file?: Buffer; + error?: string; + response?: any; +} diff --git a/src/utils/init.ts b/src/utils/init.ts index f5b36782..e349bbbd 100644 --- a/src/utils/init.ts +++ b/src/utils/init.ts @@ -4,6 +4,9 @@ import {eq} from "drizzle-orm"; import * as drizzleDb from "@/db"; import {retentionJob} from "@/lib/tasks"; import {generateRSAKeys} from "@/utils/rsa-keys"; +import {Provider} from "react"; +import type {ProviderKind} from "@/features/notifications/types"; +import {StorageProviderKind} from "@/features/storages/types"; export async function init() { @@ -47,6 +50,25 @@ async function createSettingsIfNotExist() { console.log("====Init Setting : Update ===="); await db.update(drizzleDb.schemas.setting).set(configSettings).where(eq(drizzleDb.schemas.setting.name, "system")); } + + const [existingLocalChannelStorage] = await db.select().from(drizzleDb.schemas.storageChannel).where(eq(drizzleDb.schemas.storageChannel.provider, "local")).limit(1); + + const localChannelValues = { + provider: "local" as StorageProviderKind, + enabled: true, + name: "System", + config: {} + } + + if (!existingLocalChannelStorage) { + console.log("====Local Storage : Create ===="); + await db.insert(drizzleDb.schemas.storageChannel).values(localChannelValues); + } else { + console.log("====Local Storage : Update ===="); + await db.update(drizzleDb.schemas.storageChannel).set(localChannelValues).where(eq(drizzleDb.schemas.storageChannel.provider, "local")); + } + + } async function createDefaultOrganization() {