gui: work in progress

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-17 21:47:37 +02:00
parent b9fe6127ff
commit 206ae54dea
53 changed files with 2632 additions and 88 deletions

View File

@@ -0,0 +1,352 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { map, path, pipe, sort, uniq } from "ramda";
import { toDots } from "@/utils/object";
import { Prisma } from "@prisma/client";
import {
zChartBreakdowns,
zChartEvents,
zChartType,
zTimeInterval,
} from "@/utils/validation";
import { type IChartBreakdown, type IChartEvent } from "@/types";
type ResultItem = {
label: string | null;
count: number;
date: string;
};
function propertyNameToSql(name: string) {
if (name.includes(".")) {
return name
.split(".")
.map((item, index) => (index === 0 ? item : `'${item}'`))
.join("->");
}
return name;
}
export const config = {
api: {
responseLimit: false,
},
};
async function getChartData({
chartType,
event,
breakdowns,
interval,
startDate,
endDate,
}: {
chartType: string;
event: IChartEvent;
breakdowns: IChartBreakdown[];
interval: string;
startDate: Date;
endDate: Date;
}) {
const select = [`count(*)::int as count`];
const where = [];
const groupBy = [];
const orderBy = [];
switch (chartType) {
case "bar": {
orderBy.push("count DESC");
break;
}
case "linear": {
select.push(`date_trunc('${interval}', "createdAt") as date`);
groupBy.push("date");
orderBy.push("date");
break;
}
}
if (event) {
const { name, filters } = event;
where.push(`name = '${name}'`);
if (filters.length > 0) {
filters.forEach((filter) => {
const { name, value } = filter;
if (name.includes(".")) {
where.push(`${propertyNameToSql(name)} = '"${value}"'`);
} else {
where.push(`${name} = '${value}'`);
}
});
}
}
if (breakdowns.length) {
const breakdown = breakdowns[0];
if (breakdown) {
select.push(`${propertyNameToSql(breakdown.name)} as label`);
groupBy.push(`label`);
}
} else {
if (event.name) {
select.push(`'${event.name}' as label`);
}
}
if (startDate) {
where.push(`"createdAt" >= '${startDate.toISOString()}'`);
}
if (endDate) {
where.push(`"createdAt" <= '${endDate.toISOString()}'`);
}
const sql = `
SELECT ${select.join(", ")}
FROM events
WHERE ${where.join(" AND ")}
GROUP BY ${groupBy.join(", ")}
ORDER BY ${orderBy.join(", ")}
`;
console.log(sql);
const result = await db.$queryRawUnsafe<ResultItem[]>(sql);
const series = result.reduce(
(acc, item) => {
const label = item.label?.trim() ?? event.displayName;
if (label) {
if (acc[label]) {
acc[label]?.push(item);
} else {
acc[label] = [item];
}
}
return {
...acc,
};
},
{} as Record<string, ResultItem[]>,
);
return Object.keys(series).map((key) => {
return {
name: breakdowns.length ? key ?? "break a leg" : event.displayName,
data: fillEmptySpotsInTimeline(
series[key] ?? [],
interval,
startDate,
endDate,
).map((item) => {
return {
...item,
label: breakdowns.length ? key ?? "break a leg" : event.displayName,
date: new Date(item.date).toISOString(),
};
}),
};
});
}
export const chartMetaRouter = createTRPCRouter({
events: protectedProcedure
// .input(z.object())
.query(async ({ input }) => {
const events = await db.event.findMany({
take: 500,
distinct: ["name"],
});
return events;
}),
properties: protectedProcedure
.input(z.object({ event: z.string() }).optional())
.query(async ({ input }) => {
const events = await db.event.findMany({
take: 500,
where: {
...(input?.event
? {
name: input.event,
}
: {}),
},
});
const properties = events.reduce((acc, event) => {
const properties = event as Record<string, unknown>;
const dotNotation = toDots(properties);
return [...acc, ...Object.keys(dotNotation)];
}, [] as string[]);
return pipe(
sort<string>((a, b) => a.length - b.length),
uniq,
)(properties);
}),
values: protectedProcedure
.input(z.object({ event: z.string(), property: z.string() }))
.query(async ({ input }) => {
const events = await db.event.findMany({
where: {
name: input.event,
properties: {
path: input.property.split(".").slice(1),
not: Prisma.DbNull,
},
createdAt: {
// Take last 30 days
gte: new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 30),
},
},
});
const values = uniq(
map(path(input.property.split(".")), events),
) as string[];
return {
types: uniq(
values.map((value) =>
Array.isArray(value) ? "array" : typeof value,
),
),
values,
};
}),
chart: protectedProcedure
.input(
z.object({
startDate: z.date().nullish(),
endDate: z.date().nullish(),
chartType: zChartType,
interval: zTimeInterval,
events: zChartEvents,
breakdowns: zChartBreakdowns,
}),
)
.query(
async ({
input: { chartType, events, breakdowns, interval, ...input },
}) => {
const startDate = input.startDate ?? new Date();
const endDate = input.endDate ?? new Date();
const series: Awaited<ReturnType<typeof getChartData>> = [];
for (const event of events) {
series.push(
...(await getChartData({
chartType,
event,
breakdowns,
interval,
startDate,
endDate,
})),
);
}
return {
series: series.sort((a, b) => {
const sumA = a.data.reduce((acc, item) => acc + item.count, 0);
const sumB = b.data.reduce((acc, item) => acc + item.count, 0);
return sumB - sumA;
}),
};
},
),
});
function fillEmptySpotsInTimeline(
items: ResultItem[],
interval: string,
startDate: Date,
endDate: Date,
) {
const result = [];
const currentDate = new Date(startDate);
currentDate.setHours(2, 0, 0, 0);
const modifiedEndDate = new Date(endDate);
modifiedEndDate.setHours(2, 0, 0, 0);
while (currentDate.getTime() <= modifiedEndDate.getTime()) {
const getYear = (date: Date) => date.getFullYear();
const getMonth = (date: Date) => date.getMonth();
const getDay = (date: Date) => date.getDate();
const getHour = (date: Date) => date.getHours();
const getMinute = (date: Date) => date.getMinutes();
const item = items.find((item) => {
const date = new Date(item.date);
if (interval === "month") {
return (
getYear(date) === getYear(currentDate) &&
getMonth(date) === getMonth(currentDate)
);
}
if (interval === "day") {
return (
getYear(date) === getYear(currentDate) &&
getMonth(date) === getMonth(currentDate) &&
getDay(date) === getDay(currentDate)
);
}
if (interval === "hour") {
return (
getYear(date) === getYear(currentDate) &&
getMonth(date) === getMonth(currentDate) &&
getDay(date) === getDay(currentDate) &&
getHour(date) === getHour(currentDate)
);
}
if (interval === "minute") {
return (
getYear(date) === getYear(currentDate) &&
getMonth(date) === getMonth(currentDate) &&
getDay(date) === getDay(currentDate) &&
getHour(date) === getHour(currentDate) &&
getMinute(date) === getMinute(currentDate)
);
}
});
if (item) {
result.push(item);
} else {
result.push({
date: currentDate.toISOString(),
count: 0,
label: null,
});
}
switch (interval) {
case "day": {
currentDate.setDate(currentDate.getDate() + 1);
break;
}
case "hour": {
currentDate.setHours(currentDate.getHours() + 1);
break;
}
case "minute": {
currentDate.setMinutes(currentDate.getMinutes() + 1);
break;
}
case "month": {
currentDate.setMonth(currentDate.getMonth() + 1);
break;
}
}
}
return sort(function (a, b) {
return new Date(a.date).getTime() - new Date(b.date).getTime();
}, result);
}