"""Сервис пользователей: upsert профиля из SSO и claim бесхозных данных (ТЗ 1.2)."""
from typing import Any, cast
from sqlalchemy import select
from sqlalchemy.engine import CursorResult
from sqlalchemy.orm import Session
from app.models import AppSetting, AppSettingGlobal, User
# Таблицы с user_id, подлежащим claim'у (user_id IS NULL → новый пользователь).
# mcp_tokens и app_settings не входят: там user_id NOT NULL, они per-user с рождения.
CLAIM_TABLES = (
"projects",
"tasks",
"tags",
"documents",
"xp_events",
"coin_events",
"garden_items",
)
def upsert_user(
db: Session,
user_id: str,
email: str,
avatar_url: str | None,
locale: str | None,
name: str | None = None,
) -> User:
"""Создать или обновить профиль по данным SSO (email/имя/аватар/локаль меняются)."""
user = db.get(User, user_id)
if user is None:
user = User(id=user_id, email=email, name=name, avatar_url=avatar_url, locale=locale)
db.add(user)
else:
user.email = email
if name:
user.name = name
user.avatar_url = avatar_url
user.locale = locale
# flush сразу: claim_* (Core UPDATE) не триггерит autoflush, и FK
# projects.user_id → users без этого падает на первом логине
db.flush()
return user
def claim_orphan_data(db: Session, user_id: str) -> int:
"""Приписать все строки без владельца вошедшему пользователю.
Claim-on-first-login: миграция оставила существующие данные с user_id NULL;
первый вошедший забирает их (идемпотентно — NULL после этого не остаётся).
"""
claimed = 0
for table in CLAIM_TABLES:
model = User.metadata.tables[table]
result = cast(
CursorResult[Any],
db.execute(model.update().where(model.c.user_id.is_(None)).values(user_id=user_id)),
)
claimed += result.rowcount or 0
return claimed
def claim_global_settings(db: Session, user_id: str) -> None:
"""Скопировать глобальные настройки прежней эпохи пользователю (идемпотентно)."""
existing = {s.key for s in db.scalars(select(AppSetting).where(AppSetting.user_id == user_id))}
for row in db.scalars(select(AppSettingGlobal)):
if row.key not in existing:
db.add(AppSetting(user_id=user_id, key=row.key, value=row.value))