mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 00:58:29 -06:00
fix: applying sonarqube suggestions and PR feedback and doing self review
This commit is contained in:
@@ -22,6 +22,9 @@ export type AuditLoggingCtx = {
|
||||
quotaId?: string;
|
||||
teamId?: string;
|
||||
integrationId?: string;
|
||||
chartId?: string;
|
||||
dashboardId?: string;
|
||||
dashboardWidgetId?: string;
|
||||
};
|
||||
|
||||
export type ActionClientCtx = {
|
||||
|
||||
@@ -13,7 +13,6 @@ type HasFindMany =
|
||||
| Prisma.ProjectTeamFindManyArgs
|
||||
| Prisma.UserFindManyArgs
|
||||
| Prisma.ContactAttributeKeyFindManyArgs
|
||||
| Prisma.ContactAttributeKeyFindManyArgs
|
||||
| Prisma.ChartFindManyArgs
|
||||
| Prisma.DashboardFindManyArgs;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { executeQuery } from "./cube-client";
|
||||
|
||||
const mockLoad = vi.fn();
|
||||
const mockTablePivot = vi.fn();
|
||||
@@ -13,12 +12,14 @@ vi.mock("@cubejs-client/core", () => ({
|
||||
describe("executeQuery", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
const resultSet = { tablePivot: mockTablePivot };
|
||||
mockLoad.mockResolvedValue(resultSet);
|
||||
mockTablePivot.mockReturnValue([{ id: "1", count: 42 }]);
|
||||
});
|
||||
|
||||
test("loads query and returns tablePivot result", async () => {
|
||||
const { executeQuery } = await import("./cube-client");
|
||||
const query = { measures: ["FeedbackRecords.count"] };
|
||||
const result = await executeQuery(query);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ let cubeClient: CubeApi | null = null;
|
||||
|
||||
function getCubeClient(): CubeApi {
|
||||
if (!cubeClient) {
|
||||
// TODO: This will fail silently if the token is not set. We need to fix this before going to production.
|
||||
const token = process.env.CUBEJS_API_TOKEN ?? "";
|
||||
cubeClient = cubejs(token, { apiUrl: getApiUrl() });
|
||||
}
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZChartConfig, ZChartQuery } from "@formbricks/types/dashboard";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import {
|
||||
createChart,
|
||||
deleteChart,
|
||||
duplicateChart,
|
||||
getChart,
|
||||
getCharts,
|
||||
updateChart,
|
||||
} from "@/modules/ee/analysis/charts/lib/charts";
|
||||
import { checkProjectAccess } from "@/modules/ee/analysis/lib/access";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { ZChartType } from "../types/analysis";
|
||||
|
||||
const checkProjectAccess = async (
|
||||
userId: string,
|
||||
environmentId: string,
|
||||
minPermission: "read" | "readWrite" | "manage"
|
||||
) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const projectId = await getProjectIdFromEnvironmentId(environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId,
|
||||
organizationId,
|
||||
access: [
|
||||
{ type: "organization", roles: ["owner", "manager"] },
|
||||
{ type: "projectTeam", minPermission, projectId },
|
||||
],
|
||||
});
|
||||
|
||||
return { organizationId, projectId };
|
||||
};
|
||||
|
||||
const ZCreateChartAction = z.object({
|
||||
environmentId: ZId,
|
||||
name: z.string().min(1),
|
||||
@@ -56,19 +42,18 @@ export const createChartAction = authenticatedActionClient.schema(ZCreateChartAc
|
||||
"readWrite"
|
||||
);
|
||||
|
||||
const chart = await prisma.chart.create({
|
||||
data: {
|
||||
name: parsedInput.name,
|
||||
type: parsedInput.type,
|
||||
projectId,
|
||||
query: parsedInput.query,
|
||||
config: parsedInput.config || {},
|
||||
createdBy: ctx.user.id,
|
||||
},
|
||||
});
|
||||
const chart = await createChart(
|
||||
projectId,
|
||||
parsedInput.name,
|
||||
parsedInput.type,
|
||||
parsedInput.query,
|
||||
parsedInput.config || {},
|
||||
ctx.user.id
|
||||
);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.chartId = chart.id;
|
||||
ctx.auditLoggingCtx.newObject = chart;
|
||||
return chart;
|
||||
}
|
||||
@@ -101,26 +86,16 @@ export const updateChartAction = authenticatedActionClient.schema(ZUpdateChartAc
|
||||
"readWrite"
|
||||
);
|
||||
|
||||
const chart = await prisma.chart.findFirst({
|
||||
where: { id: parsedInput.chartId, projectId },
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new Error("Chart not found");
|
||||
}
|
||||
|
||||
const updatedChart = await prisma.chart.update({
|
||||
where: { id: parsedInput.chartId },
|
||||
data: {
|
||||
...(parsedInput.name !== undefined && { name: parsedInput.name }),
|
||||
...(parsedInput.type !== undefined && { type: parsedInput.type }),
|
||||
...(parsedInput.query !== undefined && { query: parsedInput.query }),
|
||||
...(parsedInput.config !== undefined && { config: parsedInput.config }),
|
||||
},
|
||||
const { chart, updatedChart } = await updateChart(parsedInput.chartId, projectId, {
|
||||
name: parsedInput.name,
|
||||
type: parsedInput.type,
|
||||
query: parsedInput.query,
|
||||
config: parsedInput.config,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.chartId = parsedInput.chartId;
|
||||
ctx.auditLoggingCtx.oldObject = chart;
|
||||
ctx.auditLoggingCtx.newObject = updatedChart;
|
||||
return updatedChart;
|
||||
@@ -150,27 +125,11 @@ export const duplicateChartAction = authenticatedActionClient.schema(ZDuplicateC
|
||||
"readWrite"
|
||||
);
|
||||
|
||||
const sourceChart = await prisma.chart.findFirst({
|
||||
where: { id: parsedInput.chartId, projectId },
|
||||
});
|
||||
|
||||
if (!sourceChart) {
|
||||
throw new Error("Chart not found");
|
||||
}
|
||||
|
||||
const duplicatedChart = await prisma.chart.create({
|
||||
data: {
|
||||
name: `${sourceChart.name} (copy)`,
|
||||
type: sourceChart.type,
|
||||
projectId,
|
||||
query: sourceChart.query as object,
|
||||
config: (sourceChart.config as object) || {},
|
||||
createdBy: ctx.user.id,
|
||||
},
|
||||
});
|
||||
const duplicatedChart = await duplicateChart(parsedInput.chartId, projectId, ctx.user.id);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.chartId = duplicatedChart.id;
|
||||
ctx.auditLoggingCtx.newObject = duplicatedChart;
|
||||
return duplicatedChart;
|
||||
}
|
||||
@@ -199,20 +158,11 @@ export const deleteChartAction = authenticatedActionClient.schema(ZDeleteChartAc
|
||||
"readWrite"
|
||||
);
|
||||
|
||||
const chart = await prisma.chart.findFirst({
|
||||
where: { id: parsedInput.chartId, projectId },
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new Error("Chart not found");
|
||||
}
|
||||
|
||||
await prisma.chart.delete({
|
||||
where: { id: parsedInput.chartId },
|
||||
});
|
||||
const chart = await deleteChart(parsedInput.chartId, projectId);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.chartId = parsedInput.chartId;
|
||||
ctx.auditLoggingCtx.oldObject = chart;
|
||||
return { success: true };
|
||||
}
|
||||
@@ -236,24 +186,7 @@ export const getChartAction = authenticatedActionClient
|
||||
}) => {
|
||||
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
|
||||
|
||||
const chart = await prisma.chart.findFirst({
|
||||
where: { id: parsedInput.chartId, projectId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
query: true,
|
||||
config: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new Error("Chart not found");
|
||||
}
|
||||
|
||||
return chart;
|
||||
return getChart(parsedInput.chartId, projectId);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -273,21 +206,6 @@ export const getChartsAction = authenticatedActionClient
|
||||
}) => {
|
||||
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
|
||||
|
||||
return prisma.chart.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
query: true,
|
||||
config: true,
|
||||
widgets: {
|
||||
select: { dashboardId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return getCharts(projectId);
|
||||
}
|
||||
);
|
||||
|
||||
174
apps/web/modules/ee/analysis/charts/lib/charts.ts
Normal file
174
apps/web/modules/ee/analysis/charts/lib/charts.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import "server-only";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TChartConfig, TChartQuery } from "@formbricks/types/dashboard";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { TChartType } from "../../types/analysis";
|
||||
|
||||
const selectChart = {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
query: true,
|
||||
config: true,
|
||||
createdAt: true,
|
||||
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]);
|
||||
|
||||
return prisma.chart.create({
|
||||
data: {
|
||||
name,
|
||||
type,
|
||||
projectId,
|
||||
query,
|
||||
config: config || {},
|
||||
createdBy,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateChart = async (
|
||||
chartId: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
name?: string;
|
||||
type?: TChartType;
|
||||
query?: TChartQuery;
|
||||
config?: TChartConfig;
|
||||
}
|
||||
) => {
|
||||
validateInputs([chartId, ZId], [projectId, ZId]);
|
||||
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const chart = await tx.chart.findFirst({
|
||||
where: { id: chartId, projectId },
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new ResourceNotFoundError("Chart", chartId);
|
||||
}
|
||||
|
||||
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 }),
|
||||
},
|
||||
});
|
||||
|
||||
return { chart, updatedChart };
|
||||
});
|
||||
};
|
||||
|
||||
const getUniqueCopyName = async (baseName: string, projectId: string): Promise<string> => {
|
||||
const stripped = baseName.replace(/\s+\(copy(?:\s+\d+)?\)$/, "");
|
||||
|
||||
const existing = await prisma.chart.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
name: { startsWith: `${stripped} (copy` },
|
||||
},
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
const existingNames = new Set(existing.map((c) => c.name));
|
||||
|
||||
const firstCandidate = `${stripped} (copy)`;
|
||||
if (!existingNames.has(firstCandidate)) {
|
||||
return firstCandidate;
|
||||
}
|
||||
|
||||
let n = 2;
|
||||
while (existingNames.has(`${stripped} (copy ${n})`)) {
|
||||
n++;
|
||||
}
|
||||
return `${stripped} (copy ${n})`;
|
||||
};
|
||||
|
||||
export const duplicateChart = async (chartId: string, projectId: string, createdBy: string) => {
|
||||
validateInputs([chartId, ZId], [projectId, ZId], [createdBy, ZId]);
|
||||
|
||||
const sourceChart = await prisma.chart.findFirst({
|
||||
where: { id: chartId, projectId },
|
||||
});
|
||||
|
||||
if (!sourceChart) {
|
||||
throw new ResourceNotFoundError("Chart", chartId);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChart = async (chartId: string, projectId: string) => {
|
||||
validateInputs([chartId, ZId], [projectId, ZId]);
|
||||
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const chart = await tx.chart.findFirst({
|
||||
where: { id: chartId, projectId },
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new ResourceNotFoundError("Chart", chartId);
|
||||
}
|
||||
|
||||
await tx.chart.delete({
|
||||
where: { id: chartId },
|
||||
});
|
||||
|
||||
return chart;
|
||||
});
|
||||
};
|
||||
|
||||
export const getChart = async (chartId: string, projectId: string) => {
|
||||
validateInputs([chartId, ZId], [projectId, ZId]);
|
||||
|
||||
const chart = await prisma.chart.findFirst({
|
||||
where: { id: chartId, projectId },
|
||||
select: selectChart,
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new ResourceNotFoundError("Chart", chartId);
|
||||
}
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
export const getCharts = async (projectId: string) => {
|
||||
validateInputs([projectId, ZId]);
|
||||
|
||||
return prisma.chart.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
...selectChart,
|
||||
widgets: {
|
||||
select: { dashboardId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,32 +4,12 @@ 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 { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { checkProjectAccess } from "@/modules/ee/analysis/lib/access";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
|
||||
const checkProjectAccess = async (
|
||||
userId: string,
|
||||
environmentId: string,
|
||||
minPermission: "read" | "readWrite" | "manage"
|
||||
) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const projectId = await getProjectIdFromEnvironmentId(environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId,
|
||||
organizationId,
|
||||
access: [
|
||||
{ type: "organization", roles: ["owner", "manager"] },
|
||||
{ type: "projectTeam", minPermission, projectId },
|
||||
],
|
||||
});
|
||||
|
||||
return { organizationId, projectId };
|
||||
};
|
||||
|
||||
const ZCreateDashboardAction = z.object({
|
||||
environmentId: ZId,
|
||||
name: z.string().min(1),
|
||||
@@ -64,6 +44,7 @@ export const createDashboardAction = authenticatedActionClient.schema(ZCreateDas
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.dashboardId = dashboard.id;
|
||||
ctx.auditLoggingCtx.newObject = dashboard;
|
||||
return dashboard;
|
||||
}
|
||||
@@ -99,7 +80,7 @@ export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDas
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
throw new Error("Dashboard not found");
|
||||
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
|
||||
}
|
||||
|
||||
const updatedDashboard = await prisma.dashboard.update({
|
||||
@@ -112,6 +93,7 @@ export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDas
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.dashboardId = parsedInput.dashboardId;
|
||||
ctx.auditLoggingCtx.oldObject = dashboard;
|
||||
ctx.auditLoggingCtx.newObject = updatedDashboard;
|
||||
return updatedDashboard;
|
||||
@@ -146,7 +128,7 @@ export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDas
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
throw new Error("Dashboard not found");
|
||||
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
|
||||
}
|
||||
|
||||
await prisma.dashboard.delete({
|
||||
@@ -155,6 +137,7 @@ export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDas
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.dashboardId = parsedInput.dashboardId;
|
||||
ctx.auditLoggingCtx.oldObject = dashboard;
|
||||
return { success: true };
|
||||
}
|
||||
@@ -230,7 +213,7 @@ export const getDashboardAction = authenticatedActionClient
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
throw new Error("Dashboard not found");
|
||||
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
|
||||
}
|
||||
|
||||
return dashboard;
|
||||
@@ -245,56 +228,60 @@ const ZAddChartToDashboardAction = z.object({
|
||||
layout: ZWidgetLayout.optional().default({ x: 0, y: 0, w: 4, h: 3 }),
|
||||
});
|
||||
|
||||
export const addChartToDashboardAction = authenticatedActionClient
|
||||
.schema(ZAddChartToDashboardAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"dashboardWidget",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
|
||||
}) => {
|
||||
const { organizationId, projectId } = await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
parsedInput.environmentId,
|
||||
"readWrite"
|
||||
);
|
||||
export const addChartToDashboardAction = authenticatedActionClient.schema(ZAddChartToDashboardAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"dashboardWidget",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
|
||||
}) => {
|
||||
const { organizationId, projectId } = await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
parsedInput.environmentId,
|
||||
"readWrite"
|
||||
);
|
||||
|
||||
const [chart, dashboard] = await Promise.all([
|
||||
prisma.chart.findFirst({ where: { id: parsedInput.chartId, projectId } }),
|
||||
prisma.dashboard.findFirst({ where: { id: parsedInput.dashboardId, projectId } }),
|
||||
]);
|
||||
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 Error("Chart not found");
|
||||
}
|
||||
if (!dashboard) {
|
||||
throw new Error("Dashboard not found");
|
||||
}
|
||||
|
||||
const maxOrder = await prisma.dashboardWidget.aggregate({
|
||||
where: { dashboardId: parsedInput.dashboardId },
|
||||
_max: { order: true },
|
||||
});
|
||||
|
||||
const widget = await prisma.dashboardWidget.create({
|
||||
data: {
|
||||
dashboardId: parsedInput.dashboardId,
|
||||
chartId: parsedInput.chartId,
|
||||
title: parsedInput.title,
|
||||
layout: parsedInput.layout,
|
||||
order: (maxOrder._max.order ?? -1) + 1,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.newObject = widget;
|
||||
return widget;
|
||||
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" }
|
||||
);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.dashboardWidgetId = widget.id;
|
||||
ctx.auditLoggingCtx.newObject = widget;
|
||||
return widget;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
31
apps/web/modules/ee/analysis/lib/access.ts
Normal file
31
apps/web/modules/ee/analysis/lib/access.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import "server-only";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
userId: string,
|
||||
environmentId: string,
|
||||
minPermission: TTeamPermission
|
||||
) => {
|
||||
const environment = await getEnvironment(environmentId);
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError("environment", environmentId);
|
||||
}
|
||||
|
||||
const projectId = environment.projectId;
|
||||
const organizationId = await getOrganizationIdFromProjectId(projectId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId,
|
||||
organizationId,
|
||||
access: [
|
||||
{ type: "organization", roles: ["owner", "manager"] },
|
||||
{ type: "projectTeam", minPermission, projectId },
|
||||
],
|
||||
});
|
||||
|
||||
return { organizationId, projectId };
|
||||
};
|
||||
@@ -292,6 +292,15 @@ export const withAuditLogging = <TParsedInput = Record<string, unknown>, TResult
|
||||
case "quota":
|
||||
targetId = auditLoggingCtx.quotaId;
|
||||
break;
|
||||
case "chart":
|
||||
targetId = auditLoggingCtx.chartId;
|
||||
break;
|
||||
case "dashboard":
|
||||
targetId = auditLoggingCtx.dashboardId;
|
||||
break;
|
||||
case "dashboardWidget":
|
||||
targetId = auditLoggingCtx.dashboardWidgetId;
|
||||
break;
|
||||
default:
|
||||
targetId = UNKNOWN_DATA;
|
||||
break;
|
||||
|
||||
@@ -587,9 +587,7 @@ async function main(): Promise<void> {
|
||||
createdBy: SEED_IDS.USER_ADMIN,
|
||||
query: {
|
||||
measures: ["FeedbackRecords.count"],
|
||||
timeDimensions: [
|
||||
{ dimension: "FeedbackRecords.createdAt", granularity: "week" },
|
||||
],
|
||||
timeDimensions: [{ dimension: "FeedbackRecords.createdAt", granularity: "week" }],
|
||||
},
|
||||
config: {
|
||||
xAxisLabel: "Week",
|
||||
@@ -676,9 +674,7 @@ async function main(): Promise<void> {
|
||||
query: {
|
||||
measures: ["FeedbackRecords.count"],
|
||||
dimensions: ["FeedbackRecords.channel"],
|
||||
timeDimensions: [
|
||||
{ dimension: "FeedbackRecords.createdAt", granularity: "month" },
|
||||
],
|
||||
timeDimensions: [{ dimension: "FeedbackRecords.createdAt", granularity: "month" }],
|
||||
},
|
||||
config: {
|
||||
stacked: true,
|
||||
@@ -716,66 +712,91 @@ async function main(): Promise<void> {
|
||||
});
|
||||
|
||||
// Widgets for Overview dashboard
|
||||
await prisma.dashboardWidget.createMany({
|
||||
skipDuplicates: true,
|
||||
data: [
|
||||
{
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartNpsScore.id,
|
||||
title: "Current NPS",
|
||||
layout: { x: 0, y: 0, w: 2, h: 2 },
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartResponsesOverTime.id,
|
||||
title: "Weekly Responses",
|
||||
layout: { x: 2, y: 0, w: 4, h: 3 },
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartSatisfactionDist.id,
|
||||
title: "Satisfaction Breakdown",
|
||||
layout: { x: 0, y: 3, w: 3, h: 3 },
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartTopChannels.id,
|
||||
title: "Channel Trends",
|
||||
layout: { x: 3, y: 3, w: 3, h: 3 },
|
||||
order: 3,
|
||||
},
|
||||
],
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_OVERVIEW_NPS },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_OVERVIEW_NPS,
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartNpsScore.id,
|
||||
title: "Current NPS",
|
||||
layout: { x: 0, y: 0, w: 2, h: 2 },
|
||||
order: 0,
|
||||
},
|
||||
});
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_OVERVIEW_RESPONSES },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_OVERVIEW_RESPONSES,
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartResponsesOverTime.id,
|
||||
title: "Weekly Responses",
|
||||
layout: { x: 2, y: 0, w: 4, h: 3 },
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_OVERVIEW_SATISFACTION },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_OVERVIEW_SATISFACTION,
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartSatisfactionDist.id,
|
||||
title: "Satisfaction Breakdown",
|
||||
layout: { x: 0, y: 3, w: 3, h: 3 },
|
||||
order: 2,
|
||||
},
|
||||
});
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_OVERVIEW_CHANNELS },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_OVERVIEW_CHANNELS,
|
||||
dashboardId: dashboardOverview.id,
|
||||
chartId: chartTopChannels.id,
|
||||
title: "Channel Trends",
|
||||
layout: { x: 3, y: 3, w: 3, h: 3 },
|
||||
order: 3,
|
||||
},
|
||||
});
|
||||
|
||||
// Widgets for Survey Performance dashboard
|
||||
await prisma.dashboardWidget.createMany({
|
||||
skipDuplicates: true,
|
||||
data: [
|
||||
{
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartCompletionRate.id,
|
||||
title: "Completion by Survey",
|
||||
layout: { x: 0, y: 0, w: 6, h: 3 },
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartResponsesOverTime.id,
|
||||
title: "Response Volume",
|
||||
layout: { x: 0, y: 3, w: 4, h: 3 },
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartNpsScore.id,
|
||||
title: "NPS Tracker",
|
||||
layout: { x: 4, y: 3, w: 2, h: 2 },
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_SURVPERF_COMPLETION },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_SURVPERF_COMPLETION,
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartCompletionRate.id,
|
||||
title: "Completion by Survey",
|
||||
layout: { x: 0, y: 0, w: 6, h: 3 },
|
||||
order: 0,
|
||||
},
|
||||
});
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_SURVPERF_RESPONSES },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_SURVPERF_RESPONSES,
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartResponsesOverTime.id,
|
||||
title: "Response Volume",
|
||||
layout: { x: 0, y: 3, w: 4, h: 3 },
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
await prisma.dashboardWidget.upsert({
|
||||
where: { id: SEED_IDS.WIDGET_SURVPERF_NPS },
|
||||
update: {},
|
||||
create: {
|
||||
id: SEED_IDS.WIDGET_SURVPERF_NPS,
|
||||
dashboardId: dashboardSurveyPerf.id,
|
||||
chartId: chartNpsScore.id,
|
||||
title: "NPS Tracker",
|
||||
layout: { x: 4, y: 3, w: 2, h: 2 },
|
||||
order: 2,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`\n${"=".repeat(50)}`);
|
||||
|
||||
@@ -17,6 +17,13 @@ export const SEED_IDS = {
|
||||
CHART_TOP_CHANNELS: "clseedcharttopchann00",
|
||||
DASHBOARD_OVERVIEW: "clseeddashovervieww00",
|
||||
DASHBOARD_SURVEY_PERF: "clseeddashsurvperf000",
|
||||
WIDGET_OVERVIEW_NPS: "clseedwidgetovwnps000",
|
||||
WIDGET_OVERVIEW_RESPONSES: "clseedwidgetovwresp00",
|
||||
WIDGET_OVERVIEW_SATISFACTION: "clseedwidgetovwsat000",
|
||||
WIDGET_OVERVIEW_CHANNELS: "clseedwidgetovwchan00",
|
||||
WIDGET_SURVPERF_COMPLETION: "clseedwidgetspcomp000",
|
||||
WIDGET_SURVPERF_RESPONSES: "clseedwidgetspresp000",
|
||||
WIDGET_SURVPERF_NPS: "clseedwidgetspnps0000",
|
||||
} as const;
|
||||
|
||||
export const SEED_CREDENTIALS = {
|
||||
|
||||
Reference in New Issue
Block a user