269 lines
11 KiB
Python
269 lines
11 KiB
Python
"""SQLAlchemy ORM models for all persistent tables.
|
|
|
|
Only auth, billing, storage metadata, and marketplace data live here.
|
|
User content (notes, tasks, etc.) is NEVER persisted server-side —
|
|
it lives in E2E-encrypted blobs in S3, referenced by storage_records.
|
|
|
|
Table inventory:
|
|
users — account credentials + tier
|
|
refresh_tokens — hashed refresh token store
|
|
subscriptions — Stripe subscription records
|
|
storage_records — S3 blob metadata (no plaintext)
|
|
backup_metadata — encrypted backup manifests
|
|
plugins — marketplace plugin catalog
|
|
plugin_installations — per-user install records
|
|
plugin_reviews — admin review decisions
|
|
revenue_events — Stripe Connect 70/30 split ledger
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
DateTime,
|
|
Enum,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
Uuid,
|
|
func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.db import Base
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _uuid() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
# ── Enum types ────────────────────────────────────────────────────────────
|
|
|
|
TierEnum = Enum("free", "pro", "power", "team", name="billing_tier")
|
|
PluginStatusEnum = Enum("pending_review", "approved", "rejected", name="plugin_status")
|
|
ReviewDecisionEnum = Enum("approved", "rejected", name="review_decision")
|
|
|
|
|
|
# ── Models ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free")
|
|
stripe_customer_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
refresh_tokens: Mapped[list[RefreshToken]] = relationship(
|
|
back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
subscription: Mapped[Subscription | None] = relationship(
|
|
back_populates="user", uselist=False, cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class RefreshToken(Base):
|
|
__tablename__ = "refresh_tokens"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
user: Mapped[User] = relationship(back_populates="refresh_tokens")
|
|
|
|
|
|
class Subscription(Base):
|
|
__tablename__ = "subscriptions"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, unique=True, index=True
|
|
)
|
|
stripe_subscription_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
|
tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free")
|
|
status: Mapped[str] = mapped_column(String(50), nullable=False, default="free")
|
|
current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
user: Mapped[User] = relationship(back_populates="subscription")
|
|
|
|
|
|
class StorageRecord(Base):
|
|
__tablename__ = "storage_records"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
table_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
s3_key: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
checksum: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class BackupMetadata(Base):
|
|
__tablename__ = "backup_metadata"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
s3_key: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
timestamp: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
checksum: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
|
|
class Plugin(Base):
|
|
__tablename__ = "plugins"
|
|
|
|
id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
version: Mapped[str] = mapped_column(String(50), nullable=False, default="1.0.0")
|
|
# nullable until developer account system is built
|
|
author_id: Mapped[str | None] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
author_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
|
category: Mapped[str] = mapped_column(String(100), nullable=False, default="")
|
|
price_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
permissions: Mapped[str] = mapped_column(Text, nullable=False, default="[]") # JSON list
|
|
status: Mapped[str] = mapped_column(PluginStatusEnum, nullable=False, default="pending_review")
|
|
s3_package_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
install_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
avg_rating: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
|
rejection_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
submitted_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
installations: Mapped[list[PluginInstallation]] = relationship(
|
|
back_populates="plugin", cascade="all, delete-orphan"
|
|
)
|
|
reviews: Mapped[list[PluginReview]] = relationship(
|
|
back_populates="plugin", cascade="all, delete-orphan"
|
|
)
|
|
revenue_events: Mapped[list[RevenueEvent]] = relationship(
|
|
back_populates="plugin", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class PluginInstallation(Base):
|
|
__tablename__ = "plugin_installations"
|
|
__table_args__ = (UniqueConstraint("plugin_id", "user_id", name="uq_plugin_user"),)
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
plugin_id: Mapped[str] = mapped_column(
|
|
String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
installed_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
plugin: Mapped[Plugin] = relationship(back_populates="installations")
|
|
|
|
|
|
class PluginReview(Base):
|
|
__tablename__ = "plugin_reviews"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
plugin_id: Mapped[str] = mapped_column(
|
|
String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
reviewer_id: Mapped[str | None] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
decision: Mapped[str] = mapped_column(ReviewDecisionEnum, nullable=False)
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
reviewed_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
plugin: Mapped[Plugin] = relationship(back_populates="reviews")
|
|
|
|
|
|
class RevenueEvent(Base):
|
|
__tablename__ = "revenue_events"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
plugin_id: Mapped[str] = mapped_column(
|
|
String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
developer_share_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
stripe_transfer_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
plugin: Mapped[Plugin] = relationship(back_populates="revenue_events")
|