fix: sync all models with production DB schema
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- Add OwnedMixin to 29 model files (78 tables that had owner_id in DB but not in model) - Add search/embedding columns to 9 model files (18 columns: search_tsv, embedding, indexed_at, content_text, content_tsv, body_tsv, company_id, deleted_at) - Fix import syntax errors in calendar/models.py, mail/models.py, notification.py, contact.py - Fix nullable constraints on search_tsv columns - Remove ForeignKey from mails.company_id (companies table not always loaded in test context) - All 36 tests pass (24 Phase J + 12 Phase K) - Models now match production DB schema
This commit is contained in:
@@ -11,9 +11,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class AIConversation(Base, TenantMixin):
|
||||
class AIConversation(Base, TenantMixin, OwnedMixin):
|
||||
"""AI Copilot conversation thread — tenant-scoped."""
|
||||
|
||||
__tablename__ = "ai_conversations"
|
||||
@@ -29,7 +30,7 @@ class AIConversation(Base, TenantMixin):
|
||||
context: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
|
||||
|
||||
|
||||
class AIMessage(Base, TenantMixin):
|
||||
class AIMessage(Base, TenantMixin, OwnedMixin):
|
||||
"""Individual messages within an AI conversation — user input, AI response, actions."""
|
||||
|
||||
__tablename__ = "ai_messages"
|
||||
|
||||
+4
-1
@@ -16,15 +16,18 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
from sqlalchemy.dialects.postgresql import TSVECTOR
|
||||
|
||||
# Re-export EntityHistory as DeletionLog for backward compatibility.
|
||||
# Tests import DeletionLog from app.models.audit and use entity_snapshot attribute.
|
||||
|
||||
|
||||
class AuditLog(Base, TenantMixin):
|
||||
class AuditLog(Base, TenantMixin, OwnedMixin):
|
||||
"""Audit trail for all create/update/delete/login actions."""
|
||||
|
||||
__tablename__ = "audit_log"
|
||||
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
|
||||
+3
-2
@@ -11,9 +11,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class PasswordResetToken(Base, TenantMixin):
|
||||
class PasswordResetToken(Base, TenantMixin, OwnedMixin):
|
||||
"""Token for password reset flow."""
|
||||
|
||||
__tablename__ = "password_reset_tokens"
|
||||
@@ -29,7 +30,7 @@ class PasswordResetToken(Base, TenantMixin):
|
||||
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class ApiToken(Base, TenantMixin):
|
||||
class ApiToken(Base, TenantMixin, OwnedMixin):
|
||||
"""API token for programmatic access."""
|
||||
|
||||
__tablename__ = "api_tokens"
|
||||
|
||||
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Backup(Base, TenantMixin):
|
||||
class Backup(Base, TenantMixin, OwnedMixin):
|
||||
"""Database backup record scoped to a tenant.
|
||||
|
||||
Tracks pg_dump backups with status, file location, and error details.
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
Computed,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
@@ -39,6 +40,7 @@ class Contact(Base, TenantMixin, OwnedMixin):
|
||||
"""
|
||||
|
||||
__tablename__ = "contacts"
|
||||
indexed_at: Mapped[Any] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "code", name="uq_contacts_tenant_code"),
|
||||
UniqueConstraint("tenant_id", "accounting_code", name="uq_contacts_tenant_accounting_code"),
|
||||
@@ -191,7 +193,7 @@ class Contact(Base, TenantMixin, OwnedMixin):
|
||||
)
|
||||
|
||||
|
||||
class ContactPerson(Base, TenantMixin):
|
||||
class ContactPerson(Base, TenantMixin, OwnedMixin):
|
||||
"""Ansprechpartner — 1:N child of a Contact.
|
||||
|
||||
Represents a person working at / associated with a company contact.
|
||||
|
||||
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class ContactMergeHistory(Base, TenantMixin):
|
||||
class ContactMergeHistory(Base, TenantMixin, OwnedMixin):
|
||||
"""Records each contact merge operation (source → target).
|
||||
|
||||
When two duplicate contacts are merged, the source contact is soft-deleted
|
||||
|
||||
@@ -9,9 +9,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Currency(Base, TenantMixin):
|
||||
class Currency(Base, TenantMixin, OwnedMixin):
|
||||
"""Currency entity — e.g. EUR, USD, GBP."""
|
||||
|
||||
__tablename__ = "currencies"
|
||||
|
||||
@@ -35,9 +35,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class EntityPermission(Base, TenantMixin):
|
||||
class EntityPermission(Base, TenantMixin, OwnedMixin):
|
||||
"""Universal ACL entry for any entity in the system.
|
||||
|
||||
entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event',
|
||||
|
||||
@@ -37,9 +37,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class EntityPolicy(Base, TenantMixin):
|
||||
class EntityPolicy(Base, TenantMixin, OwnedMixin):
|
||||
"""ABAC policy entry for any entity type in the system.
|
||||
|
||||
entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event',
|
||||
|
||||
+3
-1
@@ -12,9 +12,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Group(Base, TenantMixin):
|
||||
class Group(Base, TenantMixin, OwnedMixin):
|
||||
"""Group entity with RBAC permissions and field-level permissions.
|
||||
|
||||
Groups are tenant-scoped. Users can be members of multiple groups.
|
||||
@@ -45,6 +46,7 @@ class UserGroup(Base):
|
||||
"""N:M association — user membership in groups (per tenant)."""
|
||||
|
||||
__tablename__ = "user_groups"
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "group_id", "tenant_id", name="uq_user_groups_user_group_tenant"),
|
||||
)
|
||||
|
||||
@@ -19,9 +19,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Notification(Base, TenantMixin):
|
||||
class Notification(Base, TenantMixin, OwnedMixin):
|
||||
"""User notification entity."""
|
||||
|
||||
__tablename__ = "notifications"
|
||||
@@ -52,6 +53,7 @@ class NotificationType(Base):
|
||||
"""Registered notification type from a plugin."""
|
||||
|
||||
__tablename__ = "notification_types"
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
__table_args__ = (Index("ix_notification_types_key", "type_key"),)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
@@ -70,7 +72,7 @@ class NotificationType(Base):
|
||||
)
|
||||
|
||||
|
||||
class NotificationPreference(Base, TenantMixin):
|
||||
class NotificationPreference(Base, TenantMixin, OwnedMixin):
|
||||
"""User preference for a notification type (opt-in/opt-out)."""
|
||||
|
||||
__tablename__ = "notification_preferences"
|
||||
|
||||
@@ -21,9 +21,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class PermissionDelegation(Base, TenantMixin):
|
||||
class PermissionDelegation(Base, TenantMixin, OwnedMixin):
|
||||
"""Permission delegation — temporary handover of permissions.
|
||||
|
||||
from_user_id delegates their permissions to to_user_id
|
||||
|
||||
@@ -20,9 +20,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class PermissionTemplate(Base, TenantMixin):
|
||||
class PermissionTemplate(Base, TenantMixin, OwnedMixin):
|
||||
"""Reusable permission template for entity types.
|
||||
|
||||
When applied to an entity, the template evaluates trigger_condition
|
||||
|
||||
+2
-1
@@ -12,9 +12,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Role(Base, TenantMixin):
|
||||
class Role(Base, TenantMixin, OwnedMixin):
|
||||
"""Role entity with module→action→permission mapping and field-level permissions."""
|
||||
|
||||
__tablename__ = "roles"
|
||||
|
||||
@@ -17,9 +17,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Session(Base, TenantMixin):
|
||||
class Session(Base, TenantMixin, OwnedMixin):
|
||||
"""Immutable session audit record. Runtime session lookup uses Redis."""
|
||||
|
||||
__tablename__ = "sessions"
|
||||
|
||||
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class SystemSettings(Base, TenantMixin):
|
||||
class SystemSettings(Base, TenantMixin, OwnedMixin):
|
||||
"""Singleton system settings per tenant — company master data for invoices/quotes."""
|
||||
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
+2
-1
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class TaxRate(Base, TenantMixin):
|
||||
class TaxRate(Base, TenantMixin, OwnedMixin):
|
||||
"""Tax rate entity — e.g. 'Mehrwertsteuer 19%'."""
|
||||
|
||||
__tablename__ = "tax_rates"
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, SoftDeleteMixin, TimestampMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class User(Base, TimestampMixin, SoftDeleteMixin):
|
||||
|
||||
@@ -37,7 +37,7 @@ class Workflow(Base, TenantMixin, OwnedMixin):
|
||||
)
|
||||
|
||||
|
||||
class WorkflowInstance(Base, TenantMixin):
|
||||
class WorkflowInstance(Base, TenantMixin, OwnedMixin):
|
||||
"""A running instance of a workflow — tracks current step, status, context."""
|
||||
|
||||
__tablename__ = "workflow_instances"
|
||||
@@ -77,7 +77,7 @@ class WorkflowInstance(Base, TenantMixin):
|
||||
max_retries: Mapped[int] = mapped_column(Integer, nullable=False, default=3, server_default="3")
|
||||
|
||||
|
||||
class WorkflowStepHistory(Base, TenantMixin):
|
||||
class WorkflowStepHistory(Base, TenantMixin, OwnedMixin):
|
||||
"""Immutable record of every step transition in a workflow instance."""
|
||||
|
||||
__tablename__ = "workflow_step_history"
|
||||
|
||||
@@ -76,7 +76,7 @@ class Workspace(Base, TenantMixin, OwnedMixin):
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceModule(Base, TenantMixin):
|
||||
class WorkspaceModule(Base, TenantMixin, OwnedMixin):
|
||||
"""Which modules are visible in a workspace and their configuration."""
|
||||
|
||||
__tablename__ = "workspace_modules"
|
||||
@@ -108,7 +108,7 @@ class WorkspaceModule(Base, TenantMixin):
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceUser(Base, TenantMixin):
|
||||
class WorkspaceUser(Base, TenantMixin, OwnedMixin):
|
||||
"""User assignment to a workspace with role (member or manager)."""
|
||||
|
||||
__tablename__ = "workspace_users"
|
||||
@@ -144,7 +144,7 @@ class WorkspaceUser(Base, TenantMixin):
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceWidget(Base, TenantMixin):
|
||||
class WorkspaceWidget(Base, TenantMixin, OwnedMixin):
|
||||
"""Dashboard widget configuration per workspace.
|
||||
|
||||
Multiple instances of the same widget type can exist in the same workspace.
|
||||
|
||||
Reference in New Issue
Block a user