diff --git a/DATABASE.md b/DATABASE.md index 8a28cdc..a90040a 100644 --- a/DATABASE.md +++ b/DATABASE.md @@ -4,29 +4,36 @@ ### Fresh deployment -Run **one file**: +Run **one file** in Supabase SQL Editor: ``` scripts/schema.sql ``` -Paste it in Supabase → **SQL Editor → New query** and hit Run. -That is the complete, idempotent schema — all tables, enums, indexes, RLS policies, functions, and triggers. +This creates the entire schema from scratch — all tables, enums, indexes, RLS policies, functions, and triggers. Idempotent, can be run multiple times safely. -### Existing deployment +### Existing deployment migration -The numbered scripts (`001_*` → `011_*`) are the **migration history** of the live database. -Do not re-run them. They are kept for reference only. +If you have a legacy Hackerspace database and want to sync it to the canonical schema: + +``` +scripts/012_canonical_sync.sql +``` + +This migration adds all missing columns, renames `full_name` → `display_name`, creates missing constraints, fixes the auth trigger, and updates all other tables to match the canonical schema. Also idempotent — safe to run multiple times. + +### Migration history (reference only) + +Scripts `001_*` through `011_*` represent the evolution of the live database. **Do not re-run them.** They exist for historical reference. ### What `schema.sql` contains | Section | Contents | |---------|----------| | Extensions | `uuid-ossp` | -| Enums | 14 custom types (`member_role`, `task_status`, etc.) | +| Enums | 14 custom types (`member_role`, `task_status`, `payment_platform`, etc.) | | Helper functions | `get_user_space_ids`, `user_has_role_in_space` | -| Tables | `spaces`, `space_members`, `tasks`, `projects`, `payments`, `contacts`, `knowledge_base`, `secrets`, `area_leads`, `integrations`, `comms_channels`, `comms_messages`, `activity_log` | -| Indexes | 22 performance indexes | -| RLS policies | Full row-level security on every table | -| Triggers | `on_space_created` (auto-creates 4 default channels) | -| Auth hook | `on_auth_user_created` (handles create-space and join-space signup flows) | +| Tables | 13 tables: `spaces`, `space_members`, `tasks`, `projects`, `payments`, `contacts`, `knowledge_base`, `secrets`, `area_leads`, `integrations`, `comms_channels`, `comms_messages`, `activity_log` | +| Indexes | 22 performance indexes on foreign keys and common filters | +| RLS policies | Full row-level security on all tables (access gated by space membership) | +| Triggers | `on_space_created` (auto-creates 4 default channels), `on_auth_user_created` (auth signup hook) | diff --git a/scripts/012_canonical_sync.sql b/scripts/012_canonical_sync.sql new file mode 100644 index 0000000..a013ce8 --- /dev/null +++ b/scripts/012_canonical_sync.sql @@ -0,0 +1,193 @@ +-- Migration 012: Sync existing databases to canonical schema +-- Run this on any existing Hackerspace deployment to align with the canonical schema.sql +-- Safe to run multiple times (idempotent). +-- +-- Changes: +-- • Rename space_members.full_name → display_name (to match app code everywhere) +-- • Drop redundant tasks.type text column (canonical column is task_type enum) +-- • Add missing columns to all tables that the app code expects +-- • Create missing unique constraints for upserts +-- • Update handle_space_signup trigger to match canonical version + +-- ============================================================ +-- RENAME COLUMNS +-- ============================================================ +ALTER TABLE public.space_members RENAME COLUMN full_name TO display_name; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS handle text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS bio text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS avatar_url text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS dues_paid_until timestamptz; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS stripe_customer_id text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS has_card_access boolean NOT NULL DEFAULT false; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS payment_status text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS payment_note text; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS last_payment_at timestamptz; +ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); + +-- ============================================================ +-- TASKS TABLE FIXES +-- ============================================================ +-- Drop the redundant text 'type' column if it exists (canonical column is task_type enum) +ALTER TABLE public.tasks DROP COLUMN IF EXISTS type CASCADE; + +-- Ensure all canonical task columns exist +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS priority text NOT NULL DEFAULT 'medium'; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS requested_by uuid REFERENCES auth.users(id) ON DELETE SET NULL; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS requested_by_name text; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS claimed_by_name text; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS assigned_to_name text; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS completed_at timestamptz; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS last_done_at timestamptz; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS subtask_total integer NOT NULL DEFAULT 0; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS subtask_completed integer NOT NULL DEFAULT 0; +ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS progress integer NOT NULL DEFAULT 0; + +-- ============================================================ +-- SPACES TABLE +-- ============================================================ +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS city text; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS address text; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS timezone text DEFAULT 'America/Phoenix'; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS invite_code text; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS require_approval boolean NOT NULL DEFAULT true; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS public_member_directory boolean NOT NULL DEFAULT false; +ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS webhook_secret text; + +-- ============================================================ +-- PAYMENTS TABLE +-- ============================================================ +ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS from_identifier text; +ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS from_note text; +ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS member_name text; +ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS link_status public.payment_link_status NOT NULL DEFAULT 'unlinked'; +ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS transaction_date timestamptz DEFAULT now(); + +-- ============================================================ +-- CONTACTS TABLE +-- ============================================================ +ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS code text; +ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS details text; +ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS note text; +ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS group_label text; + +-- ============================================================ +-- KNOWLEDGE BASE +-- ============================================================ +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS icon text; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS access_level text DEFAULT 'all_members'; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS area text; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS is_pinned boolean NOT NULL DEFAULT false; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by_name text; +ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by text; + +-- ============================================================ +-- SECRETS TABLE +-- ============================================================ +ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS icon text; +ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS description text; +ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS area text; +ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS title text; +-- Backfill title from label where label exists and title is null +UPDATE public.secrets SET title = label WHERE title IS NULL AND label IS NOT NULL; + +-- ============================================================ +-- AREA LEADS TABLE +-- ============================================================ +ALTER TABLE public.area_leads ADD COLUMN IF NOT EXISTS area_code text; +ALTER TABLE public.area_leads ADD COLUMN IF NOT EXISTS lead_handle text; + +-- Add unique constraint on (space_id, area_code) if not already present +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'area_leads_space_area_code_key' + ) THEN + ALTER TABLE public.area_leads ADD CONSTRAINT area_leads_space_area_code_key + UNIQUE (space_id, area_code) DEFERRABLE INITIALLY DEFERRED; + END IF; +END $$; + +-- ============================================================ +-- INTEGRATIONS TABLE +-- ============================================================ +ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS name text; +ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS description text; +ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS config jsonb; + +-- ============================================================ +-- COMMS CHANNELS +-- ============================================================ +ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS icon text; +ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS area_reference text; +ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS member_count integer NOT NULL DEFAULT 0; +ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS unread_count integer NOT NULL DEFAULT 0; + +-- ============================================================ +-- COMMS MESSAGES +-- ============================================================ +ALTER TABLE public.comms_messages ADD COLUMN IF NOT EXISTS display_name text; +ALTER TABLE public.comms_messages ADD COLUMN IF NOT EXISTS handle text; + +-- ============================================================ +-- ACTIVITY LOG +-- ============================================================ +ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS display_name text; +ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS details text; + +-- ============================================================ +-- PROJECTS TABLE +-- ============================================================ +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS name text; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS area text; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS category text; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS assignees text; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS assignee_names text[]; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS task_count integer NOT NULL DEFAULT 0; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS tasks_completed integer NOT NULL DEFAULT 0; +ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS progress integer NOT NULL DEFAULT 0; + +-- ============================================================ +-- UNIQUE INDEXES / CONSTRAINTS +-- ============================================================ +-- space_members: unique index on (space_id, email) for upsert in importMembers +CREATE UNIQUE INDEX IF NOT EXISTS idx_space_members_space_email + ON public.space_members (space_id, email) + WHERE email IS NOT NULL; + +-- ============================================================ +-- UPDATE TRIGGERS +-- ============================================================ +-- Fix handle_space_signup to match canonical version +CREATE OR REPLACE FUNCTION public.handle_space_signup() + RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER + SET search_path = public +AS $$ +DECLARE + v_space_id uuid; + v_role public.member_role; + v_display_name text; +BEGIN + v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid; + v_role := COALESCE( + NULLIF((NEW.raw_user_meta_data->>'role'), '')::public.member_role, + 'member' + ); + v_display_name := COALESCE( + NULLIF(NEW.raw_user_meta_data->>'display_name', ''), + NULLIF(NEW.raw_user_meta_data->>'full_name', ''), + NEW.email + ); + + IF v_space_id IS NULL THEN + RETURN NEW; + END IF; + + INSERT INTO public.space_members (space_id, user_id, role, status, display_name, email, approved) + VALUES (v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true) + ON CONFLICT (space_id, user_id) DO NOTHING; + + RETURN NEW; +END; +$$;