feat(data): board and project summary loaders scoped by read rules
This commit is contained in:
parent
5d3323e765
commit
153484390f
1 changed files with 133 additions and 0 deletions
133
src/lib/data/board.ts
Normal file
133
src/lib/data/board.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import "server-only";
|
||||
|
||||
import { getServerPb } from "@/lib/pb/server";
|
||||
import type {
|
||||
CardRecord,
|
||||
ClientRecord,
|
||||
ColumnRecord,
|
||||
ProjectRecord,
|
||||
} from "@/lib/pb/types";
|
||||
|
||||
export interface BoardData {
|
||||
client: ClientRecord;
|
||||
project: ProjectRecord;
|
||||
columns: ColumnRecord[];
|
||||
cards: CardRecord[];
|
||||
}
|
||||
|
||||
export interface ProjectSummary {
|
||||
project: ProjectRecord;
|
||||
client: ClientRecord;
|
||||
openCards: number;
|
||||
inboxCards: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything one board needs, loaded with the signed-in user's own PocketBase
|
||||
* token.
|
||||
*
|
||||
* Using the user's token rather than the superuser's is the point: the read
|
||||
* rules from Plan 1a do the scoping, so a client manager requesting another
|
||||
* client's board gets nothing back from the database itself. The UI is not
|
||||
* what keeps them out.
|
||||
*
|
||||
* Returns null when the project does not exist OR the user may not see it.
|
||||
* Callers render a 404 for both — distinguishing them would confirm that
|
||||
* another client's project is real.
|
||||
*/
|
||||
export async function loadBoard(
|
||||
clientSlug: string,
|
||||
projectSlug: string
|
||||
): Promise<BoardData | null> {
|
||||
const pb = await getServerPb();
|
||||
if (!pb.authStore.isValid) return null;
|
||||
|
||||
try {
|
||||
const client = await pb
|
||||
.collection("clients")
|
||||
.getFirstListItem<ClientRecord>(`slug = "${clientSlug}"`);
|
||||
|
||||
const project = await pb
|
||||
.collection("projects")
|
||||
.getFirstListItem<ProjectRecord>(
|
||||
`slug = "${projectSlug}" && client = "${client.id}"`
|
||||
);
|
||||
|
||||
const columns = await pb
|
||||
.collection("columns")
|
||||
.getFullList<ColumnRecord>({
|
||||
filter: `project = "${project.id}"`,
|
||||
sort: "position",
|
||||
});
|
||||
|
||||
const cards = await pb.collection("cards").getFullList<CardRecord>({
|
||||
filter: `project = "${project.id}" && archived = false`,
|
||||
sort: "position",
|
||||
});
|
||||
|
||||
return { client, project, columns, cards };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every project the signed-in user may see, with counts for the home page. */
|
||||
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 clientsById = new Map(clients.map((c) => [c.id, c]));
|
||||
|
||||
const summaries: ProjectSummary[] = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const client = clientsById.get(project.client);
|
||||
if (!client) continue;
|
||||
|
||||
const inbox = await pb
|
||||
.collection("columns")
|
||||
.getFirstListItem<ColumnRecord>(
|
||||
`project = "${project.id}" && is_inbox = true`
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
Loading…
Reference in a new issue