141 lines
4.6 KiB
JavaScript
141 lines
4.6 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
|
PEOPLE ROUTES — read-only, mounted under /api
|
|
|
|
GET /teams/:id/people current public members of a team
|
|
GET /people?ids=a,b,c named people, any order
|
|
|
|
The team route reads v_org_leadership, which already decides who
|
|
counts as current and public — affiliation still open, marked
|
|
public, person published. Restating those conditions here is how
|
|
they drift apart.
|
|
|
|
That view is affiliation-driven, so it can't serve the lookup
|
|
route: a person with no affiliations would vanish, and one with
|
|
several would appear more than once. The lookup reads people
|
|
directly and returns no title, because a bare slug names no seat.
|
|
|
|
Photos are filenames, as everywhere else in this API. The
|
|
component decides where they live.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
import { Hono } from "hono";
|
|
|
|
import { asBool } from "../shape.js";
|
|
|
|
const people = new Hono();
|
|
|
|
const CACHE = "public, max-age=60, stale-while-revalidate=300";
|
|
|
|
const json = (c, body) => c.json(body, 200, { "Cache-Control": CACHE });
|
|
|
|
const marks = (n) => Array(n).fill("?").join(",");
|
|
|
|
/* How many slugs one request may name. Keeps the URL sane. */
|
|
const MAX_IDS = 50;
|
|
|
|
function shapePerson(row) {
|
|
return {
|
|
id: row.id ?? row.person_id,
|
|
name: row.display_name,
|
|
// Only the team route knows a seat; the lookup route doesn't.
|
|
title: row.title ?? null,
|
|
tagline: row.tagline,
|
|
pronouns: row.pronouns,
|
|
photo: row.photo,
|
|
location_label: row.location_label,
|
|
public_email: row.public_email,
|
|
org: row.primary_org_id
|
|
? { id: row.primary_org_id, name: row.primary_org_name }
|
|
: null,
|
|
bio: splitParagraphs(row.bio),
|
|
role: row.role ?? null,
|
|
is_owner: row.is_owner === undefined ? false : asBool(row.is_owner),
|
|
};
|
|
}
|
|
|
|
/* people.bio is one run of prose with blank lines between
|
|
paragraphs — unrelated to shape.js's paragraphs(), which turns
|
|
content_block rows into text. */
|
|
function splitParagraphs(text) {
|
|
if (!text) return null;
|
|
const parts = text
|
|
.split(/\n\s*\n/)
|
|
.map((part) => part.trim())
|
|
.filter(Boolean);
|
|
return parts.length ? parts : null;
|
|
}
|
|
|
|
/* ── One team's people ─────────────────────────────────────── */
|
|
|
|
people.get("/teams/:id/people", (c) => {
|
|
const db = c.get("db");
|
|
const id = c.req.param("id");
|
|
|
|
const team = db
|
|
.prepare(
|
|
`SELECT id, org_id, name, tagline, color, logo
|
|
FROM teams
|
|
WHERE id = ? AND is_published = 1`,
|
|
)
|
|
.get(id);
|
|
|
|
if (!team) return c.json({ error: "No such team" }, 404);
|
|
|
|
// The view orders by org first, which isn't what a single team
|
|
// wants, so the order is restated here.
|
|
const rows = db
|
|
.prepare(
|
|
`SELECT * FROM v_org_leadership
|
|
WHERE team_id = ?
|
|
ORDER BY is_owner DESC,
|
|
sort_order,
|
|
COALESCE(sort_name, display_name),
|
|
display_name`,
|
|
)
|
|
.all(id);
|
|
|
|
return json(c, { team, people: rows.map(shapePerson) });
|
|
});
|
|
|
|
/* ── People by id ──────────────────────────────────────────────
|
|
For hand-picked lists in a section. Order is the caller's, so
|
|
there's no ORDER BY here.
|
|
───────────────────────────────────────────────────────────── */
|
|
|
|
people.get("/people", (c) => {
|
|
const db = c.get("db");
|
|
|
|
const ids = [
|
|
...new Set(
|
|
(c.req.query("ids") ?? "")
|
|
.split(",")
|
|
.map((part) => part.trim())
|
|
.filter(Boolean),
|
|
),
|
|
].slice(0, MAX_IDS);
|
|
|
|
if (ids.length === 0) return json(c, { people: [] });
|
|
|
|
const rows = db
|
|
.prepare(
|
|
`SELECT p.id,
|
|
p.display_name,
|
|
p.pronouns,
|
|
p.photo,
|
|
p.tagline,
|
|
p.location_label,
|
|
p.public_email,
|
|
p.bio,
|
|
o.id AS primary_org_id,
|
|
o.name AS primary_org_name
|
|
FROM people p
|
|
LEFT JOIN organizations o ON o.id = p.primary_org_id
|
|
WHERE p.id IN (${marks(ids.length)})
|
|
AND p.is_published = 1`,
|
|
)
|
|
.all(...ids);
|
|
|
|
return json(c, { people: rows.map(shapePerson) });
|
|
});
|
|
|
|
export default people;
|