refactor(models): extract Timestamp/UpdatedAt mixins; fix stale status comment
TimestampMixin (created_at) + UpdatedAtMixin (updated_at) replace the repeated column blocks across 9 + 3 models. Models that index created_at or default it Python-side keep their own column on purpose. Verified zero schema drift via an identical alembic-check diff before/after.
This commit is contained in:
parent
022505ff18
commit
f0ed7d1e2c
1 changed files with 29 additions and 48 deletions
|
|
@ -19,7 +19,24 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||
from app.db import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
class TimestampMixin:
|
||||
"""Adds a server-defaulted `created_at`. Models that index created_at or order it
|
||||
differently (QuotaEvent, Notification, Message) keep their own column on purpose."""
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
|
||||
class UpdatedAtMixin:
|
||||
"""Adds an `updated_at` that server-defaults on insert and bumps on every UPDATE."""
|
||||
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
|
@ -57,9 +74,6 @@ class User(Base):
|
|||
)
|
||||
# Free-form UI preferences (theme, color scheme, font scale, default filters…).
|
||||
preferences: Mapped[dict | None] = mapped_column(JSON)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
token: Mapped["OAuthToken | None"] = relationship(
|
||||
back_populates="user", uselist=False, cascade="all, delete-orphan"
|
||||
|
|
@ -69,7 +83,7 @@ class User(Base):
|
|||
)
|
||||
|
||||
|
||||
class OAuthToken(Base):
|
||||
class OAuthToken(Base, UpdatedAtMixin):
|
||||
__tablename__ = "oauth_tokens"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
|
@ -81,14 +95,11 @@ class OAuthToken(Base):
|
|||
access_token: Mapped[str | None] = mapped_column(String)
|
||||
expiry: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
scopes: Mapped[str | None] = mapped_column(String)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates="token")
|
||||
|
||||
|
||||
class AuthToken(Base):
|
||||
class AuthToken(Base, TimestampMixin):
|
||||
"""Single-use, expiring token for email verification or password reset. Only the SHA-256
|
||||
HASH of the token is stored, so a DB leak can't be used to verify/reset; the raw token
|
||||
lives only in the emailed link. Consumed (used_at set) on first valid use."""
|
||||
|
|
@ -103,9 +114,6 @@ class AuthToken(Base):
|
|||
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
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 Invite(Base):
|
||||
|
|
@ -128,7 +136,7 @@ class Invite(Base):
|
|||
decided_by: Mapped[str | None] = mapped_column(String(320)) # admin email
|
||||
|
||||
|
||||
class DemoWhitelist(Base):
|
||||
class DemoWhitelist(Base, TimestampMixin):
|
||||
"""Emails that may enter the shared demo account from the login page (no Google OAuth).
|
||||
A hidden, admin-curated list of "keys to the same door" — every entry logs into the one
|
||||
shared demo user. Distinct from Invite (which gates real Google sign-in)."""
|
||||
|
|
@ -139,12 +147,9 @@ class DemoWhitelist(Base):
|
|||
email: Mapped[str] = mapped_column(String(320), unique=True, index=True)
|
||||
note: Mapped[str | None] = mapped_column(Text)
|
||||
added_by: Mapped[str | None] = mapped_column(String(320)) # admin email
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
|
||||
class Channel(Base):
|
||||
class Channel(Base, TimestampMixin):
|
||||
"""A YouTube channel. Shared across all users (one channel's videos are the same
|
||||
for everyone), so its expensive metadata is fetched and stored only once."""
|
||||
|
||||
|
|
@ -170,15 +175,12 @@ class Channel(Base):
|
|||
backfill_done: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, server_default="false"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
videos: Mapped[list["Video"]] = relationship(back_populates="channel")
|
||||
subscriptions: Mapped[list["Subscription"]] = relationship(back_populates="channel")
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
class Subscription(Base, TimestampMixin):
|
||||
"""Per-user link to a channel, with the user's own priority/visibility overrides."""
|
||||
|
||||
__tablename__ = "subscriptions"
|
||||
|
|
@ -202,15 +204,12 @@ class Subscription(Base):
|
|||
Boolean, default=False, server_default="false"
|
||||
)
|
||||
subscribed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates="subscriptions")
|
||||
channel: Mapped["Channel"] = relationship(back_populates="subscriptions")
|
||||
|
||||
|
||||
class Video(Base):
|
||||
class Video(Base, TimestampMixin):
|
||||
"""A YouTube video. Shared across users; per-user state lives elsewhere."""
|
||||
|
||||
__tablename__ = "videos"
|
||||
|
|
@ -260,14 +259,11 @@ class Video(Base):
|
|||
DateTime(timezone=True), index=True
|
||||
)
|
||||
unavailable_reason: Mapped[str | None] = mapped_column(String(24))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
channel: Mapped["Channel"] = relationship(back_populates="videos")
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
class Tag(Base, TimestampMixin):
|
||||
"""A label. System tags (user_id NULL) are computed automatically and shared by all
|
||||
users; user tags (user_id set) are personal. category groups them in the UI."""
|
||||
|
||||
|
|
@ -283,9 +279,6 @@ class Tag(Base):
|
|||
category: Mapped[str] = mapped_column(
|
||||
String(16), default="other", server_default="other"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
|
||||
class ChannelTag(Base):
|
||||
|
|
@ -312,7 +305,7 @@ class ChannelTag(Base):
|
|||
confidence: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
|
||||
class VideoState(Base):
|
||||
class VideoState(Base, UpdatedAtMixin):
|
||||
"""Per-user watch state for a video. Rows exist only for non-default states; the
|
||||
absence of a row means 'new/unseen'."""
|
||||
|
||||
|
|
@ -326,7 +319,7 @@ class VideoState(Base):
|
|||
video_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("videos.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
# new | watched | saved | hidden
|
||||
# new | watched | hidden ("saved" is now Watch-later playlist membership, not a status)
|
||||
status: Mapped[str] = mapped_column(String(12), default="new", server_default="new")
|
||||
watched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
# Resume position (seconds into the video) for the in-app player. Orthogonal to
|
||||
|
|
@ -337,9 +330,6 @@ class VideoState(Base):
|
|||
Integer, default=0, server_default="0", nullable=False
|
||||
)
|
||||
progress_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class ApiQuotaUsage(Base):
|
||||
|
|
@ -422,7 +412,7 @@ class SystemConfig(Base):
|
|||
)
|
||||
|
||||
|
||||
class Playlist(Base):
|
||||
class Playlist(Base, TimestampMixin, UpdatedAtMixin):
|
||||
"""A per-user named, ordered collection of videos from the shared catalog.
|
||||
|
||||
`kind`: 'user' (normal) or 'watch_later' (built-in, undeletable, one per user).
|
||||
|
|
@ -450,12 +440,6 @@ class Playlist(Base):
|
|||
# baseline `dirty` is derived against. NULL = no baseline yet (treated as dirty).
|
||||
synced_fingerprint: Mapped[str | None] = mapped_column(String)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
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()
|
||||
)
|
||||
|
||||
items: Mapped[list["PlaylistItem"]] = relationship(
|
||||
back_populates="playlist",
|
||||
|
|
@ -560,7 +544,7 @@ class Message(Base):
|
|||
)
|
||||
|
||||
|
||||
class MessageKey(Base):
|
||||
class MessageKey(Base, TimestampMixin):
|
||||
"""A user's end-to-end-encryption key material for direct messaging (phase 2).
|
||||
|
||||
One row per user. `public_key` (base64 SPKI, ECDH P-256) is shared with others so they can
|
||||
|
|
@ -581,6 +565,3 @@ class MessageKey(Base):
|
|||
salt: Mapped[str] = mapped_column(String(64))
|
||||
wrap_iv: Mapped[str] = mapped_column(String(32))
|
||||
key_check: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue