feat(me): show actionable personal tasks with unavailable states
CI / check (push) Failing after 53s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

This commit is contained in:
Simo committed 2026-09-11 00:15:46 +02:00
1 parent d9ea93a2e7
commit a537c55793
9 files changed
+435 -12

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
fail: false,
queries: [] as { query: string; params: unknown[] }[],
}));
vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { drizzle } = await import("drizzle-orm/mysql-proxy");
return {
...schema,
db: drizzle(async (query, params) => {
state.queries.push({ query, params });
if (state.fail) throw new Error("offline");
return { rows: query.includes("count(*)") ? [[0]] : [] };
}),
};
});
import { loadDashboardTasks } from "./dashboard-tasks";
describe("personal dashboard tasks", () => {
beforeEach(() => {
state.fail = false;
state.queries = [];
});
it("rejects invalid user ids before accessing the database", async () => {
await expect(loadDashboardTasks(0)).rejects.toThrow("Invalid user");
expect(state.queries).toHaveLength(0);
});
it("scopes every query to the signed-in user and only offers future published registrations", async () => {
expect(
await loadDashboardTasks(42, new Date("2026-09-11T12:00:00Z")),
).toEqual({ support: 0, friendRequests: 0, registeredEvent: false });
expect(state.queries).toHaveLength(3);
for (const { params } of state.queries) expect(params).toContain(42);
const support = state.queries.find(({ query }) =>
query.includes("website_help_center_tickets"),
);
expect(support?.query).toContain(
"`website_help_center_tickets`.`open` = ?",
);
expect(support?.query).toContain(
"order by `website_help_center_ticket_replies`.`id` desc limit 1) <> ?",
);
const event = state.queries.find(({ query }) =>
query.includes("website_events"),
);
expect(event?.params).toEqual([
42,
"published",
"2026-09-11 12:00:00.000",
1,
]);
});
it("does not report empty tasks when the database is unavailable", async () => {
state.fail = true;
expect(await loadDashboardTasks(42)).toEqual({
support: null,
friendRequests: null,
registeredEvent: null,
});
});
});
+61
View File
@@ -0,0 +1,61 @@
import "server-only";
import { and, asc, count, eq, gt, sql } from "drizzle-orm";
import {
db,
MessengerFriendrequests,
WebsiteEvent,
WebsiteEventRegistration,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
/** Caller supplies the authenticated session user, never a request parameter. */
export async function loadDashboardTasks(userId: number, now = new Date()) {
if (!Number.isSafeInteger(userId) || userId <= 0)
throw new Error("Invalid user");
const [supportRows, friendRows, eventRows] = await Promise.all([
db
.select({ value: count() })
.from(WebsiteHelpCenterTickets)
.where(
and(
eq(WebsiteHelpCenterTickets.userId, userId),
eq(WebsiteHelpCenterTickets.open, true),
sql`(select ${WebsiteHelpCenterTicketReplies.userId} from ${WebsiteHelpCenterTicketReplies} where ${WebsiteHelpCenterTicketReplies.ticketId} = ${WebsiteHelpCenterTickets.id} order by ${WebsiteHelpCenterTicketReplies.id} desc limit 1) <> ${userId}`,
),
)
.catch(() => null),
db
.select({ value: count() })
.from(MessengerFriendrequests)
.where(eq(MessengerFriendrequests.userToId, userId))
.catch(() => null),
db
.select({
id: WebsiteEvent.id,
title: WebsiteEvent.title,
startsAt: WebsiteEvent.startsAt,
})
.from(WebsiteEventRegistration)
.innerJoin(
WebsiteEvent,
eq(WebsiteEvent.id, WebsiteEventRegistration.eventId),
)
.where(
and(
eq(WebsiteEventRegistration.userId, userId),
eq(WebsiteEvent.status, "published"),
gt(WebsiteEvent.startsAt, now),
),
)
.orderBy(asc(WebsiteEvent.startsAt), asc(WebsiteEvent.id))
.limit(1)
.catch(() => null),
]);
return {
support: supportRows === null ? null : Number(supportRows[0]?.value ?? 0),
friendRequests:
friendRows === null ? null : Number(friendRows[0]?.value ?? 0),
registeredEvent: eventRows === null ? null : (eventRows[0] ?? false),
};
}