Files
dify/api/models/human_input_v2.py

1415 lines
50 KiB
Python

"""Persistence models for retained Human Input v2 Contact, channel, IM sync, and inbox capabilities.
The models use explicit logical owner references where database foreign keys are intentionally absent.
Repository queries must therefore preserve complete owner predicates and explicit loading strategies.
Current IM Identity and Binding rows use their Channel as the persistence boundary.
Historical synchronization rows retain their captured Channel references.
"""
from __future__ import annotations
from collections.abc import Sequence
from datetime import datetime
from enum import StrEnum
from typing import Annotated, Literal
import sqlalchemy as sa
from pydantic import (
BaseModel,
ConfigDict,
Field,
JsonValue,
NaiveDatetime,
RootModel,
TypeAdapter,
)
from sqlalchemy import orm
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.human_input_v2.entities import (
EmailProviderType as _EmailProviderType,
)
from core.human_input_v2.entities import (
IMProvider as _IMProvider,
)
from core.human_input_v2.entities import (
IMSyncRemovalReason as _IMSyncRemovalReason,
)
from core.human_input_v2.entities import (
IMSyncResultType as _IMSyncResultType,
)
from core.human_input_v2.entities import (
IMSyncRunStatus as _IMSyncRunStatus,
)
from core.human_input_v2.im_integration.adapters.entities import (
IMEventIngressKind as _IMEventIngressKind,
)
from core.human_input_v2.im_integration.change_log import (
IMReconciliationOperation as _IMReconciliationOperation,
)
from core.human_input_v2.im_integration.change_log import (
IMReconciliationSubjectKind as _IMReconciliationSubjectKind,
)
from core.human_input_v2.im_message_inbox import IM_INBOX_PROVIDER_METADATA_MAX_LENGTH
from core.human_input_v2.resolved_form import ResolvedForm
from core.workflow.nodes.human_input.enums import (
HumanInputFormKind,
HumanInputFormStatus,
)
from core.workflow.nodes.human_input_v2.entities import RecipientConfig
from libs.datetime_utils import naive_utc_now
from libs.uuid_utils import uuidv7
from repositories.human_input_v2.channel_credentials import ChannelEncryptedCredentials as _ChannelEncryptedCredentials
from repositories.human_input_v2.delivery_attempt_repository import DeliveryStatus
from repositories.human_input_v2.delivery_repository import (
DeliveryTargetType,
EmailTargetSnapshot,
IMUserTargetSnapshot,
InitiatorSnapshot,
SubmissionAuthType,
TargetSnapshot,
)
from repositories.human_input_v2.im_channel_repository import IMChannelStatus
from .base import DefaultFieldsDCMixin, TypeBase
from .types import EnumText, FrozenPydanticModelColumn, LongText, StringUUID
class _ImmutableJSONModel(BaseModel):
"""Strict immutable base for structured JSON persistence values."""
model_config = ConfigDict(extra="forbid", frozen=True, strict=True, validate_default=True)
class _ImmutableJSONObject(RootModel[dict[str, JsonValue]]):
"""Strict immutable base for intentionally opaque JSON object payloads."""
model_config = ConfigDict(frozen=True, strict=True, validate_default=True)
class IMIdentityRawPayload(RootModel[dict[str, JsonValue]]):
"""Opaque provider identity payload retained only for diagnostics."""
model_config = ConfigDict(frozen=True, strict=True, validate_default=True)
class IMSyncDirectoryEntryPayload(_ImmutableJSONObject):
"""Opaque provider directory entry captured by one synchronization run."""
class IMSyncContactSnapshot(_ImmutableJSONModel):
"""Immutable contact values needed to display historical sync results."""
contact_id: str = Field(description="Contact identifier captured by the sync result.")
name: str = Field(description="Contact display name captured by the sync result.")
email: str | None = Field(default=None, description="Contact email captured by the sync result.")
avatar_file_id: str | None = Field(default=None, description="Avatar file identifier captured by the result.")
created_at: NaiveDatetime | None = Field(
default=None,
description="Contact creation time captured by new results; absent from historical snapshots.",
)
class IMSyncIdentitySnapshot(_ImmutableJSONModel):
"""Immutable last-known IM identity values retained after removal."""
identity_id: str = Field(description="IM identity identifier captured by the sync result.")
provider: _IMProvider = Field(
strict=False,
description="Provider that owned the captured IM identity.",
)
provider_user_id: str = Field(description="Provider user identifier captured by the sync result.")
display_name: str | None = Field(default=None, description="Provider display name captured by the result.")
email: str | None = Field(default=None, description="Provider email captured by the result.")
class IMIdentityReconciliationSnapshot(_ImmutableJSONModel):
"""Minimal current IM identity state retained by the reconciliation change log."""
subject_kind: Literal[_IMReconciliationSubjectKind.IDENTITY] = _IMReconciliationSubjectKind.IDENTITY
identity_id: str
provider: _IMProvider = Field(strict=False)
provider_user_id: str
display_name: str | None = None
email: str | None = None
normalized_email: str | None = None
last_seen_sync_run_id: str | None = None
class IMBindingReconciliationSnapshot(_ImmutableJSONModel):
"""Minimal current IM binding state retained by the reconciliation change log."""
subject_kind: Literal[_IMReconciliationSubjectKind.BINDING] = _IMReconciliationSubjectKind.BINDING
binding_id: str
identity_id: str
contact_id: str
type IMReconciliationChangeSnapshot = Annotated[
IMIdentityReconciliationSnapshot | IMBindingReconciliationSnapshot,
Field(discriminator="subject_kind"),
]
_IM_RECONCILIATION_SNAPSHOT_ADAPTER: TypeAdapter[IMReconciliationChangeSnapshot] = TypeAdapter(
IMReconciliationChangeSnapshot
)
class ContactSubjectType(StrEnum):
"""Immutable Contact subject discriminator, never a current Contact type."""
ACCOUNT = "account"
EXTERNAL = "external"
class HumanInputContactIdentity(DefaultFieldsDCMixin, TypeBase):
"""Immutable Contact identity shared by every durable Contact reference.
Account profile, membership, Platform visibility, and authorization state
deliberately remain with their source owners. External mutable facts live
in exactly one ``HumanInputExternalContactProfile``.
"""
__tablename__ = "human_input_contact_identities"
__table_args__ = (
sa.CheckConstraint(
"(subject_type = 'account' AND account_id IS NOT NULL) OR "
"(subject_type = 'external' AND account_id IS NULL)",
name="human_input_contact_identities_subject_type_ck",
),
sa.UniqueConstraint(
"account_id",
name="human_input_contact_identities_account_id_uq",
),
{
"comment": (
"Immutable Human Input Contact identities. Mutable Account and External Contact profile facts "
"live with their source owners."
)
},
)
subject_type: Mapped[ContactSubjectType] = mapped_column(
EnumText(ContactSubjectType),
nullable=False,
comment="Immutable Account or External subject discriminator.",
)
account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical accounts.id reference for Account subjects only.",
)
external_profile: Mapped[HumanInputExternalContactProfile | None] = relationship(
lambda: HumanInputExternalContactProfile,
primaryjoin=lambda: sa.and_(
HumanInputContactIdentity.id == orm.foreign(HumanInputExternalContactProfile.contact_id),
HumanInputContactIdentity.subject_type == ContactSubjectType.EXTERNAL,
),
back_populates="identity",
viewonly=True,
lazy="raise",
init=False,
)
class HumanInputExternalContactProfile(TypeBase):
"""Current workspace-owned profile for one External Contact identity."""
__tablename__ = "human_input_external_contact_profiles"
__table_args__ = (
sa.UniqueConstraint(
"tenant_id",
"normalized_email",
name="hiecp_tenant_normalized_email_uq",
),
sa.Index(
"hiecp_tenant_normalized_name_idx",
"tenant_id",
"normalized_name",
),
{
"comment": (
"Current workspace-owned External Contact profiles. Deletion removes both this profile and its "
"Contact identity."
)
},
)
contact_id: Mapped[str] = mapped_column(
StringUUID,
primary_key=True,
comment="Logical human_input_contact_identities.id reference for one External subject.",
)
tenant_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Owning tenants.id used by every current External Contact lookup.",
)
name: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Workspace-managed display name.",
)
normalized_name: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Canonical search value maintained by External Contact writes.",
)
email: Mapped[str] = mapped_column(
sa.String(320),
nullable=False,
comment="Workspace-managed deliverable Email address.",
)
normalized_email: Mapped[str] = mapped_column(
sa.String(320),
nullable=False,
comment="Canonical Email equality value maintained by External Contact writes.",
)
avatar_file_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical upload_files.id reference owned by the same workspace.",
)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
insert_default=naive_utc_now,
init=False,
server_default=sa.func.current_timestamp(),
)
updated_at: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
insert_default=naive_utc_now,
init=False,
server_default=sa.func.current_timestamp(),
onupdate=sa.func.current_timestamp(),
)
identity: Mapped[HumanInputContactIdentity] = relationship(
lambda: HumanInputContactIdentity,
primaryjoin=lambda: orm.foreign(HumanInputExternalContactProfile.contact_id) == HumanInputContactIdentity.id,
back_populates="external_profile",
viewonly=True,
lazy="raise",
init=False,
)
class HumanInputPlatformContactWorkspaceEntry(DefaultFieldsDCMixin, TypeBase):
"""EE-only workspace allow-list entry for an Organization Account contact.
The entry does not own the Contact and does not duplicate workspace
membership or the externally resolved Contact type. Its existence means that
an EE Organization Account contact without current membership is explicitly
available in one workspace. The Enterprise Contact repository must ensure the
referenced Contact is an Account subject. Membership changes never mutate
the Contact identity.
"""
__tablename__ = "human_input_platform_contact_workspace_entries"
__table_args__ = (
sa.UniqueConstraint(
"tenant_id",
"contact_id",
name="hipcwe_tenant_contact_uq",
),
sa.Index("hipcwe_tenant_created_at_id_idx", "tenant_id", "created_at", "id"),
sa.Index("hipcwe_contact_id_idx", "contact_id"),
{
"comment": (
"EE-only workspace allow-list for Organization Account contacts. Workspace membership and External "
"contact ownership must not create rows in this table."
)
},
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False, comment="Logical foreign key to tenants.id.")
contact_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to human_input_contact_identities.id.",
)
added_by_account_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to accounts.id for the administrator who added this directory entry.",
)
class IMMessageEvent(TypeBase):
"""Authenticated callback facts and successful processing time."""
__tablename__ = "im_message_event"
__table_args__ = (
sa.UniqueConstraint(
"provider",
"event_id",
name="im_message_event_provider_event_uq",
),
{"comment": "Durable authenticated IM callback records."},
)
id: Mapped[str] = mapped_column(
StringUUID,
primary_key=True,
insert_default=lambda: str(uuidv7()),
default_factory=lambda: str(uuidv7()),
init=False,
)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
insert_default=naive_utc_now,
default_factory=naive_utc_now,
init=False,
server_default=sa.func.current_timestamp(),
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
kw_only=True,
comment="Logical local Channel routing identifier without physical ownership.",
)
provider: Mapped[_IMProvider] = mapped_column(
EnumText(_IMProvider),
nullable=False,
kw_only=True,
comment="Authenticated Provider discriminator.",
)
tenant_id: Mapped[str] = mapped_column(
sa.String(IM_INBOX_PROVIDER_METADATA_MAX_LENGTH),
nullable=False,
kw_only=True,
comment="Stable authenticated Provider tenant identifier.",
)
event_id: Mapped[str | None] = mapped_column(
sa.String(IM_INBOX_PROVIDER_METADATA_MAX_LENGTH),
nullable=True,
default=None,
kw_only=True,
comment="Real Provider event ID when supplied.",
)
provider_event_time: Mapped[datetime | None] = mapped_column(
sa.DateTime,
nullable=True,
default=None,
kw_only=True,
comment="Provider event timestamp when supplied.",
)
received_at: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
kw_only=True,
comment="Dify receive timestamp for this delivery.",
)
provider_event_type: Mapped[str | None] = mapped_column(
sa.String(IM_INBOX_PROVIDER_METADATA_MAX_LENGTH),
nullable=True,
default=None,
kw_only=True,
comment="Provider-owned event discriminator.",
)
ingress_kind: Mapped[_IMEventIngressKind] = mapped_column(
EnumText(_IMEventIngressKind),
nullable=False,
kw_only=True,
comment="Ingress contract used to construct the Provider payload snapshot.",
)
payload: Mapped[str] = mapped_column(
LongText,
nullable=False,
kw_only=True,
comment="Authenticated Provider-native payload.",
)
request_ip_address: Mapped[str | None] = mapped_column(
sa.Text,
nullable=True,
default=None,
kw_only=True,
comment="HTTP request IP when available; absent for stream and historical events.",
)
processed_at: Mapped[datetime | None] = mapped_column(
sa.DateTime,
nullable=True,
default=None,
kw_only=True,
comment="Timestamp when callback processing completed successfully.",
)
class HumanInputEmailProvider(DefaultFieldsDCMixin, TypeBase):
"""Single workspace-level email provider used for Human Input delivery."""
# TODO(QuantumGhost): Rename this model and table to HumanInputEmailChannel.
__tablename__ = "human_input_email_providers"
__table_args__ = (
sa.UniqueConstraint("tenant_id", name="human_input_email_providers_tenant_uq"),
{"comment": "Workspace-level Human Input email delivery configuration."},
)
provider: Mapped[_EmailProviderType] = mapped_column(
EnumText(_EmailProviderType),
nullable=False,
comment="Configured email provider discriminator.",
)
sender_email: Mapped[str] = mapped_column(
sa.String(320), nullable=False, comment="Configured sender email address."
)
encrypted_credentials: Mapped[_ChannelEncryptedCredentials] = mapped_column(
FrozenPydanticModelColumn(_ChannelEncryptedCredentials),
nullable=False,
comment="Encrypted complete Channel credentials stored as JSON.",
)
tenant_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to tenants.id.",
)
config_version: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=1,
comment="Monotonic Email configuration revision used for compare-and-swap.",
)
sender_name: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
default="",
comment="Optional sender display name.",
)
configured_by_account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical foreign key to accounts.id for the latest configuration write.",
)
class HumanInputIMChannel(DefaultFieldsDCMixin, TypeBase):
"""One current IM Channel in a repository-generated owner slot.
``owner_key`` deliberately replaces a polymorphic database foreign key.
Its unique constraint serializes concurrent first creation for both
workspace and deployment owners without Redis or a separate lock row.
"""
__tablename__ = "human_input_im_channels"
__table_args__ = (
sa.UniqueConstraint(
"owner_key",
name="human_input_im_channels_owner_key_uq",
),
sa.UniqueConstraint(
"webhook_id",
name="human_input_im_channels_webhook_id_uq",
),
sa.CheckConstraint(
"config_version > 0",
name=sa.schema.conv("human_input_im_channels_config_version_positive_ck"),
),
{
"comment": (
"Current owner-scoped Human Input IM Channel configuration. "
"Directory, Binding, Sync, and inbox records remain separately owned."
)
},
)
owner_key: Mapped[str] = mapped_column(
sa.String(50),
nullable=False,
comment="Canonical owner slot: workspace:<tenant_id> or deployment.",
)
provider: Mapped[_IMProvider] = mapped_column(
EnumText(_IMProvider),
nullable=False,
comment="Configured IM provider discriminator.",
)
provider_tenant_id: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Confirmed provider-side organization, tenant, or workspace identifier.",
)
encrypted_credentials: Mapped[_ChannelEncryptedCredentials] = mapped_column(
FrozenPydanticModelColumn(_ChannelEncryptedCredentials),
nullable=False,
comment="Versioned opaque encrypted IM Channel credential envelope.",
)
app_identifier: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Safe provider application identifier used by credential-free projections.",
)
webhook_id: Mapped[str] = mapped_column(
sa.String(32),
nullable=False,
comment="Server-generated globally unique route ID used to derive webhook URLs.",
)
status: Mapped[IMChannelStatus] = mapped_column(
EnumText(IMChannelStatus),
nullable=False,
default=IMChannelStatus.CONNECTED,
comment="Stored credential-safe Channel status snapshot.",
)
config_version: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=1,
comment="Monotonic numeric version paired with the Channel ID for CAS.",
)
configured_by_account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Latest configuring Dify Account; null for deployment-owned writes.",
)
status_reason: Mapped[str | None] = mapped_column(
LongText,
nullable=True,
default=None,
comment="Operator-safe status explanation without provider payload or credentials.",
)
class HumanInputIMIdentity(DefaultFieldsDCMixin, TypeBase):
"""Current Provider user synchronized through one IM Channel.
Searchable and match-critical fields are stored in columns. ``raw_payload``
retains the Provider response for diagnostics without making it query state.
Channel ownership and Provider configuration remain on the parent Channel.
"""
__tablename__ = "human_input_im_identities"
__table_args__ = (
sa.UniqueConstraint(
"channel_id",
"provider_user_id",
name="human_input_im_identities_channel_provider_user_uq",
),
sa.CheckConstraint(
"length(trim(provider_user_id)) > 0",
name="human_input_im_identities_provider_user_nonblank",
),
sa.Index("hiimi_channel_id_idx", "channel_id", "id"),
sa.Index("hiimi_channel_email_idx", "channel_id", "normalized_email"),
sa.Index("hiimi_channel_name_idx", "channel_id", "normalized_name"),
{
"comment": (
"Current Provider users synchronized through one IM Channel. "
"Channel ownership remains outside this table."
)
},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_channels.id reference.",
)
provider_user_id: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Provider-native user identifier within the owning Channel.",
)
raw_payload: Mapped[IMIdentityRawPayload] = mapped_column(
FrozenPydanticModelColumn(IMIdentityRawPayload),
nullable=False,
comment="Latest opaque Provider payload retained for diagnostics.",
)
last_seen_sync_run_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_sync_runs.id reference for the latest observation.",
)
last_seen_at: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
comment="Timestamp of the latest successful Provider observation.",
)
display_name: Mapped[str | None] = mapped_column(
sa.String(255),
nullable=True,
default=None,
comment="Latest canonical non-blank Provider display name.",
)
normalized_name: Mapped[str | None] = mapped_column(
sa.String(255),
nullable=True,
default=None,
comment="Canonical display name used by persistence queries.",
)
email: Mapped[str | None] = mapped_column(
sa.String(320),
nullable=True,
default=None,
comment="Latest canonical non-blank Provider email.",
)
normalized_email: Mapped[str | None] = mapped_column(
sa.String(320),
nullable=True,
default=None,
comment="Canonical email used by matching and persistence queries.",
)
avatar_file_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical upload_files.id reference for the current IM avatar.",
)
class HumanInputIMBinding(DefaultFieldsDCMixin, TypeBase):
"""Default Contact-to-IM-identity Binding for one IM Channel."""
__tablename__ = "human_input_im_bindings"
__table_args__ = (
sa.UniqueConstraint(
"channel_id",
"contact_id",
name="human_input_im_bindings_channel_contact_uq",
),
sa.UniqueConstraint(
"channel_id",
"im_identity_id",
name="human_input_im_bindings_channel_identity_uq",
),
{"comment": "Default Contact-to-IM-identity Bindings for one IM Channel."},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_channels.id reference.",
)
contact_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_contact_identities.id reference.",
)
im_identity_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_identities.id reference.",
)
bound_by_account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Latest Dify Account that manually selected this Binding, when available.",
)
class HumanInputIMBindingWorkspaceOverride(DefaultFieldsDCMixin, TypeBase):
"""Workspace-specific override for one default IM Binding."""
__tablename__ = "human_input_im_workspace_binding_overrides"
__table_args__ = (
sa.UniqueConstraint(
"channel_id",
"tenant_id",
"contact_id",
name="hiimwbo_channel_tenant_contact_uq",
),
sa.UniqueConstraint(
"channel_id",
"tenant_id",
"im_identity_id",
name="hiimwbo_channel_tenant_identity_uq",
),
sa.Index("hiimwbo_channel_identity_idx", "channel_id", "im_identity_id"),
{"comment": "Workspace-specific Binding overrides for one IM Channel."},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_channels.id reference.",
)
tenant_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Target tenants.id whose effective Binding is overridden.",
)
contact_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_contact_identities.id reference.",
)
im_identity_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical human_input_im_identities.id reference.",
)
bound_by_account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Dify Account that selected this workspace override.",
)
class HumanInputIMSyncRun(DefaultFieldsDCMixin, TypeBase):
"""One manually triggered organization IM directory synchronization.
Preventing parallel queued or running rows is a service transaction invariant:
lock the owning channel row before creating a run. A portable partial
unique index is deliberately not used. A worker may apply current identities
and bindings only while ``channel_config_version`` still matches the
owning channel.
"""
__tablename__ = "human_input_im_sync_runs"
__table_args__ = (
sa.CheckConstraint("channel_config_version > 0", name="captured_version_positive"),
sa.CheckConstraint(
"(started_by_account_id IS NOT NULL AND started_by_sys_user_id IS NULL) OR "
"(started_by_account_id IS NULL AND started_by_sys_user_id IS NOT NULL)",
name="initiator_exclusive",
),
sa.CheckConstraint(
"added_count >= 0 AND not_matched_count >= 0 AND failed_count >= 0 AND removed_count >= 0 "
"AND skipped_count >= 0",
name="result_counts_nonnegative",
),
sa.Index("hiimsr_channel_created_idx", "channel_id", "created_at", "id"),
sa.Index(
"hiimsr_channel_status_created_idx",
"channel_id",
"status",
"created_at",
),
{"comment": "Manual IM directory synchronization lifecycle and aggregate counts."},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to human_input_im_channels.id.",
)
channel_config_version: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
comment="Channel configuration revision captured when this synchronization was created.",
)
provider: Mapped[_IMProvider] = mapped_column(
EnumText(_IMProvider),
nullable=False,
comment="Provider snapshot used by this synchronization.",
)
status: Mapped[_IMSyncRunStatus] = mapped_column(
EnumText(_IMSyncRunStatus),
nullable=False,
comment="Current synchronization lifecycle state.",
)
added_count: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=0,
comment="Number of entries newly matched and bound.",
)
not_matched_count: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=0,
comment="Number of entries requiring later manual handling.",
)
failed_count: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=0,
comment="Number of entries that failed reconciliation.",
)
removed_count: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=0,
comment="Number of removed binding facts, including one unbound-identity fact when applicable.",
)
skipped_count: Mapped[int] = mapped_column(
sa.Integer,
nullable=False,
default=0,
comment="Number of entries intentionally skipped.",
)
started_by_account_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical accounts.id reference for CE/SaaS initiators; null only for EE sys_user initiators.",
)
started_by_sys_user_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical reference to upstream EE sys_users.id; mutually exclusive with started_by_account_id.",
)
started_at: Mapped[datetime | None] = mapped_column(
sa.DateTime,
nullable=True,
default=None,
comment="Timestamp when a worker started processing the run.",
)
finished_at: Mapped[datetime | None] = mapped_column(
sa.DateTime,
nullable=True,
default=None,
comment="Terminal completion timestamp.",
)
error_code: Mapped[str | None] = mapped_column(
sa.String(100),
nullable=True,
default=None,
comment="Machine-readable terminal failure code.",
)
error_message: Mapped[str | None] = mapped_column(
LongText,
nullable=True,
default=None,
comment="Operator-safe terminal failure summary.",
)
results: Mapped[list[HumanInputIMSyncResult]] = relationship(
lambda: HumanInputIMSyncResult,
primaryjoin=lambda: HumanInputIMSyncRun.id == orm.foreign(HumanInputIMSyncResult.sync_run_id),
back_populates="sync_run",
viewonly=True,
lazy="raise",
init=False,
)
class HumanInputIMSyncResult(DefaultFieldsDCMixin, TypeBase):
"""Immutable reconciliation result for one entry, removed binding, or diagnostic.
Provider identifiers and normalized email remain queryable columns. JSON is
limited to the raw provider input and immutable display snapshots.
"""
__tablename__ = "human_input_im_sync_results"
__table_args__ = (
sa.UniqueConstraint(
"sync_run_id",
"operation_key",
name="human_input_im_sync_results_run_operation_uq",
),
sa.Index(
"hiimsres_run_type_created_idx",
"sync_run_id",
"result_type",
"created_at",
"id",
),
sa.Index(
"hiimsres_channel_contact_created_idx",
"channel_id",
"contact_id",
"created_at",
),
sa.Index(
"hiimsres_channel_identity_created_idx",
"channel_id",
"im_identity_id",
"created_at",
),
{"comment": "Append-only per-entry, removed-binding, and diagnostic IM synchronization outcomes."},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment=(
"Denormalized logical foreign key to human_input_im_channels.id; "
"must match the channel referenced by sync_run_id."
),
)
sync_run_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to human_input_im_sync_runs.id.",
)
result_type: Mapped[_IMSyncResultType] = mapped_column(
EnumText(_IMSyncResultType),
nullable=False,
comment="Stable result bucket used by pagination.",
)
operation_key: Mapped[str | None] = mapped_column(
sa.String(255),
nullable=True,
default=None,
comment="Deterministic run-local idempotency key; null only for historical results.",
)
provider_user_id: Mapped[str | None] = mapped_column(
sa.String(255),
nullable=True,
default=None,
comment="External provider user identifier observed for this result; not a logical foreign key.",
)
display_name: Mapped[str | None] = mapped_column(
sa.String(255),
nullable=True,
default=None,
comment="Provider display name observed for this result.",
)
email: Mapped[str | None] = mapped_column(
sa.String(320),
nullable=True,
default=None,
comment="Provider email observed for this result.",
)
normalized_email: Mapped[str | None] = mapped_column(
sa.String(320),
nullable=True,
default=None,
comment="Lower-cased provider email used during matching.",
)
contact_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment="Logical foreign key to human_input_contact_identities.id, when a contact was matched.",
)
im_identity_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment=(
"Historical logical foreign key to human_input_im_identities.id; the target row may be deleted after sync."
),
)
im_binding_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
comment=(
"Historical logical foreign key to human_input_im_bindings.id; the target row may be deleted or replaced."
),
)
removal_reason: Mapped[_IMSyncRemovalReason | None] = mapped_column(
EnumText(_IMSyncRemovalReason),
nullable=True,
default=None,
comment="Stable removal reason for removed results only.",
)
reason_code: Mapped[str | None] = mapped_column(
sa.String(100),
nullable=True,
default=None,
comment="Machine-readable failure, skip, or mismatch reason.",
)
reason_message: Mapped[str | None] = mapped_column(
LongText,
nullable=True,
default=None,
comment="Operator-safe detail for diagnostics.",
)
directory_entry_payload: Mapped[IMSyncDirectoryEntryPayload | None] = mapped_column(
FrozenPydanticModelColumn(IMSyncDirectoryEntryPayload),
nullable=True,
default=None,
comment="Immutable provider entry payload Pydantic model observed during this run.",
)
contact_snapshot: Mapped[IMSyncContactSnapshot | None] = mapped_column(
FrozenPydanticModelColumn(IMSyncContactSnapshot),
nullable=True,
default=None,
comment="Immutable contact snapshot Pydantic model used for historical display.",
)
identity_snapshot: Mapped[IMSyncIdentitySnapshot | None] = mapped_column(
FrozenPydanticModelColumn(IMSyncIdentitySnapshot),
nullable=True,
default=None,
comment="Immutable last-known IM identity Pydantic model for removed results.",
)
sync_run: Mapped[HumanInputIMSyncRun] = relationship(
lambda: HumanInputIMSyncRun,
primaryjoin=lambda: orm.foreign(HumanInputIMSyncResult.sync_run_id) == HumanInputIMSyncRun.id,
back_populates="results",
viewonly=True,
lazy="raise",
init=False,
)
identity: Mapped[HumanInputIMIdentity | None] = relationship(
lambda: HumanInputIMIdentity,
primaryjoin=lambda: orm.foreign(HumanInputIMSyncResult.im_identity_id) == HumanInputIMIdentity.id,
viewonly=True,
lazy="raise",
init=False,
)
binding: Mapped[HumanInputIMBinding | None] = relationship(
lambda: HumanInputIMBinding,
primaryjoin=lambda: orm.foreign(HumanInputIMSyncResult.im_binding_id) == HumanInputIMBinding.id,
viewonly=True,
lazy="raise",
init=False,
)
class HumanInputIMReconciliationChange(DefaultFieldsDCMixin, TypeBase):
"""Append-only before/after fact for one committed identity or IM binding mutation."""
__tablename__ = "human_input_im_reconciliation_changes"
__table_args__ = (
sa.UniqueConstraint(
"sync_run_id",
"operation_key",
name="human_input_im_reconciliation_changes_run_operation_uq",
),
sa.CheckConstraint(
"before_snapshot IS NOT NULL OR after_snapshot IS NOT NULL",
name="snapshot_present",
),
sa.CheckConstraint(
"(operation = 'create' AND before_snapshot IS NULL AND after_snapshot IS NOT NULL) OR "
"(operation = 'delete' AND before_snapshot IS NOT NULL AND after_snapshot IS NULL) OR "
"(operation NOT IN ('create', 'delete') AND before_snapshot IS NOT NULL AND after_snapshot IS NOT NULL)",
name="snapshot_operation_shape",
),
sa.CheckConstraint(
"(subject_kind = 'identity' AND im_binding_id IS NULL) OR "
"(subject_kind = 'binding' AND im_binding_id IS NOT NULL)",
name="subject_identifier_shape",
),
sa.Index(
"hiimrc_run_subject_committed_idx",
"sync_run_id",
"subject_kind",
"committed_at",
"id",
),
sa.Index("hiimrc_channel_committed_idx", "channel_id", "committed_at", "id"),
{"comment": "Append-only IM identity and IM binding reconciliation mutation history."},
)
channel_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to human_input_im_channels.id.",
)
sync_run_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
comment="Logical foreign key to human_input_im_sync_runs.id.",
)
operation_key: Mapped[str] = mapped_column(
sa.String(255),
nullable=False,
comment="Deterministic run-local idempotency key.",
)
subject_kind: Mapped[_IMReconciliationSubjectKind] = mapped_column(
EnumText(_IMReconciliationSubjectKind), nullable=False
)
operation: Mapped[_IMReconciliationOperation] = mapped_column(EnumText(_IMReconciliationOperation), nullable=False)
reason_code: Mapped[str] = mapped_column(sa.String(100), nullable=False)
im_identity_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
committed_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False)
im_binding_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
contact_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
before_snapshot: Mapped[IMReconciliationChangeSnapshot | None] = mapped_column(
FrozenPydanticModelColumn(
_IM_RECONCILIATION_SNAPSHOT_ADAPTER,
model_types=(
IMIdentityReconciliationSnapshot,
IMBindingReconciliationSnapshot,
),
),
nullable=True,
default=None,
)
after_snapshot: Mapped[IMReconciliationChangeSnapshot | None] = mapped_column(
FrozenPydanticModelColumn(
_IM_RECONCILIATION_SNAPSHOT_ADAPTER,
model_types=(
IMIdentityReconciliationSnapshot,
IMBindingReconciliationSnapshot,
),
),
nullable=True,
default=None,
)
class HumanInputForm(DefaultFieldsDCMixin, TypeBase):
__tablename__ = "hitlv2_forms"
__table_args__ = (
sa.UniqueConstraint(
"workflow_run_id",
"node_execution_id",
name="hitlv2_forms_execution_uq",
),
sa.Index("hitlv2_forms_run_id_idx", "workflow_run_id", "id"),
# Equality predicates precede the ID cursor used by expiration scans.
sa.Index("hitlv2_forms_expiration_scan_idx", "tenant_id", "app_id", "form_kind", "status", "id"),
sa.Index("hitlv2_forms_node_deadline_idx", "form_kind", "status", "expiration_time", "id"),
sa.Index("hitlv2_forms_global_deadline_idx", "form_kind", "status", "global_timeout_deadline", "id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
workflow_run_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
# correspond to WorkflowNodeExecutionModel.id
node_execution_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
form_kind: Mapped[HumanInputFormKind] = mapped_column(
EnumText(HumanInputFormKind),
nullable=False,
default=HumanInputFormKind.RUNTIME,
kw_only=True,
)
status: Mapped[HumanInputFormStatus] = mapped_column(
EnumText(HumanInputFormStatus),
nullable=False,
default=HumanInputFormStatus.WAITING,
server_default=HumanInputFormStatus.WAITING.value,
kw_only=True,
comment="Persisted form outcome; terminal states are not recomputed from deadlines.",
)
# Form level expiration time.
expiration_time: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
)
# global timeout deadline for this form / workflow execution.
global_timeout_deadline: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
)
# The form definition after resolving all runtime related variables.
resolved_form: Mapped[ResolvedForm] = mapped_column(FrozenPydanticModelColumn(ResolvedForm), nullable=False)
# The following fields are not `None` once the form has been submitted.
submitted_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True)
submitted_by_recipient_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
submitted_by_delivery_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None, kw_only=True)
# submission_data
selected_action_id: Mapped[str | None] = mapped_column(sa.String(200), nullable=True)
# original submission inputs data without any transformation or sanitization.
raw_submission_inputs: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
# validated and sanitized submission data, a mapping from submitted form data to
# variable segments.
#
# For special reference types such as File / FileSegment, the File ownership has been
# validated.
normalized_submission_data: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
class RecipientSubjectType(StrEnum):
CONTACT = "contact"
EMAIL = "email"
END_USER = "end_user"
class RecipientSources(RootModel[Sequence[RecipientConfig]]):
model_config = ConfigDict(frozen=True, strict=True, validate_default=True)
class RecipientSnapshot(BaseModel):
model_config = ConfigDict(frozen=True, strict=True, validate_default=True)
# The name of the recipeint when the recipient is created.
#
# For recipeint without a name (Dyanmic Email / Onetime Email / EndUser), this
# Field is set to `None`.
name: str | None = None
class HumanInputRecipient(DefaultFieldsDCMixin, TypeBase):
__tablename__ = "hitlv2_recipients"
__table_args__ = (
sa.UniqueConstraint(
"form_id",
"subject_type",
"subject_value",
name="hitlv2_recipients_subject_uq",
),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
form_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
subject_type: Mapped[RecipientSubjectType] = mapped_column(
EnumText(RecipientSubjectType),
nullable=False,
)
# The interpretation of identity_value depends on `subject_value`:
#
# - For RecipientSubjectType.CONTACT, this field stores the value of HumanInputContactIdentity.id
# - For RecipientSubjectType.END_USER, this field stores the value of EndUser.id
# - For RecipientSubjectType.EMAIL, this field stores the normalized email of the recipient.
subject_value: Mapped[str] = mapped_column(sa.String(255), nullable=False)
sources: Mapped[RecipientSources] = mapped_column(
FrozenPydanticModelColumn(RecipientSources),
nullable=False,
)
snapshot: Mapped[RecipientSnapshot] = mapped_column(
FrozenPydanticModelColumn(RecipientSnapshot),
nullable=False,
)
class DeliveryType(StrEnum):
IM_CARD = "im_card"
COSNOLE_WEB = "console_web"
STANDALONE_WEB = "standalone_web"
_TARGET_SNAPSHOT_ADAPTER: TypeAdapter[TargetSnapshot] = TypeAdapter(TargetSnapshot)
class HumanInputDelivery(DefaultFieldsDCMixin, TypeBase):
__tablename__ = "hitlv2_deliveries"
__table_args__ = (
sa.Index("hitlv2_deliveries_token_idx", "token_hash"),
sa.Index("hitlv2_deliveries_form_id_idx", "form_id", "id"),
sa.Index("hitlv2_deliveries_form_target_idx", "form_id", "target_type"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
form_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
token_hash: Mapped[str] = mapped_column(sa.String(64), nullable=False)
recipient_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
auth_type: Mapped[SubmissionAuthType] = mapped_column(
EnumText(SubmissionAuthType),
nullable=False,
)
target_type: Mapped[DeliveryTargetType] = mapped_column(EnumText(DeliveryTargetType), nullable=False)
target_snapshot: Mapped[TargetSnapshot] = mapped_column(
FrozenPydanticModelColumn(
_TARGET_SNAPSHOT_ADAPTER,
model_types=(IMUserTargetSnapshot, EmailTargetSnapshot, InitiatorSnapshot),
),
nullable=False,
)
class DeliveryResponse(RootModel[dict[str, JsonValue]]):
model_config = ConfigDict(frozen=True, strict=True, validate_default=True)
class HumanInputDeliveryAttempt(DefaultFieldsDCMixin, TypeBase):
__tablename__ = "hitlv2_delivery_attempts"
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
form_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
delivery_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
status: Mapped[DeliveryStatus] = mapped_column(EnumText(DeliveryStatus), nullable=False)
error_message: Mapped[str | None] = mapped_column(sa.String(200), nullable=True)
response: Mapped[DeliveryResponse] = mapped_column(FrozenPydanticModelColumn(DeliveryResponse), nullable=False)
class AuditEventType(StrEnum):
# The form is opened and viewed.
FORM_VIEW = "form_view"
# An Email OTP send operation
OTP_SEND = "otp_send"
# An Email OTP verification
OTP_VERIFICATION = "otp_verification"
# This seems to break the IM abstration???
IM_CALLBACK_VERIFICATION = "im_callback_verification"
#
FORM_SUBMISSION = "form_submission"
class FormViewEvent(_ImmutableJSONModel):
type: Literal[AuditEventType.FORM_VIEW] = AuditEventType.FORM_VIEW
# None means the request IP was unavailable.
viewer_ip_address: str | None
class OtpSendEvent(_ImmutableJSONModel):
type: Literal[AuditEventType.OTP_SEND] = AuditEventType.OTP_SEND
recipient_id: str
recipient_email: str
sender_ip_address: str | None
class OtpVerificationEvent(_ImmutableJSONModel):
type: Literal[AuditEventType.OTP_VERIFICATION] = AuditEventType.OTP_VERIFICATION
recipient_id: str
recipient_email: str
succeeded: bool
submitter_ip_address: str | None
reason: str # wrong verification code or wrong ticket.
class IMCallbackVerificationEvent(_ImmutableJSONModel):
# We might need to break the abstration.
type: Literal[AuditEventType.IM_CALLBACK_VERIFICATION] = AuditEventType.IM_CALLBACK_VERIFICATION
succeeded: bool
request_ip_address: str | None
reason: str # fail reasons
class ServiceAPIFormSubmissionEvent(_ImmutableJSONModel):
"""Distinguish the approving recipient grant from the Service API caller."""
# TODO(QuantumGhost): we need to check what other information needed to be stored
# in the audit event later.
type: Literal[AuditEventType.FORM_SUBMISSION] = AuditEventType.FORM_SUBMISSION
recipient_id: str
end_user_id: str
type AuditEvent = Annotated[
FormViewEvent | OtpSendEvent | OtpVerificationEvent | IMCallbackVerificationEvent | ServiceAPIFormSubmissionEvent,
Field(discriminator="type"),
]
AUDIT_EVENT_ADAPTER: TypeAdapter[AuditEvent] = TypeAdapter(AuditEvent)
class HumanInputAuditEvent(DefaultFieldsDCMixin, TypeBase):
__tablename__ = "hitlv2_audit_events"
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
form_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
payload: Mapped[AuditEvent] = mapped_column(
FrozenPydanticModelColumn(
AUDIT_EVENT_ADAPTER,
model_types=(
FormViewEvent,
OtpSendEvent,
OtpVerificationEvent,
IMCallbackVerificationEvent,
ServiceAPIFormSubmissionEvent,
),
),
nullable=False,
)
__all__ = [
"ContactSubjectType",
"HumanInputContactIdentity",
"HumanInputEmailProvider",
"HumanInputExternalContactProfile",
"HumanInputIMBinding",
"HumanInputIMBindingWorkspaceOverride",
"HumanInputIMChannel",
"HumanInputIMIdentity",
"HumanInputIMReconciliationChange",
"HumanInputIMSyncResult",
"HumanInputIMSyncRun",
"HumanInputPlatformContactWorkspaceEntry",
"IMBindingReconciliationSnapshot",
"IMIdentityRawPayload",
"IMIdentityReconciliationSnapshot",
"IMMessageEvent",
"IMReconciliationChangeSnapshot",
"IMSyncContactSnapshot",
"IMSyncDirectoryEntryPayload",
"IMSyncIdentitySnapshot",
]