- Config: try ENV_FILE, .env, ../.env for loading; trim trailing slash from BaseURL - Log BASE_URL at server startup for verification - .env.example: document BASE_URL - Tasks, projects, tags, migrations and related API/handlers Made-with: Cursor
309 lines
11 KiB
SQL
309 lines
11 KiB
SQL
-- Calendar & Contacts API Schema
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- Users
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
week_start_day SMALLINT NOT NULL DEFAULT 0,
|
|
date_format TEXT NOT NULL DEFAULT 'MM/dd/yyyy',
|
|
time_format TEXT NOT NULL DEFAULT '12h',
|
|
default_event_duration_minutes INTEGER NOT NULL DEFAULT 60,
|
|
default_reminder_minutes INTEGER NOT NULL DEFAULT 10,
|
|
show_weekends BOOLEAN NOT NULL DEFAULT true,
|
|
working_hours_start TEXT NOT NULL DEFAULT '09:00',
|
|
working_hours_end TEXT NOT NULL DEFAULT '17:00',
|
|
notifications_email BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;
|
|
|
|
-- Refresh Tokens
|
|
CREATE TABLE refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens (user_id);
|
|
|
|
-- API Keys
|
|
CREATE TABLE api_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
scopes JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
revoked_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_api_keys_user_id ON api_keys (user_id);
|
|
|
|
-- Calendars
|
|
CREATE TABLE calendars (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
color TEXT NOT NULL DEFAULT '#3B82F6',
|
|
is_public BOOLEAN NOT NULL DEFAULT false,
|
|
public_token TEXT,
|
|
count_for_availability BOOLEAN NOT NULL DEFAULT true,
|
|
default_reminder_minutes INTEGER,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_calendars_owner_id ON calendars (owner_id);
|
|
CREATE UNIQUE INDEX idx_calendars_public_token ON calendars (public_token) WHERE public_token IS NOT NULL;
|
|
|
|
-- Calendar Members
|
|
CREATE TABLE calendar_members (
|
|
calendar_id UUID NOT NULL REFERENCES calendars(id),
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
|
|
PRIMARY KEY (calendar_id, user_id)
|
|
);
|
|
|
|
-- Events
|
|
CREATE TABLE events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
calendar_id UUID NOT NULL REFERENCES calendars(id),
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
location TEXT,
|
|
start_time TIMESTAMPTZ NOT NULL,
|
|
end_time TIMESTAMPTZ NOT NULL,
|
|
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
all_day BOOLEAN NOT NULL DEFAULT false,
|
|
recurrence_rule TEXT,
|
|
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
updated_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_events_calendar_start ON events (calendar_id, start_time);
|
|
CREATE INDEX idx_events_start_time ON events (start_time);
|
|
CREATE INDEX idx_events_tags ON events USING GIN (tags);
|
|
|
|
-- Event Reminders
|
|
CREATE TABLE event_reminders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
minutes_before INTEGER NOT NULL CHECK (minutes_before >= 0 AND minutes_before <= 10080)
|
|
);
|
|
|
|
CREATE INDEX idx_event_reminders_event_id ON event_reminders (event_id);
|
|
|
|
-- Event Attendees
|
|
CREATE TABLE event_attendees (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
user_id UUID REFERENCES users(id),
|
|
email TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'tentative'))
|
|
);
|
|
|
|
CREATE INDEX idx_event_attendees_event_id ON event_attendees (event_id);
|
|
|
|
-- Event Exceptions (for recurrence)
|
|
CREATE TABLE event_exceptions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
exception_date DATE NOT NULL,
|
|
action TEXT NOT NULL DEFAULT 'skip' CHECK (action IN ('skip'))
|
|
);
|
|
|
|
CREATE INDEX idx_event_exceptions_event_id ON event_exceptions (event_id);
|
|
|
|
-- Event Attachments
|
|
CREATE TABLE event_attachments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
file_url TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX idx_event_attachments_event_id ON event_attachments (event_id);
|
|
|
|
-- Contacts
|
|
CREATE TABLE contacts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
first_name TEXT,
|
|
last_name TEXT,
|
|
email TEXT,
|
|
phone TEXT,
|
|
company TEXT,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_contacts_owner_id ON contacts (owner_id);
|
|
CREATE INDEX idx_contacts_search ON contacts (owner_id, first_name, last_name, email, company);
|
|
|
|
-- Booking Links
|
|
CREATE TABLE booking_links (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
calendar_id UUID NOT NULL REFERENCES calendars(id),
|
|
token TEXT NOT NULL UNIQUE,
|
|
duration_minutes INTEGER NOT NULL,
|
|
buffer_minutes INTEGER NOT NULL DEFAULT 0,
|
|
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
working_hours JSONB NOT NULL DEFAULT '{}',
|
|
active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_booking_links_token ON booking_links (token);
|
|
|
|
-- Audit Logs
|
|
CREATE TABLE audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
entity_type TEXT NOT NULL,
|
|
entity_id UUID NOT NULL,
|
|
action TEXT NOT NULL,
|
|
user_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_audit_logs_entity ON audit_logs (entity_type, entity_id);
|
|
|
|
-- Calendar Subscriptions (external iCal URL sources)
|
|
CREATE TABLE calendar_subscriptions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
calendar_id UUID NOT NULL REFERENCES calendars(id),
|
|
source_url TEXT NOT NULL,
|
|
last_synced_at TIMESTAMPTZ,
|
|
sync_interval_minutes INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_calendar_subscriptions_calendar_id ON calendar_subscriptions (calendar_id);
|
|
|
|
-- Projects (Layer 2)
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
color TEXT NOT NULL DEFAULT '#3B82F6',
|
|
is_shared BOOLEAN NOT NULL DEFAULT false,
|
|
deadline TIMESTAMPTZ,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_projects_owner_id ON projects (owner_id);
|
|
|
|
-- Project Members (Layer 2 shared projects)
|
|
CREATE TABLE project_members (
|
|
project_id UUID NOT NULL REFERENCES projects(id),
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
|
|
PRIMARY KEY (project_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX idx_project_members_project_id ON project_members (project_id);
|
|
|
|
-- Tags (Layer 2)
|
|
CREATE TABLE tags (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
name TEXT NOT NULL,
|
|
color TEXT NOT NULL DEFAULT '#6B7280',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_tags_owner_id ON tags (owner_id);
|
|
|
|
-- Tasks (Layer 1 core)
|
|
CREATE TABLE tasks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
status TEXT NOT NULL DEFAULT 'todo' CHECK (status IN ('todo', 'in_progress', 'done', 'archived')),
|
|
priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'critical')),
|
|
due_date TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ,
|
|
project_id UUID REFERENCES projects(id),
|
|
parent_id UUID REFERENCES tasks(id),
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
recurrence_rule TEXT
|
|
);
|
|
|
|
CREATE INDEX idx_tasks_owner_id ON tasks (owner_id);
|
|
CREATE INDEX idx_tasks_status ON tasks (owner_id, status) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tasks_priority ON tasks (owner_id, priority) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tasks_due_date ON tasks (owner_id, due_date) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tasks_project_id ON tasks (project_id) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tasks_parent_id ON tasks (parent_id) WHERE deleted_at IS NULL;
|
|
|
|
-- Task Tags (many-to-many)
|
|
CREATE TABLE task_tags (
|
|
task_id UUID NOT NULL REFERENCES tasks(id),
|
|
tag_id UUID NOT NULL REFERENCES tags(id),
|
|
PRIMARY KEY (task_id, tag_id)
|
|
);
|
|
|
|
CREATE INDEX idx_task_tags_task_id ON task_tags (task_id);
|
|
CREATE INDEX idx_task_tags_tag_id ON task_tags (tag_id);
|
|
|
|
-- Task Dependencies (Layer 3)
|
|
CREATE TABLE task_dependencies (
|
|
task_id UUID NOT NULL REFERENCES tasks(id),
|
|
blocks_task_id UUID NOT NULL REFERENCES tasks(id),
|
|
PRIMARY KEY (task_id, blocks_task_id),
|
|
CHECK (task_id != blocks_task_id)
|
|
);
|
|
|
|
CREATE INDEX idx_task_dependencies_task_id ON task_dependencies (task_id);
|
|
CREATE INDEX idx_task_dependencies_blocks ON task_dependencies (blocks_task_id);
|
|
|
|
-- Task Reminders (Layer 2)
|
|
CREATE TABLE task_reminders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
task_id UUID NOT NULL REFERENCES tasks(id),
|
|
type TEXT NOT NULL CHECK (type IN ('push', 'email', 'webhook', 'telegram', 'nostr')),
|
|
config JSONB NOT NULL DEFAULT '{}',
|
|
scheduled_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_task_reminders_task_id ON task_reminders (task_id);
|
|
CREATE INDEX idx_task_reminders_scheduled ON task_reminders (scheduled_at);
|
|
|
|
-- Task Webhooks (Layer 3)
|
|
CREATE TABLE task_webhooks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
url TEXT NOT NULL,
|
|
events JSONB NOT NULL DEFAULT '[]',
|
|
secret TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_task_webhooks_owner_id ON task_webhooks (owner_id);
|