Files
ZY-Agent/backend/packages/harness/deerflow/persistence/workspace_membership/model.py
T
1445043649 d2d2d29c34 feat(persistence): add WorkspaceMembershipRow ORM model
新建 backend/packages/harness/deerflow/persistence/workspace_membership/
{__init__.py, model.py},注册到 Base.metadata。

WorkspaceMembershipRow schema(来自 workspace-schema-design §2.2 锁定版):
  - workspace_id String(36) FK workspaces.id ON DELETE CASCADE — PK part
  - user_id String(36) FK users.id ON DELETE CASCADE — PK part
  - role String(16) NOT NULL(Stage 0 仅写 'owner',schema 允许 'admin'/'member'
    为 Stage 2 RBAC 准备)
  - invited_by String(36) FK users.id ON DELETE SET NULL nullable(Stage 2
    invitation 流程用)
  - joined_at DateTime(tz=True)

索引:
  - idx_workspace_memberships_user (user_id, workspace_id) — 倒查索引让
    /auth/me 列 user 所属 workspaces 走索引
  - idx_one_owner_per_workspace partial UNIQUE on workspace_id WHERE role='owner'
    —— 一 workspace 严格 1 个 owner;sqlite_where + postgresql_where 双驱动
    并存(实测两边都识别)

Stage 0 PR3 T3.6(提前到 T3.4-T3.5 之前;WorkspaceRepository.list_by_user
等会 JOIN 这张表)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:09:19 +08:00

63 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ORM model for workspace memberships (which user is in which workspace, with what role)."""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import DateTime, ForeignKey, Index, String, text
from sqlalchemy.orm import Mapped, mapped_column
from deerflow.persistence.base import Base
class WorkspaceMembershipRow(Base):
__tablename__ = "workspace_memberships"
workspace_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("workspaces.id", ondelete="CASCADE"),
primary_key=True,
comment="所属 workspaceworkspace 删除时级联清掉成员记录",
)
user_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("users.id", ondelete="CASCADE"),
primary_key=True,
comment="成员 user_id;用户删除时级联清掉成员记录",
)
role: Mapped[str] = mapped_column(
String(16),
nullable=False,
comment='角色字符串:Stage 0 仅写 "owner"Stage 2 RBAC 打开 "admin"/"member"。用 String(16) 而非 enum 以便未来加 "viewer"/"auditor" 不动 schema',
)
invited_by: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
comment="邀请人 user_idStage 2 invitation 流程才写);邀请人被删时此字段清空(不影响成员记录本身)",
)
joined_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(UTC),
comment="加入 workspace 的时间(UTC",
)
__table_args__ = (
# 倒查索引:列出 user 所属的所有 workspace/auth/me 用)。
# 复合主键 (workspace_id, user_id) 的前导列是 workspace_id
# 所以"按 user_id 查"需要单独的索引。
Index("idx_workspace_memberships_user", "user_id", "workspace_id"),
# 一个 workspace 严格 1 个 owner —— partial unique on role='owner'。
# SQLite + Postgres 都支持 WHERE 子句的 partial unique;双驱动
# 维护两套等价 where 表达式。
Index(
"idx_one_owner_per_workspace",
"workspace_id",
unique=True,
sqlite_where=text("role = 'owner'"),
postgresql_where=text("role = 'owner'"),
),
{"comment": ("工作空间成员表(multi-tenant RBAC)。复合 PK (workspace_id, user_id);每个 workspace 必有恰好 1 个 ownerpartial unique 约束保证)。Stage 0 仅写 ownerStage 2 RBAC 打开 admin/member。")},
)