Mega push vol 4
This commit is contained in:
66
supabase/migrations/013_kanban_labels.sql
Normal file
66
supabase/migrations/013_kanban_labels.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- Kanban Labels System
|
||||
-- Adds label/tag support to kanban cards for categorization and filtering
|
||||
|
||||
-- Labels table (org-scoped)
|
||||
CREATE TABLE IF NOT EXISTS kanban_labels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL DEFAULT '#6366f1',
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(org_id, name)
|
||||
);
|
||||
|
||||
-- Card-Label junction table (many-to-many)
|
||||
CREATE TABLE IF NOT EXISTS card_labels (
|
||||
card_id UUID NOT NULL REFERENCES kanban_cards(id) ON DELETE CASCADE,
|
||||
label_id UUID NOT NULL REFERENCES kanban_labels(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (card_id, label_id)
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE kanban_labels ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE card_labels ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Labels accessible to org members
|
||||
CREATE POLICY "Labels accessible to org members" ON kanban_labels
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM org_members m
|
||||
WHERE m.org_id = kanban_labels.org_id
|
||||
AND m.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Card labels inherit card access
|
||||
CREATE POLICY "Card labels inherit card access" ON card_labels
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM kanban_cards c
|
||||
JOIN kanban_columns col ON c.column_id = col.id
|
||||
JOIN kanban_boards b ON col.board_id = b.id
|
||||
JOIN org_members m ON b.org_id = m.org_id
|
||||
WHERE c.id = card_labels.card_id
|
||||
AND m.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_kanban_labels_org ON kanban_labels(org_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_card_labels_card ON card_labels(card_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_card_labels_label ON card_labels(label_id);
|
||||
|
||||
-- Seed default labels for existing organizations
|
||||
INSERT INTO kanban_labels (org_id, name, color)
|
||||
SELECT DISTINCT o.id, label.name, label.color
|
||||
FROM organizations o
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('Bug', '#ef4444'),
|
||||
('Feature', '#22c55e'),
|
||||
('Enhancement', '#3b82f6'),
|
||||
('Documentation', '#a855f7'),
|
||||
('Urgent', '#f97316')
|
||||
) AS label(name, color)
|
||||
ON CONFLICT (org_id, name) DO NOTHING;
|
||||
101
supabase/migrations/014_document_enhancements.sql
Normal file
101
supabase/migrations/014_document_enhancements.sql
Normal file
@@ -0,0 +1,101 @@
|
||||
-- Migration: Document enhancements for file paths, positions, and kanban type
|
||||
-- Adds path column for Google Drive-like unique paths
|
||||
-- Adds position column for file ordering within folders
|
||||
-- Extends type enum to include 'kanban' for kanban boards as files
|
||||
|
||||
-- Add path column (computed from parent hierarchy)
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS path TEXT;
|
||||
|
||||
-- Add position column for ordering files within a folder
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS position INTEGER DEFAULT 0;
|
||||
|
||||
-- Update the type constraint to allow 'kanban'
|
||||
-- First drop the existing constraint if it exists
|
||||
ALTER TABLE documents DROP CONSTRAINT IF EXISTS documents_type_check;
|
||||
|
||||
-- Add new constraint with kanban type
|
||||
ALTER TABLE documents ADD CONSTRAINT documents_type_check
|
||||
CHECK (type IN ('folder', 'document', 'kanban'));
|
||||
|
||||
-- Function to compute and update document path
|
||||
CREATE OR REPLACE FUNCTION compute_document_path(doc_id UUID)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
result TEXT := '';
|
||||
current_doc RECORD;
|
||||
path_parts TEXT[] := '{}';
|
||||
BEGIN
|
||||
-- Start with the document itself
|
||||
SELECT * INTO current_doc FROM documents WHERE id = doc_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Build path from document up to root
|
||||
WHILE current_doc IS NOT NULL LOOP
|
||||
path_parts := array_prepend(current_doc.name, path_parts);
|
||||
|
||||
IF current_doc.parent_id IS NULL THEN
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
SELECT * INTO current_doc FROM documents WHERE id = current_doc.parent_id;
|
||||
END LOOP;
|
||||
|
||||
-- Join with '/' separator
|
||||
result := '/' || array_to_string(path_parts, '/');
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to update path on insert/update
|
||||
CREATE OR REPLACE FUNCTION update_document_path()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.path := compute_document_path(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to auto-update path
|
||||
DROP TRIGGER IF EXISTS documents_path_trigger ON documents;
|
||||
CREATE TRIGGER documents_path_trigger
|
||||
BEFORE INSERT OR UPDATE OF name, parent_id ON documents
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_document_path();
|
||||
|
||||
-- Function to get next position in folder
|
||||
CREATE OR REPLACE FUNCTION get_next_document_position(folder_id UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
max_pos INTEGER;
|
||||
BEGIN
|
||||
SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
|
||||
FROM documents
|
||||
WHERE parent_id IS NOT DISTINCT FROM folder_id;
|
||||
|
||||
RETURN max_pos;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Update existing documents to have computed paths
|
||||
UPDATE documents SET path = compute_document_path(id) WHERE path IS NULL;
|
||||
|
||||
-- Update existing documents to have sequential positions within their folders
|
||||
WITH numbered AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY created_at) - 1 AS new_pos
|
||||
FROM documents
|
||||
WHERE position IS NULL OR position = 0
|
||||
)
|
||||
UPDATE documents d
|
||||
SET position = n.new_pos
|
||||
FROM numbered n
|
||||
WHERE d.id = n.id;
|
||||
|
||||
-- Create index on path for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_path ON documents(path);
|
||||
|
||||
-- Create index on parent_id and position for faster ordering
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_parent_position ON documents(parent_id, position);
|
||||
45
supabase/migrations/015_migrate_kanban_to_documents.sql
Normal file
45
supabase/migrations/015_migrate_kanban_to_documents.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- Migration: Move existing kanban boards to documents table
|
||||
-- This creates document entries for each kanban_board with type 'kanban'
|
||||
-- The content field stores reference to the board_id for backwards compatibility
|
||||
|
||||
-- Insert existing kanban boards as documents
|
||||
INSERT INTO documents (id, org_id, parent_id, type, name, path, position, content, created_by, created_at, updated_at)
|
||||
SELECT
|
||||
kb.id,
|
||||
kb.org_id,
|
||||
NULL as parent_id,
|
||||
'kanban' as type,
|
||||
kb.name,
|
||||
'/' || kb.name as path,
|
||||
COALESCE((
|
||||
SELECT COUNT(*) FROM documents d
|
||||
WHERE d.org_id = kb.org_id AND d.parent_id IS NULL
|
||||
), 0) as position,
|
||||
jsonb_build_object(
|
||||
'type', 'kanban',
|
||||
'board_id', kb.id
|
||||
) as content,
|
||||
COALESCE(kb.created_by, (
|
||||
SELECT user_id FROM org_members
|
||||
WHERE org_id = kb.org_id
|
||||
ORDER BY invited_at ASC
|
||||
LIMIT 1
|
||||
)) as created_by,
|
||||
kb.created_at,
|
||||
kb.created_at as updated_at
|
||||
FROM kanban_boards kb
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM documents d
|
||||
WHERE d.id = kb.id
|
||||
);
|
||||
|
||||
-- Update any duplicate paths by appending board ID
|
||||
UPDATE documents
|
||||
SET path = path || '-' || SUBSTRING(id::text, 1, 8)
|
||||
WHERE type = 'kanban'
|
||||
AND id IN (
|
||||
SELECT d1.id
|
||||
FROM documents d1
|
||||
JOIN documents d2 ON d1.path = d2.path AND d1.id != d2.id
|
||||
WHERE d1.type = 'kanban'
|
||||
);
|
||||
41
supabase/migrations/016_document_locks.sql
Normal file
41
supabase/migrations/016_document_locks.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Document locks: track who is currently editing a document
|
||||
-- Uses a heartbeat model: editors must refresh their lock periodically
|
||||
-- Stale locks (no heartbeat for 60s) are considered expired
|
||||
|
||||
CREATE TABLE document_locks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
locked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_heartbeat TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(document_id)
|
||||
);
|
||||
|
||||
-- Index for fast lookups
|
||||
CREATE INDEX idx_document_locks_document ON document_locks(document_id);
|
||||
CREATE INDEX idx_document_locks_heartbeat ON document_locks(last_heartbeat);
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE document_locks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Anyone in the org can view locks (to see who's editing)
|
||||
CREATE POLICY "Org members can view document locks" ON document_locks FOR SELECT
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM documents d
|
||||
JOIN org_members om ON d.org_id = om.org_id
|
||||
WHERE d.id = document_locks.document_id AND om.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Users can manage their own locks
|
||||
CREATE POLICY "Users can insert their own locks" ON document_locks FOR INSERT
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Users can update their own locks" ON document_locks FOR UPDATE
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Users can delete their own locks" ON document_locks FOR DELETE
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
-- Allow taking over expired locks (heartbeat older than 60 seconds)
|
||||
CREATE POLICY "Anyone can delete expired locks" ON document_locks FOR DELETE
|
||||
USING (last_heartbeat < now() - interval '60 seconds');
|
||||
28
supabase/migrations/017_avatars_storage.sql
Normal file
28
supabase/migrations/017_avatars_storage.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Create storage bucket for avatars
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('avatars', 'avatars', true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Allow authenticated users to upload to org-avatars folder
|
||||
CREATE POLICY "Authenticated users can upload org avatars"
|
||||
ON storage.objects FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'org-avatars');
|
||||
|
||||
-- Allow authenticated users to update (upsert) their org avatars
|
||||
CREATE POLICY "Authenticated users can update org avatars"
|
||||
ON storage.objects FOR UPDATE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'org-avatars');
|
||||
|
||||
-- Allow public read access to all avatars
|
||||
CREATE POLICY "Public read access for avatars"
|
||||
ON storage.objects FOR SELECT
|
||||
TO public
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- Allow authenticated users to delete org avatars
|
||||
CREATE POLICY "Authenticated users can delete org avatars"
|
||||
ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'org-avatars');
|
||||
Reference in New Issue
Block a user