import { z } from 'zod'; import { db, getReportById, getReportsByDashboardId } from '@openpanel/db'; import { zReportInput } from '@openpanel/validation'; import { getProjectAccess } from '../access'; import { TRPCAccessError } from '../errors'; import { createTRPCRouter, protectedProcedure } from '../trpc'; export const reportRouter = createTRPCRouter({ list: protectedProcedure .input( z.object({ dashboardId: z.string(), projectId: z.string(), }), ) .query(async ({ input: { dashboardId, projectId }, ctx }) => { return getReportsByDashboardId(dashboardId); }), create: protectedProcedure .input( z.object({ report: zReportInput.omit({ projectId: true }), dashboardId: z.string(), }), ) .mutation(async ({ input: { report, dashboardId }, ctx }) => { const dashboard = await db.dashboard.findUniqueOrThrow({ where: { id: dashboardId, }, }); const access = await getProjectAccess({ userId: ctx.session.userId, projectId: dashboard.projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } return db.report.create({ data: { projectId: dashboard.projectId, dashboardId, name: report.name, events: report.events, interval: report.interval, breakdowns: report.breakdowns, chartType: report.chartType, lineType: report.lineType, range: report.range === 'custom' ? '30d' : report.range, formula: report.formula, previous: report.previous ?? false, unit: report.unit, criteria: report.criteria, metric: report.metric === 'count' ? 'sum' : report.metric, funnelGroup: report.funnelGroup, funnelWindow: report.funnelWindow, }, }); }), update: protectedProcedure .input( z.object({ reportId: z.string(), report: zReportInput.omit({ projectId: true }), }), ) .mutation(async ({ input: { report, reportId }, ctx }) => { const dbReport = await db.report.findUniqueOrThrow({ where: { id: reportId, }, }); const access = await getProjectAccess({ userId: ctx.session.userId, projectId: dbReport.projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } return db.report.update({ where: { id: reportId, }, data: { name: report.name, events: report.events, interval: report.interval, breakdowns: report.breakdowns, chartType: report.chartType, lineType: report.lineType, range: report.range === 'custom' ? '30d' : report.range, formula: report.formula, previous: report.previous ?? false, unit: report.unit, criteria: report.criteria, metric: report.metric === 'count' ? 'sum' : report.metric, funnelGroup: report.funnelGroup, funnelWindow: report.funnelWindow, }, }); }), delete: protectedProcedure .input( z.object({ reportId: z.string(), }), ) .mutation(async ({ input: { reportId }, ctx }) => { const report = await db.report.findUniqueOrThrow({ where: { id: reportId, }, }); const access = await getProjectAccess({ userId: ctx.session.userId, projectId: report.projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } return db.report.delete({ where: { id: reportId, }, }); }), duplicate: protectedProcedure .input( z.object({ reportId: z.string(), }), ) .mutation(async ({ input: { reportId }, ctx }) => { const report = await db.report.findUniqueOrThrow({ where: { id: reportId, }, }); const access = await getProjectAccess({ userId: ctx.session.userId, projectId: report.projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } return db.report.create({ data: { projectId: report.projectId, dashboardId: report.dashboardId, name: `Copy of ${report.name}`, events: report.events!, interval: report.interval, breakdowns: report.breakdowns!, chartType: report.chartType, lineType: report.lineType, range: report.range, formula: report.formula, previous: report.previous, unit: report.unit, criteria: report.criteria, metric: report.metric, funnelGroup: report.funnelGroup, funnelWindow: report.funnelWindow, }, }); }), get: protectedProcedure .input( z.object({ reportId: z.string(), }), ) .query(async ({ input: { reportId }, ctx }) => { return getReportById(reportId); }), updateLayout: protectedProcedure .input( z.object({ reportId: z.string(), layout: z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number(), minW: z.number().optional(), minH: z.number().optional(), maxW: z.number().optional(), maxH: z.number().optional(), }), }), ) .mutation(async ({ input: { reportId, layout }, ctx }) => { const report = await db.report.findUniqueOrThrow({ where: { id: reportId, }, }); const access = await getProjectAccess({ userId: ctx.session.userId, projectId: report.projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } // Upsert the layout (create if doesn't exist, update if it does) return db.reportLayout.upsert({ where: { reportId: reportId, }, create: { reportId: reportId, x: layout.x, y: layout.y, w: layout.w, h: layout.h, minW: layout.minW, minH: layout.minH, maxW: layout.maxW, maxH: layout.maxH, }, update: { x: layout.x, y: layout.y, w: layout.w, h: layout.h, minW: layout.minW, minH: layout.minH, maxW: layout.maxW, maxH: layout.maxH, }, }); }), getLayouts: protectedProcedure .input( z.object({ dashboardId: z.string(), projectId: z.string(), }), ) .query(async ({ input: { dashboardId, projectId }, ctx }) => { const access = await getProjectAccess({ userId: ctx.session.userId, projectId: projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } return db.reportLayout.findMany({ where: { report: { dashboardId: dashboardId, }, }, include: { report: true, }, }); }), resetLayout: protectedProcedure .input( z.object({ dashboardId: z.string(), projectId: z.string(), }), ) .mutation(async ({ input: { dashboardId, projectId }, ctx }) => { const access = await getProjectAccess({ userId: ctx.session.userId, projectId: projectId, }); if (!access) { throw TRPCAccessError('You do not have access to this project'); } // Delete all layout data for reports in this dashboard return db.reportLayout.deleteMany({ where: { report: { dashboardId: dashboardId, }, }, }); }), });