Files
gh-christianlouis-dmarq/backend/app/services/workspaces.py
T
2026-05-23 19:05:55 +02:00

117 lines
3.5 KiB
Python

"""Workspace/tenant helpers for MSP mode foundations."""
from __future__ import annotations
from typing import Optional
from sqlalchemy.orm import Query, Session
from app.models.domain import Domain
from app.models.mail_source import MailSource
from app.models.user import User
from app.models.workspace import Workspace
DEFAULT_WORKSPACE_SLUG = "default"
DEFAULT_WORKSPACE_NAME = "Default Workspace"
def normalize_workspace_slug(value: str) -> str:
"""Normalize a workspace slug for stable lookups."""
slug = (value or "").strip().lower()
cleaned = []
previous_dash = False
for char in slug:
if char.isalnum():
cleaned.append(char)
previous_dash = False
elif not previous_dash:
cleaned.append("-")
previous_dash = True
return "".join(cleaned).strip("-")
def get_or_create_default_workspace(db: Session, *, commit: bool = True) -> Workspace:
"""Return the single-tenant default workspace, creating it when needed."""
workspace = db.query(Workspace).filter(Workspace.slug == DEFAULT_WORKSPACE_SLUG).first()
if workspace:
return workspace
workspace = Workspace(
slug=DEFAULT_WORKSPACE_SLUG,
name=DEFAULT_WORKSPACE_NAME,
description="Automatically created for existing single-tenant installs.",
active=True,
)
db.add(workspace)
if commit:
db.commit()
db.refresh(workspace)
else:
db.flush()
return workspace
def assign_default_workspace_to_unscoped_rows(
db: Session,
*,
commit: bool = True,
) -> Workspace:
"""Attach legacy unscoped rows to the default workspace."""
workspace = get_or_create_default_workspace(db, commit=commit)
for model in (Domain, MailSource, User):
db.query(model).filter(model.workspace_id.is_(None)).update(
{model.workspace_id: workspace.id},
synchronize_session=False,
)
if commit:
db.commit()
else:
db.flush()
return workspace
def resolve_workspace(
db: Session,
*,
workspace_id: Optional[int] = None,
slug: Optional[str] = None,
) -> Workspace:
"""Resolve a workspace, defaulting to the single-tenant workspace."""
if workspace_id is not None:
workspace = (
db.query(Workspace)
.filter(Workspace.id == workspace_id, Workspace.active.is_(True))
.first()
)
if workspace:
return workspace
raise ValueError("Workspace not found")
if slug:
normalized = normalize_workspace_slug(slug)
workspace = (
db.query(Workspace)
.filter(Workspace.slug == normalized, Workspace.active.is_(True))
.first()
)
if workspace:
return workspace
raise ValueError("Workspace not found")
return assign_default_workspace_to_unscoped_rows(db)
def workspace_domain_query(db: Session, workspace: Workspace) -> Query:
"""Return the default scoped domain query for a workspace."""
return db.query(Domain).filter(Domain.workspace_id == workspace.id)
def workspace_mail_source_query(db: Session, workspace: Workspace) -> Query:
"""Return the default scoped mail-source query for a workspace."""
return db.query(MailSource).filter(MailSource.workspace_id == workspace.id)
def workspace_user_query(db: Session, workspace: Workspace) -> Query:
"""Return the default scoped user query for a workspace."""
return db.query(User).filter(User.workspace_id == workspace.id)