v1.3 - added an sqlite db and built data structure
This commit is contained in:
parent
b0fba52c0e
commit
5efdafbb97
37 changed files with 6414 additions and 1988 deletions
147
server/src/shape.js
Normal file
147
server/src/shape.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
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 };
|
||||
Loading…
Add table
Add a link
Reference in a new issue