Staff report: query Attachment.get per file id (no IN clause)

The {IN: [...]} operator object for `id` crashes Civi APIv3 on this
install — Civi's error renderer calls htmlentities() on the array
value and dies, producing the 500 with the html error page seen at
6c3e8dd's deploy.

Single-id calls succeed (confirmed in the user's API Explorer test
against id=150). So loop one call per file id and run them in
parallel via Promise.allSettled. Reports typically reference a
handful of files, so the round-trip overhead is small and individual
file failures don't poison the whole report.
This commit is contained in:
Joel Brock
2026-06-10 11:34:54 -07:00
parent 6c3e8dd6a0
commit e22c9226d8
+22 -24
View File
@@ -374,35 +374,33 @@ async function buildLivePayload(orgId: number): Promise<StaffReportPayload> {
const urlByFileId = new Map<number, string>(); const urlByFileId = new Map<number, string>();
if (fileIds.size > 0) { if (fileIds.size > 0) {
try { // APIv3 Attachment.get doesn't accept an IN-clause cleanly on this Civi
const attachRes = await civi3<{ id: string | number; url?: string }>( // install — passing {IN: [...]} for `id` crashes Civi's error renderer
"Attachment", // on htmlentities(array). The same call with `id: <single>` works
"get", // (verified in API Explorer), so we loop one call per file id. Reports
{ // typically reference a handful of files, so the round-trip cost is
// APIv3 takes id as an IN-clause via the {IN: [...]} operator object. // small. Each request is independent; we issue them in parallel.
id: { IN: Array.from(fileIds) }, const lookups = await Promise.allSettled(
// APIv3 expects `return` as a comma-separated string. Passing an Array.from(fileIds).map((fid) =>
// array crashes Civi's error pathway on htmlentities() — that's civi3<{ id: string | number; url?: string }>("Attachment", "get", {
// v4 syntax. id: fid,
return: "id,url", return: "id,url",
// sequential: 1 forces values to be returned as a plain array
// rather than an object keyed by id. The civi3 helper normalizes
// either, but the array form is the canonical APIv3 client shape.
sequential: 1, sequential: 1,
options: { limit: 0 }, }).then((r) => ({ fid, row: r.values?.[0] })),
}, ),
); );
for (const a of attachRes.values ?? []) { for (const result of lookups) {
const fid = Number(a.id); if (result.status === "rejected") {
if (Number.isFinite(fid) && typeof a.url === "string" && a.url.length > 0) {
urlByFileId.set(fid, a.url);
}
}
} catch (e) {
console.warn( console.warn(
"[staff/report] Attachment.get (v3) failed; file links will lack fcs:", "[staff/report] Attachment.get (v3) failed for one file:",
e instanceof Error ? e.message : String(e), result.reason instanceof Error ? result.reason.message : String(result.reason),
); );
continue;
}
const { fid, row } = result.value;
if (row && typeof row.url === "string" && row.url.length > 0) {
urlByFileId.set(fid, row.url);
}
} }
} }