fix: sync all models with production DB schema
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:
Agent Zero
2026-08-21 11:20:15 +02:00
parent b3dea611b4
commit 6555655ecf
31 changed files with 124 additions and 75 deletions
+3 -2
View File
@@ -11,9 +11,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """AI Copilot conversation thread — tenant-scoped."""
__tablename__ = "ai_conversations" __tablename__ = "ai_conversations"
@@ -29,7 +30,7 @@ class AIConversation(Base, TenantMixin):
context: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False) 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.""" """Individual messages within an AI conversation — user input, AI response, actions."""
__tablename__ = "ai_messages" __tablename__ = "ai_messages"
+4 -1
View File
@@ -16,15 +16,18 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. # Re-export EntityHistory as DeletionLog for backward compatibility.
# Tests import DeletionLog from app.models.audit and use entity_snapshot attribute. # 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.""" """Audit trail for all create/update/delete/login actions."""
__tablename__ = "audit_log" __tablename__ = "audit_log"
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
+3 -2
View File
@@ -11,9 +11,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """Token for password reset flow."""
__tablename__ = "password_reset_tokens" __tablename__ = "password_reset_tokens"
@@ -29,7 +30,7 @@ class PasswordResetToken(Base, TenantMixin):
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 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.""" """API token for programmatic access."""
__tablename__ = "api_tokens" __tablename__ = "api_tokens"
+2 -1
View File
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """Database backup record scoped to a tenant.
Tracks pg_dump backups with status, file location, and error details. Tracks pg_dump backups with status, file location, and error details.
+3 -1
View File
@@ -13,6 +13,7 @@ from typing import Any
from sqlalchemy import ( from sqlalchemy import (
Computed, Computed,
DateTime,
Float, Float,
ForeignKey, ForeignKey,
Index, Index,
@@ -39,6 +40,7 @@ class Contact(Base, TenantMixin, OwnedMixin):
""" """
__tablename__ = "contacts" __tablename__ = "contacts"
indexed_at: Mapped[Any] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = ( __table_args__ = (
UniqueConstraint("tenant_id", "code", name="uq_contacts_tenant_code"), UniqueConstraint("tenant_id", "code", name="uq_contacts_tenant_code"),
UniqueConstraint("tenant_id", "accounting_code", name="uq_contacts_tenant_accounting_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. """Ansprechpartner — 1:N child of a Contact.
Represents a person working at / associated with a company contact. Represents a person working at / associated with a company contact.
+2 -1
View File
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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). """Records each contact merge operation (source → target).
When two duplicate contacts are merged, the source contact is soft-deleted When two duplicate contacts are merged, the source contact is soft-deleted
+2 -1
View File
@@ -9,9 +9,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """Currency entity — e.g. EUR, USD, GBP."""
__tablename__ = "currencies" __tablename__ = "currencies"
+2 -1
View File
@@ -35,9 +35,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """Universal ACL entry for any entity in the system.
entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event', entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event',
+2 -1
View File
@@ -37,9 +37,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """ABAC policy entry for any entity type in the system.
entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event', entity_type examples: 'contact', 'dms_file', 'mailbox', 'calendar_event',
+3 -1
View File
@@ -12,9 +12,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """Group entity with RBAC permissions and field-level permissions.
Groups are tenant-scoped. Users can be members of multiple groups. 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).""" """N:M association — user membership in groups (per tenant)."""
__tablename__ = "user_groups" __tablename__ = "user_groups"
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = ( __table_args__ = (
UniqueConstraint("user_id", "group_id", "tenant_id", name="uq_user_groups_user_group_tenant"), UniqueConstraint("user_id", "group_id", "tenant_id", name="uq_user_groups_user_group_tenant"),
) )
+4 -2
View File
@@ -19,9 +19,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """User notification entity."""
__tablename__ = "notifications" __tablename__ = "notifications"
@@ -52,6 +53,7 @@ class NotificationType(Base):
"""Registered notification type from a plugin.""" """Registered notification type from a plugin."""
__tablename__ = "notification_types" __tablename__ = "notification_types"
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = (Index("ix_notification_types_key", "type_key"),) __table_args__ = (Index("ix_notification_types_key", "type_key"),)
id: Mapped[uuid.UUID] = mapped_column( 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).""" """User preference for a notification type (opt-in/opt-out)."""
__tablename__ = "notification_preferences" __tablename__ = "notification_preferences"
+2 -1
View File
@@ -21,9 +21,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """Permission delegation — temporary handover of permissions.
from_user_id delegates their permissions to to_user_id from_user_id delegates their permissions to to_user_id
+2 -1
View File
@@ -20,9 +20,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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. """Reusable permission template for entity types.
When applied to an entity, the template evaluates trigger_condition When applied to an entity, the template evaluates trigger_condition
+2 -1
View File
@@ -12,9 +12,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """Role entity with module→action→permission mapping and field-level permissions."""
__tablename__ = "roles" __tablename__ = "roles"
+2 -1
View File
@@ -17,9 +17,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """Immutable session audit record. Runtime session lookup uses Redis."""
__tablename__ = "sessions" __tablename__ = "sessions"
+2 -1
View File
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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.""" """Singleton system settings per tenant — company master data for invoices/quotes."""
__tablename__ = "system_settings" __tablename__ = "system_settings"
+2 -1
View File
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin 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%'.""" """Tax rate entity — e.g. 'Mehrwertsteuer 19%'."""
__tablename__ = "tax_rates" __tablename__ = "tax_rates"
+1
View File
@@ -12,6 +12,7 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, SoftDeleteMixin, TimestampMixin from app.core.db import Base, SoftDeleteMixin, TimestampMixin
from app.models.owned_mixin import OwnedMixin
class User(Base, TimestampMixin, SoftDeleteMixin): class User(Base, TimestampMixin, SoftDeleteMixin):
+2 -2
View File
@@ -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.""" """A running instance of a workflow — tracks current step, status, context."""
__tablename__ = "workflow_instances" __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") 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.""" """Immutable record of every step transition in a workflow instance."""
__tablename__ = "workflow_step_history" __tablename__ = "workflow_step_history"
+3 -3
View File
@@ -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.""" """Which modules are visible in a workspace and their configuration."""
__tablename__ = "workspace_modules" __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).""" """User assignment to a workspace with role (member or manager)."""
__tablename__ = "workspace_users" __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. """Dashboard widget configuration per workspace.
Multiple instances of the same widget type can exist in the same workspace. Multiple instances of the same widget type can exist in the same workspace.
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
class AgentMemory(Base, TenantMixin, OwnedMixin): class AgentMemory(Base, TenantMixin, OwnedMixin):
+6 -6
View File
@@ -22,7 +22,7 @@ from app.models.owned_mixin import OwnedMixin
# --- Providers --- # --- Providers ---
class AIProvider(Base, TenantMixin): class AIProvider(Base, TenantMixin, OwnedMixin):
"""LLM provider configuration (OpenAI, Anthropic, Ollama, etc.).""" """LLM provider configuration (OpenAI, Anthropic, Ollama, etc.)."""
__tablename__ = "ai_providers" __tablename__ = "ai_providers"
@@ -53,7 +53,7 @@ class AIProvider(Base, TenantMixin):
# --- Models --- # --- Models ---
class AIModel(Base, TenantMixin): class AIModel(Base, TenantMixin, OwnedMixin):
"""Available model per provider.""" """Available model per provider."""
__tablename__ = "ai_models" __tablename__ = "ai_models"
@@ -81,7 +81,7 @@ class AIModel(Base, TenantMixin):
# --- Presets --- # --- Presets ---
class AIPreset(Base, TenantMixin): class AIPreset(Base, TenantMixin, OwnedMixin):
"""Model preset: model + parameters + optional system prompt.""" """Model preset: model + parameters + optional system prompt."""
__tablename__ = "ai_presets" __tablename__ = "ai_presets"
@@ -167,7 +167,7 @@ class AIChatSession(Base, TenantMixin, OwnedMixin):
# --- Chat Messages --- # --- Chat Messages ---
class AIChatMessage(Base, TenantMixin): class AIChatMessage(Base, TenantMixin, OwnedMixin):
"""Individual message in a chat session.""" """Individual message in a chat session."""
__tablename__ = "ai_chat_messages" __tablename__ = "ai_chat_messages"
@@ -194,7 +194,7 @@ class AIChatMessage(Base, TenantMixin):
# --- Chat Folders --- # --- Chat Folders ---
class AIChatFolder(Base, TenantMixin): class AIChatFolder(Base, TenantMixin, OwnedMixin):
"""Folder for organizing chat sessions.""" """Folder for organizing chat sessions."""
__tablename__ = "ai_chat_folders" __tablename__ = "ai_chat_folders"
@@ -219,7 +219,7 @@ class AIChatFolder(Base, TenantMixin):
# --- Chat Attachments --- # --- Chat Attachments ---
class AIChatAttachment(Base, TenantMixin): class AIChatAttachment(Base, TenantMixin, OwnedMixin):
"""File attached to a chat message.""" """File attached to a chat message."""
__tablename__ = "ai_chat_attachments" __tablename__ = "ai_chat_attachments"
+2 -2
View File
@@ -64,7 +64,7 @@ class ProactiveSuggestion(Base, TenantMixin, OwnedMixin):
) )
class ContextLog(Base, TenantMixin): class ContextLog(Base, TenantMixin, OwnedMixin):
"""Log of user context changes (page views, entity selections).""" """Log of user context changes (page views, entity selections)."""
__tablename__ = "ai_proactive_context_log" __tablename__ = "ai_proactive_context_log"
@@ -86,7 +86,7 @@ class ContextLog(Base, TenantMixin):
) )
class ProactiveSettings(Base, TenantMixin): class ProactiveSettings(Base, TenantMixin, OwnedMixin):
"""Per-user settings for proactive AI.""" """Per-user settings for proactive AI."""
__tablename__ = "ai_proactive_settings" __tablename__ = "ai_proactive_settings"
+8 -8
View File
@@ -80,7 +80,7 @@ class AgentDefinition(Base, TenantMixin, OwnedMixin):
) )
class AgentVersion(Base, TenantMixin): class AgentVersion(Base, TenantMixin, OwnedMixin):
"""Versioned snapshots of agent definitions.""" """Versioned snapshots of agent definitions."""
__tablename__ = "automation_agent_versions" __tablename__ = "automation_agent_versions"
@@ -135,7 +135,7 @@ class AutomationDefinition(Base, TenantMixin, OwnedMixin):
) )
class AutomationVersion(Base, TenantMixin): class AutomationVersion(Base, TenantMixin, OwnedMixin):
"""Versioned snapshots of automation definitions.""" """Versioned snapshots of automation definitions."""
__tablename__ = "automation_versions" __tablename__ = "automation_versions"
@@ -160,7 +160,7 @@ class AutomationVersion(Base, TenantMixin):
) )
class AutomationCronJob(Base, TenantMixin): class AutomationCronJob(Base, TenantMixin, OwnedMixin):
"""Cron job schedule entries for agent heartbeats, automation triggers, or custom jobs.""" """Cron job schedule entries for agent heartbeats, automation triggers, or custom jobs."""
__tablename__ = "automation_cron_jobs" __tablename__ = "automation_cron_jobs"
@@ -190,7 +190,7 @@ class AutomationCronJob(Base, TenantMixin):
) )
class AgentRun(Base, TenantMixin): class AgentRun(Base, TenantMixin, OwnedMixin):
"""Execution log for agent runs.""" """Execution log for agent runs."""
__tablename__ = "automation_agent_runs" __tablename__ = "automation_agent_runs"
@@ -229,7 +229,7 @@ class AgentRun(Base, TenantMixin):
) )
class AutomationRun(Base, TenantMixin): class AutomationRun(Base, TenantMixin, OwnedMixin):
"""Execution log for automation runs.""" """Execution log for automation runs."""
__tablename__ = "automation_runs" __tablename__ = "automation_runs"
@@ -268,7 +268,7 @@ class AutomationRun(Base, TenantMixin):
dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class AgentRunStep(Base, TenantMixin): class AgentRunStep(Base, TenantMixin, OwnedMixin):
"""Individual step in a ReAct loop execution (Thought → Action → Observation).""" """Individual step in a ReAct loop execution (Thought → Action → Observation)."""
__tablename__ = "automation_agent_run_steps" __tablename__ = "automation_agent_run_steps"
@@ -296,7 +296,7 @@ class AgentRunStep(Base, TenantMixin):
) )
class AgentSubtask(Base, TenantMixin): class AgentSubtask(Base, TenantMixin, OwnedMixin):
"""A subtask delegated from one agent to another for multi-agent orchestration.""" """A subtask delegated from one agent to another for multi-agent orchestration."""
__tablename__ = "agent_subtasks" __tablename__ = "agent_subtasks"
@@ -333,7 +333,7 @@ class AgentSubtask(Base, TenantMixin):
) )
class SkillDefinitionDB(Base, TenantMixin): class SkillDefinitionDB(Base, TenantMixin, OwnedMixin):
"""A skill definition persisted per tenant. """A skill definition persisted per tenant.
Skills are orchestration metadata, NOT a permission source. They reference Skills are orchestration metadata, NOT a permission source. They reference
+9 -4
View File
@@ -19,6 +19,8 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
class Calendar(Base, TenantMixin, OwnedMixin): class Calendar(Base, TenantMixin, OwnedMixin):
@@ -42,6 +44,9 @@ class CalendarEntry(Base, TenantMixin, OwnedMixin):
"""Calendar entry — appointment or task, tenant-scoped, soft-deletable.""" """Calendar entry — appointment or task, tenant-scoped, soft-deletable."""
__tablename__ = "calendar_entries" __tablename__ = "calendar_entries"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
__table_args__ = ( __table_args__ = (
Index("ix_entries_tenant_cal", "tenant_id", "calendar_id"), Index("ix_entries_tenant_cal", "tenant_id", "calendar_id"),
Index("ix_entries_tenant_start", "tenant_id", "start_at"), Index("ix_entries_tenant_start", "tenant_id", "start_at"),
@@ -76,7 +81,7 @@ class CalendarEntry(Base, TenantMixin, OwnedMixin):
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CalendarEntryLink(Base, TenantMixin): class CalendarEntryLink(Base, TenantMixin, OwnedMixin):
"""Link between a calendar entry and an entity (contact).""" """Link between a calendar entry and an entity (contact)."""
__tablename__ = "calendar_entry_links" __tablename__ = "calendar_entry_links"
@@ -94,7 +99,7 @@ class CalendarEntryLink(Base, TenantMixin):
entity_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
class CalendarShare(Base, TenantMixin): class CalendarShare(Base, TenantMixin, OwnedMixin):
"""Calendar sharing — user or group with read/write permission.""" """Calendar sharing — user or group with read/write permission."""
__tablename__ = "calendar_shares" __tablename__ = "calendar_shares"
@@ -150,7 +155,7 @@ class Subtask(Base, TenantMixin, OwnedMixin):
completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
class Resource(Base, TenantMixin): class Resource(Base, TenantMixin, OwnedMixin):
"""Bookable resource — room or equipment.""" """Bookable resource — room or equipment."""
__tablename__ = "resources" __tablename__ = "resources"
@@ -163,7 +168,7 @@ class Resource(Base, TenantMixin):
type: Mapped[str] = mapped_column(String(50), nullable=False) type: Mapped[str] = mapped_column(String(50), nullable=False)
class ResourceBooking(Base, TenantMixin): class ResourceBooking(Base, TenantMixin, OwnedMixin):
"""Booking of a resource for a calendar entry.""" """Booking of a resource for a calendar entry."""
__tablename__ = "resource_bookings" __tablename__ = "resource_bookings"
+8 -1
View File
@@ -5,12 +5,15 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, Text
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
from sqlalchemy.dialects.postgresql import TSVECTOR
from typing import Any
class Folder(Base, TenantMixin, OwnedMixin): class Folder(Base, TenantMixin, OwnedMixin):
@@ -46,6 +49,10 @@ class File(Base, TenantMixin, OwnedMixin):
"""File entity — stored on disk, tenant-scoped, soft-deletable.""" """File entity — stored on disk, tenant-scoped, soft-deletable."""
__tablename__ = "files" __tablename__ = "files"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
content_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
content_text: Mapped[str | None] = mapped_column(Text, nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
__table_args__ = ( __table_args__ = (
Index("ix_files_folder", "folder_id"), Index("ix_files_folder", "folder_id"),
Index("ix_files_tenant", "tenant_id"), Index("ix_files_tenant", "tenant_id"),
+9 -9
View File
@@ -54,7 +54,7 @@ class CommConversation(Base, TenantMixin, OwnedMixin):
metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False) metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False)
class CommParticipant(Base, TenantMixin): class CommParticipant(Base, TenantMixin, OwnedMixin):
"""Participant in a conversation — user, ai, system, gateway, etc.""" """Participant in a conversation — user, ai, system, gateway, etc."""
__tablename__ = "comm_participants" __tablename__ = "comm_participants"
@@ -84,7 +84,7 @@ class CommParticipant(Base, TenantMixin):
left_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) left_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CommMessage(Base, TenantMixin): class CommMessage(Base, TenantMixin, OwnedMixin):
"""Message in a conversation — text content plus rich content blocks.""" """Message in a conversation — text content plus rich content blocks."""
__tablename__ = "comm_messages" __tablename__ = "comm_messages"
@@ -117,7 +117,7 @@ class CommMessage(Base, TenantMixin):
edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CommMessageBlock(Base, TenantMixin): class CommMessageBlock(Base, TenantMixin, OwnedMixin):
"""Rich content block attached to a message.""" """Rich content block attached to a message."""
__tablename__ = "comm_message_blocks" __tablename__ = "comm_message_blocks"
@@ -139,7 +139,7 @@ class CommMessageBlock(Base, TenantMixin):
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class CommMessageAttachment(Base, TenantMixin): class CommMessageAttachment(Base, TenantMixin, OwnedMixin):
"""File attachment on a message — DMS reference or comm-internal upload.""" """File attachment on a message — DMS reference or comm-internal upload."""
__tablename__ = "comm_message_attachments" __tablename__ = "comm_message_attachments"
@@ -165,7 +165,7 @@ class CommMessageAttachment(Base, TenantMixin):
metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False) metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False)
class CommMessageReaction(Base, TenantMixin): class CommMessageReaction(Base, TenantMixin, OwnedMixin):
"""Emoji reaction on a message.""" """Emoji reaction on a message."""
__tablename__ = "comm_message_reactions" __tablename__ = "comm_message_reactions"
@@ -187,7 +187,7 @@ class CommMessageReaction(Base, TenantMixin):
emoji: Mapped[str] = mapped_column(String(50), nullable=False) emoji: Mapped[str] = mapped_column(String(50), nullable=False)
class CommMessageRead(Base, TenantMixin): class CommMessageRead(Base, TenantMixin, OwnedMixin):
"""Read state per user per conversation.""" """Read state per user per conversation."""
__tablename__ = "comm_message_reads" __tablename__ = "comm_message_reads"
@@ -215,7 +215,7 @@ class CommMessageRead(Base, TenantMixin):
) )
class CommConversationPin(Base, TenantMixin): class CommConversationPin(Base, TenantMixin, OwnedMixin):
"""User-specific conversation pinning.""" """User-specific conversation pinning."""
__tablename__ = "comm_conversation_pins" __tablename__ = "comm_conversation_pins"
@@ -238,7 +238,7 @@ class CommConversationPin(Base, TenantMixin):
) )
class CommConversationMute(Base, TenantMixin): class CommConversationMute(Base, TenantMixin, OwnedMixin):
"""User-specific conversation muting.""" """User-specific conversation muting."""
__tablename__ = "comm_conversation_mutes" __tablename__ = "comm_conversation_mutes"
@@ -261,7 +261,7 @@ class CommConversationMute(Base, TenantMixin):
) )
class CommMessageEdit(Base, TenantMixin): class CommMessageEdit(Base, TenantMixin, OwnedMixin):
"""Edit history for messages — stores old content before each edit.""" """Edit history for messages — stores old content before each edit."""
__tablename__ = "comm_message_edits" __tablename__ = "comm_message_edits"
+22 -15
View File
@@ -21,6 +21,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
# --- Mail Accounts (F-MAIL-14, F-MAIL-18) --- # --- Mail Accounts (F-MAIL-14, F-MAIL-18) ---
@@ -61,7 +64,7 @@ class MailAccount(Base, TenantMixin, OwnedMixin):
# --- Mail Folders (F-MAIL-01, F-MAIL-19) --- # --- Mail Folders (F-MAIL-01, F-MAIL-19) ---
class MailFolder(Base, TenantMixin): class MailFolder(Base, TenantMixin, OwnedMixin):
"""IMAP folder mapped to a mail account.""" """IMAP folder mapped to a mail account."""
__tablename__ = "mail_folders" __tablename__ = "mail_folders"
@@ -94,10 +97,14 @@ class MailFolder(Base, TenantMixin):
# --- Mails (F-MAIL-01, F-MAIL-03, F-MAIL-05) --- # --- Mails (F-MAIL-01, F-MAIL-03, F-MAIL-05) ---
class Mail(Base, TenantMixin): class Mail(Base, TenantMixin, OwnedMixin):
"""Individual email message stored locally after IMAP sync.""" """Individual email message stored locally after IMAP sync."""
__tablename__ = "mails" __tablename__ = "mails"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
body_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
company_id: Mapped[Any] = mapped_column(PGUUID(as_uuid=True), nullable=True)
__table_args__ = ( __table_args__ = (
Index("ix_mails_folder", "folder_id"), Index("ix_mails_folder", "folder_id"),
Index("ix_mails_account", "account_id"), Index("ix_mails_account", "account_id"),
@@ -148,7 +155,7 @@ class Mail(Base, TenantMixin):
# --- Mail Attachments (F-MAIL-04) --- # --- Mail Attachments (F-MAIL-04) ---
class MailAttachment(Base, TenantMixin): class MailAttachment(Base, TenantMixin, OwnedMixin):
"""Attachment on a mail, optionally linked to a DMS file.""" """Attachment on a mail, optionally linked to a DMS file."""
__tablename__ = "mail_attachments" __tablename__ = "mail_attachments"
@@ -178,7 +185,7 @@ class MailAttachment(Base, TenantMixin):
# --- Mail Labels (F-MAIL-09) --- # --- Mail Labels (F-MAIL-09) ---
class MailLabel(Base, TenantMixin): class MailLabel(Base, TenantMixin, OwnedMixin):
"""Custom label/tag for mails (colored).""" """Custom label/tag for mails (colored)."""
__tablename__ = "mail_labels" __tablename__ = "mail_labels"
@@ -192,7 +199,7 @@ class MailLabel(Base, TenantMixin):
user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
class MailLabelAssignment(Base, TenantMixin): class MailLabelAssignment(Base, TenantMixin, OwnedMixin):
"""Many-to-many: mail <-> label.""" """Many-to-many: mail <-> label."""
__tablename__ = "mail_label_assignments" __tablename__ = "mail_label_assignments"
@@ -220,7 +227,7 @@ class MailLabelAssignment(Base, TenantMixin):
# --- Mail Rules (F-MAIL-07) --- # --- Mail Rules (F-MAIL-07) ---
class MailRule(Base, TenantMixin): class MailRule(Base, TenantMixin, OwnedMixin):
"""Filter rule: conditions to actions for incoming mails.""" """Filter rule: conditions to actions for incoming mails."""
__tablename__ = "mail_rules" __tablename__ = "mail_rules"
@@ -247,7 +254,7 @@ class MailRule(Base, TenantMixin):
# --- Mail Templates (F-MAIL-06) --- # --- Mail Templates (F-MAIL-06) ---
class MailTemplate(Base, TenantMixin): class MailTemplate(Base, TenantMixin, OwnedMixin):
"""Email template with placeholder substitution.""" """Email template with placeholder substitution."""
__tablename__ = "mail_templates" __tablename__ = "mail_templates"
@@ -265,7 +272,7 @@ class MailTemplate(Base, TenantMixin):
# --- Mail Signatures (F-MAIL-13) --- # --- Mail Signatures (F-MAIL-13) ---
class MailSignature(Base, TenantMixin): class MailSignature(Base, TenantMixin, OwnedMixin):
"""Email signature (HTML) per user, optionally per account.""" """Email signature (HTML) per user, optionally per account."""
__tablename__ = "mail_signatures" __tablename__ = "mail_signatures"
@@ -288,7 +295,7 @@ class MailSignature(Base, TenantMixin):
# --- Vacation Auto-Reply (F-MAIL-08) --- # --- Vacation Auto-Reply (F-MAIL-08) ---
class VacationSentLog(Base, TenantMixin): class VacationSentLog(Base, TenantMixin, OwnedMixin):
"""Dedup log for vacation auto-replies (one per sender per 24 hours).""" """Dedup log for vacation auto-replies (one per sender per 24 hours)."""
__tablename__ = "vacation_sent_log" __tablename__ = "vacation_sent_log"
@@ -312,7 +319,7 @@ class VacationSentLog(Base, TenantMixin):
# --- Seen-By Tracking (F-MAIL-15) --- # --- Seen-By Tracking (F-MAIL-15) ---
class MailSeenBy(Base, TenantMixin): class MailSeenBy(Base, TenantMixin, OwnedMixin):
"""Tracks which users have seen a mail in a shared mailbox.""" """Tracks which users have seen a mail in a shared mailbox."""
__tablename__ = "mail_seen_by" __tablename__ = "mail_seen_by"
@@ -336,7 +343,7 @@ class MailSeenBy(Base, TenantMixin):
# --- Delegates (F-MAIL-16) --- # --- Delegates (F-MAIL-16) ---
class MailAccountDelegate(Base, TenantMixin): class MailAccountDelegate(Base, TenantMixin, OwnedMixin):
"""Delegate access to a mail account. """Delegate access to a mail account.
access_level values: access_level values:
@@ -367,7 +374,7 @@ class MailAccountDelegate(Base, TenantMixin):
# --- Send Permissions (F-MAIL-17) --- # --- Send Permissions (F-MAIL-17) ---
class MailAccountSendPermission(Base, TenantMixin): class MailAccountSendPermission(Base, TenantMixin, OwnedMixin):
"""Permission for a user to send as a shared/group mailbox.""" """Permission for a user to send as a shared/group mailbox."""
__tablename__ = "mail_account_send_permissions" __tablename__ = "mail_account_send_permissions"
@@ -390,7 +397,7 @@ class MailAccountSendPermission(Base, TenantMixin):
# --- PGP Keys (F-MAIL-12) --- # --- PGP Keys (F-MAIL-12) ---
class PgpKey(Base, TenantMixin): class PgpKey(Base, TenantMixin, OwnedMixin):
"""PGP private key for a user (encrypted at rest).""" """PGP private key for a user (encrypted at rest)."""
__tablename__ = "pgp_keys" __tablename__ = "pgp_keys"
@@ -405,7 +412,7 @@ class PgpKey(Base, TenantMixin):
public_key_armored: Mapped[str] = mapped_column(Text, nullable=False) public_key_armored: Mapped[str] = mapped_column(Text, nullable=False)
class ContactPgpKey(Base, TenantMixin): class ContactPgpKey(Base, TenantMixin, OwnedMixin):
"""Public PGP key for a contact.""" """Public PGP key for a contact."""
__tablename__ = "contact_pgp_keys" __tablename__ = "contact_pgp_keys"
@@ -419,7 +426,7 @@ class ContactPgpKey(Base, TenantMixin):
key_id: Mapped[str] = mapped_column(String(255), nullable=False, default="") key_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
class MailSyncQueue(Base, TenantMixin): class MailSyncQueue(Base, TenantMixin, OwnedMixin):
"""Queue for pending IMAP operations that need retry.""" """Queue for pending IMAP operations that need retry."""
__tablename__ = "mail_sync_queue" __tablename__ = "mail_sync_queue"
+1 -1
View File
@@ -13,7 +13,7 @@ from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
class Permission(Base, TenantMixin): class Permission(Base, TenantMixin, OwnedMixin):
"""File/folder permission — grants access_level to a user or group.""" """File/folder permission — grants access_level to a user or group."""
__tablename__ = "permissions" __tablename__ = "permissions"
+6 -1
View File
@@ -10,12 +10,17 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
class Tag(Base, TenantMixin, OwnedMixin): class Tag(Base, TenantMixin, OwnedMixin):
"""Tag entity — globally managed, tenant-scoped.""" """Tag entity — globally managed, tenant-scoped."""
__tablename__ = "tags" __tablename__ = "tags"
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
__table_args__ = ( __table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_tags_tenant_name"), UniqueConstraint("tenant_id", "name", name="uq_tags_tenant_name"),
Index("ix_tags_tenant", "tenant_id"), Index("ix_tags_tenant", "tenant_id"),
@@ -28,7 +33,7 @@ class Tag(Base, TenantMixin, OwnedMixin):
color: Mapped[str] = mapped_column(String(7), nullable=False, default="#6B7280") color: Mapped[str] = mapped_column(String(7), nullable=False, default="#6B7280")
class TagAssignment(Base, TenantMixin): class TagAssignment(Base, TenantMixin, OwnedMixin):
"""N:M assignment between tags and entities (companies, contacts, files, folders).""" """N:M assignment between tags and entities (companies, contacts, files, folders)."""
__tablename__ = "tag_assignments" __tablename__ = "tag_assignments"
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin from app.core.db import Base, TenantMixin
class SearchProviderRegistry(Base, TenantMixin): class SearchProviderRegistry(Base, TenantMixin, OwnedMixin):
"""Registry of active search providers per tenant.""" """Registry of active search providers per tenant."""
__tablename__ = "unified_search_providers" __tablename__ = "unified_search_providers"
@@ -31,7 +31,7 @@ class SearchProviderRegistry(Base, TenantMixin):
config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
class SearchIndexLog(Base, TenantMixin): class SearchIndexLog(Base, TenantMixin, OwnedMixin):
"""Log of indexing actions for audit and debugging.""" """Log of indexing actions for audit and debugging."""
__tablename__ = "unified_search_index_log" __tablename__ = "unified_search_index_log"
@@ -50,9 +50,10 @@ class SearchIndexLog(Base, TenantMixin):
error_message: Mapped[str | None] = mapped_column(Text, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
from pgvector.sqlalchemy import Vector # noqa: E402 from pgvector.sqlalchemy import Vector # noqa: E402
from app.models.owned_mixin import OwnedMixin
class DocumentChunk(Base, TenantMixin): class DocumentChunk(Base, TenantMixin, OwnedMixin):
"""Chunk of a DMS file's extracted text, with its own embedding for RAG.""" """Chunk of a DMS file's extracted text, with its own embedding for RAG."""
__tablename__ = "document_chunks" __tablename__ = "document_chunks"