Clean up the schema: drop unused tables, rename scopes, publishable awards
Migrations 020–023, with the code that reads each: - 020 drops people_lists, people_list_members, v_chapters and v_person_affiliations. Nothing queried any of them. - 021 renames event_sections to event_scopes and events.section_id to scope_id, finishing what 015 described. The API sends `scopes` and `scope_id`, and useEvents, EventListCards and EventCalendar take `scope`. It also inserts national/regional/partner, which only the retired seed ever created: a database built from migrations alone had no scope for the Retreats bands. - 022 drops events.sort_order and people.sort_order. Events now sort by date (upcoming soonest first, past latest first, undated last) on /events, org pages and the countdown. People were only ever sorted by sort_name on the site. Every other sort_order stays. - 023 rebuilds teams with created_at and updated_at plus a touch trigger, so the teams editor gets the same optimistic concurrency as the other entities. Awards can be drafted: is_published (added in 011) is on both descriptor halves with a Publishing group, and the award list, award page, org awards and event awards leave drafts out. The migration runner now turns foreign keys off around the per-file transactions and runs foreign_key_check before each commit. PRAGMA foreign_keys is a no-op inside a transaction, so 009's warning was right and a rebuild of a referenced table (023) couldn't be written otherwise. CLAUDE.md is updated to match. Also removes the stray src/App.tsx.save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8790f861fe
commit
25e592bfea
19 changed files with 339 additions and 423 deletions
|
|
@ -299,28 +299,27 @@ const events = {
|
|||
columns: [
|
||||
"id",
|
||||
"title",
|
||||
"section_id",
|
||||
"scope_id",
|
||||
"event_type",
|
||||
"date_label",
|
||||
"starts_on",
|
||||
"status",
|
||||
"is_published",
|
||||
"sort_order",
|
||||
"updated_at",
|
||||
],
|
||||
// No host filter: hosts are rows in another table now, and the
|
||||
// engine's filters are columns on this one. The events a host
|
||||
// owns are on that host's own page.
|
||||
filters: ["section_id", "event_type", "status", "is_published"],
|
||||
filters: ["scope_id", "event_type", "status", "is_published"],
|
||||
search: ["title", "id", "theme"],
|
||||
order: "sort_order, starts_on DESC, title",
|
||||
order: "starts_on IS NULL, starts_on DESC, title",
|
||||
},
|
||||
|
||||
columns: [
|
||||
text("section_id", { required: true }),
|
||||
text("scope_id", { required: true }),
|
||||
|
||||
// What kind of gathering, as against section_id's which band of
|
||||
// the page. Declared required even though the column has a
|
||||
// What kind of gathering, as against scope_id's whose gathering
|
||||
// it is. Declared required even though the column has a
|
||||
// DEFAULT: every select renders a blank first option, so without
|
||||
// it a new event files itself as a retreat while nobody is
|
||||
// looking. An existing row always loads with its value set, so
|
||||
|
|
@ -344,7 +343,6 @@ const events = {
|
|||
text("color"),
|
||||
text("gradient"),
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
bool("in_timeline"),
|
||||
|
||||
// A repeating schedule. Columns rather than a side table: the
|
||||
|
|
@ -410,12 +408,11 @@ const people = {
|
|||
"tagline",
|
||||
"locality",
|
||||
"is_published",
|
||||
"sort_order",
|
||||
"updated_at",
|
||||
],
|
||||
filters: ["is_published"],
|
||||
search: ["display_name", "sort_name", "id"],
|
||||
order: "sort_order, sort_name, display_name",
|
||||
order: "sort_name, display_name",
|
||||
},
|
||||
|
||||
columns: [
|
||||
|
|
@ -433,7 +430,6 @@ const people = {
|
|||
text("country"),
|
||||
text("location_label"),
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
],
|
||||
|
||||
extensions: [
|
||||
|
|
@ -505,18 +501,15 @@ const people = {
|
|||
// · deleting a team with members fails the same way, rather than
|
||||
// quietly detaching them. Emptying the members list first is
|
||||
// now something the form can do.
|
||||
//
|
||||
// No concurrency column: teams have no updated_at. Adding one
|
||||
// means rebuilding a STRICT table for a row that one person edits
|
||||
// at a time, which is not a trade worth making yet.
|
||||
const teams = {
|
||||
key: "teams",
|
||||
table: "teams",
|
||||
idColumn: "id",
|
||||
idKind: "slug",
|
||||
concurrency: "updated_at",
|
||||
|
||||
list: {
|
||||
columns: ["id", "org_id", "name", "tagline", "is_published", "sort_order"],
|
||||
columns: ["id", "org_id", "name", "tagline", "is_published", "sort_order", "updated_at"],
|
||||
filters: ["org_id", "is_published"],
|
||||
search: ["name", "id", "tagline"],
|
||||
order: "org_id, sort_order, name",
|
||||
|
|
@ -575,8 +568,8 @@ const awards = {
|
|||
idKind: "slug",
|
||||
|
||||
list: {
|
||||
columns: ["id", "org_id", "name", "description", "sort_order"],
|
||||
filters: ["org_id"],
|
||||
columns: ["id", "org_id", "name", "description", "is_published", "sort_order"],
|
||||
filters: ["org_id", "is_published"],
|
||||
search: ["name", "id", "description"],
|
||||
order: "org_id, sort_order, name",
|
||||
},
|
||||
|
|
@ -586,6 +579,7 @@ const awards = {
|
|||
text("name", { required: true }),
|
||||
text("description"),
|
||||
text("logo"),
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
],
|
||||
};
|
||||
|
|
@ -787,7 +781,7 @@ export const OPTION_QUERIES = {
|
|||
"SELECT id, name AS label, kind FROM organizations ORDER BY kind, name",
|
||||
regions:
|
||||
"SELECT id, name AS label FROM organizations WHERE kind = 'region' ORDER BY name",
|
||||
event_sections: "SELECT id, name AS label FROM event_sections ORDER BY sort_order",
|
||||
event_scopes: "SELECT id, name AS label FROM event_scopes ORDER BY sort_order",
|
||||
events: "SELECT id, title AS label FROM events ORDER BY starts_on DESC, title",
|
||||
people: "SELECT id, display_name AS label FROM people ORDER BY sort_name, display_name",
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ export function tx(db, fn) {
|
|||
|
||||
Migrations only ever go forward. To undo something, write a
|
||||
new migration.
|
||||
|
||||
Foreign keys are off while migrations run, the recipe from
|
||||
the SQLite docs for changing a table's shape. PRAGMA
|
||||
foreign_keys is a no-op inside a transaction, so it has to be
|
||||
set here, around the per-file transactions, rather than in
|
||||
the file. With it on, rebuilding a table something references
|
||||
(drop the old one, rename the new one into place) either
|
||||
cascades into the children or fails the commit. Instead each
|
||||
file ends with a foreign_key_check, and any orphan it leaves
|
||||
rolls that file back.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function migrate(db, { log = console.log } = {}) {
|
||||
|
|
@ -92,25 +102,41 @@ export function migrate(db, { log = console.log } = {}) {
|
|||
.sort();
|
||||
|
||||
let applied = 0;
|
||||
const enforced = db.prepare("PRAGMA foreign_keys").get().foreign_keys;
|
||||
db.exec("PRAGMA foreign_keys = OFF");
|
||||
|
||||
for (const file of files) {
|
||||
const version = Number.parseInt(file.slice(0, 3), 10);
|
||||
try {
|
||||
for (const file of files) {
|
||||
const version = Number.parseInt(file.slice(0, 3), 10);
|
||||
|
||||
if (!Number.isInteger(version) || version < 1) {
|
||||
throw new Error(`Migration "${file}" must start with a number, e.g. 001_`);
|
||||
if (!Number.isInteger(version) || version < 1) {
|
||||
throw new Error(`Migration "${file}" must start with a number, e.g. 001_`);
|
||||
}
|
||||
if (version <= current) continue;
|
||||
|
||||
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf8");
|
||||
|
||||
tx(db, () => {
|
||||
db.exec(sql);
|
||||
|
||||
const orphans = db.prepare("PRAGMA foreign_key_check").all();
|
||||
if (orphans.length > 0) {
|
||||
const where = orphans
|
||||
.slice(0, 5)
|
||||
.map((o) => `${o.table} row ${o.rowid} → ${o.parent}`)
|
||||
.join(", ");
|
||||
throw new Error(`${file} leaves ${orphans.length} foreign key violation(s): ${where}`);
|
||||
}
|
||||
|
||||
// Not parameterisable, but version is a validated integer.
|
||||
db.exec(`PRAGMA user_version = ${version}`);
|
||||
});
|
||||
|
||||
log(`migrated → ${file}`);
|
||||
applied += 1;
|
||||
}
|
||||
if (version <= current) continue;
|
||||
|
||||
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf8");
|
||||
|
||||
tx(db, () => {
|
||||
db.exec(sql);
|
||||
// Not parameterisable, but version is a validated integer.
|
||||
db.exec(`PRAGMA user_version = ${version}`);
|
||||
});
|
||||
|
||||
log(`migrated → ${file}`);
|
||||
applied += 1;
|
||||
} finally {
|
||||
if (enforced) db.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
|
||||
const final = db.prepare("PRAGMA user_version").get().user_version;
|
||||
|
|
|
|||
23
server/src/migrations/020_drop_unused.sql
Normal file
23
server/src/migrations/020_drop_unused.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 020 DROP WHAT NOTHING READS
|
||||
--
|
||||
-- people_lists and people_list_members were planned for
|
||||
-- hand-picked rosters and never got a route, a descriptor or a
|
||||
-- component. v_chapters and v_person_affiliations have no query
|
||||
-- against them: the routes read organizations and affiliations
|
||||
-- directly.
|
||||
--
|
||||
-- Views first, then the child table before its parent. No trigger
|
||||
-- references either table, and nothing has a foreign key into
|
||||
-- them, so no rebuild and no PRAGMA foreign_keys dance.
|
||||
--
|
||||
-- PRAGMA user_version; -- reads 19 before this file
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
DROP VIEW IF EXISTS v_chapters;
|
||||
DROP VIEW IF EXISTS v_person_affiliations;
|
||||
|
||||
DROP TABLE IF EXISTS people_list_members;
|
||||
DROP TABLE IF EXISTS people_lists;
|
||||
|
||||
PRAGMA user_version = 20; -- ← set to this migration's number
|
||||
32
server/src/migrations/021_event_scopes.sql
Normal file
32
server/src/migrations/021_event_scopes.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 021 event_sections → event_scopes
|
||||
--
|
||||
-- 015 established that the table is a scope list — whose gathering
|
||||
-- an event is — and kept the old name to avoid a rename across the
|
||||
-- codebase. This is that rename: the table, and events.section_id
|
||||
-- to scope_id. The API field and the useEvents filter follow in
|
||||
-- the same change.
|
||||
--
|
||||
-- Both renames run with legacy_alter_table off (the default), so
|
||||
-- SQLite carries them into the REFERENCES clause on events, the
|
||||
-- index on section_id, and v_events, which selects e.*.
|
||||
--
|
||||
-- It also inserts the three scopes 015 only relabelled. The
|
||||
-- retired seed script was what created national, regional and
|
||||
-- partner, so a database built from migrations alone had no row
|
||||
-- for the Retreats page's three bands to point at. INSERT OR
|
||||
-- IGNORE with 015's names and order: a no-op where they exist.
|
||||
--
|
||||
-- PRAGMA user_version; -- reads 20 before this file
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE event_sections RENAME TO event_scopes;
|
||||
|
||||
ALTER TABLE events RENAME COLUMN section_id TO scope_id;
|
||||
|
||||
INSERT OR IGNORE INTO event_scopes (id, name, sort_order) VALUES
|
||||
('national', 'National', 10),
|
||||
('regional', 'Regional', 20),
|
||||
('partner', 'Partner', 50);
|
||||
|
||||
PRAGMA user_version = 21; -- ← set to this migration's number
|
||||
36
server/src/migrations/022_drop_sort_orders.sql
Normal file
36
server/src/migrations/022_drop_sort_orders.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 022 DROP events.sort_order AND people.sort_order
|
||||
--
|
||||
-- events.sort_order dates from the seed, when event dates were
|
||||
-- free text ("March/April 2026") and a typed number was the only
|
||||
-- way to order a list. Events have real dates now, and a new one
|
||||
-- landed at 0 — ahead of everything — until someone renumbered.
|
||||
-- The routes order by date instead.
|
||||
--
|
||||
-- people.sort_order was never read by the public site: people
|
||||
-- sort by sort_name everywhere. It only reordered the admin list.
|
||||
--
|
||||
-- Every other sort_order stays. On child collections it is the
|
||||
-- row's position, written by the admin engine from drag order;
|
||||
-- on organizations, teams, awards, timeline_entries and
|
||||
-- event_scopes it is an ordering the site actually uses.
|
||||
--
|
||||
-- SQLite refuses DROP COLUMN while an index names the column, so
|
||||
-- the three indexes that do go first and come back without it.
|
||||
-- No view names either column (v_events selects e.*).
|
||||
--
|
||||
-- PRAGMA user_version; -- reads 21 before this file
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
DROP INDEX IF EXISTS events_section_idx;
|
||||
DROP INDEX IF EXISTS events_type_idx;
|
||||
DROP INDEX IF EXISTS people_sort_idx;
|
||||
|
||||
ALTER TABLE events DROP COLUMN sort_order;
|
||||
ALTER TABLE people DROP COLUMN sort_order;
|
||||
|
||||
CREATE INDEX events_scope_idx ON events (scope_id, is_published, starts_on);
|
||||
CREATE INDEX events_type_idx ON events (event_type, is_published, starts_on);
|
||||
CREATE INDEX people_sort_idx ON people (is_published, sort_name);
|
||||
|
||||
PRAGMA user_version = 22; -- ← set to this migration's number
|
||||
87
server/src/migrations/023_teams_updated_at.sql
Normal file
87
server/src/migrations/023_teams_updated_at.sql
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 023 teams GETS created_at AND updated_at
|
||||
--
|
||||
-- Every other top-level entity has updated_at, which the admin
|
||||
-- engine compares on save so two people editing the same row
|
||||
-- can't silently overwrite each other. teams was left out because
|
||||
-- ADD COLUMN can't take DEFAULT (datetime('now')) on a STRICT
|
||||
-- table, so this is the rebuild — the recipe from the SQLite docs.
|
||||
--
|
||||
-- The runner turns foreign keys off around every migration and
|
||||
-- runs foreign_key_check before committing (see migrate() in
|
||||
-- db.js). So DROP TABLE doesn't touch affiliations, whose
|
||||
-- (team_id, org_id) reference keeps pointing at the name `teams`,
|
||||
-- which the rename puts back underneath it.
|
||||
--
|
||||
-- legacy_alter_table is on for the rename: v_org_leadership,
|
||||
-- v_timeline and the timeline_entries ref triggers name `teams`.
|
||||
-- Between the DROP and the RENAME that table doesn't exist, and
|
||||
-- the modern rename re-parses every view and trigger and fails on
|
||||
-- them. Legacy mode leaves them alone; they resolve to the new
|
||||
-- table by name. Turned back off before the end, since the
|
||||
-- connection outlives this file.
|
||||
--
|
||||
-- DROP TABLE takes the two AFTER DELETE triggers with it (and does
|
||||
-- not fire them), so they're recreated below exactly as 002 and
|
||||
-- 007 wrote them, along with teams_touch. Triggers go last: the
|
||||
-- runner may drop statements that follow a BEGIN...END body.
|
||||
--
|
||||
-- Verify after:
|
||||
--
|
||||
-- PRAGMA user_version; -- 23
|
||||
-- PRAGMA foreign_key_check; -- no rows
|
||||
-- SELECT name FROM sqlite_master WHERE tbl_name = 'teams';
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
PRAGMA legacy_alter_table = ON;
|
||||
|
||||
CREATE TABLE teams_new (
|
||||
id TEXT PRIMARY KEY, -- slug: 'board', 'nw-leadership'
|
||||
org_id TEXT NOT NULL REFERENCES organizations (id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
tagline TEXT,
|
||||
color TEXT,
|
||||
logo TEXT,
|
||||
is_published INTEGER NOT NULL DEFAULT 1 CHECK (is_published IN (0, 1)),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (id, org_id)
|
||||
) STRICT;
|
||||
|
||||
-- Columns listed explicitly rather than SELECT *, so this breaks
|
||||
-- loudly if the old shape isn't what this file assumes. Existing
|
||||
-- rows get the migration time for both timestamps.
|
||||
INSERT INTO teams_new
|
||||
(id, org_id, name, tagline, color, logo, is_published, sort_order)
|
||||
SELECT
|
||||
id, org_id, name, tagline, color, logo, is_published, sort_order
|
||||
FROM teams;
|
||||
|
||||
DROP TABLE teams;
|
||||
|
||||
ALTER TABLE teams_new RENAME TO teams;
|
||||
|
||||
CREATE INDEX teams_org_idx ON teams (org_id, sort_order);
|
||||
|
||||
PRAGMA legacy_alter_table = OFF;
|
||||
|
||||
CREATE TRIGGER teams_cleanup
|
||||
AFTER DELETE ON teams
|
||||
BEGIN
|
||||
DELETE FROM content_blocks WHERE owner_kind = 'team' AND owner_id = old.id;
|
||||
DELETE FROM links WHERE owner_kind = 'team' AND owner_id = old.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER timeline_teams_cleanup
|
||||
AFTER DELETE ON teams
|
||||
BEGIN
|
||||
DELETE FROM timeline_entries WHERE ref_kind = 'team' AND ref_id = old.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER teams_touch
|
||||
AFTER UPDATE ON teams
|
||||
FOR EACH ROW WHEN new.updated_at = old.updated_at
|
||||
BEGIN
|
||||
UPDATE teams SET updated_at = datetime('now') WHERE id = new.id;
|
||||
END;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
CONTENT ROUTES — read-only, mounted under /api
|
||||
|
||||
GET /events list + the section ids
|
||||
GET /events list + the event scopes
|
||||
GET /events/:id one event, full body, people
|
||||
GET /organizations list, ?kind=region|chapter|…
|
||||
GET /organizations/:id one organization's page
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
from the dates when it isn't set. Components read one field and
|
||||
don't reimplement the rules.
|
||||
|
||||
`event_type` is orthogonal to `section_id`: the section is which
|
||||
band of the Retreats page an event belongs to, the type is what
|
||||
`event_type` is orthogonal to `scope_id`: the scope is whose
|
||||
gathering it is (national, regional, partner…), the type is what
|
||||
kind of gathering it is. A region can run a class and a partner
|
||||
can run a retreat, so neither implies the other and both ship on
|
||||
every event.
|
||||
|
|
@ -120,7 +120,7 @@ function shapeEvent(row, links, cardBlocks, hosts = []) {
|
|||
|
||||
return {
|
||||
id: row.id,
|
||||
section_id: row.section_id,
|
||||
scope_id: row.scope_id,
|
||||
event_type: row.event_type,
|
||||
|
||||
title: row.title,
|
||||
|
|
@ -399,9 +399,7 @@ function attachTeams(db, orgs) {
|
|||
for (const org of orgs) org.teams = byOrg.get(org.id) ?? [];
|
||||
}
|
||||
|
||||
/* The awards this organization gives. awards has no is_published
|
||||
column, so every row is public the moment it exists — see the
|
||||
note in the route below. */
|
||||
/* The awards this organization gives, drafts left out. */
|
||||
function attachAwards(db, orgs) {
|
||||
const ids = orgs.map((o) => o.id);
|
||||
if (ids.length === 0) return;
|
||||
|
|
@ -414,7 +412,7 @@ function attachAwards(db, orgs) {
|
|||
JOIN people p ON p.id = pa.person_id AND p.is_published = 1
|
||||
WHERE pa.award_id = a.id AND pa.is_public = 1) AS recipient_count
|
||||
FROM awards a
|
||||
WHERE a.org_id IN (${marks(ids.length)})
|
||||
WHERE a.org_id IN (${marks(ids.length)}) AND a.is_published = 1
|
||||
ORDER BY a.sort_order, a.name`,
|
||||
)
|
||||
.all(...ids);
|
||||
|
|
@ -437,22 +435,33 @@ function attachAwards(db, orgs) {
|
|||
}
|
||||
|
||||
/* ── Events ────────────────────────────────────────────────────
|
||||
Flat, with the section ids alongside. Retreats.tsx owns the
|
||||
section titles and colours and filters this list by section_id.
|
||||
Flat, with the event scopes alongside. Retreats.tsx owns the
|
||||
band titles and colours and filters this list by scope_id.
|
||||
|
||||
By date, the way the cards split them: upcoming soonest first,
|
||||
then past most recent first, undated at the end of each. There
|
||||
is no hand-typed order; a tie falls to the title.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
const EVENT_ORDER = `
|
||||
effective_status = 'past',
|
||||
starts_on IS NULL,
|
||||
CASE WHEN effective_status = 'past' THEN NULL ELSE starts_on END,
|
||||
starts_on DESC,
|
||||
title`;
|
||||
|
||||
content.get("/events", (c) => {
|
||||
const db = c.get("db");
|
||||
|
||||
const sections = db
|
||||
.prepare(`SELECT id, name, sort_order FROM event_sections ORDER BY sort_order`)
|
||||
const scopes = db
|
||||
.prepare(`SELECT id, name, sort_order FROM event_scopes ORDER BY sort_order`)
|
||||
.all();
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT * FROM v_events
|
||||
WHERE is_published = 1
|
||||
ORDER BY section_id, sort_order`,
|
||||
ORDER BY ${EVENT_ORDER}`,
|
||||
)
|
||||
.all();
|
||||
|
||||
|
|
@ -470,7 +479,7 @@ content.get("/events", (c) => {
|
|||
),
|
||||
);
|
||||
|
||||
return json(c, { sections, events });
|
||||
return json(c, { scopes, events });
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -507,7 +516,7 @@ content.get("/events/:id", (c) => {
|
|||
a.name AS award_name, a.logo AS award_logo,
|
||||
pa.person_id, p.display_name, p.photo
|
||||
FROM person_awards pa
|
||||
JOIN awards a ON a.id = pa.award_id
|
||||
JOIN awards a ON a.id = pa.award_id AND a.is_published = 1
|
||||
JOIN people p ON p.id = pa.person_id AND p.is_published = 1
|
||||
WHERE pa.event_id = ? AND pa.is_public = 1
|
||||
ORDER BY a.sort_order, a.name, COALESCE(p.sort_name, p.display_name)`,
|
||||
|
|
@ -622,7 +631,7 @@ content.get("/organizations/:id", (c) => {
|
|||
FROM v_events e
|
||||
JOIN event_hosts eh ON eh.event_id = e.id AND eh.org_id = ?
|
||||
WHERE e.is_published = 1
|
||||
ORDER BY e.sort_order`,
|
||||
ORDER BY ${EVENT_ORDER}`,
|
||||
)
|
||||
.all(id);
|
||||
|
||||
|
|
@ -694,12 +703,8 @@ content.get("/teams/:id", (c) => {
|
|||
GET /awards?org=ngu awards a given organization gives
|
||||
GET /awards/:id one award and who has received it
|
||||
|
||||
`awards` has no is_published column: an award is public the
|
||||
moment somebody creates it, and there is no way to draft one.
|
||||
That was fine while awards only appeared as a line on a
|
||||
person's record; it is thinner ground now that each has a URL.
|
||||
A plain ADD COLUMN with DEFAULT 1 fixes it without a rebuild —
|
||||
worth doing before this ships.
|
||||
An unpublished award is a draft: left out of the list, and a
|
||||
404 at its own URL, the same as any other unpublished row.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
content.get("/awards", (c) => {
|
||||
|
|
@ -718,7 +723,7 @@ content.get("/awards", (c) => {
|
|||
WHERE pa.award_id = a.id AND pa.is_public = 1) AS recipient_count
|
||||
FROM awards a
|
||||
LEFT JOIN organizations o ON o.id = a.org_id AND o.is_published = 1
|
||||
${org ? "WHERE a.org_id = ?" : ""}
|
||||
WHERE a.is_published = 1 ${org ? "AND a.org_id = ?" : ""}
|
||||
ORDER BY a.sort_order, a.name`,
|
||||
)
|
||||
.all(...(org ? [org] : []));
|
||||
|
|
@ -741,7 +746,7 @@ content.get("/awards/:id", (c) => {
|
|||
`SELECT a.*, o.name AS org_name, o.kind AS org_kind
|
||||
FROM awards a
|
||||
LEFT JOIN organizations o ON o.id = a.org_id AND o.is_published = 1
|
||||
WHERE a.id = ?`,
|
||||
WHERE a.id = ? AND a.is_published = 1`,
|
||||
)
|
||||
.get(id);
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ home.get("/front-page", (c) => {
|
|||
.prepare(
|
||||
`SELECT * FROM v_events
|
||||
WHERE ${notOver}
|
||||
ORDER BY starts_on, sort_order
|
||||
ORDER BY starts_on, title
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get();
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ people.get("/people/:id", (c) => {
|
|||
WHERE person_id = ?
|
||||
) x
|
||||
JOIN v_events e ON e.id = x.event_id AND e.is_published = 1
|
||||
ORDER BY e.starts_on IS NULL, e.starts_on DESC, e.sort_order, x.sort_order`,
|
||||
ORDER BY e.starts_on IS NULL, e.starts_on DESC, e.title, x.sort_order`,
|
||||
)
|
||||
.all(id, id);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue