Fix BASE_URL config loading, add tasks/projects; robust .env path resolution
- 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
This commit is contained in:
54
sqlc/queries/projects.sql
Normal file
54
sqlc/queries/projects.sql
Normal file
@@ -0,0 +1,54 @@
|
||||
-- name: CreateProject :one
|
||||
INSERT INTO projects (id, owner_id, name, color, is_shared, deadline, sort_order)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, false), $6, COALESCE($7, 0))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetProjectByID :one
|
||||
SELECT * FROM projects
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
|
||||
|
||||
-- name: ListProjects :many
|
||||
SELECT * FROM projects
|
||||
WHERE owner_id = @owner_id AND deleted_at IS NULL
|
||||
ORDER BY sort_order ASC, name ASC;
|
||||
|
||||
-- name: ListProjectsForUser :many
|
||||
SELECT p.* FROM projects p
|
||||
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = @user_id
|
||||
WHERE (p.owner_id = @user_id OR pm.user_id = @user_id)
|
||||
AND p.deleted_at IS NULL
|
||||
ORDER BY p.sort_order ASC, p.name ASC;
|
||||
|
||||
-- name: UpdateProject :one
|
||||
UPDATE projects
|
||||
SET name = COALESCE(sqlc.narg('name'), name),
|
||||
color = COALESCE(sqlc.narg('color'), color),
|
||||
is_shared = COALESCE(sqlc.narg('is_shared'), is_shared),
|
||||
deadline = sqlc.narg('deadline'),
|
||||
sort_order = COALESCE(sqlc.narg('sort_order'), sort_order),
|
||||
updated_at = now()
|
||||
WHERE id = @id AND owner_id = @owner_id AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: SoftDeleteProject :exec
|
||||
UPDATE projects SET deleted_at = now(), updated_at = now()
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
|
||||
|
||||
-- name: AddProjectMember :exec
|
||||
INSERT INTO project_members (project_id, user_id, role)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (project_id, user_id) DO UPDATE SET role = EXCLUDED.role;
|
||||
|
||||
-- name: RemoveProjectMember :exec
|
||||
DELETE FROM project_members
|
||||
WHERE project_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: ListProjectMembers :many
|
||||
SELECT pm.project_id, pm.user_id, pm.role, u.email
|
||||
FROM project_members pm
|
||||
JOIN users u ON u.id = pm.user_id
|
||||
WHERE pm.project_id = $1;
|
||||
|
||||
-- name: GetProjectMember :one
|
||||
SELECT * FROM project_members
|
||||
WHERE project_id = $1 AND user_id = $2;
|
||||
40
sqlc/queries/tags.sql
Normal file
40
sqlc/queries/tags.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- name: CreateTag :one
|
||||
INSERT INTO tags (id, owner_id, name, color)
|
||||
VALUES ($1, $2, $3, COALESCE($4, '#6B7280'))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTagByID :one
|
||||
SELECT * FROM tags
|
||||
WHERE id = $1 AND owner_id = $2;
|
||||
|
||||
-- name: ListTagsByOwner :many
|
||||
SELECT * FROM tags
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name ASC;
|
||||
|
||||
-- name: UpdateTag :one
|
||||
UPDATE tags
|
||||
SET name = COALESCE(sqlc.narg('name'), name),
|
||||
color = COALESCE(sqlc.narg('color'), color)
|
||||
WHERE id = @id AND owner_id = @owner_id
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteTag :exec
|
||||
DELETE FROM tags WHERE id = $1 AND owner_id = $2;
|
||||
|
||||
-- name: AttachTagToTask :exec
|
||||
INSERT INTO task_tags (task_id, tag_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (task_id, tag_id) DO NOTHING;
|
||||
|
||||
-- name: DetachTagFromTask :exec
|
||||
DELETE FROM task_tags
|
||||
WHERE task_id = $1 AND tag_id = $2;
|
||||
|
||||
-- name: ListTagsByTask :many
|
||||
SELECT t.* FROM tags t
|
||||
JOIN task_tags tt ON tt.tag_id = t.id
|
||||
WHERE tt.task_id = $1;
|
||||
|
||||
-- name: ListTaskIDsByTag :many
|
||||
SELECT task_id FROM task_tags WHERE tag_id = $1;
|
||||
25
sqlc/queries/task_dependencies.sql
Normal file
25
sqlc/queries/task_dependencies.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- name: AddTaskDependency :exec
|
||||
INSERT INTO task_dependencies (task_id, blocks_task_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (task_id, blocks_task_id) DO NOTHING;
|
||||
|
||||
-- name: RemoveTaskDependency :exec
|
||||
DELETE FROM task_dependencies
|
||||
WHERE task_id = $1 AND blocks_task_id = $2;
|
||||
|
||||
-- name: ListTaskBlockers :many
|
||||
SELECT t.* FROM tasks t
|
||||
JOIN task_dependencies td ON td.blocks_task_id = t.id
|
||||
WHERE td.task_id = $1 AND t.deleted_at IS NULL;
|
||||
|
||||
-- name: ListTasksBlockedBy :many
|
||||
SELECT t.* FROM tasks t
|
||||
JOIN task_dependencies td ON td.task_id = t.id
|
||||
WHERE td.blocks_task_id = $1 AND t.deleted_at IS NULL;
|
||||
|
||||
-- name: CheckDirectCircularDependency :one
|
||||
-- Direct cycle: blocks_task_id is blocked by task_id (would create A->B and B->A)
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM task_dependencies
|
||||
WHERE task_id = $2 AND blocks_task_id = $1
|
||||
) AS has_circle;
|
||||
26
sqlc/queries/task_reminders.sql
Normal file
26
sqlc/queries/task_reminders.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- name: CreateTaskReminder :one
|
||||
INSERT INTO task_reminders (id, task_id, type, config, scheduled_at)
|
||||
VALUES ($1, $2, $3, COALESCE($4, '{}'), $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTaskReminderByID :one
|
||||
SELECT * FROM task_reminders
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListTaskReminders :many
|
||||
SELECT * FROM task_reminders
|
||||
WHERE task_id = $1
|
||||
ORDER BY scheduled_at ASC;
|
||||
|
||||
-- name: ListTaskRemindersDueBefore :many
|
||||
SELECT tr.*, t.owner_id, t.title
|
||||
FROM task_reminders tr
|
||||
JOIN tasks t ON t.id = tr.task_id
|
||||
WHERE tr.scheduled_at <= $1
|
||||
AND t.deleted_at IS NULL;
|
||||
|
||||
-- name: DeleteTaskReminder :exec
|
||||
DELETE FROM task_reminders WHERE id = $1;
|
||||
|
||||
-- name: DeleteTaskRemindersByTask :exec
|
||||
DELETE FROM task_reminders WHERE task_id = $1;
|
||||
16
sqlc/queries/task_webhooks.sql
Normal file
16
sqlc/queries/task_webhooks.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- name: CreateTaskWebhook :one
|
||||
INSERT INTO task_webhooks (id, owner_id, url, events, secret)
|
||||
VALUES ($1, $2, $3, COALESCE($4, '[]'), $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTaskWebhookByID :one
|
||||
SELECT * FROM task_webhooks
|
||||
WHERE id = $1 AND owner_id = $2;
|
||||
|
||||
-- name: ListTaskWebhooksByOwner :many
|
||||
SELECT * FROM task_webhooks
|
||||
WHERE owner_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: DeleteTaskWebhook :exec
|
||||
DELETE FROM task_webhooks WHERE id = $1 AND owner_id = $2;
|
||||
125
sqlc/queries/tasks.sql
Normal file
125
sqlc/queries/tasks.sql
Normal file
@@ -0,0 +1,125 @@
|
||||
-- name: CreateTask :one
|
||||
INSERT INTO tasks (id, owner_id, title, description, status, priority, due_date, project_id, parent_id, sort_order, recurrence_rule)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTaskByID :one
|
||||
SELECT * FROM tasks
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetTaskByIDForUpdate :one
|
||||
SELECT * FROM tasks
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
-- name: ListTasks :many
|
||||
SELECT t.* FROM tasks t
|
||||
WHERE t.owner_id = @owner_id
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.parent_id IS NULL
|
||||
AND (sqlc.narg('status')::TEXT IS NULL OR t.status = sqlc.narg('status')::TEXT)
|
||||
AND (sqlc.narg('priority')::TEXT IS NULL OR t.priority = sqlc.narg('priority')::TEXT)
|
||||
AND (sqlc.narg('project_id')::UUID IS NULL OR t.project_id = sqlc.narg('project_id')::UUID)
|
||||
AND (sqlc.narg('due_from')::TIMESTAMPTZ IS NULL OR t.due_date >= sqlc.narg('due_from')::TIMESTAMPTZ)
|
||||
AND (sqlc.narg('due_to')::TIMESTAMPTZ IS NULL OR t.due_date <= sqlc.narg('due_to')::TIMESTAMPTZ)
|
||||
AND (
|
||||
sqlc.narg('cursor_time')::TIMESTAMPTZ IS NULL
|
||||
OR (t.created_at, t.id) < (sqlc.narg('cursor_time')::TIMESTAMPTZ, sqlc.narg('cursor_id')::UUID)
|
||||
)
|
||||
ORDER BY t.created_at DESC, t.id DESC
|
||||
LIMIT @lim;
|
||||
|
||||
-- name: ListTasksByDueDate :many
|
||||
SELECT t.* FROM tasks t
|
||||
WHERE t.owner_id = @owner_id
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.parent_id IS NULL
|
||||
AND (sqlc.narg('status')::TEXT IS NULL OR t.status = sqlc.narg('status')::TEXT)
|
||||
AND (sqlc.narg('priority')::TEXT IS NULL OR t.priority = sqlc.narg('priority')::TEXT)
|
||||
AND (sqlc.narg('project_id')::UUID IS NULL OR t.project_id = sqlc.narg('project_id')::UUID)
|
||||
AND (sqlc.narg('due_from')::TIMESTAMPTZ IS NULL OR t.due_date >= sqlc.narg('due_from')::TIMESTAMPTZ)
|
||||
AND (sqlc.narg('due_to')::TIMESTAMPTZ IS NULL OR t.due_date <= sqlc.narg('due_to')::TIMESTAMPTZ)
|
||||
ORDER BY COALESCE(t.due_date, '9999-12-31'::timestamptz) ASC NULLS LAST, t.id ASC
|
||||
LIMIT @lim;
|
||||
|
||||
-- name: ListTasksByPriority :many
|
||||
SELECT t.* FROM tasks t
|
||||
WHERE t.owner_id = @owner_id
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.parent_id IS NULL
|
||||
AND (sqlc.narg('status')::TEXT IS NULL OR t.status = sqlc.narg('status')::TEXT)
|
||||
AND (sqlc.narg('priority')::TEXT IS NULL OR t.priority = sqlc.narg('priority')::TEXT)
|
||||
AND (sqlc.narg('project_id')::UUID IS NULL OR t.project_id = sqlc.narg('project_id')::UUID)
|
||||
AND (sqlc.narg('due_from')::TIMESTAMPTZ IS NULL OR t.due_date >= sqlc.narg('due_from')::TIMESTAMPTZ)
|
||||
AND (sqlc.narg('due_to')::TIMESTAMPTZ IS NULL OR t.due_date <= sqlc.narg('due_to')::TIMESTAMPTZ)
|
||||
ORDER BY CASE t.priority
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
ELSE 5
|
||||
END ASC, t.created_at DESC, t.id DESC
|
||||
LIMIT @lim;
|
||||
|
||||
-- name: ListTasksWithTag :many
|
||||
SELECT DISTINCT t.* FROM tasks t
|
||||
JOIN task_tags tt ON tt.task_id = t.id
|
||||
WHERE t.owner_id = @owner_id
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.parent_id IS NULL
|
||||
AND tt.tag_id = ANY(@tag_ids)
|
||||
AND (sqlc.narg('status')::TEXT IS NULL OR t.status = sqlc.narg('status')::TEXT)
|
||||
AND (sqlc.narg('project_id')::UUID IS NULL OR t.project_id = sqlc.narg('project_id')::UUID)
|
||||
ORDER BY t.created_at DESC, t.id DESC
|
||||
LIMIT @lim;
|
||||
|
||||
-- name: UpdateTask :one
|
||||
UPDATE tasks
|
||||
SET title = COALESCE(sqlc.narg('title'), title),
|
||||
description = COALESCE(sqlc.narg('description'), description),
|
||||
status = COALESCE(sqlc.narg('status'), status),
|
||||
priority = COALESCE(sqlc.narg('priority'), priority),
|
||||
due_date = sqlc.narg('due_date'),
|
||||
project_id = sqlc.narg('project_id'),
|
||||
sort_order = COALESCE(sqlc.narg('sort_order'), sort_order),
|
||||
recurrence_rule = sqlc.narg('recurrence_rule'),
|
||||
updated_at = now()
|
||||
WHERE id = @id AND owner_id = @owner_id AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: SoftDeleteTask :exec
|
||||
UPDATE tasks SET deleted_at = now(), updated_at = now()
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
|
||||
|
||||
-- name: HardDeleteTask :exec
|
||||
DELETE FROM tasks WHERE id = $1 AND owner_id = $2;
|
||||
|
||||
-- name: MarkTaskComplete :one
|
||||
UPDATE tasks
|
||||
SET status = 'done', completed_at = now(), updated_at = now()
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: MarkTaskUncomplete :one
|
||||
UPDATE tasks
|
||||
SET status = COALESCE(sqlc.narg('new_status'), 'todo'), completed_at = NULL, updated_at = now()
|
||||
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListSubtasks :many
|
||||
SELECT * FROM tasks
|
||||
WHERE parent_id = $1 AND owner_id = $2 AND deleted_at IS NULL
|
||||
ORDER BY sort_order ASC, created_at ASC;
|
||||
|
||||
-- name: CountSubtasksByStatus :one
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'done') AS done_count,
|
||||
COUNT(*) AS total_count
|
||||
FROM tasks
|
||||
WHERE parent_id = $1 AND deleted_at IS NULL;
|
||||
|
||||
-- name: ListTasksWithRecurrence :many
|
||||
SELECT * FROM tasks
|
||||
WHERE owner_id = $1 AND deleted_at IS NULL
|
||||
AND recurrence_rule IS NOT NULL
|
||||
AND parent_id IS NULL;
|
||||
109
sqlc/schema.sql
109
sqlc/schema.sql
@@ -197,3 +197,112 @@ CREATE TABLE calendar_subscriptions (
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user