Initial commit with Phase 0 Scaffolding
This commit is contained in:
parent
b393607602
commit
c545d4b17d
51 changed files with 7064 additions and 4 deletions
13
backend/app/models/__init__.py
Normal file
13
backend/app/models/__init__.py
Normal 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"]
|
||||
32
backend/app/models/annotation_state.py
Normal file
32
backend/app/models/annotation_state.py
Normal 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")
|
||||
58
backend/app/models/document.py
Normal file
58
backend/app/models/document.py
Normal 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"),
|
||||
)
|
||||
31
backend/app/models/settings.py
Normal file
31
backend/app/models/settings.py
Normal 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(),
|
||||
)
|
||||
45
backend/app/models/version.py
Normal file
45
backend/app/models/version.py
Normal 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"),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue