diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index a4bdcf09..3b32668c 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -1,88 +1,92 @@ -import { PageParams } from "@/types/next"; +import {PageParams} from "@/types/next"; import { - Page, - PageContent, - PageDescription, - PageTitle, + Page, + PageContent, + PageDescription, + PageTitle, } from "@/features/layout/components/page"; -import { db } from "@/db"; +import {db} from "@/db"; import * as drizzleDb from "@/db"; import {eq, isNull} from "drizzle-orm"; -import { notFound } from "next/navigation"; -import { ButtonDeleteAgent } from "@/features/agents/components/agent-delete-button"; -import { capitalizeFirstLetter } from "@/utils/text"; -import { generateEdgeKey } from "@/utils/edge_key"; -import { getServerUrl } from "@/utils/get-server-url"; -import { AgentContentPage } from "@/features/agents/components/agent-content"; -import { AgentDialog } from "@/features/agents/components/agent-dialog"; +import {notFound} from "next/navigation"; +import {ButtonDeleteAgent} from "@/features/agents/components/agent-delete-button"; +import {capitalizeFirstLetter} from "@/utils/text"; +import {generateEdgeKey} from "@/utils/edge_key"; +import {getServerUrl} from "@/utils/get-server-url"; +import {AgentContentPage} from "@/features/agents/components/agent-content"; +import {AgentDialog} from "@/features/agents/components/agent-dialog"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; export default async function RoutePage( - props: PageParams<{ agentId: string }>, + props: PageParams<{ agentId: string }>, ) { - const { agentId } = await props.params; + const {agentId} = await props.params; - const agent = await db.query.agent.findFirst({ - where: eq(drizzleDb.schemas.agent.id, agentId), - with: { - databases: true, - organizations: true, - }, - }); + const agent = await db.query.agent.findFirst({ + where: eq(drizzleDb.schemas.agent.id, agentId), + with: { + databases: true, + organizations: true, + }, + }); - const organizations = await db.query.organization.findMany({ - where: (fields) => isNull(fields.deletedAt), - with: { - members: true, - }, - }); + const organizations = await db.query.organization.findMany({ + where: (fields) => isNull(fields.deletedAt), + with: { + members: true, + }, + }); - if (!agent) { - notFound(); - } + if (!agent) { + notFound(); + } - const isOwnerByAnOrganization = agent.organizationId + agent.databases = maskDatabasesSecretsForClient(agent.databases); - if (isOwnerByAnOrganization){ - notFound(); - } + const isOwnerByAnOrganization = agent.organizationId - const organizationIds = agent.organizations.map(org => org.organizationId) + if (isOwnerByAnOrganization) { + notFound(); + } - const edgeKey = await generateEdgeKey(agent.overrideUrl ?? getServerUrl(), agent.id); + const organizationIds = agent.organizations.map(org => org.organizationId) - return ( - -
- -
- {capitalizeFirstLetter(agent.name)} -
-
-
- -
-
- + const edgeKey = await generateEdgeKey(agent.overrideUrl ?? getServerUrl(), agent.id); + + return ( + +
+ +
+ {capitalizeFirstLetter(agent.name)} +
+
+
+ +
+
+ +
+
+
-
- -
- {agent.description && ( - - {agent.description} - - )} - - - - - ); + {agent.description && ( + + {agent.description} + + )} + + + + + ); } diff --git a/app/(customer)/dashboard/(admin)/agents/page.tsx b/app/(customer)/dashboard/(admin)/agents/page.tsx index a17511db..1b821cdc 100644 --- a/app/(customer)/dashboard/(admin)/agents/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/page.tsx @@ -8,6 +8,7 @@ import * as drizzleDb from "@/db"; import {and, desc, eq, isNull, not} from "drizzle-orm"; import {Metadata} from "next"; import {AgentDialog} from "@/features/agents/components/agent-dialog"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; export const metadata: Metadata = { title: "Agents", @@ -27,6 +28,11 @@ export default async function RoutePage(props: PageParams<{}>) { notFound(); } + // Never send secret credential fields to the browser. + for (const a of agents) { + a.databases = maskDatabasesSecretsForClient(a.databases); + } + return ( diff --git a/app/(customer)/dashboard/(organization)/migration/page.tsx b/app/(customer)/dashboard/(organization)/migration/page.tsx index c0d89bd8..80e281ec 100644 --- a/app/(customer)/dashboard/(organization)/migration/page.tsx +++ b/app/(customer)/dashboard/(organization)/migration/page.tsx @@ -5,6 +5,7 @@ import {getOrganization} from "@/lib/auth/auth"; import {Metadata} from "next"; import {db} from "@/db"; import {MigrationTool} from "@/features/migration/components/migration-tool"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; export const metadata: Metadata = { title: "Projects", @@ -47,6 +48,11 @@ export default async function RoutePage(props: PageParams<{}>) { }, }, }); + + for (const p of projects) { + p.databases = maskDatabasesSecretsForClient(p.databases); + } + return ( diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index 02cb0e46..51c0e753 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -10,6 +10,7 @@ import { BackupModalProvider } from "@/features/database/components/backup-modal import { DatabaseContent } from "@/features/database/components/database-content"; import { getHealthLast12hLogs } from "@/db/services/healthcheck"; import { LogsModalProvider } from "@/features/logs/components/logs-modal-context"; +import { maskConfigForDashboard } from "@/features/database/utils/credential-fields"; export default async function RoutePage( props: PageParams<{ @@ -49,6 +50,8 @@ export default async function RoutePage( redirect("/dashboard/projects"); } + dbItem.config = maskConfigForDashboard(dbItem.dbms, dbItem.config) ?? null; + const totalBackups = await db .select({ count: drizzleDb.schemas.backup.id }) .from(drizzleDb.schemas.backup) diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx index 52575c95..aa8bfa3e 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx @@ -13,6 +13,7 @@ import { capitalizeFirstLetter, isUUID } from "@/utils/text"; import { ProjectDialog } from "@/features/projects/components/project-dialog"; import { ProjectWith } from "@/db/schema/06_project"; import { getOrganizationAvailableDatabases } from "@/db/services/database"; +import { maskDatabasesSecretsForClient } from "@/features/database/utils/credential-fields"; import { getOrganizationChannels } from "@/db/services/notification-channel"; import { getOrganizationStorageChannels } from "@/db/services/storage-channel"; import { RetentionPolicySheet } from "@/features/database/components/retention-policy-sheet"; @@ -63,9 +64,10 @@ export default async function RoutePage( redirect("/dashboard/projects"); } - const availableDatabases = await getOrganizationAvailableDatabases( - organization.id, - proj.id, + proj.databases = maskDatabasesSecretsForClient(proj.databases); + + const availableDatabases = maskDatabasesSecretsForClient( + await getOrganizationAvailableDatabases(organization.id, proj.id), ); const isMember = activeMember?.role === "member"; diff --git a/app/(customer)/dashboard/(organization)/projects/page.tsx b/app/(customer)/dashboard/(organization)/projects/page.tsx index 244a8266..63fad38e 100644 --- a/app/(customer)/dashboard/(organization)/projects/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/page.tsx @@ -8,8 +8,8 @@ import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder"; import {Metadata} from "next"; import {ProjectDialog} from "@/features/projects/components/project-dialog"; -import {DatabaseWith} from "@/db/schema/07_database"; import {getOrganizationAvailableDatabases} from "@/db/services/database"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; export const metadata: Metadata = { title: "Projects", @@ -36,7 +36,13 @@ export default async function RoutePage(props: PageParams<{}>) { }); const isMember = activeMember?.role === "member"; - const availableDatabases = await getOrganizationAvailableDatabases(organization.id) + for (const p of projects) { + p.databases = maskDatabasesSecretsForClient(p.databases); + } + + const availableDatabases = maskDatabasesSecretsForClient( + await getOrganizationAvailableDatabases(organization.id), + ) return ( diff --git a/app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx index 4dedfdac..34c4daad 100644 --- a/app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx @@ -15,6 +15,7 @@ import {generateEdgeKey} from "@/utils/edge_key"; import {getServerUrl} from "@/utils/get-server-url"; import {AgentContentPage} from "@/features/agents/components/agent-content"; import {AgentDialog} from "@/features/agents/components/agent-dialog"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {currentUser} from "@/lib/auth/current-user"; import {computeOrganizationPermissions} from "@/lib/acl/organization-acl"; @@ -33,11 +34,9 @@ export default async function RoutePage( notFound(); } - - const {canManageAgents} = computeOrganizationPermissions(activeMember); - if (!canManageAgents){ + if (!canManageAgents) { notFound(); } @@ -54,6 +53,8 @@ export default async function RoutePage( notFound(); } + agent.databases = maskDatabasesSecretsForClient(agent.databases); + const hasAccess = agent.organizationId === organization.id || agent.organizations.some(org => org.organizationId === organization.id); @@ -81,7 +82,8 @@ export default async function RoutePage( />
- +
)} diff --git a/src/components/common/database-card.tsx b/src/components/common/database-card.tsx index bf04a8c9..80682d62 100644 --- a/src/components/common/database-card.tsx +++ b/src/components/common/database-card.tsx @@ -1,6 +1,6 @@ "use client"; import Image from "next/image"; -import {useState} from "react"; +import {ReactNode, useState, MouseEvent} from "react"; import {Card} from "@/components/ui/card"; import {ConnectionIndicator} from "@/components/common/connection-indicator"; import {formatDateLastContact} from "@/utils/date-formatting"; @@ -14,14 +14,15 @@ export type DatabaseCardProps = { selectable?: boolean; selected?: boolean; onToggleSelect?: (databaseId: string) => void; - deleteButton?: React.ReactNode; + deleteButton?: ReactNode; + configureButton?: ReactNode; }; export const DatabaseCard = (props: DatabaseCardProps) => { - const {data: database, withDetails = true, selectable = false, selected = false, onToggleSelect, deleteButton} = props; + const {data: database, withDetails = true, selectable = false, selected = false, onToggleSelect, deleteButton, configureButton} = props; const [isCopied, setIsCopied] = useState(false); - const handleCopy = (e: React.MouseEvent) => { + const handleCopy = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); navigator.clipboard.writeText(database.agentDatabaseId ?? ""); @@ -106,9 +107,14 @@ export const DatabaseCard = (props: DatabaseCardProps) => { - {deleteButton ? ( -
e.preventDefault()}> + {(deleteButton || configureButton) ? ( +
{ + if (e.currentTarget.contains(e.target as Node)) { + e.preventDefault(); + } + }}> + {configureButton} {deleteButton}
) : withDetails ? ( diff --git a/src/db/migrations/0081_loud_black_widow.sql b/src/db/migrations/0081_loud_black_widow.sql new file mode 100644 index 00000000..4f88b424 --- /dev/null +++ b/src/db/migrations/0081_loud_black_widow.sql @@ -0,0 +1 @@ +ALTER TABLE "databases" ADD COLUMN "config" jsonb; \ No newline at end of file diff --git a/src/db/migrations/meta/0081_snapshot.json b/src/db/migrations/meta/0081_snapshot.json new file mode 100644 index 00000000..e350d56b --- /dev/null +++ b/src/db/migrations/meta/0081_snapshot.json @@ -0,0 +1,3336 @@ +{ + "id": "7d56f8d6-8e8f-4ac3-b2ed-d2de1de8210e", + "prevId": "aa28c4ff-2b10-4981-a0dd-7f12c91d43e5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_notification_channel_id": { + "name": "default_notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "onboarding": { + "name": "onboarding", + "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": { + "settings_default_notification_channel_id_notification_channel_id_fk": { + "name": "settings_default_notification_channel_id_notification_channel_id_fk", + "tableFrom": "settings", + "tableTo": "notification_channel", + "columnsFrom": [ + "default_notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 + }, + "user_id": { + "name": "user_id", + "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 + }, + "aaguid": { + "name": "aaguid", + "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_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "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 + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": 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": "bigint", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "migrated": { + "name": "migrated", + "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": { + "idx_backups_status_core": { + "name": "idx_backups_status_core", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_backups_evolution": { + "name": "idx_backups_evolution", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'success' AND deleted_at IS NULL AND file_size IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": false, + "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 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "idx_databases_availability": { + "name": "idx_databases_availability", + "columns": [ + { + "expression": "last_contact", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "deleted_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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_hourly": { + "name": "gfs_hourly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "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" + }, + "retention_policies_project_id_projects_id_fk": { + "name": "retention_policies_project_id_projects_id_fk", + "tableFrom": "retention_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "retention_policy_owner_xor": { + "name": "retention_policy_owner_xor", + "value": "num_nonnulls(\"retention_policies\".\"database_id\", \"retention_policies\".\"project_id\") = 1" + } + }, + "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 + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "override_url": { + "name": "override_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_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": { + "idx_agents_availability": { + "name": "idx_agents_availability", + "columns": [ + { + "expression": "last_contact", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "is_archived = false AND deleted_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_organization_id_organization_id_fk": { + "name": "agents_organization_id_organization_id_fk", + "tableFrom": "agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_agents": { + "name": "organization_agents", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_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": { + "organization_agents_organization_id_organization_id_fk": { + "name": "organization_agents_organization_id_organization_id_fk", + "tableFrom": "organization_agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_agents_agent_id_agents_id_fk": { + "name": "organization_agents_agent_id_agents_id_fk", + "tableFrom": "organization_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_agents_organization_id_agent_id_unique": { + "name": "organization_agents_organization_id_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "agent_id" + ] + } + }, + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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": 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": { + "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" + }, + "alert_policy_project_id_projects_id_fk": { + "name": "alert_policy_project_id_projects_id_fk", + "tableFrom": "alert_policy", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "alert_policy_owner_xor": { + "name": "alert_policy_owner_xor", + "value": "num_nonnulls(\"alert_policy\".\"database_id\", \"alert_policy\".\"project_id\") = 1" + } + }, + "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": { + "idx_notif_log_critical_24h": { + "name": "idx_notif_log_critical_24h", + "columns": [ + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "event IN ('error_backup', 'error_restore', 'error_health_agent', 'error_health_database')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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": 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": { + "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" + }, + "storage_policy_project_id_projects_id_fk": { + "name": "storage_policy_project_id_projects_id_fk", + "tableFrom": "storage_policy", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "storage_policy_owner_xor": { + "name": "storage_policy_owner_xor", + "value": "num_nonnulls(\"storage_policy\".\"database_id\", \"storage_policy\".\"project_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "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": { + "idx_backup_storage_treemap": { + "name": "idx_backup_storage_treemap", + "columns": [ + { + "expression": "storage_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'success' AND size IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.healthcheck_log": { + "name": "healthcheck_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "healthcheck_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "healthcheck_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "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 + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp (6) with time zone", + "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 (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_log": { + "name": "job_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "restoration_id": { + "name": "restoration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "logged_at": { + "name": "logged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "job_log_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "job_log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "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": { + "job_log_backup_id_backups_id_fk": { + "name": "job_log_backup_id_backups_id_fk", + "tableFrom": "job_log", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "job_log_restoration_id_restorations_id_fk": { + "name": "job_log_restoration_id_restorations_id_fk", + "tableFrom": "job_log", + "tableTo": "restorations", + "columnsFrom": [ + "restoration_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", + "nextcloud", + "teams", + "pushover", + "apprise" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report", + "error_health_agent", + "error_health_database" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive", + "google-cloud-storage", + "blob" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.healthcheck_status": { + "name": "healthcheck_status", + "schema": "public", + "values": [ + "success", + "failed" + ] + }, + "public.healthcheck_kind": { + "name": "healthcheck_kind", + "schema": "public", + "values": [ + "database", + "agent" + ] + }, + "public.job_log_entry_type": { + "name": "job_log_entry_type", + "schema": "public", + "values": [ + "log", + "command" + ] + }, + "public.job_log_level": { + "name": "job_log_level", + "schema": "public", + "values": [ + "debug", + "info", + "warn", + "error" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "postgresql-cluster", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "redis", + "valkey", + "firebird", + "mssql", + "docker-volume" + ] + }, + "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": { + "public.mv_kpi_backup_counts": { + "columns": { + "available_count": { + "name": "available_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_done": { + "name": "total_done", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "possession_rate_pct": { + "name": "possession_rate_pct", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "singleton": { + "name": "singleton", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n COUNT(*) FILTER (WHERE status = 'success' AND deleted_at IS NULL) AS available_count,\n COUNT(*) FILTER (WHERE status IN ('success', 'failed')) AS total_done,\n ROUND(\n COUNT(*) FILTER (WHERE status = 'success' AND deleted_at IS NULL)::numeric\n / NULLIF(COUNT(*) FILTER (WHERE status IN ('success', 'failed')), 0) * 100, 1\n ) AS possession_rate_pct,\n 1 AS singleton\n FROM backups\n", + "name": "mv_kpi_backup_counts", + "schema": "public", + "isExisting": false, + "materialized": true + }, + "public.mv_kpi_dbms_treemap": { + "columns": { + "dbms": { + "name": "dbms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "database_count": { + "name": "database_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_count": { + "name": "backup_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n db.dbms,\n SUM(b.file_size) AS total_bytes,\n COUNT(DISTINCT db.id) AS database_count,\n COUNT(b.id) AS backup_count\n FROM backups b\n JOIN databases db ON db.id = b.database_id\n WHERE b.status = 'success' AND b.deleted_at IS NULL AND b.file_size IS NOT NULL\n GROUP BY db.dbms\n ORDER BY total_bytes DESC\n", + "name": "mv_kpi_dbms_treemap", + "schema": "public", + "isExisting": false, + "materialized": true + }, + "public.mv_kpi_evolution_monthly": { + "columns": { + "period": { + "name": "period", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "success_count": { + "name": "success_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "failed_count": { + "name": "failed_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n DATE_TRUNC('day', created_at) AS period,\n COALESCE(\n SUM(file_size) FILTER (\n WHERE status = 'success' AND deleted_at IS NULL AND file_size IS NOT NULL\n ), 0\n ) AS total_bytes,\n COUNT(*) FILTER (WHERE status = 'success') AS success_count,\n COUNT(*) FILTER (WHERE status = 'failed') AS failed_count\n FROM backups\n WHERE status IN ('success', 'failed')\n GROUP BY DATE_TRUNC('day', created_at)\n ORDER BY period ASC\n", + "name": "mv_kpi_evolution_monthly", + "schema": "public", + "isExisting": false, + "materialized": true + }, + "public.mv_kpi_storage_treemap": { + "columns": { + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "channel_name": { + "name": "channel_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_count": { + "name": "backup_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n sc.id AS channel_id,\n sc.name AS channel_name,\n sc.provider,\n SUM(bs.size) AS total_bytes,\n COUNT(bs.id) AS backup_count\n FROM backup_storage bs\n JOIN storage_channel sc ON sc.id = bs.storage_channel_id\n JOIN backups b ON b.id = bs.backup_id\n WHERE bs.status = 'success' AND bs.size IS NOT NULL AND b.deleted_at IS NULL\n GROUP BY sc.id, sc.name, sc.provider\n ORDER BY total_bytes DESC\n", + "name": "mv_kpi_storage_treemap", + "schema": "public", + "isExisting": false, + "materialized": true + } + }, + "_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 8502a9f7..deea886f 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -568,6 +568,13 @@ "when": 1785824737806, "tag": "0080_sparkling_gamma_corps", "breakpoints": true + }, + { + "idx": 81, + "version": "7", + "when": 1786602822645, + "tag": "0081_loud_black_widow", + "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 fef2ec93..8bb4410f 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, bigint, index, check} from "drizzle-orm/pg-core"; +import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum, bigint, index, check, jsonb} from "drizzle-orm/pg-core"; import {Agent, agent, AgentWith} from "./08_agent"; import {Project, project} from "./06_project"; import {relations, sql} from "drizzle-orm"; @@ -20,6 +20,7 @@ export const database = pgTable("databases", { backupPolicy: text("backup_policy"), isWaitingForBackup: boolean("is_waiting_for_backup").default(false).notNull(), backupToRestore: text("backup_to_restore"), + config: jsonb("config").$type>(), healthErrorCount: integer("health_error_count"), agentId: uuid("agent_id") .references(() => agent.id, {onDelete: "cascade"}), diff --git a/src/features/agents/actions/agents.action.ts b/src/features/agents/actions/agents.action.ts index 3155ee12..3bf7014b 100644 --- a/src/features/agents/actions/agents.action.ts +++ b/src/features/agents/actions/agents.action.ts @@ -7,6 +7,7 @@ import {db} from "@/db"; import * as drizzleDb from "@/db"; import {slugify} from "@/utils/slugify"; import {getHealthLast12hLogs} from "@/db/services/healthcheck"; +import {maskDatabasesSecretsForClient} from "@/features/database/utils/credential-fields"; const verifySlugUniqueness = async (slug: string, agentId?: string) => { const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug); @@ -91,6 +92,10 @@ export const getAgentAction = userAction.inputSchema(z.string()).action(async ({ } }); + if (agent) { + agent.databases = maskDatabasesSecretsForClient(agent.databases); + } + return { data: agent, health: agent ? await getHealthLast12hLogs({ id: agent.id }) : [] diff --git a/src/features/agents/components/agent-content.tsx b/src/features/agents/components/agent-content.tsx index 93f58514..c011d76b 100644 --- a/src/features/agents/components/agent-content.tsx +++ b/src/features/agents/components/agent-content.tsx @@ -21,6 +21,9 @@ import {CardsWithPagination} from "@/components/common/cards-with-pagination"; import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card"; import {HealthCheckGraph} from "@/features/database/components/health-grid"; import {HealthcheckLog} from "@/db/schema/15_healthcheck-log"; +import { DatabaseConfigModal } from "@/features/database/components/database-config-modal"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; type AgentContentPageProps = { edgeKey: string; @@ -118,17 +121,26 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent, canDeleteDatabas
- {agent.databases.length > 0 && ( -
-
-
-

Managed Databases

-

- Resources currently connected to this agent. -

-
+
+
+
+

Managed Databases

+

+ Resources currently connected to this agent. +

- + + + Add database + + } + /> +
+ + {agent.databases.length > 0 ? ( -
- )} + ) : ( +

No databases yet. Add one to push its config to the agent.

+ )} +
) } \ No newline at end of file diff --git a/src/features/agents/components/agent-database-card.tsx b/src/features/agents/components/agent-database-card.tsx index 026c49a4..923b0966 100644 --- a/src/features/agents/components/agent-database-card.tsx +++ b/src/features/agents/components/agent-database-card.tsx @@ -3,6 +3,9 @@ import { Database } from "@/db/schema/07_database"; import { DatabaseCard } from "@/components/common/database-card"; import { DatabaseDeleteButton } from "@/features/agents/components/database-delete-button"; +import { DatabaseConfigModal } from "@/features/database/components/database-config-modal"; +import { Button } from "@/components/ui/button"; +import { Settings } from "lucide-react"; export type AgentDatabaseCardProps = { data: Database; @@ -17,6 +20,19 @@ export const AgentDatabaseCard = (props: AgentDatabaseCardProps) => { e.stopPropagation()}> + + + } + /> + ) : undefined + } deleteButton={ canDeleteDatabases && database.agentId ? ( , + version: string | undefined, + masterKey: Buffer | null, + agentId: string, +): void { + if (entry.config == null) return; + + if (!masterKey || !isAgentVersionAtLeast(version, MIN_AGENT_VERSION_DB_CONFIG)) { + if (!masterKey) { + log.error({name: "applyConfigEncryption", agentId}, "Master key unavailable; config withheld"); + } + delete entry.config; + return; + } + + try { + decryptConfigSecretsInPlace(entry.config, masterKey); + entry.config_ciphertext = encryptJsonGcm(entry.config, masterKey); + entry.config_encrypted = true; + delete entry.config; + } catch (err) { + log.error({error: err, name: "applyConfigEncryption", agentId}, "Config encryption failed; config withheld"); + delete entry.config; + } +} diff --git a/src/features/agents/utils/status/status.helpers.ts b/src/features/agents/utils/status/status.helpers.ts index aaf04d8a..1e1b3831 100644 --- a/src/features/agents/utils/status/status.helpers.ts +++ b/src/features/agents/utils/status/status.helpers.ts @@ -4,7 +4,7 @@ import {Agent} from "@/db/schema/08_agent"; import {DatabaseWith} from "@/db/schema/07_database"; import * as drizzleDb from "@/db"; import {db as dbClient} from "@/db"; -import {and, eq, inArray, desc, sql} from "drizzle-orm"; +import {and, eq, inArray, desc, sql, isNull, isNotNull} from "drizzle-orm"; import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types"; import {withUpdatedAt} from "@/db/utils"; import {Setting} from "@/db/schema/01_setting"; @@ -15,6 +15,8 @@ import {dispatchStorage} from "@/features/storages/utils/storages.dispatch"; import {getMasterServerKeyContent} from "@/features/agents/utils/keys.server"; import {getDatabaseStorageChannels, PingDatabaseStorageChannels} from "./storage-channels.helpers"; import {applyStorageEncryption} from "./storage-encryption.helpers"; +import {applyConfigEncryption} from "./config-encryption.helpers"; +import {isAgentVersionAtLeast, MIN_AGENT_VERSION_DB_CONFIG} from "@/utils/status-crypto"; import {handleFailedRestoration} from "./restoration.helpers"; import {resolveBackupCron} from "@/features/database/utils/policy-resolution"; @@ -30,24 +32,30 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat log.error({name: "handleDatabases"}, "Master key unavailable; storages will be sent in plaintext"); } - const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null, backupSize: number | null, cron: string | null) => ({ - generatedId: database.agentDatabaseId, - dbms: database.dbms, - storages: storages, - encrypt: settings.encryption, - data: { - backup: { - action: backupAction, - cron, - }, - restore: { - action: restoreAction, - file: UrlBackup, - metaFile: urlMeta, - size: backupSize + const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null, backupSize: number | null, cron: string | null) => { + const rawConfig = database.deletedAt ? null : database.config; + const config = + rawConfig == null + ? null + : { + name: database.name, + type: database.dbms, + ...(rawConfig as Record), + generated_id: database.agentDatabaseId, + }; + + return { + generatedId: database.agentDatabaseId, + dbms: database.dbms, + storages: storages, + encrypt: settings.encryption, + data: { + backup: {action: backupAction, cron}, + restore: {action: restoreAction, file: UrlBackup, metaFile: urlMeta, size: backupSize}, }, - }, - }); + config, + }; + }; for (const db of body.databases) { @@ -102,6 +110,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat const entry = formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null, null, databaseCreated.backupPolicy ?? null); applyStorageEncryption(entry, body.version, masterKey, agent.id); + applyConfigEncryption(entry, body.version, masterKey, agent.id); databasesResponse.push(entry); } } else { @@ -232,8 +241,34 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat const storages = await getDatabaseStorageChannels(databaseUpdated.id) const entry = formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta, backupSize, resolvedCron); applyStorageEncryption(entry, body.version, masterKey, agent.id); + applyConfigEncryption(entry, body.version, masterKey, agent.id); + databasesResponse.push(entry); + } + } + + if (isAgentVersionAtLeast(body.version, MIN_AGENT_VERSION_DB_CONFIG)) { + const seenIds = new Set(body.databases.map((d) => d.generatedId)); + + const dashboardDbs = await dbClient.query.database.findMany({ + where: and( + eq(drizzleDb.schemas.database.agentId, agent.id), + isNotNull(drizzleDb.schemas.database.config), + isNull(drizzleDb.schemas.database.deletedAt), + ), + with: {project: true}, + }); + + for (const dashDb of dashboardDbs) { + if (dashDb.agentDatabaseId && seenIds.has(dashDb.agentDatabaseId)) continue; + + const storages = await getDatabaseStorageChannels(dashDb.id); + const resolvedCron = resolveBackupCron(dashDb as DatabaseWith); + const entry = formatDatabase(dashDb, false, false, null, storages, null, null, resolvedCron); + applyStorageEncryption(entry, body.version, masterKey, agent.id); + applyConfigEncryption(entry, body.version, masterKey, agent.id); databasesResponse.push(entry); } } + return databasesResponse; } diff --git a/src/features/database/actions/database-config.action.ts b/src/features/database/actions/database-config.action.ts new file mode 100644 index 00000000..414ab847 --- /dev/null +++ b/src/features/database/actions/database-config.action.ts @@ -0,0 +1,85 @@ +"use server"; + +import { z } from "zod"; +import { db } from "@/db"; +import * as drizzleDb from "@/db"; +import { and, eq } from "drizzle-orm"; +import { userAction, ActionError } from "@/lib/safe-actions/actions"; +import { withUpdatedAt } from "@/db/utils"; +import { + DatabaseConfigFormSchema, + pruneAgentConfig, +} from "@/features/database/schemas/database-config.schema"; +import { assertCanManageAgentDatabases } from "@/features/database/utils/database-acl"; +import { getMasterServerKeyContent } from "@/features/agents/utils/keys.server"; +import { encryptConfigSecrets } from "@/features/database/utils/credential-crypto"; + +export const upsertDatabaseConfigAction = userAction + .inputSchema( + z.object({ + agentId: z.uuid(), + databaseId: z.uuid().optional(), + agentDatabaseId: z.uuid().optional(), + data: DatabaseConfigFormSchema, + }), + ) + .action(async ({ parsedInput }) => { + const { agentId, databaseId, agentDatabaseId, data } = parsedInput; + + try { + await assertCanManageAgentDatabases(agentId); + } catch (e) { + throw new ActionError(e instanceof Error ? e.message : "Not authorized"); + } + + let masterKey: Buffer; + try { + masterKey = await getMasterServerKeyContent(); + } catch { + throw new ActionError("Encryption key unavailable; cannot store credentials"); + } + + const nextConfig = pruneAgentConfig(data.config as Record); + + if (databaseId) { + const existing = await db.query.database.findFirst({ + where: and( + eq(drizzleDb.schemas.database.id, databaseId), + eq(drizzleDb.schemas.database.agentId, agentId), + ), + }); + if (!existing) throw new ActionError("Database not found"); + + const config = encryptConfigSecrets( + data.dbms, + nextConfig, + existing.config as Record | null, + masterKey, + ); + + const [updated] = await db + .update(drizzleDb.schemas.database) + .set(withUpdatedAt({ name: data.name, dbms: data.dbms, config })) + .where(and( + eq(drizzleDb.schemas.database.id, databaseId), + eq(drizzleDb.schemas.database.agentId, agentId), + )) + .returning(); + if (!updated) throw new ActionError("Database not found"); + return updated; + } + + const config = encryptConfigSecrets(data.dbms, nextConfig, undefined, masterKey); + + const [created] = await db + .insert(drizzleDb.schemas.database) + .values({ + agentId, + name: data.name, + dbms: data.dbms, + config, + ...(agentDatabaseId ? { agentDatabaseId } : {}), + }) + .returning(); + return created; + }); diff --git a/src/features/database/components/config/database-config-fields.tsx b/src/features/database/components/config/database-config-fields.tsx new file mode 100644 index 00000000..c50ed64a --- /dev/null +++ b/src/features/database/components/config/database-config-fields.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { UseFormReturn } from "react-hook-form"; +import { EDbmsSchema } from "@/db/schema/types"; +import { databaseFieldDefs } from "@/features/database/schemas/database-config.schema"; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { PasswordInput } from "@/components/ui/password-input"; +import { Switch } from "@/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { X } from "lucide-react"; + +type Props = { dbms: EDbmsSchema; form: UseFormReturn }; + +export const DatabaseConfigFields = ({ dbms, form }: Props) => { + const defs = databaseFieldDefs[dbms] ?? []; + return ( +
+ {defs.map((def) => ( + ( + + {def.label} + + {def.widget === "password" ? ( + + ) : def.widget === "switch" ? ( + + ) : def.widget === "select" ? ( +
+ + {def.clearable && field.value ? ( + + ) : null} +
+ ) : ( + + )} +
+ +
+ )} + /> + ))} +
+ ); +}; diff --git a/src/features/database/components/database-config-modal.tsx b/src/features/database/components/database-config-modal.tsx new file mode 100644 index 00000000..89ef88df --- /dev/null +++ b/src/features/database/components/database-config-modal.tsx @@ -0,0 +1,293 @@ +"use client"; + +import {ReactNode, useEffect, useMemo, useState} from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from "@/components/ui/select"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Info } from "lucide-react"; +import { ButtonWithLoading } from "@/components/common/button-with-loading"; +import { Database } from "@/db/schema/07_database"; +import { EDbmsSchema } from "@/db/schema/types"; +import { + DatabaseConfigFormSchema, + DatabaseConfigFormType, + databaseTypeOptions, + defaultConfigFor, + toAgentConfigJson, + fromAgentConfigJson, + agentConfigSignature, + UUID_RE, +} from "@/features/database/schemas/database-config.schema"; +import { DatabaseConfigFields } from "@/features/database/components/config/database-config-fields"; +import { upsertDatabaseConfigAction } from "@/features/database/actions/database-config.action"; +import { DB_SECRET_FIELDS } from "@/features/database/utils/credential-fields"; + +type Props = { agentId: string; database?: Database; trigger: ReactNode }; + +export const DatabaseConfigModal = ({ agentId, database, trigger }: Props) => { + const router = useRouter(); + const [open, setOpen] = useState(false); + const [tab, setTab] = useState<"form" | "json">("form"); + const [jsonText, setJsonText] = useState(""); + const [jsonError, setJsonError] = useState(null); + + const initial = useMemo( + () => + database + ? { + form: { + name: database.name, + dbms: database.dbms, + config: (database.config as Record) ?? {}, + }, + generatedId: database.agentDatabaseId ?? undefined, + } + : { + form: { name: "", dbms: "postgresql" as EDbmsSchema, config: defaultConfigFor("postgresql") }, + generatedId: undefined as string | undefined, + }, + [database], + ); + + const [generatedId, setGeneratedId] = useState(initial.generatedId); + + const form = useZodForm({ + schema: DatabaseConfigFormSchema, + defaultValues: initial.form, + }); + + const dbms = form.watch("dbms") as EDbmsSchema; + const watched = form.watch(); + const lockedDbms = database?.dbms; + + const initialSignature = useMemo( + () => agentConfigSignature(initial.form, initial.generatedId), + [initial], + ); + + useEffect(() => { + if (tab === "form") { + setJsonText(JSON.stringify(toAgentConfigJson(watched, generatedId), null, 2)); + setJsonError(null); + } + }, [JSON.stringify(watched), generatedId, tab]); + + const onJsonChange = (text: string) => { + setJsonText(text); + try { + const parsed = JSON.parse(text); + const { form: mapped, generatedId: gid } = fromAgentConfigJson(parsed); + if (lockedDbms && mapped.dbms !== lockedDbms) { + setJsonError(`"type" cannot be changed after creation (expected "${lockedDbms}")`); + return; + } + // @ts-expect-error — reset target is a union across discriminated dbms variants + form.reset(mapped, { keepDefaultValues: true }); + if (gid && !UUID_RE.test(gid)) { + setGeneratedId(undefined); + setJsonError("generated_id must be a valid UUID"); + } else { + setGeneratedId(gid); + setJsonError(null); + } + } catch (e) { + setJsonError((e as Error).message); + } + }; + + const onDbmsChange = (value: EDbmsSchema) => { + form.reset( + // @ts-expect-error — reset target is a union across discriminated dbms variants + { name: form.getValues("name"), dbms: value, config: defaultConfigFor(value) }, + { keepDefaultValues: true }, + ); + setGeneratedId(database?.agentDatabaseId ?? undefined); + setJsonError(null); + }; + + const resetAll = () => { + // @ts-expect-error — reset target is a union across discriminated dbms variants + form.reset(initial.form); + setGeneratedId(initial.generatedId); + setTab("form"); + setJsonError(null); + }; + + const mutation = useMutation({ + mutationFn: async (values: DatabaseConfigFormType) => { + const result = await upsertDatabaseConfigAction({ + agentId, + databaseId: database?.id, + agentDatabaseId: database ? undefined : generatedId, + data: values, + }); + if (result?.serverError) { + toast.error(result.serverError); + return; + } + if (result?.validationErrors) { + toast.error("Invalid configuration"); + return; + } + toast.success(database ? "Database updated" : "Database created"); + setOpen(false); + router.refresh(); + }, + }); + + const isDirty = + agentConfigSignature(watched, generatedId) !== initialSignature; + const canSubmit = isDirty && !jsonError && !mutation.isPending; + + const isFromFile = !!database && database.config == null; + const isOverriding = !!database && database.config != null; + const hasStoredSecret = + !!database && (DB_SECRET_FIELDS[database.dbms]?.length ?? 0) > 0; + + return ( + { + setOpen(o); + resetAll(); + }} + > + {trigger} + e.preventDefault()}> + + {database ? "Configure database" : "Add database"} + + Define the connection settings pushed to the agent on status. + + + + {isFromFile && ( + + + Defined in the agent's databases.json + + Saving a configuration here will override it - the agent will use these dashboard + values instead of its local file. + + + )} + + {isOverriding && ( + + + Dashboard configuration active + + These values take precedence over any matching databases.json entry on the agent. + + + )} + + {hasStoredSecret && ( +

+ The password is masked. Leave it unchanged to keep the current one; type a new value to replace it. +

+ )} + +
{ + await mutation.mutateAsync(v); + }}> + setTab(v as "form" | "json")}> + + Form + JSON + + + + ( + + Name * + + + + + + )} + /> + + ( + + Type * + + {lockedDbms && ( +

+ The type cannot be changed after creation. +

+ )} + +
+ )} + /> + + +
+ + +

+ Paste an agent config entry (the same shape as databases.json). +

+