Files
paliad/internal/db/migrations/060_user_pinned_projects.up.sql
m 8412328dec feat(t-paliad-149) PR1 step 1/3: backend — migration 060 + PinService + BuildTreeWithOptions
Migration 060 (paliad.user_pinned_projects): per-user, RLS owner-only, ON
DELETE CASCADE on both FKs.

PinService (Pin / Unpin / IsPinned / PinnedSet / ListPinned): visibility-
gates pin (can't pin what you can't see) but not unpin (so users can clean
up after losing access). PinnedSet returns a map for O(1) lookups during
tree stitching.

ProjectService.BuildTreeWithOptions extends BuildTree with chip-driven
filtering. New ProjectTreeNode fields are additive (Pinned,
InheritedVisibility, OpenDeadlinesSubtree, OverdueDeadlinesSubtree,
MatchKind) so the old BuildTree(ctx, userID) call still works for legacy
callers. New options:

  Scope: All / Mine / Pinned (Mine + Pinned both expand to path-closure
  with InheritedVisibility flag on greyed ancestors)
  StatusIn / TypeIn: chip-narrowing whitelists
  HasOpenDeadlines: per-node or subtree-aggregated, depending on
  IncludeSubtreeCounts
  SearchTerm: case-fold contains on title/reference/clientmatter, then
  prune to {matches ∪ ancestors ∪ descendants} with match_kind tagged
  IncludeSubtreeCounts: post-order DFS sums, O(N)

GET /api/projects/tree gains query params: scope, status, type,
has_open_deadlines, q, subtree_counts. Zero query string preserves
legacy behaviour.

POST/DELETE /api/projects/{id}/pin and GET /api/user-pinned-projects
wired. Service registered in cmd/server/main.go and dbServices.

build + vet clean.

Design: docs/design-projects-page-2026-05-07.md §4.7, §8.1, §8.3.
2026-05-07 22:21:45 +02:00

54 lines
2.2 KiB
SQL

-- t-paliad-149 PR 1: per-user project pins.
--
-- Design: docs/design-projects-page-2026-05-07.md §4.7 (godel,
-- m-locked 2026-05-07).
--
-- Stores per-user pinned projects. A user pins a project to mark it
-- as a favourite for the /projects page (chip "Angepinnt" filters
-- the tree to pinned-only; star marker on every row toggles state).
--
-- RLS scopes every operation to the calling user — pins are personal
-- working state, not project-team metadata. There is no cross-user
-- visibility in v1; no global_admin override.
--
-- ON DELETE CASCADE on both FKs: project deletion removes pin rows;
-- user deletion removes their pins. No referential drift possible.
--
-- Sections:
-- 1. CREATE paliad.user_pinned_projects (with RLS).
-- 2. Indexes.
-- ============================================================================
-- 1. paliad.user_pinned_projects
-- ============================================================================
CREATE TABLE paliad.user_pinned_projects (
user_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE,
pinned_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, project_id)
);
-- ============================================================================
-- 2. Indexes
-- ============================================================================
-- Hot path: list pins for a user, most-recently-pinned first. The PK
-- already covers (user_id, project_id), but a separate index ordered
-- by pinned_at DESC keeps the "Angepinnt" filter chip fast even as a
-- user accumulates many pins.
CREATE INDEX user_pinned_projects_user_idx
ON paliad.user_pinned_projects (user_id, pinned_at DESC);
-- ============================================================================
-- 3. RLS
-- ============================================================================
ALTER TABLE paliad.user_pinned_projects ENABLE ROW LEVEL SECURITY;
-- Owner-only access. No global_admin override.
CREATE POLICY user_pinned_projects_owner_all
ON paliad.user_pinned_projects FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());