mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge pull request #360 from Portabase/fix/remove-database-link-to-agent
fix: remove-database-link-to-agent
This commit is contained in:
@@ -81,7 +81,7 @@ export default async function RoutePage(
|
|||||||
</PageDescription>
|
</PageDescription>
|
||||||
)}
|
)}
|
||||||
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
<AgentContentPage agent={agent} edgeKey={edgeKey} />
|
<AgentContentPage agent={agent} edgeKey={edgeKey} canDeleteDatabases={true} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default async function RoutePage(
|
|||||||
</PageDescription>
|
</PageDescription>
|
||||||
)}
|
)}
|
||||||
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
<AgentContentPage agent={agent} edgeKey={edgeKey}/>
|
<AgentContentPage agent={agent} edgeKey={edgeKey} canDeleteDatabases={!!agent.organizationId}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
|
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.insert(drizzleDb.schemas.healthcheckLog)
|
.insert(drizzleDb.schemas.healthcheckLog)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
@@ -16,16 +14,17 @@ export type DatabaseCardProps = {
|
|||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onToggleSelect?: (databaseId: string) => void;
|
onToggleSelect?: (databaseId: string) => void;
|
||||||
|
deleteButton?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseCard = (props: DatabaseCardProps) => {
|
export const DatabaseCard = (props: DatabaseCardProps) => {
|
||||||
const {data: database, withDetails = true, selectable = false, selected = false, onToggleSelect} = props;
|
const {data: database, withDetails = true, selectable = false, selected = false, onToggleSelect, deleteButton} = props;
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = (e: React.MouseEvent) => {
|
const handleCopy = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigator.clipboard.writeText(database.agentDatabaseId);
|
navigator.clipboard.writeText(database.agentDatabaseId ?? "");
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
};
|
};
|
||||||
@@ -107,7 +106,12 @@ export const DatabaseCard = (props: DatabaseCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{withDetails && (
|
{deleteButton ? (
|
||||||
|
<div className="mt-4 flex items-center justify-end pt-3 border-t border-border/50"
|
||||||
|
onClick={(e) => e.preventDefault()}>
|
||||||
|
{deleteButton}
|
||||||
|
</div>
|
||||||
|
) : withDetails ? (
|
||||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||||
<span
|
<span
|
||||||
className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span>
|
className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span>
|
||||||
@@ -118,7 +122,7 @@ export const DatabaseCard = (props: DatabaseCardProps) => {
|
|||||||
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform"/>
|
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "databases" ALTER COLUMN "agent_database_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "databases" ALTER COLUMN "agent_id" DROP NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -491,6 +491,13 @@
|
|||||||
"when": 1782937885439,
|
"when": 1782937885439,
|
||||||
"tag": "0069_worried_alex_wilder",
|
"tag": "0069_worried_alex_wilder",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1783264084846,
|
||||||
|
"tag": "0070_illegal_joseph",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ import {JobLog, jobLog} from "@/db/schema/17_job-log";
|
|||||||
|
|
||||||
export const database = pgTable("databases", {
|
export const database = pgTable("databases", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
agentDatabaseId: uuid("agent_database_id").notNull().defaultRandom(),
|
agentDatabaseId: uuid("agent_database_id").defaultRandom(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
dbms: dbmsEnum("dbms").notNull(),
|
dbms: dbmsEnum("dbms").notNull(),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
@@ -22,7 +22,6 @@ export const database = pgTable("databases", {
|
|||||||
backupToRestore: text("backup_to_restore"),
|
backupToRestore: text("backup_to_restore"),
|
||||||
healthErrorCount: integer("health_error_count"),
|
healthErrorCount: integer("health_error_count"),
|
||||||
agentId: uuid("agent_id")
|
agentId: uuid("agent_id")
|
||||||
.notNull()
|
|
||||||
.references(() => agent.id, {onDelete: "cascade"}),
|
.references(() => agent.id, {onDelete: "cascade"}),
|
||||||
lastContact: timestamp("last_contact"),
|
lastContact: timestamp("last_contact"),
|
||||||
projectId: uuid("project_id")
|
projectId: uuid("project_id")
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ export async function getOrganizationAvailableDatabases(
|
|||||||
projectId?: string,
|
projectId?: string,
|
||||||
) {
|
) {
|
||||||
const availableDatabases = (await db.query.database.findMany({
|
const availableDatabases = (await db.query.database.findMany({
|
||||||
where: (db, { eq, or, isNull }) =>
|
where: (db, { eq, or, and, isNull }) =>
|
||||||
|
and(
|
||||||
projectId
|
projectId
|
||||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||||
: isNull(db.projectId),
|
: isNull(db.projectId),
|
||||||
|
isNull(db.deletedAt),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
agent: {
|
agent: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { and, eq, gte, isNotNull, lt } from "drizzle-orm";
|
import { and, eq, gte, isNotNull, isNull, lt } from "drizzle-orm";
|
||||||
import { dispatchNotification } from "@/features/notifications/utils/notifications.dispatch";
|
import { dispatchNotification } from "@/features/notifications/utils/notifications.dispatch";
|
||||||
import { EventPayload } from "@/features/notifications/types";
|
import { EventPayload } from "@/features/notifications/types";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
@@ -113,7 +113,10 @@ export async function checkAgentsHealthError() {
|
|||||||
|
|
||||||
export async function checkDatabasesHealthError() {
|
export async function checkDatabasesHealthError() {
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
where: and(
|
||||||
|
isNotNull(drizzleDb.schemas.database.lastContact),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
agent: true,
|
agent: true,
|
||||||
alertPolicies: true,
|
alertPolicies: true,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { getOrganization } from "@/lib/auth/auth";
|
import { getOrganization } from "@/lib/auth/auth";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { project } from "@/db/schema/06_project";
|
import { project } from "@/db/schema/06_project";
|
||||||
|
|
||||||
@@ -10,7 +10,9 @@ export async function getOrganizationProject(organizationId: string) {
|
|||||||
return db.query.project.findFirst({
|
return db.query.project.findFirst({
|
||||||
where: eq(project.organizationId, organizationId),
|
where: eq(project.organizationId, organizationId),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: {
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,9 @@ export const getOrganizationProjectDatabases = async ({
|
|||||||
eq(drizzleDb.schemas.project.id, projectId),
|
eq(drizzleDb.schemas.project.id, projectId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: {
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {userAction} from "@/lib/safe-actions/actions";
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
@@ -25,7 +25,9 @@ export const updateAgentOrganizationsAction = userAction
|
|||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
with: {
|
with: {
|
||||||
organizations: true,
|
organizations: true,
|
||||||
databases: true
|
databases: {
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}) as AgentWith;
|
}) as AgentWith;
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ export const updateAgentOrganizationsAction = userAction
|
|||||||
|
|
||||||
if (projectIds.length > 0) {
|
if (projectIds.length > 0) {
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
where: (db, { inArray }) => inArray(db.projectId, projectIds),
|
where: (db, { inArray, and, isNull }) => and(inArray(db.projectId, projectIds), isNull(db.deletedAt)),
|
||||||
columns: { id: true }
|
columns: { id: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||||
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
import {eq, and, ne, count, desc, isNull} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {slugify} from "@/utils/slugify";
|
import {slugify} from "@/utils/slugify";
|
||||||
@@ -87,7 +87,9 @@ export const getAgentAction = userAction.schema(z.string()).action(async ({parse
|
|||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, parsedInput),
|
where: eq(drizzleDb.schemas.agent.id, parsedInput),
|
||||||
with: {
|
with: {
|
||||||
databases: true
|
databases: {
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
|||||||
type AgentContentPageProps = {
|
type AgentContentPageProps = {
|
||||||
edgeKey: string;
|
edgeKey: string;
|
||||||
agent: AgentWithDatabases
|
agent: AgentWithDatabases
|
||||||
|
canDeleteDatabases?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPageProps) => {
|
export const AgentContentPage = ({edgeKey, agent: initialAgent, canDeleteDatabases = false}: AgentContentPageProps) => {
|
||||||
|
|
||||||
const {data} = useQuery({
|
const {data} = useQuery({
|
||||||
queryKey: ["agent-data", initialAgent.id],
|
queryKey: ["agent-data", initialAgent.id],
|
||||||
@@ -121,6 +122,7 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
|
|||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
)}
|
)}
|
||||||
cardItem={AgentDatabaseCard}
|
cardItem={AgentDatabaseCard}
|
||||||
|
canDeleteDatabases={canDeleteDatabases}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Database} from "@/db/schema/07_database";
|
import { Database } from "@/db/schema/07_database";
|
||||||
import {DatabaseCard} from "@/components/common/database-card";
|
import { DatabaseCard } from "@/components/common/database-card";
|
||||||
|
import { DatabaseDeleteButton } from "@/features/agents/components/database-delete-button";
|
||||||
|
|
||||||
export type agentDatabaseCardProps = {
|
export type AgentDatabaseCardProps = {
|
||||||
data: Database;
|
data: Database;
|
||||||
|
canDeleteDatabases?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => {
|
export const AgentDatabaseCard = (props: AgentDatabaseCardProps) => {
|
||||||
const {data: database} = props;
|
const { data: database, canDeleteDatabases = false } = props;
|
||||||
|
|
||||||
return <DatabaseCard withDetails={false} data={database}/>;
|
return (
|
||||||
|
<DatabaseCard
|
||||||
|
withDetails={false}
|
||||||
|
data={database}
|
||||||
|
deleteButton={
|
||||||
|
canDeleteDatabases && database.agentId ? (
|
||||||
|
<DatabaseDeleteButton
|
||||||
|
databaseId={database.id}
|
||||||
|
agentId={database.agentId}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {Trash2} from "lucide-react";
|
||||||
|
import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
||||||
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {deleteDatabaseAction} from "@/features/database/actions/database-delete.action";
|
||||||
|
|
||||||
|
export type DatabaseDeleteButtonProps = {
|
||||||
|
databaseId: string;
|
||||||
|
agentId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatabaseDeleteButton = (props: DatabaseDeleteButtonProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => deleteDatabaseAction({databaseId: props.databaseId}),
|
||||||
|
onSuccess: async (result: any) => {
|
||||||
|
if (result.data?.success) {
|
||||||
|
toast.success(result.data.actionSuccess.message);
|
||||||
|
await queryClient.invalidateQueries({queryKey: ["agent-data", props.agentId]});
|
||||||
|
} else {
|
||||||
|
toast.error(result.data?.actionError?.message || "Unknown error occurred.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error("Failed to delete database.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonWithConfirm
|
||||||
|
title="Delete database"
|
||||||
|
description="Are you sure you want to delete this database? It will be unlinked from this agent."
|
||||||
|
button={{
|
||||||
|
main: {
|
||||||
|
text: "",
|
||||||
|
variant: "outline",
|
||||||
|
size: "sm",
|
||||||
|
icon: <Trash2 color="red"/>,
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Delete",
|
||||||
|
icon: <Trash2/>,
|
||||||
|
variant: "destructive",
|
||||||
|
onClick: () => {
|
||||||
|
mutation.mutate();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "outline",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
isPending={mutation.isPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {BackupWith, RestorationWith} from "@/db/schema/07_database";
|
import {BackupWith, RestorationWith} from "@/db/schema/07_database";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
@@ -19,7 +19,10 @@ export const getDatabaseDataAction = userAction
|
|||||||
const {databaseId} = parsedInput;
|
const {databaseId} = parsedInput;
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function assertDatabasesInOrgProject(projectId: string, databaseIds
|
|||||||
eq(drizzleDb.schemas.project.id, projectId),
|
eq(drizzleDb.schemas.project.id, projectId),
|
||||||
eq(drizzleDb.schemas.project.organizationId, organization.id),
|
eq(drizzleDb.schemas.project.organizationId, organization.id),
|
||||||
),
|
),
|
||||||
with: {databases: true},
|
with: {databases: {where: isNull(drizzleDb.schemas.database.deletedAt)}},
|
||||||
});
|
});
|
||||||
if (!project) throw new ActionError("Project not found.");
|
if (!project) throw new ActionError("Project not found.");
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export type RestorePreviewRow = {
|
|||||||
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
|
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
|
||||||
const project = await db.query.project.findFirst({
|
const project = await db.query.project.findFirst({
|
||||||
where: eq(drizzleDb.schemas.project.id, projectId),
|
where: eq(drizzleDb.schemas.project.id, projectId),
|
||||||
with: {databases: true},
|
with: {databases: {where: isNull(drizzleDb.schemas.database.deletedAt)}},
|
||||||
});
|
});
|
||||||
const nameById = new Map((project?.databases ?? []).map((d) => [d.id, d.name] as const));
|
const nameById = new Map((project?.databases ?? []).map((d) => [d.id, d.name] as const));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import {z} from "zod";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {Database} from "@/db/schema/07_database";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
import {zString} from "@/lib/zod";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|
||||||
|
type DeleteDatabaseInput = {
|
||||||
|
databaseId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatabaseNotFoundError extends Error {
|
||||||
|
constructor(databaseId: string) {
|
||||||
|
super(`Database not found or update failed: ${databaseId}`);
|
||||||
|
this.name = "DatabaseNotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnauthorizedError extends Error {
|
||||||
|
constructor(databaseId: string) {
|
||||||
|
super(`Not authorized to delete this database: ${databaseId}`);
|
||||||
|
this.name = "UnauthorizedError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertCanDeleteDatabase(databaseId: string): Promise<void> {
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
with: {
|
||||||
|
agent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database || !database.agent) {
|
||||||
|
throw new DatabaseNotFoundError(databaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await currentUser();
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedError(databaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = database.agent;
|
||||||
|
const isAdmin = user.role === "superadmin" || user.role === "admin";
|
||||||
|
|
||||||
|
let authorized: boolean;
|
||||||
|
if (agent.organizationId === null) {
|
||||||
|
authorized = isAdmin;
|
||||||
|
} else {
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember();
|
||||||
|
const canManage = activeMember
|
||||||
|
? computeOrganizationPermissions(activeMember).canManageAgents
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const hasAccess =
|
||||||
|
!!organization && agent.organizationId === organization.id;
|
||||||
|
authorized = canManage && hasAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorized) {
|
||||||
|
throw new UnauthorizedError(databaseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDatabaseService(input: DeleteDatabaseInput): Promise<Database> {
|
||||||
|
const {databaseId} = input;
|
||||||
|
|
||||||
|
await assertCanDeleteDatabase(databaseId);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(drizzleDb.schemas.retentionPolicy)
|
||||||
|
.where(eq(drizzleDb.schemas.retentionPolicy.databaseId, databaseId))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(drizzleDb.schemas.alertPolicy)
|
||||||
|
.where(eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(drizzleDb.schemas.storagePolicy)
|
||||||
|
.where(eq(drizzleDb.schemas.storagePolicy.databaseId, databaseId))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const [updatedDatabase] = await db
|
||||||
|
.update(drizzleDb.schemas.database)
|
||||||
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
|
agentId: null,
|
||||||
|
agentDatabaseId: null,
|
||||||
|
projectId: null,
|
||||||
|
backupPolicy: null,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.where(eq(drizzleDb.schemas.database.id, databaseId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!updatedDatabase) {
|
||||||
|
throw new DatabaseNotFoundError(databaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDatabaseAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
databaseId: zString(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput}): Promise<ServerActionResult<Database>> => {
|
||||||
|
try {
|
||||||
|
const deletedDatabase = await deleteDatabaseService(parsedInput);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: deletedDatabase,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Database has been successfully deleted.",
|
||||||
|
messageParams: {
|
||||||
|
databaseId: parsedInput.databaseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Not authorized to delete this database.",
|
||||||
|
status: 403,
|
||||||
|
messageParams: {
|
||||||
|
databaseId: parsedInput.databaseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof DatabaseNotFoundError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Database not found or update failed",
|
||||||
|
status: 404,
|
||||||
|
messageParams: {
|
||||||
|
databaseId: parsedInput.databaseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to delete database.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
messageParams: {
|
||||||
|
databaseId: parsedInput.databaseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import {db} from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq, isNull} from "drizzle-orm";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {storeBackupFiles} from "@/features/storages/utils/storages.helpers";
|
import {storeBackupFiles} from "@/features/storages/utils/storages.helpers";
|
||||||
import {getFileExtension} from "@/utils/common";
|
import {getFileExtension} from "@/utils/common";
|
||||||
@@ -19,7 +19,10 @@ export const uploadBackupAction = userAction
|
|||||||
const databaseId = formData.get("databaseId") as string;
|
const databaseId = formData.get("databaseId") as string;
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
alertPolicies: true,
|
alertPolicies: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {ServerActionResult} from "@/types/action-type";
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {getTodayISODate} from "@/utils/date-formatting";
|
import {getTodayISODate} from "@/utils/date-formatting";
|
||||||
@@ -24,7 +24,10 @@ export const migrationAction = userAction.schema(
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
const targetDatabase = await db.query.database.findFirst({
|
const targetDatabase = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, targetDatabaseId),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, targetDatabaseId),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {ProjectSchema} from "@/features/projects/schemas/projects.schema";
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import {Project} from "@/db/schema/06_project";
|
import {Project} from "@/db/schema/06_project";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
@@ -88,7 +88,9 @@ export const updateProjectAction = userAction
|
|||||||
const existing = await db.query.project.findFirst({
|
const existing = await db.query.project.findFirst({
|
||||||
where: eq(drizzleDb.schemas.project.id, parsedInput.projectId),
|
where: eq(drizzleDb.schemas.project.id, parsedInput.projectId),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: {
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
||||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
export async function resolveDatabaseAccess(id: string, user: ApiKeyContextUser) {
|
export async function resolveDatabaseAccess(id: string, user: ApiKeyContextUser) {
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(user);
|
const accessibleIds = await getAccessibleDatabaseIds(user);
|
||||||
if (accessibleIds.includes(id)) return "ok";
|
if (accessibleIds.includes(id)) return "ok";
|
||||||
const exists = await db.query.database.findFirst({
|
const exists = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
columns: { id: true },
|
columns: { id: true },
|
||||||
});
|
});
|
||||||
return exists ? "forbidden" : "not_found";
|
return exists ? "forbidden" : "not_found";
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ export const backupCleanTask = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, backup.databaseId),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, backup.databaseId),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
|
),
|
||||||
with: {alertPolicies: true},
|
with: {alertPolicies: true},
|
||||||
});
|
});
|
||||||
if (database) {
|
if (database) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const log = logger.child({ module: "tasks/database" });
|
|||||||
export const retentionCleanTask = async () => {
|
export const retentionCleanTask = async () => {
|
||||||
try {
|
try {
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
|
where: isNull(drizzleDb.schemas.database.deletedAt),
|
||||||
with: {
|
with: {
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
backups: {
|
backups: {
|
||||||
|
|||||||
Reference in New Issue
Block a user