-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add stats api #1085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add stats api #1085
Changes from all commits
0702085
7e55b5b
62d0567
3134bec
67ef6eb
3d7bf62
a7c1919
fa3feb2
022d4a3
89a6bd3
cff16b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { format } from "date-fns/format"; | ||
| import sumBy from "lodash/sumBy"; | ||
| import prisma from "@/utils/prisma"; | ||
| import { Prisma } from "@/generated/prisma/client"; | ||
| import type { StatsByPeriodQuery } from "@/app/api/user/stats/by-period/validation"; | ||
|
|
||
| export type StatsByPeriodResponse = Awaited< | ||
| ReturnType<typeof getStatsByPeriod> | ||
| >; | ||
|
|
||
| async function getEmailStatsByPeriod( | ||
| options: StatsByPeriodQuery & { emailAccountId: string }, | ||
| ) { | ||
| const { period, fromDate, toDate, emailAccountId } = options; | ||
|
|
||
| // Build date conditions without starting with AND | ||
| const dateConditions: Prisma.Sql[] = []; | ||
| if (fromDate) { | ||
| dateConditions.push(Prisma.sql`date >= ${new Date(fromDate)}`); | ||
| } | ||
| if (toDate) { | ||
| dateConditions.push(Prisma.sql`date <= ${new Date(toDate)}`); | ||
| } | ||
|
|
||
| // Using raw query with properly typed parameters | ||
| type StatsResult = { | ||
| startOfPeriod: Date; | ||
| totalCount: bigint; | ||
| inboxCount: bigint; | ||
| readCount: bigint; | ||
| sentCount: bigint; | ||
| unread: bigint; | ||
| notInbox: bigint; | ||
| }; | ||
|
|
||
| // Create WHERE clause properly | ||
| const whereClause = Prisma.sql`WHERE "emailAccountId" = ${emailAccountId}`; | ||
| const dateClause = | ||
| dateConditions.length > 0 | ||
| ? Prisma.sql` AND ${Prisma.join(dateConditions, " AND ")}` | ||
| : Prisma.sql``; | ||
|
|
||
| // Convert period and dateFormat to string literals in PostgreSQL | ||
| return prisma.$queryRaw<StatsResult[]>` | ||
| SELECT | ||
| DATE_TRUNC(${Prisma.raw(`'${period}'`)}, date) AS "startOfPeriod", | ||
| COUNT(*) AS "totalCount", | ||
| SUM(CASE WHEN inbox = true THEN 1 ELSE 0 END) AS "inboxCount", | ||
| SUM(CASE WHEN inbox = false THEN 1 ELSE 0 END) AS "notInbox", | ||
| SUM(CASE WHEN read = true THEN 1 ELSE 0 END) AS "readCount", | ||
| SUM(CASE WHEN read = false THEN 1 ELSE 0 END) AS unread, | ||
| SUM(CASE WHEN sent = true THEN 1 ELSE 0 END) AS "sentCount" | ||
| FROM "EmailMessage" | ||
| ${whereClause}${dateClause} | ||
| GROUP BY "startOfPeriod" | ||
| ORDER BY "startOfPeriod" | ||
| `; | ||
| } | ||
|
|
||
| export async function getStatsByPeriod( | ||
| options: StatsByPeriodQuery & { | ||
| emailAccountId: string; | ||
| }, | ||
| ) { | ||
| // Get all stats in a single query | ||
| const stats = await getEmailStatsByPeriod(options); | ||
|
|
||
| // Transform stats to match the expected format | ||
| const formattedStats = stats.map((stat) => { | ||
| const startOfPeriodFormatted = format(stat.startOfPeriod, "LLL dd, y"); | ||
|
|
||
| return { | ||
| startOfPeriod: startOfPeriodFormatted, | ||
| All: Number(stat.totalCount), | ||
| Sent: Number(stat.sentCount), | ||
| Read: Number(stat.readCount), | ||
| Unread: Number(stat.unread), | ||
| Unarchived: Number(stat.inboxCount), | ||
| Archived: Number(stat.notInbox), | ||
| }; | ||
| }); | ||
|
|
||
| // Calculate totals | ||
| const totalAll = sumBy(stats, (stat) => Number(stat.totalCount)); | ||
| const totalInbox = sumBy(stats, (stat) => Number(stat.inboxCount)); | ||
| const totalRead = sumBy(stats, (stat) => Number(stat.readCount)); | ||
| const totalSent = sumBy(stats, (stat) => Number(stat.sentCount)); | ||
|
|
||
| return { | ||
| result: formattedStats, | ||
| allCount: totalAll, | ||
| inboxCount: totalInbox, | ||
| readCount: totalRead, | ||
| sentCount: totalSent, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,9 @@ | ||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||
| import { zodPeriod } from "@inboxzero/tinybird"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export const statsByPeriodQuerySchema = z.object({ | ||||||||||||||||||||||
| period: zodPeriod, | ||||||||||||||||||||||
| fromDate: z.coerce.number().nullish(), | ||||||||||||||||||||||
| toDate: z.coerce.number().nullish(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+4
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add validation to reject NaN and ensure valid timestamps.
Apply this diff: export const statsByPeriodQuerySchema = z.object({
period: zodPeriod,
- fromDate: z.coerce.number().nullish(),
- toDate: z.coerce.number().nullish(),
+ fromDate: z.coerce.number().finite().nullish(),
+ toDate: z.coerce.number().finite().nullish(),
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| export type StatsByPeriodQuery = z.infer<typeof statsByPeriodQuerySchema>; | ||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.