Database cleanup, better UUID passing and construction (User model).

This commit is contained in:
Leo Vasanko
2026-01-27 01:25:52 +00:00
parent e0e1a644de
commit ece0ac9a16
5 changed files with 68 additions and 56 deletions
+9 -20
View File
@@ -44,7 +44,6 @@ from paskia.db.structs import (
_ResetTokenData,
_RoleData,
_SessionData,
_UserData,
)
from paskia.util.passphrase import is_well_formed as _is_passphrase
@@ -178,15 +177,9 @@ def build_permission(uuid: UUID) -> Permission:
def build_user(uuid: UUID) -> User:
u = _db._data.users[uuid]
return User(
uuid=uuid,
display_name=u.display_name,
role_uuid=u.role,
created_at=u.created_at,
last_seen=u.last_seen,
visits=u.visits,
)
user = _db._data.users[uuid]
user.uuid = uuid
return user
def build_role(uuid: UUID) -> Role:
@@ -809,16 +802,10 @@ def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
"""Create a new user."""
if new_user.uuid in _db._data.users:
raise ValueError(f"User {new_user.uuid} already exists")
if new_user.role_uuid not in _db._data.roles:
raise ValueError(f"Role {new_user.role_uuid} not found")
if new_user.role not in _db._data.roles:
raise ValueError(f"Role {new_user.role} not found")
with _db.transaction("Created user", ctx):
_db._data.users[new_user.uuid] = _UserData(
display_name=new_user.display_name,
role=new_user.role_uuid,
created_at=new_user.created_at or datetime.now(timezone.utc),
last_seen=new_user.last_seen,
visits=new_user.visits,
)
_db._data.users[new_user.uuid] = new_user
def update_user_display_name(
@@ -1331,13 +1318,15 @@ def bootstrap(
)
# Create admin user
_db._data.users[user_uuid] = _UserData(
admin_user = User(
display_name=admin_name,
role=role_uuid,
created_at=now,
last_seen=None,
visits=0,
)
admin_user.uuid = user_uuid
_db._data.users[user_uuid] = admin_user
# Create reset token
_db._data.reset_tokens[reset_key] = _ResetTokenData(
+26 -13
View File
@@ -2,6 +2,7 @@ from datetime import datetime
from uuid import UUID
import msgspec
import uuid7
class Permission(msgspec.Struct, omit_defaults=True):
@@ -25,14 +26,34 @@ class Org(msgspec.Struct):
roles: list[Role] = [] # roles belonging to this org
class User(msgspec.Struct):
uuid: UUID
class User(msgspec.Struct, dict=True):
display_name: str
role_uuid: UUID
created_at: datetime | None = None
role: UUID
created_at: datetime
last_seen: datetime | None = None
visits: int = 0
def __post_init__(self):
self.uuid: UUID | None = None # Convenience field, not serialized
@classmethod
def create(
cls,
display_name: str,
role: UUID,
created_at: datetime | None = None,
) -> "User":
"""Create a new User with auto-generated uuid7."""
from datetime import timezone
user = cls(
display_name=display_name,
role=role,
created_at=created_at or datetime.now(timezone.utc),
)
user.uuid = uuid7.create(user.created_at)
return user
class Credential(msgspec.Struct):
uuid: UUID
@@ -103,14 +124,6 @@ class _RoleData(msgspec.Struct):
permissions: dict[UUID, bool] = {} # permission_uuid -> True
class _UserData(msgspec.Struct):
display_name: str
role: UUID
created_at: datetime
last_seen: datetime | None
visits: int
class _CredentialData(msgspec.Struct):
credential_id: bytes
user: UUID
@@ -141,7 +154,7 @@ class _DatabaseData(msgspec.Struct, omit_defaults=True):
permissions: dict[UUID, _PermissionData]
orgs: dict[UUID, _OrgData]
roles: dict[UUID, _RoleData]
users: dict[UUID, _UserData]
users: dict[UUID, User]
credentials: dict[UUID, _CredentialData]
sessions: dict[str, _SessionData]
reset_tokens: dict[bytes, _ResetTokenData]
+3 -7
View File
@@ -463,16 +463,12 @@ async def admin_create_user(
role_obj = next((r for r in roles if r.display_name == role_name), None)
if not role_obj:
raise ValueError("Role not found in organization")
user_uuid = uuid4()
user = UserDC(
uuid=user_uuid,
user = UserDC.create(
display_name=display_name,
role_uuid=role_obj.uuid,
visits=0,
created_at=None,
role=role_obj.uuid,
)
db.create_user(user, ctx=ctx)
return {"uuid": str(user_uuid)}
return {"uuid": str(user.uuid)}
@app.patch("/orgs/{org_uuid}/users/{user_uuid}/role")
+14 -12
View File
@@ -55,13 +55,13 @@ async def migrate_from_sql(
from paskia.db.operations import DB as JSONDB
from paskia.db.structs import (
User,
_CredentialData,
_OrgData,
_PermissionData,
_ResetTokenData,
_RoleData,
_SessionData,
_UserData,
)
# Initialize source SQL database
@@ -163,15 +163,17 @@ async def migrate_from_sql(
result = await session.execute(select(UserModel))
user_models = result.scalars().all()
for um in user_models:
user = um.as_dataclass()
user_key: UUID = user.uuid
json_db._data.users[user_key] = _UserData(
display_name=user.display_name,
role=user.role_uuid,
created_at=user.created_at or datetime.now(timezone.utc),
last_seen=user.last_seen,
visits=user.visits,
legacy_user = um.as_dataclass()
user_key: UUID = legacy_user.uuid
new_user = User(
display_name=legacy_user.display_name,
role=legacy_user.role_uuid,
created_at=legacy_user.created_at or datetime.now(timezone.utc),
last_seen=legacy_user.last_seen,
visits=legacy_user.visits,
)
new_user.uuid = user_key
json_db._data.users[user_key] = new_user
print(f" Migrated {len(user_models)} users")
# Migrate credentials
@@ -238,9 +240,9 @@ async def migrate_from_sql(
)
print(f" Migrated {len(token_models)} reset tokens")
# Queue and flush all changes with actor "migrate"
json_db._current_actor = "migrate"
json_db._queue_change()
# Queue and flush all changes using the transaction mechanism
with json_db.transaction("migrate"):
pass # All data already added to _data, transaction commits on exit
from paskia.db.jsonl import flush_changes
await flush_changes(json_db.db_path, json_db._pending_changes)
+16 -4
View File
@@ -30,10 +30,22 @@ from paskia.db import (
Org,
ResetToken,
Role,
User,
)
# Legacy User class for SQL schema (uses 'role_uuid' not 'role')
@dataclass
class _LegacyUser:
"""User as stored in the old SQL schema with role_uuid field."""
uuid: UUID
display_name: str
role_uuid: UUID
created_at: datetime | None = None
last_seen: datetime | None = None
visits: int = 0
# Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
@dataclass
class SqlPermission:
@@ -129,8 +141,8 @@ class UserModel(Base):
)
visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
def as_dataclass(self) -> User:
return User(
def as_dataclass(self) -> "_LegacyUser":
return _LegacyUser(
uuid=UUID(bytes=self.uuid),
display_name=self.display_name,
role_uuid=UUID(bytes=self.role_uuid),
@@ -140,7 +152,7 @@ class UserModel(Base):
)
@staticmethod
def from_dataclass(user: User):
def from_dataclass(user: "_LegacyUser"):
return UserModel(
uuid=user.uuid.bytes,
display_name=user.display_name,