M2 labels backend: labels + note_labels, CRUD, note-label set, filter
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 9s
CI & Build / Build & push image (push) Successful in 32s

- Label + NoteLabel models; migration 0004 (labels unique per owner + note_labels
  join, cascade).
- /api/labels: list/create(idempotent)/rename(clash-checked)/delete, owner-scoped.
- PUT /api/notes/<id>/labels to set a note's labels (validated against owned).
- Note responses now include labels[] (merged via one explicit join query — no
  lazy relationship); GET /api/notes?...&label=<id> filters by label.
- DB-free auth-guard tests for labels endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-19 21:43:21 -04:00
co-authored by Claude Opus 4.8
parent fdaf5c370c
commit 4d1fc1bdf9
7 changed files with 267 additions and 14 deletions
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from . import Base
class Label(Base):
__tablename__ = "labels"
__table_args__ = (UniqueConstraint("owner_id", "name", name="uq_labels_owner_name"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
owner_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(Text(), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
class NoteLabel(Base):
"""Join between a note and a label (both owned by the same user)."""
__tablename__ = "note_labels"
note_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("notes.id", ondelete="CASCADE"), primary_key=True
)
label_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("labels.id", ondelete="CASCADE"), primary_key=True
)