refactor(dashboards): address review on removeWidgetFromDashboard

- Drop the prisma.$transaction wrapper; find + delete is two sequential
  steps, doesn't need a transaction.
- Drop the redundant ResourceNotFoundError catch branch; the trailing
  `throw error` already lets it bubble.
- Let action-client infer ctx / parsedInput types.

Tests: cover the two catch branches (Prisma -> DatabaseError, unknown
rethrow) so the new function is fully line-covered.
This commit is contained in:
Dhruwang
2026-05-12 16:11:45 +05:30
parent ddd2d5e983
commit 10c09f00a8
3 changed files with 46 additions and 41 deletions
@@ -334,34 +334,24 @@ const ZRemoveWidgetFromDashboardAction = z.object({
export const removeWidgetFromDashboardAction = authenticatedActionClient
.inputSchema(ZRemoveWidgetFromDashboardAction)
.action(
withAuditLogging(
"deleted",
"dashboardWidget",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZRemoveWidgetFromDashboardAction>;
}) => {
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
withAuditLogging("deleted", "dashboardWidget", async ({ ctx, parsedInput }) => {
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
const widget = await removeWidgetFromDashboard(
parsedInput.dashboardId,
workspaceId,
parsedInput.widgetId
);
const widget = await removeWidgetFromDashboard(
parsedInput.dashboardId,
workspaceId,
parsedInput.widgetId
);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.workspaceId = workspaceId;
ctx.auditLoggingCtx.dashboardWidgetId = widget.id;
ctx.auditLoggingCtx.oldObject = widget;
return { success: true };
}
)
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.workspaceId = workspaceId;
ctx.auditLoggingCtx.dashboardWidgetId = widget.id;
ctx.auditLoggingCtx.oldObject = widget;
return { success: true };
})
);
@@ -48,6 +48,7 @@ vi.mock("@formbricks/database", () => {
findFirst: vi.fn(),
findMany: vi.fn(),
},
dashboardWidget: txWidget,
$transaction: vi.fn((cb: any) => cb({ dashboard: txDash, chart: txChart, dashboardWidget: txWidget })),
},
};
@@ -704,5 +705,24 @@ describe("Dashboard Service", () => {
).rejects.toMatchObject({ name: "ResourceNotFoundError", resourceType: "DashboardWidget" });
expect(mockTxWidget.delete).not.toHaveBeenCalled();
});
test("wraps Prisma errors in DatabaseError", async () => {
mockTxWidget.findFirst.mockRejectedValue(makePrismaError("P9999"));
const { removeWidgetFromDashboard } = await import("./dashboards");
await expect(
removeWidgetFromDashboard(mockDashboardId, mockWorkspaceId, mockWidgetId)
).rejects.toMatchObject({ name: "DatabaseError" });
});
test("rethrows unknown errors", async () => {
const error = new Error("boom");
mockTxWidget.findFirst.mockRejectedValue(error);
const { removeWidgetFromDashboard } = await import("./dashboards");
await expect(removeWidgetFromDashboard(mockDashboardId, mockWorkspaceId, mockWidgetId)).rejects.toBe(
error
);
});
});
});
@@ -309,21 +309,16 @@ export const removeWidgetFromDashboard = async (
validateInputs([dashboardId, ZId], [workspaceId, ZId], [widgetId, ZId]);
try {
return await prisma.$transaction(async (tx) => {
const widget = await tx.dashboardWidget.findFirst({
where: { id: widgetId, dashboard: { id: dashboardId, workspaceId } },
});
if (!widget) {
throw new ResourceNotFoundError("DashboardWidget", widgetId);
}
return tx.dashboardWidget.delete({ where: { id: widgetId } });
const widget = await prisma.dashboardWidget.findFirst({
where: { id: widgetId, dashboard: { id: dashboardId, workspaceId } },
});
} catch (error) {
if (error instanceof ResourceNotFoundError) {
throw error;
if (!widget) {
throw new ResourceNotFoundError("DashboardWidget", widgetId);
}
return await prisma.dashboardWidget.delete({ where: { id: widgetId } });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}