Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions _headers
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@

/api/*
Cache-Control: no-store

/apps/*.html
Cache-Control: public, max-age=0, must-revalidate

/apps/assets/*
Cache-Control: public, max-age=31536000, immutable
87 changes: 87 additions & 0 deletions scripts/generate-arthuriana-seed.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node
// Reads characters.js and emits seed SQL for the AR_* Supabase tables.
// Usage: node scripts/generate-arthuriana-seed.mjs > supabase/migrations/20260517122000_arthuriana_seed.sql

import { createRequire } from 'module';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srcPath = path.resolve(__dirname, '../timeline-scratch/src/Arthuriana/characters.js');

// characters.js uses ES module syntax (export const). We strip exports and eval.
let src = readFileSync(srcPath, 'utf8');
src = src
.replace(/^export\s+const\s+/gm, 'const ')
.replace(/^export\s+/gm, '');

// Use Function constructor to eval in module scope (avoids top-level await issues)
const fn = new Function(src + '\nreturn { CHARACTERS, REALMS, REALM_BY_ID, KNIGHT_ORDER_BY_REALM, REALM_LABEL };');
const { CHARACTERS, REALMS, REALM_BY_ID, KNIGHT_ORDER_BY_REALM, REALM_LABEL } = fn();

function esc(s) {
if (s == null) return 'NULL';
return "'" + String(s).replace(/'/g, "''") + "'";
}

function escJson(obj) {
if (obj == null) return 'NULL';
return "'" + JSON.stringify(obj).replace(/'/g, "''") + "'::jsonb";
}

const lines = [];
lines.push('-- Generated by scripts/generate-arthuriana-seed.mjs — do not edit by hand');
lines.push('');

// Build reverse map: realm_id -> [knight ids in order]
const knightOrderMap = {}; // character_id -> round_table_order
for (const [realmId, knights] of Object.entries(KNIGHT_ORDER_BY_REALM)) {
knights.forEach((charId, idx) => {
knightOrderMap[charId] = idx + 1; // 1-based
});
}

// AR_Realms
lines.push('-- AR_Realms');
REALMS.forEach((r, i) => {
lines.push(
`INSERT INTO "AR_Realms" (realm_id, label, arc_width, sort_order) VALUES (${esc(r.id)}, ${esc(r.label)}, ${r.width}, ${i + 1}) ON CONFLICT (realm_id) DO NOTHING;`
);
});
lines.push('');

// AR_Characters
lines.push('-- AR_Characters');
CHARACTERS.forEach((c) => {
const realmId = REALM_BY_ID[c.id] || null;
const rto = knightOrderMap[c.id] ?? null;
lines.push(
`INSERT INTO "AR_Characters" (character_id, name, title, group_id, realm_id, blazon, round_table_order) VALUES (${esc(c.id)}, ${esc(c.name)}, ${esc(c.title)}, ${esc(c.group)}, ${realmId ? esc(realmId) : 'NULL'}, ${escJson(c.blazon)}, ${rto !== null ? rto : 'NULL'}) ON CONFLICT (character_id) DO NOTHING;`
);
});
lines.push('');

// AR_Relations
lines.push('-- AR_Relations');
CHARACTERS.forEach((c) => {
(c.relations || []).forEach((r) => {
lines.push(
`INSERT INTO "AR_Relations" (from_id, to_id, relation_type) VALUES (${esc(c.id)}, ${esc(r.to)}, ${esc(r.type)}) ON CONFLICT (from_id, to_id, relation_type) DO NOTHING;`
);
});
});
lines.push('');

// AR_Sources
lines.push('-- AR_Sources');
CHARACTERS.forEach((c) => {
(c.sources || []).forEach((s, idx) => {
lines.push(
`INSERT INTO "AR_Sources" (character_id, source, source_text, sort_order) VALUES (${esc(c.id)}, ${esc(s.source)}, ${esc(s.text)}, ${idx + 1}) ON CONFLICT DO NOTHING;`
);
});
});
lines.push('');

process.stdout.write(lines.join('\n') + '\n');
39 changes: 39 additions & 0 deletions supabase/migrations/20260517120000_arthuriana_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- Arthuriana: realms, characters, relations, sources

CREATE TABLE IF NOT EXISTS "AR_Realms" (
realm_id TEXT PRIMARY KEY,
label TEXT NOT NULL,
arc_width INTEGER NOT NULL DEFAULT 40,
sort_order INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS "AR_Characters" (
character_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
group_id TEXT NOT NULL DEFAULT 'other',
realm_id TEXT REFERENCES "AR_Realms"(realm_id),
blazon JSONB,
round_table_order INTEGER
);

CREATE TABLE IF NOT EXISTS "AR_Relations" (
id SERIAL PRIMARY KEY,
from_id TEXT NOT NULL REFERENCES "AR_Characters"(character_id),
to_id TEXT NOT NULL REFERENCES "AR_Characters"(character_id),
relation_type TEXT NOT NULL,
UNIQUE (from_id, to_id, relation_type)
);

CREATE TABLE IF NOT EXISTS "AR_Sources" (
id SERIAL PRIMARY KEY,
character_id TEXT NOT NULL REFERENCES "AR_Characters"(character_id),
source TEXT NOT NULL,
source_text TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS ar_chars_realm_idx ON "AR_Characters"(realm_id);
CREATE INDEX IF NOT EXISTS ar_rels_from_idx ON "AR_Relations"(from_id);
CREATE INDEX IF NOT EXISTS ar_rels_to_idx ON "AR_Relations"(to_id);
CREATE INDEX IF NOT EXISTS ar_sources_char_idx ON "AR_Sources"(character_id);
11 changes: 11 additions & 0 deletions supabase/migrations/20260517121000_arthuriana_rls.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- RLS: public read-only for all Arthuriana tables

ALTER TABLE "AR_Realms" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "AR_Characters" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "AR_Relations" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "AR_Sources" ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Public read" ON "AR_Realms" FOR SELECT USING (true);
CREATE POLICY "Public read" ON "AR_Characters" FOR SELECT USING (true);
CREATE POLICY "Public read" ON "AR_Relations" FOR SELECT USING (true);
CREATE POLICY "Public read" ON "AR_Sources" FOR SELECT USING (true);
Loading
Loading