Converted reports and share routes.

This commit is contained in:
Mike Cao 2025-01-28 10:21:56 -08:00
parent dcac7b7c96
commit 6c9f1ad06b
23 changed files with 574 additions and 5 deletions

View file

@ -0,0 +1,91 @@
import { z } from 'zod';
import { parseRequest } from 'lib/request';
import { deleteReport, getReport, updateReport } from 'queries';
import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth';
import { unauthorized, json, notFound, ok } from 'lib/response';
import { reportTypeParam } from 'lib/schema';
export async function GET(request: Request, { params }: { params: Promise<{ reportId: string }> }) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const { reportId } = await params;
const report = await getReport(reportId);
if (!(await canViewReport(auth, report))) {
return unauthorized();
}
report.parameters = JSON.parse(report.parameters);
return json(report);
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ reportId: string }> },
) {
const schema = z.object({
websiteId: z.string().uuid(),
type: reportTypeParam,
name: z.string().max(200),
description: z.string().max(500),
parameters: z.object({}).passthrough(),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { reportId } = await params;
const { websiteId, type, name, description, parameters } = body;
const report = await getReport(reportId);
if (!report) {
return notFound();
}
if (!(await canUpdateReport(auth, report))) {
return unauthorized();
}
const result = await updateReport(reportId, {
websiteId,
userId: auth.user.id,
type,
name,
description,
parameters: JSON.stringify(parameters),
} as any);
return json(result);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ reportId: string }> },
) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const { reportId } = await params;
const report = await getReport(reportId);
if (!(await canDeleteReport(auth, report))) {
return unauthorized();
}
await deleteReport(reportId);
return ok();
}

View file

@ -0,0 +1,50 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getFunnel } from 'queries';
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
steps: z
.array(
z.object({
type: z.string(),
value: z.string(),
}),
)
.min(2),
window: z.number().positive(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
steps,
window,
dateRange: { startDate, endDate },
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getFunnel(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
steps,
windowMinutes: +window,
});
return json(data);
}

View file

@ -0,0 +1,53 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getGoals } from 'queries/analytics/reports/getGoals';
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
goals: z
.array(
z.object({
type: z.string().regex(/url|event|event-data/),
value: z.string(),
goal: z.number(),
operator: z
.string()
.regex(/count|sum|average/)
.refine(data => data['type'] === 'event-data'),
property: z.string().refine(data => data['type'] === 'event-data'),
}),
)
.min(1),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
dateRange: { startDate, endDate },
goals,
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getGoals(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
goals,
});
return json(data);
}

View file

@ -0,0 +1,71 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getInsights } from 'queries';
function convertFilters(filters: any[]) {
return filters.reduce((obj, filter) => {
obj[filter.name] = filter;
return obj;
}, {});
}
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
fields: z
.array(
z.object({
name: z.string(),
type: z.string(),
label: z.string(),
}),
)
.min(1),
filters: z.array(
z.object({
name: z.string(),
type: z.string(),
operator: z.string(),
value: z.string(),
}),
),
groups: z.array(
z.object({
name: z.string(),
type: z.string(),
}),
),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
dateRange: { startDate, endDate },
fields,
filters,
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getInsights(websiteId, fields, {
...convertFilters(filters),
startDate: new Date(startDate),
endDate: new Date(endDate),
});
return json(data);
}

View file

@ -0,0 +1,46 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getJourney } from 'queries';
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
steps: z.number().min(3).max(7),
startStep: z.string(),
endStep: z.string(),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
dateRange: { startDate, endDate },
steps,
startStep,
endStep,
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getJourney(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
steps,
startStep,
endStep,
});
return json(data);
}

View file

@ -0,0 +1,41 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getRetention } from 'queries';
import { timezoneParam } from 'lib/schema';
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
timezone: timezoneParam,
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
dateRange: { startDate, endDate },
timezone,
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getRetention(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
timezone,
});
return json(data);
}

View file

@ -0,0 +1,75 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { timezoneParam, unitParam } from 'lib/schema';
import { getRevenue } from 'queries/analytics/reports/getRevenue';
import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues';
export async function GET(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
}),
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId, startDate, endDate } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getRevenueValues(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
});
return json(data);
}
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
unit: unitParam,
}),
timezone: timezoneParam,
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
currency,
timezone,
dateRange: { startDate, endDate, unit },
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getRevenue(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
unit,
timezone,
currency,
});
return json(data);
}

View file

@ -0,0 +1,73 @@
import { z } from 'zod';
import { pagingParams } from 'lib/schema';
import { parseRequest } from 'lib/request';
import { canViewTeam, canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { getReports } from 'queries/prisma/report';
export async function GET(request: Request) {
const schema = z.object({
...pagingParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { page, search, pageSize, websiteId, teamId } = query;
const userId = auth.user.id;
const filters = {
page,
pageSize,
search,
};
if (
(websiteId && !(await canViewWebsite(auth, websiteId))) ||
(teamId && !(await canViewTeam(auth, teamId)))
) {
return unauthorized();
}
const data = await getReports(
{
where: {
OR: [
...(websiteId ? [{ websiteId }] : []),
...(teamId
? [
{
website: {
deletedAt: null,
teamId,
},
},
]
: []),
...(userId && !websiteId && !teamId
? [
{
website: {
deletedAt: null,
userId,
},
},
]
: []),
],
},
include: {
website: {
select: {
domain: true,
},
},
},
},
filters,
);
return json(data);
}

View file

@ -0,0 +1,40 @@
import { z } from 'zod';
import { canViewWebsite } from 'lib/auth';
import { unauthorized, json } from 'lib/response';
import { parseRequest } from 'lib/request';
import { getUTM } from 'queries';
import { timezoneParam } from 'lib/schema';
export async function POST(request: Request) {
const schema = z.object({
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
timezone: timezoneParam,
}),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const {
websiteId,
dateRange: { startDate, endDate, timezone },
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getUTM(websiteId, {
startDate: new Date(startDate),
endDate: new Date(endDate),
timezone,
});
return json(data);
}

View file

@ -0,0 +1,19 @@
import { json, notFound } from 'lib/response';
import { getSharedWebsite } from 'queries';
import { createToken } from 'next-basics';
import { secret } from 'lib/crypto';
export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const { shareId } = await params;
const website = await getSharedWebsite(shareId);
if (!website) {
return notFound();
}
const data = { websiteId: website.id };
const token = createToken(data, secret());
return json({ ...data, token });
}

View file

@ -243,7 +243,7 @@ async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams)
const data = await prisma.client[model].findMany({
...criteria,
...{
...(size > 0 && { take: +size, skip: +size * (page - 1) }),
...(size > 0 && { take: +size, skip: +size * (+page - 1) }),
...(orderBy && {
orderBy: [
{
@ -266,7 +266,7 @@ async function pagedRawQuery(
) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (page - 1);
const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc';
const statements = [

View file

@ -30,7 +30,17 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value),
message: 'Invalid unit',
});
export const roleParam = z.string().regex(/team-member|team-view-only|team-manager/);
export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
export const reportTypeParam = z.enum([
'funnel',
'insights',
'retention',
'utm',
'goals',
'journey',
'revenue',
]);
export const filterParams = {
url: z.string().optional(),

View file

@ -26,8 +26,8 @@ export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export interface PageParams {
search?: string;
page?: number;
pageSize?: number;
page?: string;
pageSize?: string;
orderBy?: string;
sortDescending?: boolean;
}