Files
leocrm/app/models/workflow.py
Agent Zero 6555655ecf
Check Cross-Plugin Imports / check (push) Has been cancelled
fix: sync all models with production DB schema
- 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
2026-08-21 11:20:15 +02:00

104 lines
4.8 KiB
Python

"""Workflow, WorkflowInstance, and WorkflowStepHistory models — tenant-scoped with RLS."""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
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 Workflow(Base, TenantMixin, OwnedMixin):
"""Workflow definition — a template of steps stored as JSONB, tenant-scoped."""
__tablename__ = "workflows"
__table_args__ = (
Index("ix_workflows_tenant_active", "tenant_id", "is_active"),
Index("ix_workflows_tenant_trigger", "tenant_id", "trigger_event"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
trigger_event: Mapped[str | None] = mapped_column(String(100), nullable=True)
steps: Mapped[list[dict[str, Any]]] = mapped_column(JSONB, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
class WorkflowInstance(Base, TenantMixin, OwnedMixin):
"""A running instance of a workflow — tracks current step, status, context."""
__tablename__ = "workflow_instances"
__table_args__ = (
Index("ix_wf_instances_tenant_status", "tenant_id", "status"),
Index("ix_wf_instances_tenant_workflow", "tenant_id", "workflow_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
workflow_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("workflows.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending")
# pending → in_progress → completed / rejected / cancelled
current_step_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
context: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
initiated_by: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
timeout_hours: Mapped[int | None] = mapped_column(Integer, nullable=True)
timeout_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# G-RUN: Durable/Resume semantics
resume_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
resume_reason: Mapped[str | None] = mapped_column(String(50), nullable=True) # wait, approval, event, webhook, cron
step_state: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False, server_default="{}")
idempotency_key: Mapped[str | None] = mapped_column(String(255), nullable=True)
lock_owner: Mapped[str | None] = mapped_column(String(100), nullable=True)
lock_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
max_retries: Mapped[int] = mapped_column(Integer, nullable=False, default=3, server_default="3")
class WorkflowStepHistory(Base, TenantMixin, OwnedMixin):
"""Immutable record of every step transition in a workflow instance."""
__tablename__ = "workflow_step_history"
__table_args__ = (Index("ix_wf_step_history_tenant_instance", "tenant_id", "instance_id"),)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
instance_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("workflow_instances.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
step_index: Mapped[int] = mapped_column(Integer, nullable=False)
step_type: Mapped[str] = mapped_column(String(50), nullable=False)
action: Mapped[str] = mapped_column(
String(50), nullable=False
) # entered, approved, rejected, skipped, cancelled
actor_id: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
details: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)