#!/usr/bin/env node // scripts/list-civi-entities.mjs // // Lists what entities APIv4 exposes on this Civi instance, with a focus // on file/attachment-shaped ones. Run this when Attachment.create comes // back "API does not exist", to figure out what the real upload path is. // // USAGE // node --env-file=.env.local scripts/list-civi-entities.mjs import { Buffer } from "node:buffer"; async function civi(entity, action, params) { const { CIVI_BASE_URL, CIVI_API_KEY, CIVI_SITE_KEY, CIVI_HTTP_AUTH_USER, CIVI_HTTP_AUTH_PASS, } = process.env; if (!CIVI_BASE_URL || !CIVI_API_KEY || !CIVI_SITE_KEY) { throw new Error("Missing CIVI_BASE_URL / CIVI_API_KEY / CIVI_SITE_KEY."); } const url = `${CIVI_BASE_URL}/civicrm/ajax/api4/${entity}/${action}`; const headers = { "Content-Type": "application/x-www-form-urlencoded", "X-Civi-Auth": `Bearer ${CIVI_API_KEY}`, "X-Civi-Key": CIVI_SITE_KEY, }; if (CIVI_HTTP_AUTH_USER && CIVI_HTTP_AUTH_PASS) { headers["Authorization"] = "Basic " + Buffer.from(`${CIVI_HTTP_AUTH_USER}:${CIVI_HTTP_AUTH_PASS}`).toString("base64"); } const res = await fetch(url, { method: "POST", headers, body: new URLSearchParams({ params: JSON.stringify(params) }), }); const text = await res.text(); let json; try { json = JSON.parse(text); } catch { throw new Error(`${entity}.${action} non-JSON (HTTP ${res.status}): ${text}`); } if (!res.ok || json.error_message) { throw new Error(`${entity}.${action} HTTP ${res.status}: ${json.error_message ?? text}`); } return json; } console.log("Probing APIv4 entities…\n"); // All registered entities. const all = await civi("Entity", "get", { select: ["name"], orderBy: { name: "ASC" } }); const names = (all.values ?? []).map((r) => r.name); console.log(`Total APIv4 entities: ${names.length}\n`); const fileShaped = names.filter((n) => /attach|file|document|upload/i.test(n), ); console.log("File/attachment-shaped entities present:"); for (const n of fileShaped) console.log(` - ${n}`); if (fileShaped.length === 0) console.log(" (none)"); console.log("\nFor each, list available actions:"); for (const ent of fileShaped) { try { const a = await civi(ent, "getActions", { select: ["name"] }); const actions = (a.values ?? []).map((r) => r.name).join(", "); console.log(`\n ${ent}: ${actions}`); } catch (err) { console.log(`\n ${ent}: `); } } // Also probe: does the legacy APIv3 Attachment.create exist? APIv4 // extension surface is different from APIv3, and the form may need to // fall back to v3 for files. Round-trip a getfields call as a probe. console.log("\nAPIv3 probe (extern/rest.php):"); try { const { CIVI_BASE_URL, CIVI_API_KEY, CIVI_SITE_KEY, CIVI_HTTP_AUTH_USER, CIVI_HTTP_AUTH_PASS, } = process.env; const headers = { "Content-Type": "application/x-www-form-urlencoded", }; if (CIVI_HTTP_AUTH_USER && CIVI_HTTP_AUTH_PASS) { headers["Authorization"] = "Basic " + Buffer.from(`${CIVI_HTTP_AUTH_USER}:${CIVI_HTTP_AUTH_PASS}`).toString("base64"); } const url = `${CIVI_BASE_URL}/civicrm/ajax/rest`; const body = new URLSearchParams({ entity: "Attachment", action: "getfields", api_key: CIVI_API_KEY, key: CIVI_SITE_KEY, json: "1", }); const res = await fetch(url, { method: "POST", headers, body }); const text = await res.text(); let j; try { j = JSON.parse(text); } catch { j = null; } if (j && !j.is_error) { const fields = j.values ? Object.keys(j.values) : []; console.log(` APIv3 Attachment.getfields OK. Fields: ${fields.join(", ")}`); } else { console.log(` APIv3 Attachment.getfields response:`, text.slice(0, 400)); } } catch (err) { console.log(` APIv3 probe failed: ${err.message}`); }