147 lines
5.7 KiB
JavaScript
147 lines
5.7 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
|
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 };
|