/* ═══════════════════════════════════════════════════════════════ SHAPE Blocks and links are polymorphic: any organization, event, person or team can own them. Every list endpoint therefore needs the same move — fetch the parent rows, then fetch all their children in one query each and stitch. The alternative is a query per row, which at a dozen events is invisible and at two hundred is not. Three queries is three queries whatever the row count, so it may as well be right now. One limit worth knowing: SQLite caps bound parameters per statement (999 on older builds). If a list ever exceeds that, these need chunking. Nothing here comes close. ═══════════════════════════════════════════════════════════════ */ const placeholders = (n) => Array(n).fill("?").join(","); const asBool = (value) => value === 1; /* ── Links ───────────────────────────────────────────────────── Map of owner_id → links, in sort order. ───────────────────────────────────────────────────────────── */ export function loadLinks(db, ownerKind, ids) { const out = new Map(); if (ids.length === 0) return out; const rows = db .prepare( `SELECT owner_id, kind, platform, label, url, is_primary FROM links WHERE owner_kind = ? AND owner_id IN (${placeholders(ids.length)}) ORDER BY owner_id, sort_order`, ) .all(ownerKind, ...ids); for (const row of rows) { const link = { kind: row.kind, platform: row.platform, label: row.label, url: row.url, is_primary: asBool(row.is_primary), }; const list = out.get(row.owner_id); if (list) list.push(link); else out.set(row.owner_id, [link]); } return out; } /* ── Blocks ──────────────────────────────────────────────────── Map of owner_id → blocks for one slot, items attached. Two queries: the blocks, then every item belonging to them. Blocks with no children come back with an empty items array rather than no key, so the renderer never has to check. ───────────────────────────────────────────────────────────── */ export function loadBlocks(db, ownerKind, ids, slot = "body") { const out = new Map(); if (ids.length === 0) return out; const blockRows = db .prepare( `SELECT id, owner_id, type, text, media, href FROM content_blocks WHERE owner_kind = ? AND slot = ? AND owner_id IN (${placeholders(ids.length)}) ORDER BY owner_id, sort_order`, ) .all(ownerKind, slot, ...ids); if (blockRows.length === 0) return out; const byId = new Map(); for (const row of blockRows) { const block = { type: row.type, text: row.text, media: row.media, href: row.href, items: [], }; byId.set(row.id, block); const list = out.get(row.owner_id); if (list) list.push(block); else out.set(row.owner_id, [block]); } const blockIds = [...byId.keys()]; const itemRows = db .prepare( `SELECT block_id, text, detail, url FROM content_block_items WHERE block_id IN (${placeholders(blockIds.length)}) ORDER BY block_id, sort_order`, ) .all(...blockIds); for (const row of itemRows) { byId.get(row.block_id)?.items.push({ text: row.text, detail: row.detail, url: row.url, }); } return out; } /* ── Card description ────────────────────────────────────────── The card slot is paragraphs by convention, so it collapses to an array of strings — desc_a and desc_b become description[0] and description[1]. A non-paragraph block in the card slot is ignored here; put it in the body slot instead. ───────────────────────────────────────────────────────────── */ export function paragraphs(blocks = []) { return blocks .filter((block) => block.type === "paragraph" && block.text) .map((block) => block.text); } /* ── Split entity links ──────────────────────────────────────── Socials are lifted out of the list because cards treat them differently: Instagram is an icon, Register is a button. The underlying rows are the same table. ───────────────────────────────────────────────────────────── */ export function splitLinks(links = []) { return { actions: links.filter((link) => link.kind === "action"), socials: links.filter((link) => link.kind === "social"), website: links.find((link) => link.kind === "website")?.url ?? null, email: links.find((link) => link.kind === "email")?.label ?? null, instagram: links.find((link) => link.platform === "instagram")?.label ?? null, }; } export { asBool };