Initial commit with Phase 0 Scaffolding
Some checks failed
Automated Container Build / build-and-push (push) Failing after 3s
CI / Backend (Python) (push) Failing after 28s
CI / Frontend (TypeScript) (push) Failing after 5m8s

This commit is contained in:
Elijah 2026-06-10 18:28:17 -07:00
parent b393607602
commit c545d4b17d
51 changed files with 7064 additions and 4 deletions

View file

@ -0,0 +1,13 @@
"""
SQLAlchemy models for PaperJet.
All models use UUIDv4 string primary keys (except the singleton settings row).
Timestamps are stored as ISO8601 TEXT columns.
"""
from app.models.settings import Settings
from app.models.document import Document
from app.models.annotation_state import AnnotationState
from app.models.version import Version
__all__ = ["Settings", "Document", "AnnotationState", "Version"]

View file

@ -0,0 +1,32 @@
"""AnnotationState model — current working annotation layer per document."""
from datetime import datetime, timezone
from sqlalchemy import ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
class AnnotationState(Base):
"""
Current working annotation state for a document.
One row per document. `data` is a JSON text column containing
the full annotation array opaque to the server except during export.
"""
__tablename__ = "annotation_states"
document_id: Mapped[str] = mapped_column(
Text, ForeignKey("documents.id", ondelete="CASCADE"), primary_key=True
)
data: Mapped[str] = mapped_column(Text, default="[]") # JSON array
updated_at: Mapped[str] = mapped_column(Text, default=_now_iso, onupdate=_now_iso)
# Relationships
document: Mapped["Document"] = relationship("Document", back_populates="annotation_state")

View file

@ -0,0 +1,58 @@
"""Document model — uploaded PDFs with soft-delete support."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _uuid() -> str:
return str(uuid.uuid4())
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
class Document(Base):
"""
A single uploaded PDF document.
Supports soft deletion via `deleted_at` when set, the document
is in the trash and can be restored or permanently purged.
"""
__tablename__ = "documents"
id: Mapped[str] = mapped_column(Text, primary_key=True, default=_uuid)
title: Mapped[str] = mapped_column(Text, nullable=False)
original_filename: Mapped[str] = mapped_column(Text, nullable=False)
file_path: Mapped[str] = mapped_column(Text, nullable=False)
thumbnail_path: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
page_count: Mapped[int] = mapped_column(Integer, nullable=False)
created_at: Mapped[str] = mapped_column(Text, default=_now_iso)
updated_at: Mapped[str] = mapped_column(Text, default=_now_iso, onupdate=_now_iso)
deleted_at: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
# Relationships
annotation_state: Mapped["AnnotationState"] = relationship(
"AnnotationState",
back_populates="document",
uselist=False,
cascade="all, delete-orphan",
)
versions: Mapped[list["Version"]] = relationship(
"Version",
back_populates="document",
cascade="all, delete-orphan",
order_by="Version.created_at.desc()",
)
__table_args__ = (
Index("ix_documents_updated_at", "updated_at"),
Index("ix_documents_deleted_at", "deleted_at"),
)

View file

@ -0,0 +1,31 @@
"""Settings model — singleton row for app-wide configuration."""
from datetime import datetime, timezone
from sqlalchemy import Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
class Settings(Base):
"""
Singleton settings row (id=1).
Stores the hashed password and app-level config.
password_hash is NULL until first-run setup.
"""
__tablename__ = "settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
password_hash: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
created_at: Mapped[str] = mapped_column(
Text,
default=lambda: datetime.now(timezone.utc).isoformat(),
)
updated_at: Mapped[str] = mapped_column(
Text,
default=lambda: datetime.now(timezone.utc).isoformat(),
onupdate=lambda: datetime.now(timezone.utc).isoformat(),
)

View file

@ -0,0 +1,45 @@
"""Version model — annotation state snapshots for history/recovery."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _uuid() -> str:
return str(uuid.uuid4())
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
class Version(Base):
"""
A snapshot of a document's annotation state at a point in time.
Versions serve as a safety net for accidental annotation loss.
`kind` is either 'manual' (user-created) or 'auto' (system-created
at safety boundaries like document close or before destructive ops).
"""
__tablename__ = "versions"
id: Mapped[str] = mapped_column(Text, primary_key=True, default=_uuid)
document_id: Mapped[str] = mapped_column(
Text, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
)
label: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
data: Mapped[str] = mapped_column(Text, nullable=False) # JSON snapshot
kind: Mapped[str] = mapped_column(Text, nullable=False) # 'manual' | 'auto'
created_at: Mapped[str] = mapped_column(Text, default=_now_iso)
# Relationships
document: Mapped["Document"] = relationship("Document", back_populates="versions")
__table_args__ = (
Index("ix_versions_document_created", "document_id", "created_at"),
)