mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 03:21:51 -05:00
fix: changing dashboard api routes to be server actions
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
import { Dashboard, Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { TDashboardUpdateInput } from "@/modules/api/v2/management/dashboards/[dashboardId]/types/dashboards";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
|
||||
export const getDashboard = async (dashboardId: string): Promise<Result<Dashboard, ApiErrorResponseV2>> => {
|
||||
try {
|
||||
const dashboard = await prisma.dashboard.findUnique({
|
||||
where: { id: dashboardId },
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
return err({
|
||||
type: "not_found",
|
||||
details: [{ field: "dashboard", issue: "not found" }],
|
||||
});
|
||||
}
|
||||
|
||||
return ok(dashboard);
|
||||
} catch (error) {
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "dashboard", issue: (error as Error).message }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDashboard = async (
|
||||
dashboardId: string,
|
||||
dashboardInput: TDashboardUpdateInput
|
||||
): Promise<Result<Dashboard, ApiErrorResponseV2>> => {
|
||||
try {
|
||||
const updatedDashboard = await prisma.dashboard.update({
|
||||
where: { id: dashboardId },
|
||||
data: dashboardInput,
|
||||
});
|
||||
|
||||
return ok(updatedDashboard);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (
|
||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||
) {
|
||||
return err({
|
||||
type: "not_found",
|
||||
details: [{ field: "dashboard", issue: "not found" }],
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === PrismaErrorType.UniqueConstraintViolation) {
|
||||
return err({
|
||||
type: "conflict",
|
||||
details: [
|
||||
{ field: "name", issue: "A dashboard with this name already exists in the project" },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "dashboard", issue: (error as Error).message }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteDashboard = async (
|
||||
dashboardId: string
|
||||
): Promise<Result<Dashboard, ApiErrorResponseV2>> => {
|
||||
try {
|
||||
const deletedDashboard = await prisma.dashboard.delete({
|
||||
where: { id: dashboardId },
|
||||
});
|
||||
|
||||
return ok(deletedDashboard);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (
|
||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||
) {
|
||||
return err({
|
||||
type: "not_found",
|
||||
details: [{ field: "dashboard", issue: "not found" }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "dashboard", issue: (error as Error).message }],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TDashboardUpdateInput } from "@/modules/api/v2/management/dashboards/[dashboardId]/types/dashboards";
|
||||
import { deleteDashboard, getDashboard, updateDashboard } from "../dashboard";
|
||||
import {
|
||||
mockedDashboard,
|
||||
prismaNotFoundError,
|
||||
prismaUniqueConstraintError,
|
||||
} from "./mocks/dashboard.mock";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
dashboard: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("getDashboard", () => {
|
||||
test("returns ok if dashboard is found", async () => {
|
||||
vi.mocked(prisma.dashboard.findUnique).mockResolvedValueOnce(mockedDashboard);
|
||||
const result = await getDashboard("dashboard123");
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual(mockedDashboard);
|
||||
}
|
||||
});
|
||||
|
||||
test("returns err if dashboard not found", async () => {
|
||||
vi.mocked(prisma.dashboard.findUnique).mockResolvedValueOnce(null);
|
||||
const result = await getDashboard("nonexistent");
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("not_found");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns err on Prisma error", async () => {
|
||||
vi.mocked(prisma.dashboard.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
||||
const result = await getDashboard("error");
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toBe("internal_server_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateDashboard", () => {
|
||||
const updateInput: TDashboardUpdateInput = { name: "Updated Dashboard" };
|
||||
|
||||
test("returns ok on successful update", async () => {
|
||||
const updatedDashboard = { ...mockedDashboard, name: "Updated Dashboard" };
|
||||
vi.mocked(prisma.dashboard.update).mockResolvedValueOnce(updatedDashboard);
|
||||
const result = await updateDashboard("dashboard123", updateInput);
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data.name).toBe("Updated Dashboard");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns not_found if record does not exist", async () => {
|
||||
vi.mocked(prisma.dashboard.update).mockRejectedValueOnce(prismaNotFoundError);
|
||||
const result = await updateDashboard("nonexistent", updateInput);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("not_found");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns conflict on duplicate name", async () => {
|
||||
vi.mocked(prisma.dashboard.update).mockRejectedValueOnce(prismaUniqueConstraintError);
|
||||
const result = await updateDashboard("dashboard123", updateInput);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("conflict");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns internal_server_error on other errors", async () => {
|
||||
vi.mocked(prisma.dashboard.update).mockRejectedValueOnce(new Error("Unknown error"));
|
||||
const result = await updateDashboard("dashboard123", updateInput);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("internal_server_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDashboard", () => {
|
||||
test("returns ok on successful delete", async () => {
|
||||
vi.mocked(prisma.dashboard.delete).mockResolvedValueOnce(mockedDashboard);
|
||||
const result = await deleteDashboard("dashboard123");
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("returns not_found if record does not exist", async () => {
|
||||
vi.mocked(prisma.dashboard.delete).mockRejectedValueOnce(prismaNotFoundError);
|
||||
const result = await deleteDashboard("nonexistent");
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("not_found");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns internal_server_error on other errors", async () => {
|
||||
vi.mocked(prisma.dashboard.delete).mockRejectedValueOnce(new Error("Delete error"));
|
||||
const result = await deleteDashboard("error");
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toBe("internal_server_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
|
||||
export const mockedDashboard = {
|
||||
id: "dashboard123",
|
||||
name: "Test Dashboard",
|
||||
description: "A test dashboard",
|
||||
projectId: "project1",
|
||||
createdBy: null,
|
||||
createdAt: new Date("2026-01-28T12:00:00.000Z"),
|
||||
updatedAt: new Date("2026-01-28T12:00:00.000Z"),
|
||||
};
|
||||
|
||||
export const prismaNotFoundError = new Prisma.PrismaClientKnownRequestError("Record does not exist", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
clientVersion: "PrismaClient 4.0.0",
|
||||
});
|
||||
|
||||
export const prismaUniqueConstraintError = new Prisma.PrismaClientKnownRequestError(
|
||||
"Unique constraint failed",
|
||||
{
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "PrismaClient 4.0.0",
|
||||
}
|
||||
);
|
||||
@@ -1,185 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { hasProjectPermission } from "@/modules/api/v2/management/charts/lib/utils";
|
||||
import {
|
||||
deleteDashboard,
|
||||
getDashboard,
|
||||
updateDashboard,
|
||||
} from "@/modules/api/v2/management/dashboards/[dashboardId]/lib/dashboard";
|
||||
import {
|
||||
ZDashboardIdSchema,
|
||||
ZDashboardUpdateInput,
|
||||
} from "@/modules/api/v2/management/dashboards/[dashboardId]/types/dashboards";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
|
||||
export const GET = async (
|
||||
request: NextRequest,
|
||||
props: { params: Promise<{ dashboardId: string }> }
|
||||
) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
params: z.object({ dashboardId: ZDashboardIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { params } = parsedInput;
|
||||
|
||||
if (!params) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
});
|
||||
}
|
||||
|
||||
const dashboard = await getDashboard(params.dashboardId);
|
||||
|
||||
if (!dashboard.ok) {
|
||||
return handleApiError(request, dashboard.error as ApiErrorResponseV2);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasProjectPermission(authentication.environmentPermissions, dashboard.data.projectId, "GET")
|
||||
) {
|
||||
return handleApiError(request, {
|
||||
type: "forbidden",
|
||||
details: [{ field: "dashboard", issue: "does not have permission to access this dashboard" }],
|
||||
});
|
||||
}
|
||||
|
||||
return responses.successResponse(dashboard);
|
||||
},
|
||||
});
|
||||
|
||||
export const PUT = async (
|
||||
request: NextRequest,
|
||||
props: { params: Promise<{ dashboardId: string }> }
|
||||
) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
params: z.object({ dashboardId: ZDashboardIdSchema }),
|
||||
body: ZDashboardUpdateInput,
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params, body } = parsedInput;
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params?.dashboardId;
|
||||
}
|
||||
|
||||
if (!body || !params) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: !body ? "body" : "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const dashboard = await getDashboard(params.dashboardId);
|
||||
|
||||
if (!dashboard.ok) {
|
||||
return handleApiError(request, dashboard.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasProjectPermission(authentication.environmentPermissions, dashboard.data.projectId, "PUT")
|
||||
) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "forbidden",
|
||||
details: [{ field: "dashboard", issue: "does not have permission to update this dashboard" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const updatedDashboard = await updateDashboard(params.dashboardId, body);
|
||||
|
||||
if (!updatedDashboard.ok) {
|
||||
return handleApiError(request, updatedDashboard.error as ApiErrorResponseV2, auditLog); // NOSONAR
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = dashboard.data;
|
||||
auditLog.newObject = updatedDashboard.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(updatedDashboard);
|
||||
},
|
||||
action: "updated",
|
||||
targetType: "dashboard",
|
||||
});
|
||||
|
||||
export const DELETE = async (
|
||||
request: NextRequest,
|
||||
props: { params: Promise<{ dashboardId: string }> }
|
||||
) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
params: z.object({ dashboardId: ZDashboardIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { params } = parsedInput;
|
||||
if (auditLog) {
|
||||
auditLog.targetId = params?.dashboardId;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "params", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const dashboard = await getDashboard(params.dashboardId);
|
||||
|
||||
if (!dashboard.ok) {
|
||||
return handleApiError(request, dashboard.error as ApiErrorResponseV2, auditLog);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasProjectPermission(
|
||||
authentication.environmentPermissions,
|
||||
dashboard.data.projectId,
|
||||
"DELETE"
|
||||
)
|
||||
) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "forbidden",
|
||||
details: [{ field: "dashboard", issue: "does not have permission to delete this dashboard" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const deletedDashboard = await deleteDashboard(params.dashboardId);
|
||||
|
||||
if (!deletedDashboard.ok) {
|
||||
return handleApiError(request, deletedDashboard.error as ApiErrorResponseV2, auditLog); // NOSONAR
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.oldObject = dashboard.data;
|
||||
}
|
||||
|
||||
return responses.successResponse(deletedDashboard);
|
||||
},
|
||||
action: "deleted",
|
||||
targetType: "dashboard",
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZDashboardIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "dashboardId",
|
||||
description: "The ID of the dashboard",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
|
||||
export const ZDashboardUpdateInput = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(255).optional(),
|
||||
description: z.string().max(1000).optional().nullable(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "dashboardUpdate",
|
||||
description: "The fields to update on a dashboard.",
|
||||
});
|
||||
|
||||
export type TDashboardUpdateInput = z.infer<typeof ZDashboardUpdateInput>;
|
||||
@@ -1,84 +0,0 @@
|
||||
import { Dashboard, Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { getDashboardsQuery } from "@/modules/api/v2/management/dashboards/lib/utils";
|
||||
import {
|
||||
TDashboardInput,
|
||||
TGetDashboardsFilter,
|
||||
} from "@/modules/api/v2/management/dashboards/types/dashboards";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success";
|
||||
|
||||
export const getDashboards = async (
|
||||
projectIds: string[],
|
||||
params: TGetDashboardsFilter
|
||||
): Promise<Result<ApiResponseWithMeta<Dashboard[]>, ApiErrorResponseV2>> => {
|
||||
try {
|
||||
const query = getDashboardsQuery(projectIds, params);
|
||||
|
||||
const [dashboards, count] = await prisma.$transaction([
|
||||
prisma.dashboard.findMany({
|
||||
...query,
|
||||
}),
|
||||
prisma.dashboard.count({
|
||||
where: query.where,
|
||||
}),
|
||||
]);
|
||||
|
||||
return ok({
|
||||
data: dashboards,
|
||||
meta: {
|
||||
total: count,
|
||||
limit: params?.limit,
|
||||
offset: params?.skip,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "dashboards", issue: (error as Error).message }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDashboard = async (
|
||||
dashboardInput: TDashboardInput,
|
||||
createdBy?: string
|
||||
): Promise<Result<Dashboard, ApiErrorResponseV2>> => {
|
||||
try {
|
||||
const dashboard = await prisma.dashboard.create({
|
||||
data: {
|
||||
name: dashboardInput.name,
|
||||
description: dashboardInput.description ?? null,
|
||||
projectId: dashboardInput.projectId,
|
||||
createdBy: createdBy ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return ok(dashboard);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === PrismaErrorType.UniqueConstraintViolation) {
|
||||
return err({
|
||||
type: "conflict",
|
||||
details: [
|
||||
{ field: "name", issue: "A dashboard with this name already exists in the project" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === PrismaErrorType.RelatedRecordDoesNotExist) {
|
||||
return err({
|
||||
type: "not_found",
|
||||
details: [{ field: "projectId", issue: "Project not found" }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "dashboard", issue: (error as Error).message }],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import {
|
||||
TDashboardInput,
|
||||
TGetDashboardsFilter,
|
||||
} from "@/modules/api/v2/management/dashboards/types/dashboards";
|
||||
import { createDashboard, getDashboards } from "../dashboard";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
$transaction: vi.fn(),
|
||||
dashboard: {
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("getDashboards", () => {
|
||||
const projectIds = ["project1"];
|
||||
const params = {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
const fakeDashboards = [
|
||||
{ id: "d1", projectId: "project1", name: "Dashboard One" },
|
||||
{ id: "d2", projectId: "project1", name: "Dashboard Two" },
|
||||
];
|
||||
const count = fakeDashboards.length;
|
||||
|
||||
test("returns ok response with dashboards and meta", async () => {
|
||||
vi.mocked(prisma.$transaction).mockResolvedValueOnce([fakeDashboards, count]);
|
||||
|
||||
const result = await getDashboards(projectIds, params as TGetDashboardsFilter);
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data.data).toEqual(fakeDashboards);
|
||||
expect(result.data.meta).toEqual({
|
||||
total: count,
|
||||
limit: params.limit,
|
||||
offset: params.skip,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("returns error when prisma.$transaction throws", async () => {
|
||||
vi.mocked(prisma.$transaction).mockRejectedValueOnce(new Error("Test error"));
|
||||
|
||||
const result = await getDashboards(projectIds, params as TGetDashboardsFilter);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error?.type).toEqual("internal_server_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDashboard", () => {
|
||||
const inputDashboard: TDashboardInput = {
|
||||
projectId: "project1",
|
||||
name: "New Dashboard",
|
||||
description: "A new dashboard",
|
||||
};
|
||||
|
||||
const createdDashboard = {
|
||||
id: "d100",
|
||||
projectId: inputDashboard.projectId,
|
||||
name: inputDashboard.name,
|
||||
description: inputDashboard.description,
|
||||
createdBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
test("creates a dashboard", async () => {
|
||||
vi.mocked(prisma.dashboard.create).mockResolvedValueOnce(createdDashboard);
|
||||
|
||||
const result = await createDashboard(inputDashboard);
|
||||
expect(prisma.dashboard.create).toHaveBeenCalled();
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual(createdDashboard);
|
||||
}
|
||||
});
|
||||
|
||||
test("returns conflict error on duplicate name", async () => {
|
||||
const { prismaUniqueConstraintError } = await import(
|
||||
"@/modules/api/v2/management/dashboards/[dashboardId]/lib/tests/mocks/dashboard.mock"
|
||||
);
|
||||
vi.mocked(prisma.dashboard.create).mockRejectedValueOnce(prismaUniqueConstraintError);
|
||||
|
||||
const result = await createDashboard(inputDashboard);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toEqual("conflict");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns error when creation fails", async () => {
|
||||
vi.mocked(prisma.dashboard.create).mockRejectedValueOnce(new Error("Creation failed"));
|
||||
|
||||
const result = await createDashboard(inputDashboard);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toEqual("internal_server_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
|
||||
import { TGetDashboardsFilter } from "@/modules/api/v2/management/dashboards/types/dashboards";
|
||||
|
||||
export const getDashboardsQuery = (projectIds: string[], params?: TGetDashboardsFilter) => {
|
||||
let query: Prisma.DashboardFindManyArgs = {
|
||||
where: {
|
||||
projectId: { in: projectIds },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
};
|
||||
|
||||
if (!params) return query;
|
||||
|
||||
if (params.projectId) {
|
||||
query = {
|
||||
...query,
|
||||
where: {
|
||||
...query.where,
|
||||
projectId: params.projectId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const baseFilter = pickCommonFilter(params);
|
||||
|
||||
if (baseFilter) {
|
||||
query = buildCommonFilterQuery<Prisma.DashboardFindManyArgs>(query, baseFilter);
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { hasProjectPermission } from "@/modules/api/v2/management/charts/lib/utils";
|
||||
import { createDashboard, getDashboards } from "@/modules/api/v2/management/dashboards/lib/dashboard";
|
||||
import {
|
||||
ZDashboardInput,
|
||||
ZGetDashboardsFilter,
|
||||
} from "@/modules/api/v2/management/dashboards/types/dashboards";
|
||||
|
||||
export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetDashboardsFilter.sourceType(),
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
|
||||
if (!query) {
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
details: [{ field: "query", issue: "missing" }],
|
||||
});
|
||||
}
|
||||
|
||||
const projectIds = [
|
||||
...new Set(authentication.environmentPermissions.map((permission) => permission.projectId)),
|
||||
];
|
||||
|
||||
if (query.projectId && !projectIds.includes(query.projectId)) {
|
||||
return handleApiError(request, {
|
||||
type: "forbidden",
|
||||
details: [{ field: "projectId", issue: "does not have permission to access this project" }],
|
||||
});
|
||||
}
|
||||
|
||||
const filteredProjectIds = query.projectId ? [query.projectId] : projectIds;
|
||||
|
||||
const res = await getDashboards(filteredProjectIds, query);
|
||||
|
||||
if (res.ok) {
|
||||
return responses.successResponse(res.data);
|
||||
}
|
||||
|
||||
return handleApiError(request, res.error);
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
body: ZDashboardInput,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
if (!body) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: [{ field: "body", issue: "missing" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasProjectPermission(authentication.environmentPermissions, body.projectId, "POST")) {
|
||||
return handleApiError(
|
||||
request,
|
||||
{
|
||||
type: "forbidden",
|
||||
details: [{ field: "projectId", issue: "does not have permission to create dashboard" }],
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
}
|
||||
|
||||
const createDashboardResult = await createDashboard(body);
|
||||
|
||||
if (!createDashboardResult.ok) {
|
||||
return handleApiError(request, createDashboardResult.error, auditLog);
|
||||
}
|
||||
|
||||
if (auditLog) {
|
||||
auditLog.targetId = createDashboardResult.data.id;
|
||||
auditLog.newObject = createDashboardResult.data;
|
||||
}
|
||||
|
||||
return responses.createdResponse(createDashboardResult);
|
||||
},
|
||||
action: "created",
|
||||
targetType: "dashboard",
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZDashboardInput = z.object({
|
||||
projectId: z.string().cuid2(),
|
||||
name: z.string().trim().min(1).max(255),
|
||||
description: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
export type TDashboardInput = z.infer<typeof ZDashboardInput>;
|
||||
|
||||
export const ZGetDashboardsFilter = ZGetFilter.extend({
|
||||
projectId: z.string().cuid2().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "startDate must be before endDate",
|
||||
}
|
||||
);
|
||||
|
||||
export type TGetDashboardsFilter = z.infer<typeof ZGetDashboardsFilter>;
|
||||
Reference in New Issue
Block a user