perf(board): batch signed-in project summary reads
This commit is contained in:
parent
9778ee09d4
commit
ab81f32cdf
2 changed files with 92 additions and 44 deletions
60
src/lib/data/board.test.ts
Normal file
60
src/lib/data/board.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { loadProjectSummaries } from "@/lib/data/board";
|
||||
import { getServerPb } from "@/lib/pb/server";
|
||||
|
||||
vi.mock("@/lib/pb/server", () => ({ getServerPb: vi.fn() }));
|
||||
|
||||
describe("home project summaries", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("counts open and inbox cards across projects with one read per collection", async () => {
|
||||
const projects = [
|
||||
{ id: "p1", client: "c1", name: "Alpha" },
|
||||
{ id: "p2", client: "c1", name: "Beta" },
|
||||
];
|
||||
const clients = [{ id: "c1", name: "Client" }];
|
||||
const columns = [
|
||||
{ id: "i1", project: "p1", is_inbox: true, is_done: false },
|
||||
{ id: "d1", project: "p1", is_inbox: false, is_done: true },
|
||||
{ id: "i2", project: "p2", is_inbox: true, is_done: false },
|
||||
];
|
||||
const cards = [
|
||||
{ project: "p1", column: "i1" },
|
||||
{ project: "p1", column: "d1" },
|
||||
{ project: "p2", column: "i2" },
|
||||
{ project: "p2", column: "i2" },
|
||||
];
|
||||
const getFullList = vi.fn(async (name: string) => ({ projects, clients, columns, cards })[name as "projects" | "clients" | "columns" | "cards"]);
|
||||
const getFirstListItem = vi.fn(async (filter: string) => {
|
||||
const projectId = filter.match(/project = "([^"]+)"/)?.[1];
|
||||
const column = columns.find((item) => item.project === projectId && (filter.includes("is_inbox") ? item.is_inbox : item.is_done));
|
||||
if (!column) throw new Error("No matching column");
|
||||
return column;
|
||||
});
|
||||
const getList = vi.fn(async (_page: number, _perPage: number, options: { filter: string }) => ({
|
||||
totalItems: options.filter.includes('column = "i1"') ? 1
|
||||
: options.filter.includes('column = "i2"') ? 2
|
||||
: options.filter.includes('project = "p1"') ? 1 : 2,
|
||||
}));
|
||||
const pb = {
|
||||
authStore: { isValid: true },
|
||||
collection: vi.fn((name: string) => ({
|
||||
getFullList: () => getFullList(name),
|
||||
getFirstListItem,
|
||||
getList,
|
||||
})),
|
||||
};
|
||||
vi.mocked(getServerPb).mockResolvedValue(pb as never);
|
||||
|
||||
const summaries = await loadProjectSummaries();
|
||||
|
||||
expect(summaries.map((summary) => ({ id: summary.project.id, open: summary.openCards, inbox: summary.inboxCards }))).toEqual([
|
||||
{ id: "p1", open: 1, inbox: 1 },
|
||||
{ id: "p2", open: 2, inbox: 2 },
|
||||
]);
|
||||
expect(getFullList).toHaveBeenCalledTimes(4);
|
||||
expect(getFirstListItem).not.toHaveBeenCalled();
|
||||
expect(getList).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -76,57 +76,45 @@ export async function loadProjectSummaries(): Promise<ProjectSummary[]> {
|
|||
const pb = await getServerPb();
|
||||
if (!pb.authStore.isValid) return [];
|
||||
|
||||
const projects = await pb.collection("projects").getFullList<ProjectRecord>({
|
||||
filter: 'status != "archived"',
|
||||
sort: "name",
|
||||
});
|
||||
|
||||
if (projects.length === 0) return [];
|
||||
|
||||
const clients = await pb.collection("clients").getFullList<ClientRecord>();
|
||||
const [projects, clients, columns, cards] = await Promise.all([
|
||||
pb.collection("projects").getFullList<ProjectRecord>({
|
||||
filter: 'status != "archived"',
|
||||
sort: "name",
|
||||
}),
|
||||
pb.collection("clients").getFullList<ClientRecord>(),
|
||||
pb.collection("columns").getFullList<Pick<ColumnRecord, "id" | "project" | "is_inbox" | "is_done">>({
|
||||
fields: "id,project,is_inbox,is_done",
|
||||
}),
|
||||
pb.collection("cards").getFullList<Pick<CardRecord, "project" | "column">>({
|
||||
filter: "archived = false",
|
||||
fields: "project,column",
|
||||
}),
|
||||
]);
|
||||
const clientsById = new Map(clients.map((c) => [c.id, c]));
|
||||
|
||||
const summaries: ProjectSummary[] = [];
|
||||
const summariesByProject = new Map<string, ProjectSummary>();
|
||||
|
||||
for (const project of projects) {
|
||||
const client = clientsById.get(project.client);
|
||||
if (!client) continue;
|
||||
const summary = { project, client, openCards: 0, inboxCards: 0 };
|
||||
summaries.push(summary);
|
||||
summariesByProject.set(project.id, summary);
|
||||
}
|
||||
|
||||
const inbox = await pb
|
||||
.collection("columns")
|
||||
.getFirstListItem<ColumnRecord>(
|
||||
`project = "${project.id}" && is_inbox = true`
|
||||
)
|
||||
.catch(() => null);
|
||||
const doneColumns = new Set<string>();
|
||||
const inboxColumns = new Set<string>();
|
||||
for (const column of columns) {
|
||||
if (!summariesByProject.has(column.project)) continue;
|
||||
if (column.is_done) doneColumns.add(column.id);
|
||||
if (column.is_inbox) inboxColumns.add(column.id);
|
||||
}
|
||||
|
||||
const done = await pb
|
||||
.collection("columns")
|
||||
.getFirstListItem<ColumnRecord>(
|
||||
`project = "${project.id}" && is_done = true`
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
const openFilter = done
|
||||
? `project = "${project.id}" && archived = false && column != "${done.id}"`
|
||||
: `project = "${project.id}" && archived = false`;
|
||||
|
||||
const open = await pb
|
||||
.collection("cards")
|
||||
.getList(1, 1, { filter: openFilter, fields: "id" });
|
||||
|
||||
const inboxCount = inbox
|
||||
? await pb.collection("cards").getList(1, 1, {
|
||||
filter: `column = "${inbox.id}" && archived = false`,
|
||||
fields: "id",
|
||||
})
|
||||
: null;
|
||||
|
||||
summaries.push({
|
||||
project,
|
||||
client,
|
||||
openCards: open.totalItems,
|
||||
inboxCards: inboxCount?.totalItems ?? 0,
|
||||
});
|
||||
for (const card of cards) {
|
||||
const summary = summariesByProject.get(card.project);
|
||||
if (!summary) continue;
|
||||
if (!doneColumns.has(card.column)) summary.openCards += 1;
|
||||
if (inboxColumns.has(card.column)) summary.inboxCards += 1;
|
||||
}
|
||||
|
||||
return summaries;
|
||||
|
|
@ -183,4 +171,4 @@ export async function loadSidebarProjects(): Promise<SidebarProjectGroup[]> {
|
|||
return [...groups.values()].sort((a, b) =>
|
||||
a.client.name.localeCompare(b.client.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue