fix: applying fixes after doing self review

This commit is contained in:
TheodorTomas
2026-02-20 15:19:11 +07:00
parent b52cd51771
commit 07a131dfd3
4 changed files with 224 additions and 139 deletions

View File

@@ -42,14 +42,14 @@ export const createChartAction = authenticatedActionClient.schema(ZCreateChartAc
"readWrite"
);
const chart = await createChart(
const chart = await createChart({
projectId,
parsedInput.name,
parsedInput.type,
parsedInput.query,
parsedInput.config || {},
ctx.user.id
);
name: parsedInput.name,
type: parsedInput.type,
query: parsedInput.query,
config: parsedInput.config,
createdBy: ctx.user.id,
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;

View File

@@ -6,7 +6,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { TChartType } from "../../types/analysis";
const selectChart = {
export const selectChart = {
id: true,
name: true,
type: true,
@@ -16,24 +16,24 @@ const selectChart = {
updatedAt: true,
} as const;
export const createChart = async (
projectId: string,
name: string,
type: TChartType,
query: TChartQuery,
config: TChartConfig,
createdBy: string
) => {
validateInputs([projectId, ZId], [createdBy, ZId]);
export const createChart = async (data: {
projectId: string;
name: string;
type: TChartType;
query: TChartQuery;
config: TChartConfig;
createdBy: string;
}) => {
validateInputs([data.projectId, ZId], [data.createdBy, ZId]);
return prisma.chart.create({
data: {
name,
type,
projectId,
query,
config: config || {},
createdBy,
name: data.name,
type: data.type,
projectId: data.projectId,
query: data.query,
config: data.config,
createdBy: data.createdBy,
},
});
};
@@ -62,10 +62,10 @@ export const updateChart = async (
const updatedChart = await tx.chart.update({
where: { id: chartId },
data: {
...(data.name !== undefined && { name: data.name }),
...(data.type !== undefined && { type: data.type }),
...(data.query !== undefined && { query: data.query }),
...(data.config !== undefined && { config: data.config }),
name: data.name,
type: data.type,
query: data.query,
config: data.config,
},
});
@@ -74,7 +74,7 @@ export const updateChart = async (
};
const getUniqueCopyName = async (baseName: string, projectId: string): Promise<string> => {
const stripped = baseName.replace(/\s+\(copy(?:\s+\d+)?\)$/, "");
const stripped = baseName.replace(/ \(copy(?: \d+)?\)$/, "");
const existing = await prisma.chart.findMany({
where: {
@@ -111,15 +111,13 @@ export const duplicateChart = async (chartId: string, projectId: string, created
const uniqueName = await getUniqueCopyName(sourceChart.name, projectId);
return prisma.chart.create({
data: {
name: uniqueName,
type: sourceChart.type,
projectId,
query: sourceChart.query as object,
config: (sourceChart.config as object) || {},
createdBy,
},
return createChart({
projectId,
name: uniqueName,
type: sourceChart.type as TChartType,
query: sourceChart.query as TChartQuery,
config: (sourceChart.config as TChartConfig) ?? {},
createdBy,
});
};

View File

@@ -1,14 +1,20 @@
"use server";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { ZWidgetLayout } from "@formbricks/types/dashboard";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { checkProjectAccess } from "@/modules/ee/analysis/lib/access";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import {
addChartToDashboard,
createDashboard,
deleteDashboard,
getDashboard,
getDashboards,
updateDashboard,
} from "./lib/dashboards";
const ZCreateDashboardAction = z.object({
environmentId: ZId,
@@ -33,13 +39,11 @@ export const createDashboardAction = authenticatedActionClient.schema(ZCreateDas
"readWrite"
);
const dashboard = await prisma.dashboard.create({
data: {
name: parsedInput.name,
description: parsedInput.description,
projectId,
createdBy: ctx.user.id,
},
const dashboard = await createDashboard({
projectId,
name: parsedInput.name,
description: parsedInput.description,
createdBy: ctx.user.id,
});
ctx.auditLoggingCtx.organizationId = organizationId;
@@ -75,20 +79,9 @@ export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDas
"readWrite"
);
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
const updatedDashboard = await prisma.dashboard.update({
where: { id: parsedInput.dashboardId },
data: {
...(parsedInput.name !== undefined && { name: parsedInput.name }),
...(parsedInput.description !== undefined && { description: parsedInput.description }),
},
const { dashboard, updatedDashboard } = await updateDashboard(parsedInput.dashboardId, projectId, {
name: parsedInput.name,
description: parsedInput.description,
});
ctx.auditLoggingCtx.organizationId = organizationId;
@@ -123,17 +116,7 @@ export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDas
"readWrite"
);
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
await prisma.dashboard.delete({
where: { id: parsedInput.dashboardId },
});
const dashboard = await deleteDashboard(parsedInput.dashboardId, projectId);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
@@ -160,18 +143,7 @@ export const getDashboardsAction = authenticatedActionClient
}) => {
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
return prisma.dashboard.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
id: true,
name: true,
description: true,
createdAt: true,
updatedAt: true,
_count: { select: { widgets: true } },
},
});
return getDashboards(projectId);
}
);
@@ -192,31 +164,7 @@ export const getDashboardAction = authenticatedActionClient
}) => {
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
const dashboard = await prisma.dashboard.findFirst({
where: { id: parsedInput.dashboardId, projectId },
include: {
widgets: {
orderBy: { order: "asc" },
include: {
chart: {
select: {
id: true,
name: true,
type: true,
query: true,
config: true,
},
},
},
},
},
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
return dashboard;
return getDashboard(parsedInput.dashboardId, projectId);
}
);
@@ -245,37 +193,13 @@ export const addChartToDashboardAction = authenticatedActionClient.schema(ZAddCh
"readWrite"
);
const [chart, dashboard] = await Promise.all([
prisma.chart.findFirst({ where: { id: parsedInput.chartId, projectId } }),
prisma.dashboard.findFirst({ where: { id: parsedInput.dashboardId, projectId } }),
]);
if (!chart) {
throw new ResourceNotFoundError("Chart", parsedInput.chartId);
}
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
const widget = await prisma.$transaction(
async (tx) => {
const maxOrder = await tx.dashboardWidget.aggregate({
where: { dashboardId: parsedInput.dashboardId },
_max: { order: true },
});
return tx.dashboardWidget.create({
data: {
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
title: parsedInput.title,
layout: parsedInput.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
},
{ isolationLevel: "Serializable" }
);
const widget = await addChartToDashboard({
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
projectId,
title: parsedInput.title,
layout: parsedInput.layout,
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;

View File

@@ -0,0 +1,163 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { TWidgetLayout } from "@formbricks/types/dashboard";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { selectChart } from "@/modules/ee/analysis/charts/lib/charts";
const selectDashboard = {
id: true,
name: true,
description: true,
createdAt: true,
updatedAt: true,
} as const;
export const createDashboard = async (data: {
projectId: string;
name: string;
description?: string;
createdBy: string;
}) => {
validateInputs([data.projectId, ZId], [data.createdBy, ZId]);
return prisma.dashboard.create({
data: {
name: data.name,
description: data.description,
projectId: data.projectId,
createdBy: data.createdBy,
},
});
};
export const updateDashboard = async (
dashboardId: string,
projectId: string,
data: {
name?: string;
description?: string | null;
}
) => {
validateInputs([dashboardId, ZId], [projectId, ZId]);
return prisma.$transaction(async (tx) => {
const dashboard = await tx.dashboard.findFirst({
where: { id: dashboardId, projectId },
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", dashboardId);
}
const updatedDashboard = await tx.dashboard.update({
where: { id: dashboardId },
data: {
name: data.name,
description: data.description,
},
});
return { dashboard, updatedDashboard };
});
};
export const deleteDashboard = async (dashboardId: string, projectId: string) => {
validateInputs([dashboardId, ZId], [projectId, ZId]);
return prisma.$transaction(async (tx) => {
const dashboard = await tx.dashboard.findFirst({
where: { id: dashboardId, projectId },
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", dashboardId);
}
await tx.dashboard.delete({
where: { id: dashboardId },
});
return dashboard;
});
};
export const getDashboard = async (dashboardId: string, projectId: string) => {
validateInputs([dashboardId, ZId], [projectId, ZId]);
const dashboard = await prisma.dashboard.findFirst({
where: { id: dashboardId, projectId },
include: {
widgets: {
orderBy: { order: "asc" },
include: {
chart: {
select: selectChart,
},
},
},
},
});
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", dashboardId);
}
return dashboard;
};
export const getDashboards = async (projectId: string) => {
validateInputs([projectId, ZId]);
return prisma.dashboard.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
...selectDashboard,
_count: { select: { widgets: true } },
},
});
};
export const addChartToDashboard = async (data: {
dashboardId: string;
chartId: string;
projectId: string;
title?: string;
layout: TWidgetLayout;
}) => {
validateInputs([data.dashboardId, ZId], [data.chartId, ZId], [data.projectId, ZId]);
return prisma.$transaction(
async (tx) => {
const [chart, dashboard] = await Promise.all([
tx.chart.findFirst({ where: { id: data.chartId, projectId: data.projectId } }),
tx.dashboard.findFirst({ where: { id: data.dashboardId, projectId: data.projectId } }),
]);
if (!chart) {
throw new ResourceNotFoundError("Chart", data.chartId);
}
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", data.dashboardId);
}
const maxOrder = await tx.dashboardWidget.aggregate({
where: { dashboardId: data.dashboardId },
_max: { order: true },
});
return tx.dashboardWidget.create({
data: {
dashboardId: data.dashboardId,
chartId: data.chartId,
title: data.title,
layout: data.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
},
{ isolationLevel: "Serializable" }
);
};