← back to Nineoh Guide

db/schema.sql

163 lines

-- Unofficial 90210 Guide — canonical Postgres schema
-- Architecture-agnostic: consumed by the shared backend regardless of repo layout.
-- Rights-first design: the assets table stores LICENSE metadata, not just files,
-- so no image renders without a recorded, cleared right to use it.

create extension if not exists "uuid-ossp";

-- ── Core show / season / episode ───────────────────────────────────────────
create table if not exists shows (
  id                uuid primary key default uuid_generate_v4(),
  canonical_title   text not null,
  display_title     text not null,
  franchise_title   text,
  description_original text not null,      -- ORIGINAL, never a copied synopsis
  official_source_url  text,
  disclaimer_text   text not null,          -- app-level "unofficial" disclaimer
  created_at        timestamptz not null default now()
);

create table if not exists seasons (
  id                uuid primary key default uuid_generate_v4(),
  show_id           uuid not null references shows(id) on delete cascade,
  season_number     int  not null,
  title             text,
  episode_count     int,
  source_last_verified_at timestamptz,
  unique (show_id, season_number)
);

create table if not exists episodes (
  id                uuid primary key default uuid_generate_v4(),
  show_id           uuid not null references shows(id) on delete cascade,
  season_id         uuid references seasons(id) on delete set null,
  season_number     int  not null,
  episode_number    int  not null,
  production_code   text,
  title             text not null,
  air_date          date,
  writer            text,
  director          text,
  summary_short_original text,              -- spoiler-safe, ORIGINAL
  summary_full_original  text,              -- expanded, ORIGINAL
  recap_status      text default 'needs-recap', -- needs-recap | ai-draft | reviewed
  spoiler_rating    int default 0,
  source_url        text,
  source_type       text,
  external_ids      jsonb default '{}',     -- tvmaze/wikidata ids etc.
  last_verified_at  timestamptz,
  created_at        timestamptz not null default now(),
  unique (show_id, season_number, episode_number)
);

-- ── Cast / characters / credits ────────────────────────────────────────────
create table if not exists cast_people (
  id                uuid primary key default uuid_generate_v4(),
  name              text not null,
  biography_original text,                  -- ORIGINAL bio
  bio_status        text default 'needs-bio', -- needs-bio | ai-draft | reviewed
  official_site_url text,
  verified_social_links jsonb default '[]',
  headshot_asset_id uuid,                    -- FK added after assets exists
  publicity_risk_level text default 'editorial-only',
  last_verified_at  timestamptz,
  created_at        timestamptz not null default now()
);

create table if not exists characters (
  id                uuid primary key default uuid_generate_v4(),
  name              text not null,
  description_original text,
  show_id           uuid references shows(id) on delete cascade
);

create table if not exists credits (
  id                uuid primary key default uuid_generate_v4(),
  episode_id        uuid references episodes(id) on delete cascade,
  person_id         uuid references cast_people(id) on delete cascade,
  character_id      uuid references characters(id) on delete set null,
  credit_type       text,
  billing_order     int
);

-- ── Per-episode guest / recurring cast (factual, from TVmaze) ──────────────
create table if not exists episode_guest_cast (
  id                serial primary key,
  episode_id        uuid not null references episodes(id) on delete cascade,
  person_name       text not null,
  character_name    text,
  ord               int default 0
);
create index if not exists idx_egc_ep on episode_guest_cast(episode_id);

-- ── News snippets (short, attributed, linked out) ──────────────────────────
create table if not exists news_items (
  id                uuid primary key default uuid_generate_v4(),
  headline          text not null,
  summary_original  text,                   -- one-sentence ORIGINAL summary (written at review; null while snippet_status='indexed')
  canonical_url     text not null,
  publisher_name    text,
  published_at      timestamptz,
  topic_tags        text[] default '{}',
  person_ids        uuid[] default '{}',
  show_ids          uuid[] default '{}',
  snippet_status    text default 'pending',
  source_query      text,                   -- the Google-News query that surfaced this item
  editor_reviewed_at timestamptz,
  created_at        timestamptz not null default now(),
  unique (canonical_url)                     -- ingest-news.mjs upserts on conflict (canonical_url)
);

-- ── Assets = the RIGHTS LEDGER (the legally critical table) ─────────────────
create table if not exists assets (
  id                uuid primary key default uuid_generate_v4(),
  type              text not null,          -- image | thumbnail | ...
  file_url          text,                   -- null while PENDING a cleared license
  thumbnail_url     text,
  license_type      text not null,          -- public-domain | cc-by | cc-by-sa | licensed | PENDING
  license_url       text,
  attribution_text  text,
  usage_scope       text,                   -- editorial | marketing | icon ...
  source_url        text,
  rights_notes      text,
  approved_for_marketing boolean default false,   -- publicity-rights guard
  license_purchase_status text default 'none',      -- none | drafted-for-approval | purchased
  created_at        timestamptz not null default now()
);

-- headshot FK now that assets exists
do $$ begin
  alter table cast_people
    add constraint cast_people_headshot_fk
    foreign key (headshot_asset_id) references assets(id) on delete set null;
exception when duplicate_object then null; end $$;

-- ── UGC + DMCA (moderation-first, per Apple/Play UGC rules) ─────────────────
create table if not exists ugc_posts (
  id                uuid primary key default uuid_generate_v4(),
  user_id           uuid,
  entity_type       text,                   -- episode | cast | news
  entity_id         uuid,
  content           text,
  spoiler_flag      boolean default false,
  status            text default 'pending', -- pending | published | removed
  report_count      int default 0,
  created_at        timestamptz not null default now()
);

create table if not exists dmca_cases (
  id                uuid primary key default uuid_generate_v4(),
  complainant       text,
  material_url      text,
  notice_received_at timestamptz,
  status            text default 'open',
  resolution        text,
  counter_notice_deadline timestamptz
);

-- Fast lookups
create index if not exists idx_episodes_show_season on episodes(show_id, season_number, episode_number);
create index if not exists idx_news_published on news_items(published_at desc);
create index if not exists idx_assets_license on assets(license_type);
create index if not exists idx_assets_pending on assets(license_purchase_status) where license_purchase_status <> 'purchased';