From d2d2d29c347d2f521272b8c8905fe827a61fd555 Mon Sep 17 00:00:00 2001 From: 1445043649 <> Date: Tue, 12 May 2026 21:09:19 +0800 Subject: [PATCH] feat(persistence): add WorkspaceMembershipRow ORM model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新建 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) --- .../deerflow/persistence/models/__init__.py | 12 +++- .../workspace_membership/__init__.py | 16 +++++ .../persistence/workspace_membership/model.py | 62 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 backend/packages/harness/deerflow/persistence/workspace_membership/__init__.py create mode 100644 backend/packages/harness/deerflow/persistence/workspace_membership/model.py diff --git a/backend/packages/harness/deerflow/persistence/models/__init__.py b/backend/packages/harness/deerflow/persistence/models/__init__.py index db2bf452..710ba2fd 100644 --- a/backend/packages/harness/deerflow/persistence/models/__init__.py +++ b/backend/packages/harness/deerflow/persistence/models/__init__.py @@ -9,6 +9,7 @@ The actual ORM classes have moved to entity-specific subpackages: - ``deerflow.persistence.feedback`` - ``deerflow.persistence.user`` - ``deerflow.persistence.workspace`` (Stage 0 PR3) +- ``deerflow.persistence.workspace_membership`` (Stage 0 PR3) ``RunEventRow`` remains in ``deerflow.persistence.models.run_event`` because its storage implementation lives in ``deerflow.runtime.events.store.db`` and @@ -21,5 +22,14 @@ from deerflow.persistence.run.model import RunRow from deerflow.persistence.thread_meta.model import ThreadMetaRow from deerflow.persistence.user.model import UserRow from deerflow.persistence.workspace.model import WorkspaceRow +from deerflow.persistence.workspace_membership.model import WorkspaceMembershipRow -__all__ = ["FeedbackRow", "RunEventRow", "RunRow", "ThreadMetaRow", "UserRow", "WorkspaceRow"] +__all__ = [ + "FeedbackRow", + "RunEventRow", + "RunRow", + "ThreadMetaRow", + "UserRow", + "WorkspaceMembershipRow", + "WorkspaceRow", +] diff --git a/backend/packages/harness/deerflow/persistence/workspace_membership/__init__.py b/backend/packages/harness/deerflow/persistence/workspace_membership/__init__.py new file mode 100644 index 00000000..ff9fa3c5 --- /dev/null +++ b/backend/packages/harness/deerflow/persistence/workspace_membership/__init__.py @@ -0,0 +1,16 @@ +"""Workspace membership persistence — ORM model + repository. + +Tracks who is a member of which workspace and what their role is +within that workspace. Composite primary key ``(workspace_id, user_id)``; +``role`` follows the three-state ``owner`` / ``admin`` / ``member`` model +(Stage 0 only writes ``'owner'``; Stage 2 RBAC rollout opens admin/member). + +A workspace MUST have exactly one ``owner`` — enforced by a partial +unique index. Owner transfer is a two-row transactional swap. +""" + +from __future__ import annotations + +from deerflow.persistence.workspace_membership.model import WorkspaceMembershipRow + +__all__ = ["WorkspaceMembershipRow"] diff --git a/backend/packages/harness/deerflow/persistence/workspace_membership/model.py b/backend/packages/harness/deerflow/persistence/workspace_membership/model.py new file mode 100644 index 00000000..5422f3e4 --- /dev/null +++ b/backend/packages/harness/deerflow/persistence/workspace_membership/model.py @@ -0,0 +1,62 @@ +"""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="所属 workspace;workspace 删除时级联清掉成员记录", + ) + 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_id(Stage 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 个 owner(partial unique 约束保证)。Stage 0 仅写 owner;Stage 2 RBAC 打开 admin/member。")}, + )