Structure: - control-plane/: Registries, SIMCO directives, CI/CD templates - projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics - shared/: Libs catalog, knowledge-base Key features: - Centralized port, domain, database, and service registries - 23 SIMCO directives + 6 fundamental principles - NEXUS agent profiles with delegation rules - Validation scripts for workspace integrity - Dockerfiles for all services - Path aliases for quick reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
303 lines
11 KiB
PL/PgSQL
303 lines
11 KiB
PL/PgSQL
-- =============================================================================
|
|
-- Table: communication.messages
|
|
-- Description: Messages and chat system for teacher-student communication
|
|
-- Priority: P1 - Important for communication features
|
|
-- User Stories: US-PM-005 (Teacher Communication), US-AE-006 (Communication Management)
|
|
-- Created: 2025-11-19
|
|
-- =============================================================================
|
|
|
|
-- Drop table if exists
|
|
DROP TABLE IF EXISTS communication.messages CASCADE;
|
|
|
|
-- Create messages table
|
|
CREATE TABLE communication.messages (
|
|
-- Primary key
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Message relationships
|
|
sender_id UUID NOT NULL
|
|
REFERENCES auth_management.profiles(id) ON DELETE CASCADE,
|
|
recipient_id UUID
|
|
REFERENCES auth_management.profiles(id) ON DELETE CASCADE,
|
|
|
|
-- Context (where message belongs)
|
|
classroom_id UUID
|
|
REFERENCES social_features.classrooms(id) ON DELETE CASCADE,
|
|
thread_id UUID
|
|
REFERENCES communication.messages(id) ON DELETE CASCADE, -- For threaded conversations
|
|
parent_message_id UUID
|
|
REFERENCES communication.messages(id) ON DELETE SET NULL, -- For replies
|
|
|
|
-- Message content
|
|
subject VARCHAR(255), -- For direct messages
|
|
content TEXT NOT NULL,
|
|
message_type VARCHAR(50) NOT NULL DEFAULT 'direct',
|
|
-- Types: 'direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment'
|
|
|
|
-- Attachments and media
|
|
attachments JSONB DEFAULT '[]'::jsonb,
|
|
-- Example: [{"type": "file", "url": "...", "name": "...", "size": 1024}]
|
|
|
|
-- Message status
|
|
is_read BOOLEAN DEFAULT FALSE,
|
|
read_at TIMESTAMPTZ,
|
|
is_deleted BOOLEAN DEFAULT FALSE,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL,
|
|
|
|
-- Priority and flags
|
|
priority VARCHAR(20) DEFAULT 'normal', -- 'low', 'normal', 'high', 'urgent'
|
|
is_pinned BOOLEAN DEFAULT FALSE,
|
|
is_archived BOOLEAN DEFAULT FALSE,
|
|
requires_response BOOLEAN DEFAULT FALSE,
|
|
response_deadline TIMESTAMPTZ,
|
|
|
|
-- Reactions and engagement
|
|
reactions JSONB DEFAULT '{}'::jsonb,
|
|
-- Example: {"thumbs_up": ["user-uuid-1", "user-uuid-2"], "heart": ["user-uuid-3"]}
|
|
|
|
-- Moderation
|
|
is_flagged BOOLEAN DEFAULT FALSE,
|
|
flagged_reason TEXT,
|
|
flagged_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL,
|
|
flagged_at TIMESTAMPTZ,
|
|
moderation_status VARCHAR(50) DEFAULT 'approved',
|
|
-- 'approved', 'pending', 'flagged', 'removed'
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
-- Additional context: assignment_id, exercise_id, etc.
|
|
|
|
-- Audit fields
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
edited_at TIMESTAMPTZ,
|
|
edit_count INTEGER DEFAULT 0,
|
|
|
|
-- Constraints
|
|
CONSTRAINT messages_type_valid
|
|
CHECK (message_type IN ('direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment', 'system')),
|
|
CONSTRAINT messages_priority_valid
|
|
CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
|
|
CONSTRAINT messages_moderation_status_valid
|
|
CHECK (moderation_status IN ('approved', 'pending', 'flagged', 'removed')),
|
|
CONSTRAINT messages_recipient_or_classroom
|
|
CHECK (recipient_id IS NOT NULL OR classroom_id IS NOT NULL)
|
|
-- Must have either recipient (direct) or classroom (broadcast)
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- Indexes for performance
|
|
-- =============================================================================
|
|
|
|
-- Index for sender's sent messages
|
|
CREATE INDEX idx_messages_sender
|
|
ON communication.messages(sender_id, created_at DESC)
|
|
WHERE is_deleted = FALSE;
|
|
|
|
-- Index for recipient's inbox
|
|
CREATE INDEX idx_messages_recipient
|
|
ON communication.messages(recipient_id, created_at DESC)
|
|
WHERE is_deleted = FALSE;
|
|
|
|
-- Index for classroom messages/announcements
|
|
CREATE INDEX idx_messages_classroom
|
|
ON communication.messages(classroom_id, created_at DESC)
|
|
WHERE is_deleted = FALSE;
|
|
|
|
-- Index for unread messages (important for notifications)
|
|
CREATE INDEX idx_messages_unread
|
|
ON communication.messages(recipient_id, created_at DESC)
|
|
WHERE is_read = FALSE AND is_deleted = FALSE;
|
|
|
|
-- Index for threaded conversations
|
|
CREATE INDEX idx_messages_thread
|
|
ON communication.messages(thread_id, created_at ASC)
|
|
WHERE thread_id IS NOT NULL AND is_deleted = FALSE;
|
|
|
|
-- Index for parent-child reply structure
|
|
CREATE INDEX idx_messages_parent
|
|
ON communication.messages(parent_message_id, created_at ASC)
|
|
WHERE parent_message_id IS NOT NULL AND is_deleted = FALSE;
|
|
|
|
-- Index for flagged messages (moderation)
|
|
CREATE INDEX idx_messages_flagged
|
|
ON communication.messages(flagged_at DESC)
|
|
WHERE is_flagged = TRUE;
|
|
|
|
-- Index for messages requiring response
|
|
CREATE INDEX idx_messages_requiring_response
|
|
ON communication.messages(recipient_id, response_deadline)
|
|
WHERE requires_response = TRUE AND is_deleted = FALSE;
|
|
|
|
-- Composite index for classroom + type (efficient filtering)
|
|
CREATE INDEX idx_messages_classroom_type
|
|
ON communication.messages(classroom_id, message_type, created_at DESC)
|
|
WHERE is_deleted = FALSE;
|
|
|
|
-- GIN index for searching attachments
|
|
CREATE INDEX idx_messages_attachments
|
|
ON communication.messages USING GIN(attachments);
|
|
|
|
-- GIN index for metadata search
|
|
CREATE INDEX idx_messages_metadata
|
|
ON communication.messages USING GIN(metadata);
|
|
|
|
-- =============================================================================
|
|
-- Triggers
|
|
-- =============================================================================
|
|
|
|
-- Trigger for updated_at timestamp
|
|
CREATE OR REPLACE FUNCTION communication.update_messages_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
|
|
-- Track edits
|
|
IF OLD.content IS DISTINCT FROM NEW.content THEN
|
|
NEW.edited_at = NOW();
|
|
NEW.edit_count = COALESCE(OLD.edit_count, 0) + 1;
|
|
END IF;
|
|
|
|
-- Auto-set read_at when is_read changes to true
|
|
IF NEW.is_read = TRUE AND OLD.is_read = FALSE THEN
|
|
NEW.read_at = NOW();
|
|
END IF;
|
|
|
|
-- Auto-set deleted_at when is_deleted changes to true
|
|
IF NEW.is_deleted = TRUE AND OLD.is_deleted = FALSE THEN
|
|
NEW.deleted_at = NOW();
|
|
END IF;
|
|
|
|
-- Auto-set flagged_at when is_flagged changes to true
|
|
IF NEW.is_flagged = TRUE AND OLD.is_flagged = FALSE THEN
|
|
NEW.flagged_at = NOW();
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trigger_update_messages_timestamp
|
|
BEFORE UPDATE ON communication.messages
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION communication.update_messages_timestamp();
|
|
|
|
-- =============================================================================
|
|
-- Helper function to get unread count
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION communication.get_unread_count(
|
|
p_user_id UUID,
|
|
p_classroom_id UUID DEFAULT NULL
|
|
) RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_count INTEGER;
|
|
BEGIN
|
|
SELECT COUNT(*)
|
|
INTO v_count
|
|
FROM communication.messages
|
|
WHERE recipient_id = p_user_id
|
|
AND is_read = FALSE
|
|
AND is_deleted = FALSE
|
|
AND (p_classroom_id IS NULL OR classroom_id = p_classroom_id);
|
|
|
|
RETURN v_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- =============================================================================
|
|
-- Helper function to mark conversation as read
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION communication.mark_conversation_read(
|
|
p_user_id UUID,
|
|
p_thread_id UUID
|
|
) RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_updated INTEGER;
|
|
BEGIN
|
|
UPDATE communication.messages
|
|
SET is_read = TRUE,
|
|
read_at = NOW()
|
|
WHERE recipient_id = p_user_id
|
|
AND thread_id = p_thread_id
|
|
AND is_read = FALSE
|
|
AND is_deleted = FALSE;
|
|
|
|
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
RETURN v_updated;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- =============================================================================
|
|
-- View: Recent classroom messages (for chat widget)
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE VIEW communication.recent_classroom_messages AS
|
|
SELECT
|
|
m.id,
|
|
m.classroom_id,
|
|
m.sender_id,
|
|
p.display_name as sender_name,
|
|
p.avatar_url as sender_avatar,
|
|
m.content,
|
|
m.message_type,
|
|
m.attachments,
|
|
m.reactions,
|
|
m.is_pinned,
|
|
m.created_at,
|
|
m.edited_at,
|
|
m.edit_count,
|
|
-- Count replies
|
|
(SELECT COUNT(*) FROM communication.messages replies
|
|
WHERE replies.parent_message_id = m.id
|
|
AND replies.is_deleted = FALSE) as reply_count
|
|
FROM communication.messages m
|
|
JOIN auth_management.profiles p ON p.id = m.sender_id
|
|
WHERE m.classroom_id IS NOT NULL
|
|
AND m.is_deleted = FALSE
|
|
AND m.parent_message_id IS NULL -- Only top-level messages
|
|
ORDER BY
|
|
CASE WHEN m.is_pinned THEN 0 ELSE 1 END,
|
|
m.created_at DESC;
|
|
|
|
-- =============================================================================
|
|
-- Comments for documentation
|
|
-- =============================================================================
|
|
|
|
COMMENT ON TABLE communication.messages IS
|
|
'Messages and chat system for teacher-student communication. Supports direct messages, classroom announcements, threaded conversations, and assignment feedback.';
|
|
|
|
COMMENT ON COLUMN communication.messages.message_type IS
|
|
'Type of message: direct (1-to-1), classroom_announcement (teacher to all), classroom_chat (group), private_feedback (assignment/exercise feedback), assignment_comment';
|
|
|
|
COMMENT ON COLUMN communication.messages.attachments IS
|
|
'JSON array of file attachments with structure: [{"type": "file|image|video", "url": "...", "name": "...", "size": 1024, "mime_type": "..."}]';
|
|
|
|
COMMENT ON COLUMN communication.messages.reactions IS
|
|
'JSON object tracking emoji reactions: {"thumbs_up": ["user-uuid-1"], "heart": ["user-uuid-2", "user-uuid-3"]}';
|
|
|
|
COMMENT ON COLUMN communication.messages.thread_id IS
|
|
'For grouping messages in a conversation thread. Set to the first message ID in the thread.';
|
|
|
|
COMMENT ON COLUMN communication.messages.parent_message_id IS
|
|
'For direct replies to a specific message. Creates a tree structure within threads.';
|
|
|
|
COMMENT ON FUNCTION communication.get_unread_count IS
|
|
'Get count of unread messages for a user, optionally filtered by classroom.
|
|
Usage: SELECT communication.get_unread_count(user_uuid, classroom_uuid);';
|
|
|
|
COMMENT ON FUNCTION communication.mark_conversation_read IS
|
|
'Mark all messages in a thread as read for a user.
|
|
Usage: SELECT communication.mark_conversation_read(user_uuid, thread_uuid);';
|
|
|
|
-- =============================================================================
|
|
-- Grant permissions
|
|
-- =============================================================================
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON communication.messages TO gamilit_user;
|
|
GRANT SELECT ON communication.recent_classroom_messages TO gamilit_user;
|
|
GRANT EXECUTE ON FUNCTION communication.get_unread_count TO gamilit_user;
|
|
GRANT EXECUTE ON FUNCTION communication.mark_conversation_read TO gamilit_user;
|