refactor(db): rename SearchSpace ORM core to Workspace (Phase 2 Wave A)

Flip the symbolic name in db.py: classes SearchSpace/Role/Membership/Invite ->
Workspace*, attributes search_space_id -> workspace_id (and owner_*), relationship
attrs + back_populates pairs, and drop the Phase 1 Column("workspace_id", ...) shim
now that attribute name == column name. Enum values 'SEARCH_SPACE' and the
SearchSourceConnector class are intentionally untouched (carve-outs).

Part of the atomic SearchSpace -> Workspace rename; the suite goes green only once
Waves B-F land (no half-renamed steady state).
This commit is contained in:
CREDO23 2026-06-26 18:18:50 +02:00
parent 49d3001fbf
commit df03565940

View file

@ -292,7 +292,7 @@ INCENTIVE_TASKS_CONFIG = {
class Permission(StrEnum): class Permission(StrEnum):
""" """
Granular permissions for search space resources. Granular permissions for workspace resources.
Use '*' (FULL_ACCESS) to grant all permissions. Use '*' (FULL_ACCESS) to grant all permissions.
""" """
@ -363,10 +363,10 @@ class Permission(StrEnum):
ROLES_UPDATE = "roles:update" ROLES_UPDATE = "roles:update"
ROLES_DELETE = "roles:delete" ROLES_DELETE = "roles:delete"
# Search Space Settings # Workspace Settings
SETTINGS_VIEW = "settings:view" SETTINGS_VIEW = "settings:view"
SETTINGS_UPDATE = "settings:update" SETTINGS_UPDATE = "settings:update"
SETTINGS_DELETE = "settings:delete" # Delete the entire search space SETTINGS_DELETE = "settings:delete" # Delete the entire workspace
# API Access # API Access
API_ACCESS_MANAGE = "api_access:manage" API_ACCESS_MANAGE = "api_access:manage"
@ -515,7 +515,7 @@ class ChatVisibility(StrEnum):
Visibility/sharing level for chat threads. Visibility/sharing level for chat threads.
PRIVATE: Only the creator can see/access the chat (default) PRIVATE: Only the creator can see/access the chat (default)
SEARCH_SPACE: All members of the search space can see/access the chat SEARCH_SPACE: All members of the workspace can see/access the chat
PUBLIC: (Future) Anyone with the link can access the chat PUBLIC: (Future) Anyone with the link can access the chat
""" """
@ -605,8 +605,7 @@ class NewChatThread(BaseModel, TimestampMixin):
) )
# Foreign keys # Foreign keys
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -647,7 +646,7 @@ class NewChatThread(BaseModel, TimestampMixin):
# Auto model pin for this thread: concrete resolved global LLM # Auto model pin for this thread: concrete resolved global LLM
# config id. NULL means no pin; Auto will resolve on the next turn. # config id. NULL means no pin; Auto will resolve on the next turn.
# Single-writer invariant: only app.services.auto_model_pin_service sets # Single-writer invariant: only app.services.auto_model_pin_service sets
# or clears this column (plus bulk clears when a search space's # or clears this column (plus bulk clears when a workspace's
# chat_model_id changes). Unindexed: all reads are by primary key. # chat_model_id changes). Unindexed: all reads are by primary key.
pinned_llm_config_id = Column(Integer, nullable=True) pinned_llm_config_id = Column(Integer, nullable=True)
@ -664,7 +663,7 @@ class NewChatThread(BaseModel, TimestampMixin):
) )
# Relationships # Relationships
search_space = relationship("SearchSpace", back_populates="new_chat_threads") workspace = relationship("Workspace", back_populates="new_chat_threads")
created_by = relationship("User", back_populates="new_chat_threads") created_by = relationship("User", back_populates="new_chat_threads")
messages = relationship( messages = relationship(
"NewChatMessage", "NewChatMessage",
@ -788,8 +787,7 @@ class ExternalChatAccount(Base, TimestampMixin):
owner_user_id = Column( owner_user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True
) )
owner_search_space_id = Column( owner_workspace_id = Column(
"owner_workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True, nullable=True,
@ -825,8 +823,8 @@ class ExternalChatAccount(Base, TimestampMixin):
) )
owner = relationship("User", foreign_keys=[owner_user_id]) owner = relationship("User", foreign_keys=[owner_user_id])
owner_search_space = relationship( owner_workspace = relationship(
"SearchSpace", foreign_keys=[owner_search_space_id] "Workspace", foreign_keys=[owner_workspace_id]
) )
bindings = relationship( bindings = relationship(
"ExternalChatBinding", "ExternalChatBinding",
@ -903,8 +901,7 @@ class ExternalChatBinding(Base, TimestampMixin):
user_id = Column( user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -957,7 +954,7 @@ class ExternalChatBinding(Base, TimestampMixin):
account = relationship("ExternalChatAccount", back_populates="bindings") account = relationship("ExternalChatAccount", back_populates="bindings")
user = relationship("User", foreign_keys=[user_id]) user = relationship("User", foreign_keys=[user_id])
search_space = relationship("SearchSpace", foreign_keys=[search_space_id]) workspace = relationship("Workspace", foreign_keys=[workspace_id])
new_chat_thread = relationship("NewChatThread", foreign_keys=[new_chat_thread_id]) new_chat_thread = relationship("NewChatThread", foreign_keys=[new_chat_thread_id])
threads = relationship( threads = relationship(
"NewChatThread", "NewChatThread",
@ -1122,8 +1119,7 @@ class TokenUsage(BaseModel, TimestampMixin):
nullable=True, nullable=True,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -1139,7 +1135,7 @@ class TokenUsage(BaseModel, TimestampMixin):
# Relationships # Relationships
thread = relationship("NewChatThread", back_populates="token_usages") thread = relationship("NewChatThread", back_populates="token_usages")
message = relationship("NewChatMessage", back_populates="token_usage") message = relationship("NewChatMessage", back_populates="token_usage")
search_space = relationship("SearchSpace") workspace = relationship("Workspace")
user = relationship("User") user = relationship("User")
@ -1328,8 +1324,7 @@ class Folder(BaseModel, TimestampMixin):
nullable=True, nullable=True,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -1351,7 +1346,7 @@ class Folder(BaseModel, TimestampMixin):
folder_metadata = Column("metadata", JSONB, nullable=True) folder_metadata = Column("metadata", JSONB, nullable=True)
parent = relationship("Folder", remote_side="Folder.id", backref="children") parent = relationship("Folder", remote_side="Folder.id", backref="children")
search_space = relationship("SearchSpace", back_populates="folders") workspace = relationship("Workspace", back_populates="folders")
created_by = relationship("User", back_populates="folders") created_by = relationship("User", back_populates="folders")
documents = relationship("Document", back_populates="folder", passive_deletes=True) documents = relationship("Document", back_populates="folder", passive_deletes=True)
@ -1368,7 +1363,7 @@ class Document(BaseModel, TimestampMixin):
# filesystem two files at different paths can hold identical bytes, # filesystem two files at different paths can hold identical bytes,
# and the agent's ``write_file`` flow needs that semantic to support # and the agent's ``write_file`` flow needs that semantic to support
# copy / duplicate operations. Path uniqueness lives on # copy / duplicate operations. Path uniqueness lives on
# ``unique_identifier_hash`` (per search space). The hash remains # ``unique_identifier_hash`` (per workspace). The hash remains
# indexed because connector indexers consult it as a change-detection # indexed because connector indexers consult it as a change-detection
# / cross-source dedup hint via :func:`check_duplicate_document`. # / cross-source dedup hint via :func:`check_duplicate_document`.
# See migration 133. # See migration 133.
@ -1393,8 +1388,7 @@ class Document(BaseModel, TimestampMixin):
# Track when document was last updated by indexers, processors, or editor # Track when document was last updated by indexers, processors, or editor
updated_at = Column(TIMESTAMP(timezone=True), nullable=True, index=True) updated_at = Column(TIMESTAMP(timezone=True), nullable=True, index=True)
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -1435,7 +1429,7 @@ class Document(BaseModel, TimestampMixin):
) )
# Relationships # Relationships
search_space = relationship("SearchSpace", back_populates="documents") workspace = relationship("Workspace", back_populates="documents")
folder = relationship("Folder", back_populates="documents") folder = relationship("Folder", back_populates="documents")
created_by = relationship("User", back_populates="documents") created_by = relationship("User", back_populates="documents")
connector = relationship("SearchSourceConnector", back_populates="documents") connector = relationship("SearchSourceConnector", back_populates="documents")
@ -1520,13 +1514,12 @@ class VideoPresentation(BaseModel, TimestampMixin):
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
search_space = relationship("SearchSpace", back_populates="video_presentations") workspace = relationship("Workspace", back_populates="video_presentations")
thread_id = Column( thread_id = Column(
Integer, Integer,
@ -1550,13 +1543,12 @@ class Report(BaseModel, TimestampMixin):
String(100), nullable=True String(100), nullable=True
) # e.g. "executive_summary", "deep_research" ) # e.g. "executive_summary", "deep_research"
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
search_space = relationship("SearchSpace", back_populates="reports") workspace = relationship("Workspace", back_populates="reports")
# Versioning: reports sharing the same report_group_id are versions of the same report. # Versioning: reports sharing the same report_group_id are versions of the same report.
# For v1, report_group_id = the report's own id (set after insert). # For v1, report_group_id = the report's own id (set after insert).
@ -1581,8 +1573,7 @@ class Connection(BaseModel, TimestampMixin):
scope = Column(SQLAlchemyEnum(ConnectionScope), nullable=False, index=True) scope = Column(SQLAlchemyEnum(ConnectionScope), nullable=False, index=True)
enabled = Column(Boolean, nullable=False, default=True, server_default="true") enabled = Column(Boolean, nullable=False, default=True, server_default="true")
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True, nullable=True,
@ -1591,7 +1582,7 @@ class Connection(BaseModel, TimestampMixin):
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True
) )
search_space = relationship("SearchSpace", back_populates="connections") workspace = relationship("Workspace", back_populates="connections")
user = relationship("User", back_populates="connections") user = relationship("User", back_populates="connections")
models = relationship( models = relationship(
"Model", "Model",
@ -1695,8 +1686,7 @@ class ImageGeneration(BaseModel, TimestampMixin):
access_token = Column(String(64), nullable=True, index=True) access_token = Column(String(64), nullable=True, index=True)
# Foreign keys # Foreign keys
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -1709,11 +1699,11 @@ class ImageGeneration(BaseModel, TimestampMixin):
) )
# Relationships # Relationships
search_space = relationship("SearchSpace", back_populates="image_generations") workspace = relationship("Workspace", back_populates="image_generations")
created_by = relationship("User", back_populates="image_generations") created_by = relationship("User", back_populates="image_generations")
class SearchSpace(BaseModel, TimestampMixin): class Workspace(BaseModel, TimestampMixin):
__tablename__ = "workspaces" __tablename__ = "workspaces"
name = Column(String(100), nullable=False, index=True) name = Column(String(100), nullable=False, index=True)
@ -1735,7 +1725,7 @@ class SearchSpace(BaseModel, TimestampMixin):
# Note: ID values preserve the existing convention: # Note: ID values preserve the existing convention:
# - 0: Auto mode # - 0: Auto mode
# - Negative IDs: Global virtual models from global_llm_config.yaml # - Negative IDs: Global virtual models from global_llm_config.yaml
# - Positive IDs: User/search-space models from the models table # - Positive IDs: User/workspace models from the models table
chat_model_id = Column( chat_model_id = Column(
Integer, nullable=True, default=0, server_default="0" Integer, nullable=True, default=0, server_default="0"
) # For agent/chat operations, defaults to Auto mode ) # For agent/chat operations, defaults to Auto mode
@ -1753,71 +1743,71 @@ class SearchSpace(BaseModel, TimestampMixin):
user_id = Column( user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
) )
user = relationship("User", back_populates="search_spaces") user = relationship("User", back_populates="workspaces")
folders = relationship( folders = relationship(
"Folder", "Folder",
back_populates="search_space", back_populates="workspace",
order_by="Folder.position", order_by="Folder.position",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
documents = relationship( documents = relationship(
"Document", "Document",
back_populates="search_space", back_populates="workspace",
order_by="Document.id", order_by="Document.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
new_chat_threads = relationship( new_chat_threads = relationship(
"NewChatThread", "NewChatThread",
back_populates="search_space", back_populates="workspace",
order_by="NewChatThread.updated_at.desc()", order_by="NewChatThread.updated_at.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
podcasts = relationship( podcasts = relationship(
"Podcast", "Podcast",
back_populates="search_space", back_populates="workspace",
order_by="Podcast.id.desc()", order_by="Podcast.id.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
video_presentations = relationship( video_presentations = relationship(
"VideoPresentation", "VideoPresentation",
back_populates="search_space", back_populates="workspace",
order_by="VideoPresentation.id.desc()", order_by="VideoPresentation.id.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
reports = relationship( reports = relationship(
"Report", "Report",
back_populates="search_space", back_populates="workspace",
order_by="Report.id.desc()", order_by="Report.id.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
image_generations = relationship( image_generations = relationship(
"ImageGeneration", "ImageGeneration",
back_populates="search_space", back_populates="workspace",
order_by="ImageGeneration.id.desc()", order_by="ImageGeneration.id.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
logs = relationship( logs = relationship(
"Log", "Log",
back_populates="search_space", back_populates="workspace",
order_by="Log.id", order_by="Log.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
notifications = relationship( notifications = relationship(
"Notification", "Notification",
back_populates="search_space", back_populates="workspace",
order_by="Notification.created_at.desc()", order_by="Notification.created_at.desc()",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
search_source_connectors = relationship( search_source_connectors = relationship(
"SearchSourceConnector", "SearchSourceConnector",
back_populates="search_space", back_populates="workspace",
order_by="SearchSourceConnector.id", order_by="SearchSourceConnector.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
connections = relationship( connections = relationship(
"Connection", "Connection",
back_populates="search_space", back_populates="workspace",
order_by="Connection.id", order_by="Connection.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
passive_deletes=True, passive_deletes=True,
@ -1825,7 +1815,7 @@ class SearchSpace(BaseModel, TimestampMixin):
automations = relationship( automations = relationship(
"Automation", "Automation",
back_populates="search_space", back_populates="workspace",
order_by="Automation.id", order_by="Automation.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
passive_deletes=True, passive_deletes=True,
@ -1833,21 +1823,21 @@ class SearchSpace(BaseModel, TimestampMixin):
# RBAC relationships # RBAC relationships
roles = relationship( roles = relationship(
"SearchSpaceRole", "WorkspaceRole",
back_populates="search_space", back_populates="workspace",
order_by="SearchSpaceRole.id", order_by="WorkspaceRole.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
memberships = relationship( memberships = relationship(
"SearchSpaceMembership", "WorkspaceMembership",
back_populates="search_space", back_populates="workspace",
order_by="SearchSpaceMembership.id", order_by="WorkspaceMembership.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
invites = relationship( invites = relationship(
"SearchSpaceInvite", "WorkspaceInvite",
back_populates="search_space", back_populates="workspace",
order_by="SearchSpaceInvite.id", order_by="WorkspaceInvite.id",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
@ -1907,14 +1897,13 @@ class SearchSourceConnector(BaseModel, TimestampMixin):
indexing_frequency_minutes = Column(Integer, nullable=True) indexing_frequency_minutes = Column(Integer, nullable=True)
next_scheduled_at = Column(TIMESTAMP(timezone=True), nullable=True) next_scheduled_at = Column(TIMESTAMP(timezone=True), nullable=True)
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
search_space = relationship( workspace = relationship(
"SearchSpace", back_populates="search_source_connectors" "Workspace", back_populates="search_source_connectors"
) )
user_id = Column( user_id = Column(
@ -1937,13 +1926,12 @@ class Log(BaseModel, TimestampMixin):
) # Service/component that generated the log ) # Service/component that generated the log
log_metadata = Column(JSON, nullable=True, default={}) # Additional context data log_metadata = Column(JSON, nullable=True, default={}) # Additional context data
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
search_space = relationship("SearchSpace", back_populates="logs") workspace = relationship("Workspace", back_populates="logs")
class UserIncentiveTask(BaseModel, TimestampMixin): class UserIncentiveTask(BaseModel, TimestampMixin):
@ -2056,10 +2044,10 @@ class CreditPurchase(Base, TimestampMixin):
user = relationship("User", back_populates="credit_purchases") user = relationship("User", back_populates="credit_purchases")
class SearchSpaceRole(BaseModel, TimestampMixin): class WorkspaceRole(BaseModel, TimestampMixin):
""" """
Custom roles that can be defined per search space. Custom roles that can be defined per workspace.
Each search space can have multiple roles with different permission sets. Each workspace can have multiple roles with different permission sets.
""" """
__tablename__ = "workspace_roles" __tablename__ = "workspace_roles"
@ -2080,26 +2068,25 @@ class SearchSpaceRole(BaseModel, TimestampMixin):
# System roles (Owner, Editor, Viewer) cannot be deleted # System roles (Owner, Editor, Viewer) cannot be deleted
is_system_role = Column(Boolean, nullable=False, default=False) is_system_role = Column(Boolean, nullable=False, default=False)
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
search_space = relationship("SearchSpace", back_populates="roles") workspace = relationship("Workspace", back_populates="roles")
memberships = relationship( memberships = relationship(
"SearchSpaceMembership", back_populates="role", passive_deletes=True "WorkspaceMembership", back_populates="role", passive_deletes=True
) )
invites = relationship( invites = relationship(
"SearchSpaceInvite", back_populates="role", passive_deletes=True "WorkspaceInvite", back_populates="role", passive_deletes=True
) )
class SearchSpaceMembership(BaseModel, TimestampMixin): class WorkspaceMembership(BaseModel, TimestampMixin):
""" """
Tracks user membership in search spaces with their assigned role. Tracks user membership in workspaces with their assigned role.
Each user can be a member of multiple search spaces with different roles. Each user can be a member of multiple workspaces with different roles.
""" """
__tablename__ = "workspace_memberships" __tablename__ = "workspace_memberships"
@ -2114,8 +2101,7 @@ class SearchSpaceMembership(BaseModel, TimestampMixin):
user_id = Column( user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -2125,7 +2111,7 @@ class SearchSpaceMembership(BaseModel, TimestampMixin):
ForeignKey("workspace_roles.id", ondelete="SET NULL"), ForeignKey("workspace_roles.id", ondelete="SET NULL"),
nullable=True, nullable=True,
) )
# Indicates if this user is the original creator/owner of the search space # Indicates if this user is the original creator/owner of the workspace
is_owner = Column(Boolean, nullable=False, default=False) is_owner = Column(Boolean, nullable=False, default=False)
# Timestamp when the user joined (via invite or as creator) # Timestamp when the user joined (via invite or as creator)
joined_at = Column( joined_at = Column(
@ -2140,17 +2126,17 @@ class SearchSpaceMembership(BaseModel, TimestampMixin):
nullable=True, nullable=True,
) )
user = relationship("User", back_populates="search_space_memberships") user = relationship("User", back_populates="workspace_memberships")
search_space = relationship("SearchSpace", back_populates="memberships") workspace = relationship("Workspace", back_populates="memberships")
role = relationship("SearchSpaceRole", back_populates="memberships") role = relationship("WorkspaceRole", back_populates="memberships")
invited_by_invite = relationship( invited_by_invite = relationship(
"SearchSpaceInvite", back_populates="used_by_memberships" "WorkspaceInvite", back_populates="used_by_memberships"
) )
class SearchSpaceInvite(BaseModel, TimestampMixin): class WorkspaceInvite(BaseModel, TimestampMixin):
""" """
Invite links for search spaces. Invite links for workspaces.
Users can create invite links with specific roles that others can use to join. Users can create invite links with specific roles that others can use to join.
""" """
@ -2159,8 +2145,7 @@ class SearchSpaceInvite(BaseModel, TimestampMixin):
# Unique invite code (used in invite URLs) # Unique invite code (used in invite URLs)
invite_code = Column(String(64), nullable=False, unique=True, index=True) invite_code = Column(String(64), nullable=False, unique=True, index=True)
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -2189,11 +2174,11 @@ class SearchSpaceInvite(BaseModel, TimestampMixin):
# Optional custom name/label for the invite # Optional custom name/label for the invite
name = Column(String(100), nullable=True) name = Column(String(100), nullable=True)
search_space = relationship("SearchSpace", back_populates="invites") workspace = relationship("Workspace", back_populates="invites")
role = relationship("SearchSpaceRole", back_populates="invites") role = relationship("WorkspaceRole", back_populates="invites")
created_by = relationship("User", back_populates="created_invites") created_by = relationship("User", back_populates="created_invites")
used_by_memberships = relationship( used_by_memberships = relationship(
"SearchSpaceMembership", "WorkspaceMembership",
back_populates="invited_by_invite", back_populates="invited_by_invite",
passive_deletes=True, passive_deletes=True,
) )
@ -2220,8 +2205,7 @@ class Prompt(BaseModel, TimestampMixin):
nullable=False, nullable=False,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True, nullable=True,
@ -2238,7 +2222,7 @@ class Prompt(BaseModel, TimestampMixin):
is_public = Column(Boolean, nullable=False, default=False) is_public = Column(Boolean, nullable=False, default=False)
user = relationship("User") user = relationship("User")
search_space = relationship("SearchSpace") workspace = relationship("Workspace")
if config.AUTH_TYPE == "GOOGLE": if config.AUTH_TYPE == "GOOGLE":
@ -2250,7 +2234,7 @@ if config.AUTH_TYPE == "GOOGLE":
oauth_accounts: Mapped[list[OAuthAccount]] = relationship( oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
"OAuthAccount", lazy="joined" "OAuthAccount", lazy="joined"
) )
search_spaces = relationship("SearchSpace", back_populates="user") workspaces = relationship("Workspace", back_populates="user")
notifications = relationship( notifications = relationship(
"Notification", "Notification",
back_populates="user", back_populates="user",
@ -2259,13 +2243,13 @@ if config.AUTH_TYPE == "GOOGLE":
) )
# RBAC relationships # RBAC relationships
search_space_memberships = relationship( workspace_memberships = relationship(
"SearchSpaceMembership", "WorkspaceMembership",
back_populates="user", back_populates="user",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
created_invites = relationship( created_invites = relationship(
"SearchSpaceInvite", "WorkspaceInvite",
back_populates="created_by", back_populates="created_by",
passive_deletes=True, passive_deletes=True,
) )
@ -2387,7 +2371,7 @@ if config.AUTH_TYPE == "GOOGLE":
else: else:
class User(SQLAlchemyBaseUserTableUUID, Base): class User(SQLAlchemyBaseUserTableUUID, Base):
search_spaces = relationship("SearchSpace", back_populates="user") workspaces = relationship("Workspace", back_populates="user")
notifications = relationship( notifications = relationship(
"Notification", "Notification",
back_populates="user", back_populates="user",
@ -2396,13 +2380,13 @@ else:
) )
# RBAC relationships # RBAC relationships
search_space_memberships = relationship( workspace_memberships = relationship(
"SearchSpaceMembership", "WorkspaceMembership",
back_populates="user", back_populates="user",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
created_invites = relationship( created_invites = relationship(
"SearchSpaceInvite", "WorkspaceInvite",
back_populates="created_by", back_populates="created_by",
passive_deletes=True, passive_deletes=True,
) )
@ -2550,8 +2534,7 @@ class AgentActionLog(BaseModel):
nullable=True, nullable=True,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -2622,8 +2605,7 @@ class DocumentRevision(BaseModel):
nullable=True, nullable=True,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -2664,8 +2646,7 @@ class FolderRevision(BaseModel):
nullable=True, nullable=True,
index=True, index=True,
) )
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -2693,7 +2674,7 @@ class FolderRevision(BaseModel):
class AgentPermissionRule(BaseModel): class AgentPermissionRule(BaseModel):
"""Persistent permission rule consumed by :class:`PermissionMiddleware`. """Persistent permission rule consumed by :class:`PermissionMiddleware`.
Scoped at one of: search-space-wide (``user_id`` and ``thread_id`` NULL), Scoped at one of: workspace-wide (``user_id`` and ``thread_id`` NULL),
user-wide (``user_id`` set, ``thread_id`` NULL), or per-thread user-wide (``user_id`` set, ``thread_id`` NULL), or per-thread
(``thread_id`` set). Loaded at agent build time and converted to (``thread_id`` set). Loaded at agent build time and converted to
:class:`Rule` instances inside the agent factory. :class:`Rule` instances inside the agent factory.
@ -2701,8 +2682,7 @@ class AgentPermissionRule(BaseModel):
__tablename__ = "agent_permission_rules" __tablename__ = "agent_permission_rules"
search_space_id = Column( workspace_id = Column(
"workspace_id",
Integer, Integer,
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False, nullable=False,
@ -3080,10 +3060,10 @@ def has_all_permissions(
def get_default_roles_config() -> list[dict]: def get_default_roles_config() -> list[dict]:
""" """
Get the configuration for default system roles. Get the configuration for default system roles.
These roles are created automatically when a search space is created. These roles are created automatically when a workspace is created.
Only 3 roles are supported: Only 3 roles are supported:
- Owner: Full access to everything (assigned to search space creator) - Owner: Full access to everything (assigned to workspace creator)
- Editor: Can create/update content but cannot delete, manage roles, or change settings - Editor: Can create/update content but cannot delete, manage roles, or change settings
- Viewer: Read-only access to resources (can add comments) - Viewer: Read-only access to resources (can add comments)
@ -3093,7 +3073,7 @@ def get_default_roles_config() -> list[dict]:
return [ return [
{ {
"name": "Owner", "name": "Owner",
"description": "Full access to all search space resources and settings", "description": "Full access to all workspace resources and settings",
"permissions": DEFAULT_ROLE_PERMISSIONS["Owner"], "permissions": DEFAULT_ROLE_PERMISSIONS["Owner"],
"is_default": False, "is_default": False,
"is_system_role": True, "is_system_role": True,
@ -3107,7 +3087,7 @@ def get_default_roles_config() -> list[dict]:
}, },
{ {
"name": "Viewer", "name": "Viewer",
"description": "Read-only access to search space resources", "description": "Read-only access to workspace resources",
"permissions": DEFAULT_ROLE_PERMISSIONS["Viewer"], "permissions": DEFAULT_ROLE_PERMISSIONS["Viewer"],
"is_default": False, "is_default": False,
"is_system_role": True, "is_system_role": True,