"""Database ORM models.
This module defines all SQLAlchemy ORM models for the application.
"""
from datetime import datetime
from typing import Any
from uuid import uuid4
from sqlalchemy import (
Boolean,
DateTime,
Enum,
ForeignKey,
Integer,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.core.enums import (
AuditAction,
CommentStatus,
DocumentStatus,
RevisionAction,
UserRole,
)
from src.database.base import Base
class User(Base):
"""User account model.
Attributes:
id: Unique identifier.
uuid: Public UUID for API references.
email: User email address.
username: User display name.
hashed_password: Bcrypt hashed password.
role: User role (admin, editor, etc.).
is_active: Whether the user is active.
is_verified: Whether email is verified.
created_at: Account creation timestamp.
updated_at: Last update timestamp.
last_login: Last login timestamp.
"""
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=lambda: str(uuid4())
)
email: Mapped[str] = mapped_column(
String(255), unique=True, nullable=False, index=True
)
username: Mapped[str] = mapped_column(String(100), nullable=False)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(
Enum(UserRole), default=UserRole.VIEWER, nullable=False
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
last_login: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Relationships
documents: Mapped[list["Document"]] = relationship(
"Document", back_populates="owner", cascade="all, delete-orphan"
)
comments: Mapped[list["Comment"]] = relationship(
"Comment", back_populates="author", cascade="all, delete-orphan"
)
revisions: Mapped[list["Revision"]] = relationship(
"Revision", back_populates="author", cascade="all, delete-orphan"
)
audit_logs: Mapped[list["AuditLog"]] = relationship(
"AuditLog", back_populates="user", cascade="all, delete-orphan"
)
class Document(Base):
"""Document model for storing DOCX documents.
Attributes:
id: Unique identifier.
uuid: Public UUID for API references.
title: Document title.
description: Document description.
file_path: Path to the stored file.
file_size: File size in bytes.
mime_type: File MIME type.
status: Document status.
version: Current version number.
doc_metadata: Additional metadata as JSON.
owner_id: Reference to the owner user.
created_at: Creation timestamp.
updated_at: Last update timestamp.
deleted_at: Soft delete timestamp.
"""
__tablename__ = "documents"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=lambda: str(uuid4())
)
title: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
file_path: Mapped[str] = mapped_column(String(500), nullable=False)
file_size: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
mime_type: Mapped[str] = mapped_column(String(100), nullable=False)
status: Mapped[DocumentStatus] = mapped_column(
Enum(DocumentStatus), default=DocumentStatus.DRAFT, nullable=False
)
version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
doc_metadata: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
owner_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Relationships
owner: Mapped["User"] = relationship("User", back_populates="documents")
versions: Mapped[list["DocumentVersion"]] = relationship(
"DocumentVersion", back_populates="document", cascade="all, delete-orphan"
)
comments: Mapped[list["Comment"]] = relationship(
"Comment", back_populates="document", cascade="all, delete-orphan"
)
revisions: Mapped[list["Revision"]] = relationship(
"Revision", back_populates="document", cascade="all, delete-orphan"
)
class DocumentVersion(Base):
"""Document version history model.
Attributes:
id: Unique identifier.
document_id: Reference to the parent document.
version_number: Version number.
file_path: Path to the version file.
file_size: File size in bytes.
change_summary: Summary of changes.
created_by_id: User who created this version.
created_at: Version creation timestamp.
"""
__tablename__ = "document_versions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
document_id: Mapped[int] = mapped_column(
Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
file_path: Mapped[str] = mapped_column(String(500), nullable=False)
file_size: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
change_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Relationships
document: Mapped["Document"] = relationship("Document", back_populates="versions")
class Comment(Base):
"""Document comment/annotation model.
Attributes:
id: Unique identifier.
uuid: Public UUID for API references.
document_id: Reference to the document.
author_id: Reference to the comment author.
content: Comment text content.
status: Comment status.
paragraph_index: Paragraph where comment is attached.
start_offset: Character offset start.
end_offset: Character offset end.
parent_id: Parent comment for replies.
created_at: Creation timestamp.
updated_at: Last update timestamp.
resolved_at: Resolution timestamp.
"""
__tablename__ = "comments"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=lambda: str(uuid4())
)
document_id: Mapped[int] = mapped_column(
Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
)
author_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
content: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[CommentStatus] = mapped_column(
Enum(CommentStatus), default=CommentStatus.OPEN, nullable=False
)
paragraph_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
start_offset: Mapped[int | None] = mapped_column(Integer, nullable=True)
end_offset: Mapped[int | None] = mapped_column(Integer, nullable=True)
parent_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("comments.id", ondelete="CASCADE"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
resolved_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Relationships
document: Mapped["Document"] = relationship("Document", back_populates="comments")
author: Mapped["User"] = relationship("User", back_populates="comments")
replies: Mapped[list["Comment"]] = relationship(
"Comment", backref="parent", remote_side=[id], cascade="all, delete-orphan"
)
class Revision(Base):
"""Document revision tracking model.
Attributes:
id: Unique identifier.
uuid: Public UUID for API references.
document_id: Reference to the document.
author_id: Reference to the revision author.
action: Type of revision action.
paragraph_index: Affected paragraph index.
original_content: Original content before change.
new_content: New content after change.
is_accepted: Whether revision is accepted.
created_at: Revision creation timestamp.
accepted_at: Acceptance timestamp.
accepted_by_id: User who accepted the revision.
"""
__tablename__ = "revisions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=lambda: str(uuid4())
)
document_id: Mapped[int] = mapped_column(
Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
)
author_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
action: Mapped[RevisionAction] = mapped_column(Enum(RevisionAction), nullable=False)
paragraph_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
original_content: Mapped[str | None] = mapped_column(Text, nullable=True)
new_content: Mapped[str | None] = mapped_column(Text, nullable=True)
is_accepted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
accepted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
accepted_by_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# Relationships
document: Mapped["Document"] = relationship("Document", back_populates="revisions")
author: Mapped["User"] = relationship("User", back_populates="revisions")
class Template(Base):
"""Document template model.
Attributes:
id: Unique identifier.
uuid: Public UUID for API references.
name: Template name.
description: Template description.
file_path: Path to template file.
category: Template category.
is_public: Whether template is public.
template_metadata: Additional metadata as JSON.
created_by_id: User who created the template.
created_at: Creation timestamp.
updated_at: Last update timestamp.
"""
__tablename__ = "templates"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=lambda: str(uuid4())
)
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
file_path: Mapped[str] = mapped_column(String(500), nullable=False)
category: Mapped[str | None] = mapped_column(String(100), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
template_metadata: Mapped[dict[str, Any] | None] = mapped_column(
JSON, nullable=True
)
created_by_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
class AuditLog(Base):
"""Audit log for tracking user actions.
Attributes:
id: Unique identifier.
user_id: Reference to the user.
action: Type of action performed.
resource_type: Type of resource affected.
resource_id: ID of the affected resource.
details: Additional action details as JSON.
ip_address: Client IP address.
user_agent: Client user agent.
created_at: Action timestamp.
"""
__tablename__ = "audit_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
action: Mapped[AuditAction] = mapped_column(Enum(AuditAction), nullable=False)
resource_type: Mapped[str] = mapped_column(String(50), nullable=False)
resource_id: Mapped[str | None] = mapped_column(String(50), nullable=True)
details: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
)
# Relationships
user: Mapped["User"] = relationship("User", back_populates="audit_logs")