refactor(models): one iso() for every to_dict timestamp; mixins replace hand-rolled created_at/updated_at (#2827, milestone 296 area 3)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s
Reading all 28 models against each other: 54 `x.isoformat() if x else None` / `x.isoformat()` expressions in 23 to_dict methods, in two guarded/unguarded wordings, become iso() from models/base.py — uniform, and a row read before flush serialises as null instead of raising. Rulebook / RulebookTopic / Rule carried byte-identical copies of TimestampMixin's two columns; InvitationToken / PasswordResetToken / NoteUsageEvent carried CreatedAtMixin's — all six now use the mixin. AppLog and RetrievalLog keep their explicit created_at, commented: their composite index orders on `created_at.desc()`, which needs the column object in the class body. Schema-neutral (same column definitions) — no migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class ApiKey(Base, CreatedAtMixin):
|
class ApiKey(Base, CreatedAtMixin):
|
||||||
@@ -36,7 +36,7 @@ class ApiKey(Base, CreatedAtMixin):
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"key_prefix": self.key_prefix,
|
"key_prefix": self.key_prefix,
|
||||||
"scope": self.scope,
|
"scope": self.scope,
|
||||||
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
"last_used_at": iso(self.last_used_at),
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"revoked_at": self.revoked_at.isoformat() if self.revoked_at else None,
|
"revoked_at": iso(self.revoked_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from sqlalchemy import DateTime, Float, Index, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
|
from scribe.models.base import iso
|
||||||
|
|
||||||
|
|
||||||
class AppLog(Base):
|
class AppLog(Base):
|
||||||
@@ -20,6 +21,9 @@ class AppLog(Base):
|
|||||||
duration_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
duration_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
ip_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
ip_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
details: Mapped[str | None] = mapped_column(Text, nullable=True)
|
details: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
# Declared here rather than via CreatedAtMixin on purpose: the composite
|
||||||
|
# index below orders on `created_at.desc()`, which needs the column object
|
||||||
|
# in this class body — a mixin's column is not in scope there.
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
@@ -44,5 +48,5 @@ class AppLog(Base):
|
|||||||
"duration_ms": self.duration_ms,
|
"duration_ms": self.duration_ms,
|
||||||
"ip_address": self.ip_address,
|
"ip_address": self.ip_address,
|
||||||
"details": self.details,
|
"details": self.details,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Text
|
from sqlalchemy import DateTime, Text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
|
||||||
|
def iso(value: datetime | date | None) -> str | None:
|
||||||
|
"""ISO-8601 for a payload, None for an unset column.
|
||||||
|
|
||||||
|
Every model's to_dict serialises timestamps through this one helper so a
|
||||||
|
row read before flush (created_at still None) and a nullable column both
|
||||||
|
come out as null instead of raising on `.isoformat()`.
|
||||||
|
"""
|
||||||
|
return value.isoformat() if value else None
|
||||||
|
|
||||||
|
|
||||||
class SoftDeleteMixin:
|
class SoftDeleteMixin:
|
||||||
"""Recoverable-delete columns. NULL deleted_at = live row. deleted_batch_id
|
"""Recoverable-delete columns. NULL deleted_at = live row. deleted_batch_id
|
||||||
groups rows soft-deleted in one operation so a cascade restores as a unit."""
|
groups rows soft-deleted in one operation so a cascade restores as a unit."""
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from sqlalchemy import (
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
# The classification vocabulary (note 2786). `unclassified` is the default and
|
# The classification vocabulary (note 2786). `unclassified` is the default and
|
||||||
# THE todo state; every other status is a judgment, stamped with who made it.
|
# THE todo state; every other status is a judgment, stamped with who made it.
|
||||||
@@ -153,18 +153,18 @@ class CodeShape(Base, TimestampMixin):
|
|||||||
"snippet_id": self.snippet_id,
|
"snippet_id": self.snippet_id,
|
||||||
"reason": self.reason,
|
"reason": self.reason,
|
||||||
"classified_by": self.classified_by,
|
"classified_by": self.classified_by,
|
||||||
"classified_at": self.classified_at.isoformat() if self.classified_at else None,
|
"classified_at": iso(self.classified_at),
|
||||||
"first_seen_commit": self.first_seen_commit,
|
"first_seen_commit": self.first_seen_commit,
|
||||||
"last_seen_commit": self.last_seen_commit,
|
"last_seen_commit": self.last_seen_commit,
|
||||||
"vanished_at": self.vanished_at.isoformat() if self.vanished_at else None,
|
"vanished_at": iso(self.vanished_at),
|
||||||
"signature": self.signature,
|
"signature": self.signature,
|
||||||
"body_sha": self.body_sha,
|
"body_sha": self.body_sha,
|
||||||
"proposal": self.proposal,
|
"proposal": self.proposal,
|
||||||
"classified_sha": self.classified_sha,
|
"classified_sha": self.classified_sha,
|
||||||
"recheck_at": self.recheck_at.isoformat() if self.recheck_at else None,
|
"recheck_at": iso(self.recheck_at),
|
||||||
"diverges_from": self.diverges_from,
|
"diverges_from": self.diverges_from,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -221,5 +221,5 @@ class CodeShapeEvent(Base):
|
|||||||
"classified_by": self.classified_by,
|
"classified_by": self.classified_by,
|
||||||
"reason": self.reason,
|
"reason": self.reason,
|
||||||
"commit": self.commit,
|
"commit": self.commit,
|
||||||
"at": self.at.isoformat(),
|
"at": iso(self.at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from sqlalchemy.dialects.postgresql import JSONB
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import SoftDeleteMixin, TimestampMixin
|
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
|
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
@@ -56,8 +56,8 @@ class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"description": self.description or "",
|
"description": self.description or "",
|
||||||
"guidance": self.guidance or "",
|
"guidance": self.guidance or "",
|
||||||
"parent_id": self.parent_id,
|
"parent_id": self.parent_id,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -153,6 +153,6 @@ class DesignToken(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"rationale": self.rationale,
|
"rationale": self.rationale,
|
||||||
"supersedes": self.supersedes or [],
|
"supersedes": self.supersedes or [],
|
||||||
"order_index": self.order_index,
|
"order_index": self.order_index,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class ForgeConnection(Base, TimestampMixin):
|
class ForgeConnection(Base, TimestampMixin):
|
||||||
@@ -40,6 +40,6 @@ class ForgeConnection(Base, TimestampMixin):
|
|||||||
"kind": self.kind,
|
"kind": self.kind,
|
||||||
"base_url": self.base_url,
|
"base_url": self.base_url,
|
||||||
"host": self.host,
|
"host": self.host,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin, TimestampMixin
|
from scribe.models.base import CreatedAtMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class Group(Base, TimestampMixin):
|
class Group(Base, TimestampMixin):
|
||||||
@@ -27,8 +27,8 @@ class Group(Base, TimestampMixin):
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"created_by": self.created_by,
|
"created_by": self.created_by,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,5 +53,5 @@ class GroupMembership(Base, CreatedAtMixin):
|
|||||||
"group_id": self.group_id,
|
"group_id": self.group_id,
|
||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"role": self.role,
|
"role": self.role,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
|
from scribe.models.base import CreatedAtMixin
|
||||||
|
|
||||||
|
|
||||||
class InvitationToken(Base):
|
class InvitationToken(Base, CreatedAtMixin):
|
||||||
__tablename__ = "invitation_tokens"
|
__tablename__ = "invitation_tokens"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
@@ -15,9 +16,6 @@ class InvitationToken(Base):
|
|||||||
invited_by: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
invited_by: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("ix_invitation_tokens_token_hash", "token_hash"),
|
Index("ix_invitation_tokens_token_hash", "token_hash"),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin, SoftDeleteMixin
|
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class Milestone(Base, TimestampMixin, SoftDeleteMixin):
|
class Milestone(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
@@ -30,6 +30,6 @@ class Milestone(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"body": self.body,
|
"body": self.body,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"order_index": self.order_index,
|
"order_index": self.order_index,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin, SoftDeleteMixin
|
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class TaskStatus(str, enum.Enum):
|
class TaskStatus(str, enum.Enum):
|
||||||
@@ -105,18 +105,14 @@ class Note(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"milestone_id": self.milestone_id,
|
"milestone_id": self.milestone_id,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"priority": self.priority,
|
"priority": self.priority,
|
||||||
"due_date": self.due_date.isoformat() if self.due_date else None,
|
"due_date": iso(self.due_date),
|
||||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
"started_at": iso(self.started_at),
|
||||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
"completed_at": iso(self.completed_at),
|
||||||
"recurrence_rule": self.recurrence_rule,
|
"recurrence_rule": self.recurrence_rule,
|
||||||
"recurrence_next_spawn_at": (
|
"recurrence_next_spawn_at": iso(self.recurrence_next_spawn_at),
|
||||||
self.recurrence_next_spawn_at.isoformat()
|
|
||||||
if self.recurrence_next_spawn_at
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"is_task": self.is_task,
|
"is_task": self.is_task,
|
||||||
"note_type": self.note_type or "note",
|
"note_type": self.note_type or "note",
|
||||||
"task_kind": self.task_kind,
|
"task_kind": self.task_kind,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class NoteDraft(Base, TimestampMixin):
|
class NoteDraft(Base, TimestampMixin):
|
||||||
@@ -25,6 +25,6 @@ class NoteDraft(Base, TimestampMixin):
|
|||||||
"original_body": self.original_body,
|
"original_body": self.original_body,
|
||||||
"instruction": self.instruction,
|
"instruction": self.instruction,
|
||||||
"scope": self.scope,
|
"scope": self.scope,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class NoteSupersession(Base, CreatedAtMixin):
|
class NoteSupersession(Base, CreatedAtMixin):
|
||||||
@@ -66,5 +66,5 @@ class NoteSupersession(Base, CreatedAtMixin):
|
|||||||
"id": self.id,
|
"id": self.id,
|
||||||
"superseder_id": self.superseder_id,
|
"superseder_id": self.superseder_id,
|
||||||
"superseded_id": self.superseded_id,
|
"superseded_id": self.superseded_id,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from datetime import datetime, timezone
|
from sqlalchemy import Index, Integer, Text
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Index, Integer, Text
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
SURFACED = "surfaced"
|
SURFACED = "surfaced"
|
||||||
PULLED = "pulled"
|
PULLED = "pulled"
|
||||||
|
|
||||||
|
|
||||||
class NoteUsageEvent(Base):
|
class NoteUsageEvent(Base, CreatedAtMixin):
|
||||||
"""One row per time a note was SURFACED to the agent, or PULLED in full.
|
"""One row per time a note was SURFACED to the agent, or PULLED in full.
|
||||||
|
|
||||||
Answers the question RetrievalLog cannot: not "what did the ranker return
|
Answers the question RetrievalLog cannot: not "what did the ranker return
|
||||||
@@ -44,9 +43,6 @@ class NoteUsageEvent(Base):
|
|||||||
__tablename__ = "note_usage_events"
|
__tablename__ = "note_usage_events"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
note_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
note_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
# 'surfaced' | 'pulled'
|
# 'surfaced' | 'pulled'
|
||||||
@@ -81,7 +77,7 @@ class NoteUsageEvent(Base):
|
|||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"note_id": self.note_id,
|
"note_id": self.note_id,
|
||||||
"event": self.event,
|
"event": self.event,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ARRAY, ForeignKey, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class NoteVersion(Base, CreatedAtMixin):
|
class NoteVersion(Base, CreatedAtMixin):
|
||||||
@@ -26,7 +26,7 @@ class NoteVersion(Base, CreatedAtMixin):
|
|||||||
"tags": self.tags or [],
|
"tags": self.tags or [],
|
||||||
"pin_kind": self.pin_kind,
|
"pin_kind": self.pin_kind,
|
||||||
"pin_label": self.pin_label,
|
"pin_label": self.pin_label,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
}
|
}
|
||||||
if include_body:
|
if include_body:
|
||||||
d["body"] = self.body
|
d["body"] = self.body
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from sqlalchemy.dialects.postgresql import JSONB
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class Notification(Base, CreatedAtMixin):
|
class Notification(Base, CreatedAtMixin):
|
||||||
@@ -26,6 +26,6 @@ class Notification(Base, CreatedAtMixin):
|
|||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"payload": self.payload,
|
"payload": self.payload,
|
||||||
"read_at": self.read_at.isoformat() if self.read_at else None,
|
"read_at": iso(self.read_at),
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
|
from scribe.models.base import CreatedAtMixin
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetToken(Base):
|
class PasswordResetToken(Base, CreatedAtMixin):
|
||||||
__tablename__ = "password_reset_tokens"
|
__tablename__ = "password_reset_tokens"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
@@ -14,9 +15,6 @@ class PasswordResetToken(Base):
|
|||||||
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("ix_password_reset_tokens_token_hash", "token_hash"),
|
Index("ix_password_reset_tokens_token_hash", "token_hash"),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import enum
|
|||||||
from sqlalchemy import BigInteger, ForeignKey, Integer, Text
|
from sqlalchemy import BigInteger, ForeignKey, Integer, Text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin, SoftDeleteMixin
|
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class ProjectStatus(str, enum.Enum):
|
class ProjectStatus(str, enum.Enum):
|
||||||
@@ -48,6 +48,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"color": self.color,
|
"color": self.color,
|
||||||
"design_system_id": self.design_system_id,
|
"design_system_id": self.design_system_id,
|
||||||
"forge_connection_id": self.forge_connection_id,
|
"forge_connection_id": self.forge_connection_id,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class RepoBinding(Base, TimestampMixin):
|
class RepoBinding(Base, TimestampMixin):
|
||||||
@@ -35,6 +35,6 @@ class RepoBinding(Base, TimestampMixin):
|
|||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"project_id": self.project_id,
|
"project_id": self.project_id,
|
||||||
"repo_key": self.repo_key,
|
"repo_key": self.repo_key,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import JSONB
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
|
from scribe.models.base import iso
|
||||||
|
|
||||||
|
|
||||||
class RetrievalLog(Base):
|
class RetrievalLog(Base):
|
||||||
@@ -23,6 +24,9 @@ class RetrievalLog(Base):
|
|||||||
__tablename__ = "retrieval_logs"
|
__tablename__ = "retrieval_logs"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
# Declared here rather than via CreatedAtMixin on purpose: the composite
|
||||||
|
# index below orders on `created_at.desc()`, which needs the column object
|
||||||
|
# in this class body — a mixin's column is not in scope there.
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
@@ -54,7 +58,7 @@ class RetrievalLog(Base):
|
|||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"source": self.source,
|
"source": self.source,
|
||||||
"query": self.query,
|
"query": self.query,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from sqlalchemy import BigInteger, Boolean, Column, DateTime, ForeignKey, Index,
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import SoftDeleteMixin
|
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class Rulebook(Base, SoftDeleteMixin):
|
class Rulebook(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
__tablename__ = "rulebooks"
|
__tablename__ = "rulebooks"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
@@ -19,14 +19,6 @@ class Rulebook(Base, SoftDeleteMixin):
|
|||||||
always_on: Mapped[bool] = mapped_column(
|
always_on: Mapped[bool] = mapped_column(
|
||||||
Boolean, default=False, nullable=False, server_default="false"
|
Boolean, default=False, nullable=False, server_default="false"
|
||||||
)
|
)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True),
|
|
||||||
default=lambda: datetime.now(timezone.utc),
|
|
||||||
onupdate=lambda: datetime.now(timezone.utc),
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -35,12 +27,12 @@ class Rulebook(Base, SoftDeleteMixin):
|
|||||||
"title": self.title,
|
"title": self.title,
|
||||||
"description": self.description or "",
|
"description": self.description or "",
|
||||||
"always_on": self.always_on,
|
"always_on": self.always_on,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RulebookTopic(Base, SoftDeleteMixin):
|
class RulebookTopic(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
__tablename__ = "rulebook_topics"
|
__tablename__ = "rulebook_topics"
|
||||||
# Partial unique: a title is unique among LIVE topics in a rulebook, so a
|
# Partial unique: a title is unique among LIVE topics in a rulebook, so a
|
||||||
# trashed topic doesn't block recreating/restoring the same title.
|
# trashed topic doesn't block recreating/restoring the same title.
|
||||||
@@ -58,14 +50,6 @@ class RulebookTopic(Base, SoftDeleteMixin):
|
|||||||
title: Mapped[str] = mapped_column(Text)
|
title: Mapped[str] = mapped_column(Text)
|
||||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
order_index: Mapped[int] = mapped_column(Integer, default=0)
|
order_index: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True),
|
|
||||||
default=lambda: datetime.now(timezone.utc),
|
|
||||||
onupdate=lambda: datetime.now(timezone.utc),
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -74,12 +58,12 @@ class RulebookTopic(Base, SoftDeleteMixin):
|
|||||||
"title": self.title,
|
"title": self.title,
|
||||||
"description": self.description or "",
|
"description": self.description or "",
|
||||||
"order_index": self.order_index,
|
"order_index": self.order_index,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Rule(Base, SoftDeleteMixin):
|
class Rule(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
__tablename__ = "rules"
|
__tablename__ = "rules"
|
||||||
# Partial unique: title unique among LIVE rules in a topic (soft-deleted
|
# Partial unique: title unique among LIVE rules in a topic (soft-deleted
|
||||||
# rules don't block recreating/restoring the same title).
|
# rules don't block recreating/restoring the same title).
|
||||||
@@ -109,14 +93,6 @@ class Rule(Base, SoftDeleteMixin):
|
|||||||
why: Mapped[str | None] = mapped_column(Text, nullable=True)
|
why: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
|
how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
order_index: Mapped[int] = mapped_column(Integer, default=0)
|
order_index: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True),
|
|
||||||
default=lambda: datetime.now(timezone.utc),
|
|
||||||
onupdate=lambda: datetime.now(timezone.utc),
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -128,8 +104,8 @@ class Rule(Base, SoftDeleteMixin):
|
|||||||
"why": self.why or "",
|
"why": self.why or "",
|
||||||
"how_to_apply": self.how_to_apply or "",
|
"how_to_apply": self.how_to_apply or "",
|
||||||
"order_index": self.order_index,
|
"order_index": self.order_index,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class ProjectShare(Base, TimestampMixin):
|
class ProjectShare(Base, TimestampMixin):
|
||||||
@@ -37,8 +37,8 @@ class ProjectShare(Base, TimestampMixin):
|
|||||||
"shared_with_group_id": self.shared_with_group_id,
|
"shared_with_group_id": self.shared_with_group_id,
|
||||||
"permission": self.permission,
|
"permission": self.permission,
|
||||||
"invited_by": self.invited_by,
|
"invited_by": self.invited_by,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +74,6 @@ class NoteShare(Base, TimestampMixin):
|
|||||||
"shared_with_group_id": self.shared_with_group_id,
|
"shared_with_group_id": self.shared_with_group_id,
|
||||||
"permission": self.permission,
|
"permission": self.permission,
|
||||||
"invited_by": self.invited_by,
|
"invited_by": self.invited_by,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Index, Integer, Text, UniqueConstraint
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin, TimestampMixin, SoftDeleteMixin
|
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class System(Base, TimestampMixin, SoftDeleteMixin):
|
class System(Base, TimestampMixin, SoftDeleteMixin):
|
||||||
@@ -44,8 +44,8 @@ class System(Base, TimestampMixin, SoftDeleteMixin):
|
|||||||
"color": self.color,
|
"color": self.color,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"order_index": self.order_index,
|
"order_index": self.order_index,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import TimestampMixin
|
from scribe.models.base import TimestampMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class TaskLog(Base, TimestampMixin):
|
class TaskLog(Base, TimestampMixin):
|
||||||
@@ -21,6 +21,6 @@ class TaskLog(Base, TimestampMixin):
|
|||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"content": self.content,
|
"content": self.content,
|
||||||
"duration_minutes": self.duration_minutes,
|
"duration_minutes": self.duration_minutes,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": iso(self.updated_at),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sqlalchemy import Index, Integer, Text
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from scribe.models import Base
|
from scribe.models import Base
|
||||||
from scribe.models.base import CreatedAtMixin
|
from scribe.models.base import CreatedAtMixin, iso
|
||||||
|
|
||||||
|
|
||||||
class User(Base, CreatedAtMixin):
|
class User(Base, CreatedAtMixin):
|
||||||
@@ -26,6 +26,6 @@ class User(Base, CreatedAtMixin):
|
|||||||
"username": self.username,
|
"username": self.username,
|
||||||
"email": self.email,
|
"email": self.email,
|
||||||
"role": self.role,
|
"role": self.role,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": iso(self.created_at),
|
||||||
"has_password": self.password_hash is not None,
|
"has_password": self.password_hash is not None,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user