feat(db): all domain models, initial Alembic migration (32 tables), seed script (8 districts, 89 regions, roles)

This commit is contained in:
apuc committed 2026-09-26 19:46:44 +03:00
1 parent 71460ab56a
commit 4a610ba94a
19 files changed
+1290 -2

No files matched your search

+47 -2
View File
@@ -1,4 +1,49 @@
"""Модели данных."""
"""Модели данных (регистрация для Alembic)."""
from app.models.advice import Advice
from app.models.artifact import Artifact
from app.models.audit import AssistantAction, AssistantConfig, AuditLog
from app.models.broadcast import Broadcast, BroadcastRecipient, BroadcastTemplate
from app.models.event import Event, EventReminder
from app.models.goal import DataUpload, Goal, RegionGoalValue, Snapshot
from app.models.history import PlatformHistory
from app.models.knowledge import KnowledgeArticle, KnowledgeCategory
from app.models.region import FederalDistrict, Region
from app.models.region_card import RegionCard, RegionCardHistory, RegionLink
from app.models.request import Request, RequestComment
from app.models.task import Task, TaskComment
from app.models.user import LoginToken, Role, Session, User, UserRegion, UserRole
__all__ = ["FederalDistrict", "Region"]
__all__ = [
"Advice",
"Artifact",
"AssistantAction",
"AssistantConfig",
"AuditLog",
"Broadcast",
"BroadcastRecipient",
"BroadcastTemplate",
"DataUpload",
"Event",
"EventReminder",
"FederalDistrict",
"Goal",
"KnowledgeArticle",
"KnowledgeCategory",
"LoginToken",
"PlatformHistory",
"Region",
"RegionCard",
"RegionCardHistory",
"RegionGoalValue",
"RegionLink",
"Request",
"RequestComment",
"Role",
"Session",
"Snapshot",
"Task",
"TaskComment",
"User",
"UserRegion",
"UserRole",
]
+19
View File
@@ -0,0 +1,19 @@
"""Модели советов от регионов."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Advice(Base):
__tablename__ = "advices"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
author_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(20), default="published")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+24
View File
@@ -0,0 +1,24 @@
"""Модели артефактов (файлов)."""
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Artifact(Base):
__tablename__ = "artifacts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str | None] = mapped_column(String(300))
file_key: Mapped[str] = mapped_column(String(500))
file_name: Mapped[str] = mapped_column(String(300))
mime_type: Mapped[str | None] = mapped_column(String(100))
size_bytes: Mapped[int | None] = mapped_column(BigInteger)
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id"))
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+49
View File
@@ -0,0 +1,49 @@
"""Модели аудита и AI-ассистента."""
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, JSON, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import ActorType
from app.models.types import enum_col
class AuditLog(Base):
__tablename__ = "audit_log"
id: Mapped[int] = mapped_column(primary_key=True)
actor_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
actor_type: Mapped[ActorType] = mapped_column(enum_col(ActorType))
action: Mapped[str] = mapped_column(String(100))
entity_type: Mapped[str | None] = mapped_column(String(100))
entity_id: Mapped[str | None] = mapped_column(String(100))
payload: Mapped[dict | None] = mapped_column(JSON)
ip: Mapped[str | None] = mapped_column(String(64))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class AssistantConfig(Base):
__tablename__ = "assistant_configs"
id: Mapped[int] = mapped_column(primary_key=True)
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
permissions: Mapped[dict | None] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class AssistantAction(Base):
__tablename__ = "assistant_actions"
id: Mapped[int] = mapped_column(primary_key=True)
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
action: Mapped[str] = mapped_column(String(100))
entity_type: Mapped[str | None] = mapped_column(String(100))
entity_id: Mapped[str | None] = mapped_column(String(100))
payload: Mapped[dict | None] = mapped_column(JSON)
status: Mapped[str] = mapped_column(String(20), default="ok")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+65
View File
@@ -0,0 +1,65 @@
"""Модели рассылок."""
from datetime import datetime
from sqlalchemy import (
DateTime,
ForeignKey,
JSON,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import BroadcastStatus, BroadcastType, RecipientStatus
from app.models.types import enum_col
class BroadcastTemplate(Base):
__tablename__ = "broadcast_templates"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(String(64), unique=True)
name: Mapped[str] = mapped_column(String(200))
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
body_template: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class Broadcast(Base):
__tablename__ = "broadcasts"
id: Mapped[int] = mapped_column(primary_key=True)
template_id: Mapped[int | None] = mapped_column(ForeignKey("broadcast_templates.id"))
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
subject: Mapped[str | None] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
audience_filter: Mapped[dict | None] = mapped_column(JSON)
status: Mapped[BroadcastStatus] = mapped_column(
enum_col(BroadcastStatus), default=BroadcastStatus.DRAFT
)
scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class BroadcastRecipient(Base):
__tablename__ = "broadcast_recipients"
id: Mapped[int] = mapped_column(primary_key=True)
broadcast_id: Mapped[int] = mapped_column(ForeignKey("broadcasts.id", ondelete="CASCADE"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
status: Mapped[RecipientStatus] = mapped_column(
enum_col(RecipientStatus), default=RecipientStatus.PENDING
)
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
error: Mapped[str | None] = mapped_column(Text)
__table_args__ = (UniqueConstraint("broadcast_id", "user_id", name="uq_broadcast_user"),)
+96
View File
@@ -0,0 +1,96 @@
"""Перечисления предметной области."""
from enum import StrEnum
class MessengerType(StrEnum):
TELEGRAM = "telegram"
MAX = "max"
class UserStatus(StrEnum):
ACTIVE = "active"
BLOCKED = "blocked"
PENDING = "pending"
class RoleCode(StrEnum):
ADMIN = "admin"
COORDINATOR = "coordinator"
REGION_HEAD = "region_head"
MEMBER = "member"
DEPUTY = "deputy"
class LoginTokenStatus(StrEnum):
ACTIVE = "active"
USED = "used"
EXPIRED = "expired"
REVOKED = "revoked"
class SnapshotKind(StrEnum):
POINT0 = "point0"
CURRENT = "current"
HISTORY = "history"
class MemberType(StrEnum):
MEMBER = "member"
RESIDENT = "resident"
class TaskStatus(StrEnum):
NEW = "new"
IN_PROGRESS = "in_progress"
REVIEW = "review"
DONE = "done"
class TaskPriority(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class RequestType(StrEnum):
JOIN = "join"
ACCESS = "access"
EVENT = "event"
SUPPORT = "support"
class RequestStatus(StrEnum):
NEW = "new"
IN_PROGRESS = "in_progress"
CLOSED = "closed"
class BroadcastType(StrEnum):
INFO = "info"
EDUCATION = "education"
RATING = "rating"
class BroadcastStatus(StrEnum):
DRAFT = "draft"
SCHEDULED = "scheduled"
SENDING = "sending"
SENT = "sent"
FAILED = "failed"
class RecipientStatus(StrEnum):
PENDING = "pending"
SENT = "sent"
FAILED = "failed"
class ActorType(StrEnum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
class LinkType(StrEnum):
COMMUNITY = "community"
CHAT = "chat"
+33
View File
@@ -0,0 +1,33 @@
"""Модели календаря событий."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Event(Base):
__tablename__ = "events"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
description: Mapped[str | None] = mapped_column(Text)
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
location: Mapped[str | None] = mapped_column(String(300))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class EventReminder(Base):
__tablename__ = "event_reminders"
id: Mapped[int] = mapped_column(primary_key=True)
event_id: Mapped[int] = mapped_column(ForeignKey("events.id", ondelete="CASCADE"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
remind_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
+75
View File
@@ -0,0 +1,75 @@
"""Модели целей, срезов и метрик."""
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import SnapshotKind
from app.models.types import enum_col
class Goal(Base):
__tablename__ = "goals"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str | None] = mapped_column(String(1000))
target_value: Mapped[float | None] = mapped_column(Numeric(14, 2))
unit: Mapped[str | None] = mapped_column(String(32))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
snapshots: Mapped[list["Snapshot"]] = relationship(back_populates="goal")
class Snapshot(Base):
__tablename__ = "snapshots"
id: Mapped[int] = mapped_column(primary_key=True)
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id", ondelete="CASCADE"))
snapshot_date: Mapped[date] = mapped_column(Date)
kind: Mapped[SnapshotKind] = mapped_column(enum_col(SnapshotKind))
source: Mapped[str | None] = mapped_column(String(32))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
goal: Mapped[Goal] = relationship(back_populates="snapshots")
values: Mapped[list["RegionGoalValue"]] = relationship(back_populates="snapshot")
class RegionGoalValue(Base):
__tablename__ = "region_goal_values"
id: Mapped[int] = mapped_column(primary_key=True)
snapshot_id: Mapped[int] = mapped_column(ForeignKey("snapshots.id", ondelete="CASCADE"))
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
value: Mapped[float] = mapped_column(Numeric(14, 2))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("snapshot_id", "region_id", name="uq_snapshot_region"),)
snapshot: Mapped[Snapshot] = relationship(back_populates="values")
class DataUpload(Base):
__tablename__ = "data_uploads"
id: Mapped[int] = mapped_column(primary_key=True)
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id"))
file_key: Mapped[str] = mapped_column(String(500))
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
status: Mapped[str] = mapped_column(String(20), default="uploaded")
processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+19
View File
@@ -0,0 +1,19 @@
"""Модели истории платформы."""
from datetime import date, datetime
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class PlatformHistory(Base):
__tablename__ = "platform_history"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
event_date: Mapped[date | None] = mapped_column(Date)
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+28
View File
@@ -0,0 +1,28 @@
"""Модели базы знаний."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class KnowledgeCategory(Base):
__tablename__ = "knowledge_categories"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(200), unique=True)
class KnowledgeArticle(Base):
__tablename__ = "knowledge_articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
body: Mapped[str] = mapped_column(Text)
category_id: Mapped[int | None] = mapped_column(ForeignKey("knowledge_categories.id"))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
+46
View File
@@ -0,0 +1,46 @@
"""Модели карточки региона."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
from app.models.enums import LinkType
from app.models.types import enum_col
class RegionCard(Base):
__tablename__ = "region_cards"
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"), primary_key=True)
population: Mapped[int | None] = mapped_column(Integer)
head_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class RegionLink(Base):
__tablename__ = "region_links"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
type: Mapped[LinkType] = mapped_column(enum_col(LinkType))
title: Mapped[str | None] = mapped_column(String(200))
url: Mapped[str] = mapped_column(String(500))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class RegionCardHistory(Base):
__tablename__ = "region_card_history"
id: Mapped[int] = mapped_column(primary_key=True)
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
field: Mapped[str] = mapped_column(String(100))
old_value: Mapped[str | None] = mapped_column(String(1000))
new_value: Mapped[str | None] = mapped_column(String(1000))
changed_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
changed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+42
View File
@@ -0,0 +1,42 @@
"""Модели запросов (заявок)."""
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import RequestStatus, RequestType
from app.models.types import enum_col
class Request(Base):
__tablename__ = "requests"
id: Mapped[int] = mapped_column(primary_key=True)
type: Mapped[RequestType] = mapped_column(enum_col(RequestType))
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
subject: Mapped[str | None] = mapped_column(String(300))
body: Mapped[str | None] = mapped_column(Text)
status: Mapped[RequestStatus] = mapped_column(
enum_col(RequestStatus), default=RequestStatus.NEW
)
assigned_to: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
comments: Mapped[list["RequestComment"]] = relationship(back_populates="request")
class RequestComment(Base):
__tablename__ = "request_comments"
id: Mapped[int] = mapped_column(primary_key=True)
request_id: Mapped[int] = mapped_column(ForeignKey("requests.id", ondelete="CASCADE"))
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
request: Mapped[Request] = relationship(back_populates="comments")
+47
View File
@@ -0,0 +1,47 @@
"""Модели задач (трекера)."""
from datetime import date, datetime
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import RoleCode, TaskPriority, TaskStatus
from app.models.types import enum_col
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(300))
description: Mapped[str | None] = mapped_column(Text)
due_date: Mapped[date | None] = mapped_column(Date)
priority: Mapped[TaskPriority] = mapped_column(
enum_col(TaskPriority), default=TaskPriority.MEDIUM
)
status: Mapped[TaskStatus] = mapped_column(
enum_col(TaskStatus), default=TaskStatus.NEW
)
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
assignee_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
assignee_role: Mapped[RoleCode | None] = mapped_column(enum_col(RoleCode))
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
comments: Mapped[list["TaskComment"]] = relationship(back_populates="task")
class TaskComment(Base):
__tablename__ = "task_comments"
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"))
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
body: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
task: Mapped[Task] = relationship(back_populates="comments")
+17
View File
@@ -0,0 +1,17 @@
"""Вспомогательные типы для моделей."""
from enum import Enum as PyEnum
from sqlalchemy import Enum as SAEnum
def enum_col(enum_cls: type[PyEnum], length: int = 20) -> SAEnum:
"""Enum-колонка, хранящая значения (value), а не имена членов.
Например, BroadcastType.INFO хранится как 'info', а не 'INFO'.
"""
return SAEnum(
enum_cls,
native_enum=False,
length=length,
values_callable=lambda obj: [e.value for e in obj],
)
+108
View File
@@ -0,0 +1,108 @@
"""Модели пользователей, ролей и доступа."""
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
from app.models.enums import LoginTokenStatus, MemberType, MessengerType, RoleCode, UserStatus
from app.models.types import enum_col
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
full_name: Mapped[str | None] = mapped_column(String(200))
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
messenger_id: Mapped[str] = mapped_column(String(64))
phone: Mapped[str | None] = mapped_column(String(32))
status: Mapped[UserStatus] = mapped_column(
enum_col(UserStatus), default=UserStatus.ACTIVE
)
member_type: Mapped[MemberType | None] = mapped_column(enum_col(MemberType))
joined_at: Mapped[date | None] = mapped_column(Date)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
__table_args__ = (UniqueConstraint("messenger_type", "messenger_id", name="uq_user_messenger"),)
regions: Mapped[list["UserRegion"]] = relationship(back_populates="user")
roles: Mapped[list["UserRole"]] = relationship(
back_populates="user", foreign_keys="UserRole.user_id"
)
class UserRegion(Base):
__tablename__ = "user_regions"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
is_primary: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("user_id", "region_id", name="uq_user_region"),)
user: Mapped[User] = relationship(back_populates="regions")
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[RoleCode] = mapped_column(enum_col(RoleCode), unique=True)
name: Mapped[str] = mapped_column(String(100))
class UserRole(Base):
__tablename__ = "user_roles"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
granted_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (UniqueConstraint("user_id", "role_id", "region_id", name="uq_user_role"),)
user: Mapped[User] = relationship(back_populates="roles", foreign_keys=[user_id])
class LoginToken(Base):
__tablename__ = "login_tokens"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
token: Mapped[str] = mapped_column(String(128), unique=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
issued_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
status: Mapped[LoginTokenStatus] = mapped_column(
enum_col(LoginTokenStatus), default=LoginTokenStatus.ACTIVE
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Session(Base):
__tablename__ = "sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
token_hash: Mapped[str] = mapped_column(String(128), unique=True)
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))