From 393db14bc8ee6df59da23f12e49f65da48955f5c Mon Sep 17 00:00:00 2001 From: Tim Raderschad Date: Wed, 28 Aug 2024 23:48:18 +0200 Subject: [PATCH] feat(web): improve queries (#162) --- apps/web/package.json | 6 +- apps/web/prisma/schema.prisma | 2 +- .../sql/getEventsByTestIdForAllTime.sql | 23 +++++ .../prisma/sql/getEventsByTestIdForDay.sql | 17 ++++ .../sql/getEventsByTestIdForLast30Days.sql | 16 ++++ apps/web/src/lib/events.ts | 45 +-------- apps/web/src/server/services/EventService.ts | 73 +++++--------- apps/web/src/server/trpc/router/events.ts | 94 +++++++++++++------ pnpm-lock.yaml | 64 +++++++------ 9 files changed, 187 insertions(+), 153 deletions(-) create mode 100644 apps/web/prisma/sql/getEventsByTestIdForAllTime.sql create mode 100644 apps/web/prisma/sql/getEventsByTestIdForDay.sql create mode 100644 apps/web/prisma/sql/getEventsByTestIdForLast30Days.sql diff --git a/apps/web/package.json b/apps/web/package.json index a96476bf..ea0a48a3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "postbuild": "next-sitemap", "dev": "next dev", "postinstall": "pnpm run db:generate", - "db:generate": "prisma generate", + "db:generate": "prisma generate && prisma generate --sql", "start": "next start", "seed:events": "ts-node --compiler-options {\\\"module\\\":\\\"CommonJS\\\"} prisma/seedEvents.ts", "db:migrate": "prisma migrate dev", @@ -34,7 +34,7 @@ "@monaco-editor/react": "^4.5.1", "@next-auth/prisma-adapter": "1.0.5", "@next/mdx": "14.0.4", - "@prisma/client": "5.18.0", + "@prisma/client": "5.19.0", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -142,7 +142,7 @@ "jsdom": "^20.0.3", "postcss": "^8.4.21", "prettier-plugin-tailwindcss": "^0.1.13", - "prisma": "5.18.0", + "prisma": "5.19.0", "tailwindcss": "^3.3.1", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 95b4c2a6..fdb50f13 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] + previewFeatures = ["driverAdapters", "typedSql"] } datasource db { diff --git a/apps/web/prisma/sql/getEventsByTestIdForAllTime.sql b/apps/web/prisma/sql/getEventsByTestIdForAllTime.sql new file mode 100644 index 00000000..37128aa8 --- /dev/null +++ b/apps/web/prisma/sql/getEventsByTestIdForAllTime.sql @@ -0,0 +1,23 @@ +-- @param {String} $1:testId +-- @param {Int} $2:type + +SELECT + COUNT(*) AS eventCount, + TYPE, + createdAt, + selectedVariant +FROM + `Event` +WHERE + testId = ? + AND TYPE = ? + AND(DATE(createdAt) BETWEEN DATE(( + SELECT + createdAt FROM `Event` + ORDER BY + createdAt ASC + LIMIT 1)) + AND DATE(NOW())) +GROUP BY + DATE(createdAt), + selectedVariant \ No newline at end of file diff --git a/apps/web/prisma/sql/getEventsByTestIdForDay.sql b/apps/web/prisma/sql/getEventsByTestIdForDay.sql new file mode 100644 index 00000000..70d7733b --- /dev/null +++ b/apps/web/prisma/sql/getEventsByTestIdForDay.sql @@ -0,0 +1,17 @@ +-- @param {String} $1:testId +-- @param {Int} $2:type + +SELECT + COUNT(*) AS eventCount, + TYPE, + createdAt, + selectedVariant +FROM + `Event` +WHERE + testId = ? + AND TYPE = ? + AND DATE(createdAt) = DATE(NOW()) +GROUP BY + HOUR(createdAt), + selectedVariant \ No newline at end of file diff --git a/apps/web/prisma/sql/getEventsByTestIdForLast30Days.sql b/apps/web/prisma/sql/getEventsByTestIdForLast30Days.sql new file mode 100644 index 00000000..9111a2e1 --- /dev/null +++ b/apps/web/prisma/sql/getEventsByTestIdForLast30Days.sql @@ -0,0 +1,16 @@ +-- @param {String} $1:testId +-- @param {Int} $2:type +SELECT + COUNT(*) AS eventCount, + TYPE, + createdAt, + selectedVariant +FROM + `Event` +WHERE + testId = ? + AND TYPE = ? + AND DATE(NOW()) - INTERVAL 1 MONTH +GROUP BY + DATE(createdAt), + selectedVariant \ No newline at end of file diff --git a/apps/web/src/lib/events.ts b/apps/web/src/lib/events.ts index ff5ab048..0abccb4f 100644 --- a/apps/web/src/lib/events.ts +++ b/apps/web/src/lib/events.ts @@ -3,6 +3,7 @@ import dayjs from "dayjs"; export enum TIME_INTERVAL { DAY = "day", ALL_TIME = "all", + LAST_30_DAYS = "30d", } export const INTERVALS = [ @@ -16,7 +17,7 @@ export const INTERVALS = [ // }, { label: "Last 30 days", - value: "30d", + value: TIME_INTERVAL.LAST_30_DAYS, }, // { // label: "Year to Date", @@ -42,25 +43,12 @@ export function isSpecialTimeInterval( return Object.values(TIME_INTERVAL).includes(timeInterval as TIME_INTERVAL); } -export function getMSFromSpecialTimeInterval( - timeInterval: TIME_INTERVAL -): number { - switch (timeInterval) { - case TIME_INTERVAL.DAY: { - return 1000 * 60 * 60 * 24; - } - case TIME_INTERVAL.ALL_TIME: { - return Number.POSITIVE_INFINITY; - } - } -} - export function getFormattingByInterval(interval: string) { switch (interval) { case TIME_INTERVAL.DAY: { return "HH:mm"; } - case "30d": { + case TIME_INTERVAL.LAST_30_DAYS: { return "DD MMM"; } case TIME_INTERVAL.ALL_TIME: { @@ -111,30 +99,3 @@ export function getBaseEventsByInterval( } } } - -export function getLabelsByInterval( - interval: (typeof INTERVALS)[number]["value"], - fistEventDate: Date -): Array { - const formatting = getFormattingByInterval(interval); - switch (interval) { - case TIME_INTERVAL.DAY: { - const baseData = dayjs().set("minute", 0); - return [0, 3, 6, 9, 12, 15, 18, 21].map((hour) => - baseData.set("hour", hour).format(formatting) - ); - } - case "30d": { - return Array.from({ length: 30 }, (_, i) => - dayjs().subtract(i, "day").format(formatting) - ).reverse(); - } - case TIME_INTERVAL.ALL_TIME: { - const diff = dayjs().diff(dayjs(fistEventDate), "month"); - - return Array.from({ length: Math.max(diff, 6) }, (_, i) => - dayjs(fistEventDate).add(i, "month").format(formatting) - ).reverse(); - } - } -} diff --git a/apps/web/src/server/services/EventService.ts b/apps/web/src/server/services/EventService.ts index 9df97cb1..6e5d1630 100644 --- a/apps/web/src/server/services/EventService.ts +++ b/apps/web/src/server/services/EventService.ts @@ -1,11 +1,10 @@ -import type { AbbyEvent } from "@tryabby/core"; -import dayjs from "dayjs"; import { - TIME_INTERVAL, - getMSFromSpecialTimeInterval, - isSpecialTimeInterval, -} from "lib/events"; -import ms from "ms"; + getEventsByTestIdForAllTime, + getEventsByTestIdForDay, + getEventsByTestIdForLast30Days, +} from "@prisma/client/sql"; +import type { AbbyEvent, AbbyEventType } from "@tryabby/core"; +import { TIME_INTERVAL, isSpecialTimeInterval } from "lib/events"; import { PLANS, type PlanName, getLimitByPlan } from "server/common/plans"; import { prisma } from "server/db/client"; import { RequestCache } from "./RequestCache"; @@ -43,50 +42,28 @@ export abstract class EventService { }); } - static async getEventsByTestId(testId: string, timeInterval: string) { - const now = new Date().getTime(); - + static async getEventsByTestId( + testId: string, + timeInterval: string, + eventType: AbbyEventType + // all function should have the same type + ): Promise { if (isSpecialTimeInterval(timeInterval)) { - const specialIntervalInMs = getMSFromSpecialTimeInterval(timeInterval); - return prisma.event.findMany({ - where: { - testId, - ...(specialIntervalInMs !== Number.POSITIVE_INFINITY && - timeInterval !== TIME_INTERVAL.DAY && { - createdAt: { - gte: new Date(now - getMSFromSpecialTimeInterval(timeInterval)), - }, - }), - // Special case for day, since we want to include the current day - ...(timeInterval === TIME_INTERVAL.DAY && { - createdAt: { - gte: dayjs().startOf("day").toDate(), - }, - }), - }, - orderBy: { - createdAt: "asc", - }, - }); - } - - const parsedInterval = ms(timeInterval) as number | undefined; - - if (parsedInterval === undefined) { - throw new Error("Invalid time interval"); + if (timeInterval === TIME_INTERVAL.DAY) { + return await prisma.$queryRawTyped( + getEventsByTestIdForDay(testId, eventType) + ); + } + if (timeInterval === TIME_INTERVAL.LAST_30_DAYS) { + return await prisma.$queryRawTyped( + getEventsByTestIdForLast30Days(testId, eventType) + ); + } } - return prisma.event.findMany({ - where: { - testId, - createdAt: { - gte: new Date(now - parsedInterval), - }, - }, - orderBy: { - createdAt: "asc", - }, - }); + return await prisma.$queryRawTyped( + getEventsByTestIdForAllTime(testId, eventType) + ); } static async getEventsForCurrentPeriod(projectId: string) { diff --git a/apps/web/src/server/trpc/router/events.ts b/apps/web/src/server/trpc/router/events.ts index bc1406b0..b7198eb0 100644 --- a/apps/web/src/server/trpc/router/events.ts +++ b/apps/web/src/server/trpc/router/events.ts @@ -51,43 +51,78 @@ export const eventRouter = router({ throw new TRPCError({ code: "UNAUTHORIZED" }); } - const events = await EventService.getEventsByTestId( - input.testId, - input.interval - ); + const [_actEvents, _pingEvents] = await Promise.all([ + EventService.getEventsByTestId( + input.testId, + input.interval, + AbbyEventType.ACT + ), + EventService.getEventsByTestId( + input.testId, + input.interval, + AbbyEventType.PING + ), + ]); const potentialVariants = currentTest.options.map((o) => o.identifier); - const eventsByDate = groupBy(events, (e) => { + const baseActEvents = + getBaseEventsByInterval( + input.interval, + potentialVariants, + _actEvents[0]?.createdAt ?? new Date() + ) ?? []; + + const basePingEvents = + getBaseEventsByInterval( + input.interval, + potentialVariants, + _actEvents[0]?.createdAt ?? new Date() + ) ?? []; + + const actEventsByDate = groupBy(_actEvents, (e) => { const date = dayjs(e.createdAt); if (input.interval === TIME_INTERVAL.DAY) { // round by 3 hours const hour = Math.floor(date.hour() / 3) * 3; - return date.set("hour", hour).set("minute", 0).toISOString(); + return date + .set("hour", hour) + .set("minute", 0) + .set("second", 0) + .set("millisecond", 0) + .toISOString(); } return date.startOf("day").toISOString(); }); - const baseEvents = - getBaseEventsByInterval( - input.interval, - potentialVariants, - events[0]?.createdAt ?? new Date() - ) ?? []; + const pingEventsByDate = groupBy(_pingEvents, (e) => { + const date = dayjs(e.createdAt); + if (input.interval === TIME_INTERVAL.DAY) { + // round by 3 hours + const hour = Math.floor(date.hour() / 3) * 3; - console.dir(baseEvents, { depth: null }); + return date + .set("hour", hour) + .set("minute", 0) + .set("second", 0) + .set("millisecond", 0) + .toISOString(); + } + return date.startOf("day").toISOString(); + }); const pingEvents = uniqBy( [ - ...Object.entries(eventsByDate).map(([date, events]) => { - const tests = groupBy( - events.filter((e) => e.type === AbbyEventType.PING), - (e) => e.selectedVariant - ); + ...Object.entries(pingEventsByDate).map(([date, events]) => { + const tests = groupBy(events, (e) => e.selectedVariant); + const testCount = Object.entries(tests).reduce( (acc, [variant, events]) => { - acc[variant] = events.length; + acc[variant] = events.reduce( + (acc, e) => acc + Number(e.eventCount), + 0 + ); return acc; }, {} as Record @@ -102,7 +137,7 @@ export const eventRouter = router({ [key: string]: number | string; }; }), - ...baseEvents, + ...basePingEvents, ], (e) => e.date ) as Array<{ @@ -112,14 +147,14 @@ export const eventRouter = router({ const actEvents = uniqBy( [ - ...Object.entries(eventsByDate).map(([date, events]) => { - const tests = groupBy( - events.filter((e) => e.type === AbbyEventType.ACT), - (e) => e.selectedVariant - ); + ...Object.entries(actEventsByDate).map(([date, events]) => { + const tests = groupBy(events, (e) => e.selectedVariant); const testCount = Object.entries(tests).reduce( (acc, [variant, events]) => { - acc[variant] = events.length; + acc[variant] = events.reduce( + (acc, e) => acc + Number(e.eventCount), + 0 + ); return acc; }, {} as Record @@ -129,9 +164,12 @@ export const eventRouter = router({ testCount[variant] = 0; } }); - return { date, ...testCount }; + return { date, ...testCount } as { + date: string; + [key: string]: number | string; + }; }), - ...baseEvents, + ...baseActEvents, ], (e) => e.date ) as Array<{ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d932d1bb..9bca48a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,13 +117,13 @@ importers: version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) '@next-auth/prisma-adapter': specifier: 1.0.5 - version: 1.0.5(@prisma/client@5.18.0)(next-auth@4.22.1) + version: 1.0.5(@prisma/client@5.19.0)(next-auth@4.22.1) '@next/mdx': specifier: 14.0.4 version: 14.0.4(@mdx-js/loader@3.0.0)(@mdx-js/react@3.0.0) '@prisma/client': - specifier: 5.18.0 - version: 5.18.0(prisma@5.18.0) + specifier: 5.19.0 + version: 5.19.0(prisma@5.19.0) '@radix-ui/react-avatar': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.0.5)(@types/react@18.0.14)(react-dom@18.2.0)(react@18.2.0) @@ -441,8 +441,8 @@ importers: specifier: ^0.1.13 version: 0.1.13(prettier@2.8.8) prisma: - specifier: 5.18.0 - version: 5.18.0 + specifier: 5.19.0 + version: 5.19.0 tailwindcss: specifier: ^3.3.1 version: 3.3.2(ts-node@10.9.1) @@ -7878,13 +7878,13 @@ packages: tar-fs: 2.1.1 dev: true - /@next-auth/prisma-adapter@1.0.5(@prisma/client@5.18.0)(next-auth@4.22.1): + /@next-auth/prisma-adapter@1.0.5(@prisma/client@5.19.0)(next-auth@4.22.1): resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==} peerDependencies: '@prisma/client': '>=2.26.0 || >=3' next-auth: ^4 dependencies: - '@prisma/client': 5.18.0(prisma@5.18.0) + '@prisma/client': 5.19.0(prisma@5.19.0) next-auth: 4.22.1(next@14.1.1)(nodemailer@6.9.3)(react-dom@18.2.0)(react@18.2.0) dev: false @@ -8465,8 +8465,8 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@prisma/client@5.18.0(prisma@5.18.0): - resolution: {integrity: sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==} + /@prisma/client@5.19.0(prisma@5.19.0): + resolution: {integrity: sha512-CzOpau+q1kEWQyoQMvlnXIHqPvwmWbh48xZ4n8KWbAql0p8PC0BIgSTYW5ncxXa4JSEff0tcoxSZB874wDstdg==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -8475,35 +8475,35 @@ packages: prisma: optional: true dependencies: - prisma: 5.18.0 + prisma: 5.19.0 dev: false - /@prisma/debug@5.18.0: - resolution: {integrity: sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==} + /@prisma/debug@5.19.0: + resolution: {integrity: sha512-+b/G0ubAZlrS+JSiDhXnYV5DF/aTJ3pinktkiV/L4TtLRLZO6SVGyFELgxBsicCTWJ2ZMu5vEV/jTtYCdjFTRA==} - /@prisma/engines-version@5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169: - resolution: {integrity: sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==} + /@prisma/engines-version@5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f: + resolution: {integrity: sha512-GimI9aZIFy/yvvR11KfXRn3pliFn1QAkdebVlsXlnoh5uk0YhLblVmeYiHfsu+wDA7BeKqYT4sFfzg8mutzuWw==} - /@prisma/engines@5.18.0: - resolution: {integrity: sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==} + /@prisma/engines@5.19.0: + resolution: {integrity: sha512-UtW+0m4HYoRSSR3LoDGKF3Ud4BSMWYlLEt4slTnuP1mI+vrV3zaDoiAPmejdAT76vCN5UqnWURbkXxf66nSylQ==} requiresBuild: true dependencies: - '@prisma/debug': 5.18.0 - '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169 - '@prisma/fetch-engine': 5.18.0 - '@prisma/get-platform': 5.18.0 + '@prisma/debug': 5.19.0 + '@prisma/engines-version': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f + '@prisma/fetch-engine': 5.19.0 + '@prisma/get-platform': 5.19.0 - /@prisma/fetch-engine@5.18.0: - resolution: {integrity: sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==} + /@prisma/fetch-engine@5.19.0: + resolution: {integrity: sha512-oOiPNtmJX0cP/ebu7BBEouJvCw8T84/MFD/Hf2zlqjxkK4ojl38bB9i9J5LAxotL6WlYVThKdxc7HqoWnPOhqQ==} dependencies: - '@prisma/debug': 5.18.0 - '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169 - '@prisma/get-platform': 5.18.0 + '@prisma/debug': 5.19.0 + '@prisma/engines-version': 5.19.0-31.5fe21811a6ba0b952a3bc71400666511fe3b902f + '@prisma/get-platform': 5.19.0 - /@prisma/get-platform@5.18.0: - resolution: {integrity: sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==} + /@prisma/get-platform@5.19.0: + resolution: {integrity: sha512-s9DWkZKnuP4Y8uy6yZfvqQ/9X3/+2KYf3IZUVZz5OstJdGBJrBlbmIuMl81917wp5TuK/1k2TpHNCEdpYLPKmg==} dependencies: - '@prisma/debug': 5.18.0 + '@prisma/debug': 5.19.0 /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -22365,13 +22365,15 @@ packages: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} dev: true - /prisma@5.18.0: - resolution: {integrity: sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==} + /prisma@5.19.0: + resolution: {integrity: sha512-Pu7lUKpVyTx8cVwM26dYh8NdvMOkMnJXzE8L6cikFuR4JwyMU5NKofQkWyxJKlTT4fNjmcnibTvklV8oVMrn+g==} engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 5.18.0 + '@prisma/engines': 5.19.0 + optionalDependencies: + fsevents: 2.3.3 /proc-log@4.2.0: resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==}