Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async def replay_events(
"""Replay events by filter criteria, with optional dry-run mode."""
replay_id = f"replay-{uuid4().hex}"
result = await service.prepare_or_schedule_replay(
replay_filter=ReplayFilter.model_validate(request),
replay_filter=ReplayFilter(**request.model_dump(include=set(ReplayFilter.__dataclass_fields__))),
dry_run=request.dry_run,
replay_id=replay_id,
target_service=request.target_service,
Expand Down
9 changes: 4 additions & 5 deletions backend/app/api/routes/admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def get_system_settings(
) -> SystemSettingsSchema:
"""Get the current system-wide settings."""
result = await service.get_system_settings(admin.user_id)
return SystemSettingsSchema.model_validate(result)
return SystemSettingsSchema.model_validate(result, from_attributes=True)


@router.put(
Expand All @@ -45,9 +45,8 @@ async def update_system_settings(
service: FromDishka[AdminSettingsService],
) -> SystemSettingsSchema:
"""Replace system-wide settings."""
domain_settings = SystemSettings.model_validate(settings)
result = await service.update_system_settings(domain_settings, admin.user_id)
return SystemSettingsSchema.model_validate(result)
result = await service.update_system_settings(SystemSettings(**settings.model_dump()), admin.user_id)
return SystemSettingsSchema.model_validate(result, from_attributes=True)


@router.post(
Expand All @@ -61,4 +60,4 @@ async def reset_system_settings(
) -> SystemSettingsSchema:
"""Reset system-wide settings to defaults."""
result = await service.reset_system_settings(admin.user_id)
return SystemSettingsSchema.model_validate(result)
return SystemSettingsSchema.model_validate(result, from_attributes=True)
2 changes: 1 addition & 1 deletion backend/app/api/routes/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async def update_user(
if not existing_user:
raise HTTPException(status_code=404, detail="User not found")

domain_update = DomainUserUpdate.model_validate(user_update)
domain_update = DomainUserUpdate(**user_update.model_dump())

updated_user = await admin_user_service.update_user(
admin_user_id=admin.user_id, user_id=user_id, update=domain_update
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async def update_subscription(
notification_service: FromDishka[NotificationService],
) -> NotificationSubscription:
"""Update subscription settings for a notification channel."""
update_data = DomainSubscriptionUpdate.model_validate(subscription)
update_data = DomainSubscriptionUpdate(**subscription.model_dump())
updated_sub = await notification_service.update_subscription(
user_id=user.user_id,
channel=channel,
Expand Down
7 changes: 4 additions & 3 deletions backend/app/api/routes/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from app.api.dependencies import admin_user
from app.domain.enums import ReplayStatus
from app.domain.replay import ReplayConfig
from app.schemas_pydantic.replay import (
from app.schemas_pydantic.replay_schemas import (
CleanupResponse,
ReplayRequest,
ReplayResponse,
ReplaySession,
SessionSummary,
)
from app.schemas_pydantic.replay_models import ReplaySession
from app.services.event_replay import EventReplayService

router = APIRouter(prefix="/replay", tags=["Event Replay"], route_class=DishkaRoute, dependencies=[Depends(admin_user)])
Expand All @@ -25,7 +25,8 @@ async def create_replay_session(
service: FromDishka[EventReplayService],
) -> ReplayResponse:
"""Create a new event replay session from a configuration."""
result = await service.create_session_from_config(ReplayConfig.model_validate(replay_request))
config_fields = set(ReplayConfig.__dataclass_fields__)
result = await service.create_session_from_config(ReplayConfig(**replay_request.model_dump(include=config_fields)))
return ReplayResponse.model_validate(result)


Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/saved_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def create_saved_script(
saved_script_service: FromDishka[SavedScriptService],
) -> SavedScriptResponse:
"""Save a new script to the user's collection."""
create = DomainSavedScriptCreate.model_validate(saved_script)
create = DomainSavedScriptCreate(**saved_script.model_dump())
domain = await saved_script_service.create_saved_script(create, user.user_id)
return SavedScriptResponse.model_validate(domain)

Expand Down Expand Up @@ -68,7 +68,7 @@ async def update_saved_script(
saved_script_service: FromDishka[SavedScriptService],
) -> SavedScriptResponse:
"""Update an existing saved script."""
update_data = DomainSavedScriptUpdate.model_validate(script_update)
update_data = DomainSavedScriptUpdate(**script_update.model_dump())
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: model_dump() includes optional fields with None defaults, so the update payload will always carry None for omitted fields and a new updated_at. That can overwrite existing values unintentionally. Use model_dump(exclude_unset=True) when building the dataclass so only client-provided fields are updated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/app/api/routes/saved_scripts.py, line 71:

<comment>`model_dump()` includes optional fields with `None` defaults, so the update payload will always carry `None` for omitted fields and a new `updated_at`. That can overwrite existing values unintentionally. Use `model_dump(exclude_unset=True)` when building the dataclass so only client-provided fields are updated.</comment>

<file context>
@@ -68,7 +68,7 @@ async def update_saved_script(
 ) -> SavedScriptResponse:
     """Update an existing saved script."""
-    update_data = DomainSavedScriptUpdate.model_validate(script_update)
+    update_data = DomainSavedScriptUpdate(**script_update.model_dump())
     domain = await saved_script_service.update_saved_script(script_id, user.user_id, update_data)
     return SavedScriptResponse.model_validate(domain)
</file context>
Suggested change
update_data = DomainSavedScriptUpdate(**script_update.model_dump())
update_data = DomainSavedScriptUpdate(**script_update.model_dump(exclude_unset=True))
Fix with Cubic

domain = await saved_script_service.update_saved_script(script_id, user.user_id, update_data)
return SavedScriptResponse.model_validate(domain)

Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def update_user_settings(
settings_service: FromDishka[UserSettingsService],
) -> UserSettings:
"""Update the authenticated user's settings."""
domain_updates = DomainUserSettingsUpdate.model_validate(updates)
domain_updates = DomainUserSettingsUpdate(**updates.model_dump())
domain = await settings_service.update_user_settings(current_user.user_id, domain_updates)
return UserSettings.model_validate(domain)

Expand All @@ -68,7 +68,7 @@ async def update_notification_settings(
"""Update notification preferences."""
domain = await settings_service.update_notification_settings(
current_user.user_id,
DomainNotificationSettings.model_validate(notifications),
DomainNotificationSettings(**notifications.model_dump()),
)
return UserSettings.model_validate(domain)

Expand All @@ -82,7 +82,7 @@ async def update_editor_settings(
"""Update code editor preferences."""
domain = await settings_service.update_editor_settings(
current_user.user_id,
DomainEditorSettings.model_validate(editor),
DomainEditorSettings(**editor.model_dump()),
)
return UserSettings.model_validate(domain)

Expand Down
5 changes: 3 additions & 2 deletions backend/app/db/docs/admin_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from beanie import Document, Indexed
from pydantic import ConfigDict, Field

from app.domain.admin import AuditAction, SystemSettings
from app.domain.admin import AuditAction
from app.schemas_pydantic.admin_settings import SystemSettingsSchema


class SystemSettingsDocument(Document):
settings_id: str = "global"
config: SystemSettings = Field(default_factory=SystemSettings)
config: SystemSettingsSchema = Field(default_factory=SystemSettingsSchema)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

Expand Down
5 changes: 1 addition & 4 deletions backend/app/db/docs/saga.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@


class SagaDocument(Document):
"""Domain model for saga stored in database.

Copied from Saga/SagaInstance dataclass.
"""
"""Saga document stored in database."""

saga_id: Indexed(str, unique=True) = Field(default_factory=lambda: str(uuid4())) # type: ignore[valid-type]
saga_name: Indexed(str) # type: ignore[valid-type]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime, timedelta, timezone
from typing import Any

Expand Down Expand Up @@ -211,7 +212,7 @@ async def archive_event(self, event: DomainEvent, deleted_by: str) -> bool:
return True

async def update_replay_session(self, session_id: str, updates: ReplaySessionUpdate) -> bool:
update_dict = updates.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(updates).items() if v is not None}
if not update_dict:
return False
doc = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)
Expand Down
12 changes: 7 additions & 5 deletions backend/app/db/repositories/admin/admin_settings_repository.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import dataclasses
from datetime import datetime, timezone

import structlog

from app.db.docs.admin_settings import AuditLogDocument, SystemSettingsDocument
from app.domain.admin import AuditAction, SystemSettings
from app.schemas_pydantic.admin_settings import SystemSettingsSchema


class AdminSettingsRepository:
Expand All @@ -17,27 +19,27 @@ async def get_system_settings(
doc = await SystemSettingsDocument.find_one(SystemSettingsDocument.settings_id == "global")
if not doc:
self.logger.info("System settings not found, creating defaults")
doc = SystemSettingsDocument(config=defaults)
doc = SystemSettingsDocument(config=SystemSettingsSchema(**dataclasses.asdict(defaults)))
await doc.insert()
return doc.config
return SystemSettings(**doc.config.model_dump())

async def update_system_settings(self, settings: SystemSettings, user_id: str) -> SystemSettings:
doc = await SystemSettingsDocument.find_one(SystemSettingsDocument.settings_id == "global")
if not doc:
doc = SystemSettingsDocument()

doc.config = settings
doc.config = SystemSettingsSchema(**dataclasses.asdict(settings))
doc.updated_at = datetime.now(timezone.utc)
await doc.save()

audit_entry = AuditLogDocument(
action=AuditAction.SYSTEM_SETTINGS_UPDATED,
user_id=user_id,
changes=settings.model_dump(),
changes=dataclasses.asdict(settings),
)
await audit_entry.insert()

return doc.config
return SystemSettings(**doc.config.model_dump())

async def reset_system_settings(self, user_id: str) -> SystemSettings:
doc = await SystemSettingsDocument.find_one(SystemSettingsDocument.settings_id == "global")
Expand Down
13 changes: 7 additions & 6 deletions backend/app/db/repositories/admin/admin_user_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import re
from datetime import datetime, timezone

Expand Down Expand Up @@ -28,9 +29,9 @@
class AdminUserRepository:

async def create_user(self, create_data: DomainUserCreate) -> User:
doc = UserDocument(**create_data.model_dump())
doc = UserDocument(**dataclasses.asdict(create_data))
await doc.insert()
return User.model_validate(doc)
return User(**doc.model_dump(include=set(User.__dataclass_fields__)))

async def list_users(
self, limit: int = 100, offset: int = 0, search: str | None = None, role: UserRole | None = None
Expand All @@ -52,26 +53,26 @@ async def list_users(
query = UserDocument.find(*conditions)
total = await query.count()
docs = await query.skip(offset).limit(limit).to_list()
users = [User.model_validate(doc) for doc in docs]
users = [User(**doc.model_dump(include=set(User.__dataclass_fields__))) for doc in docs]
return UserListResult(users=users, total=total, offset=offset, limit=limit)

async def get_user_by_id(self, user_id: str) -> User | None:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
return User.model_validate(doc) if doc else None
return User(**doc.model_dump(include=set(User.__dataclass_fields__))) if doc else None

async def update_user(self, user_id: str, update_data: UserUpdate) -> User | None:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
if not doc:
return None

update_dict = update_data.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(update_data).items() if v is not None}
if "password" in update_dict:
update_dict["hashed_password"] = update_dict.pop("password")

if update_dict:
update_dict["updated_at"] = datetime.now(timezone.utc)
await doc.set(update_dict)
return User.model_validate(doc)
return User(**doc.model_dump(include=set(User.__dataclass_fields__)))

async def delete_user(self, user_id: str, cascade: bool = True) -> UserDeleteResult:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/db/repositories/dlq_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
DLQMessageListResult,
DLQMessageStatus,
DLQMessageUpdate,
DLQTopicSummary,
)
from app.domain.enums import EventType
from app.schemas_pydantic.dlq import DLQTopicSummary


class DLQRepository:
Expand Down
9 changes: 5 additions & 4 deletions backend/app/db/repositories/execution_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime, timezone
from typing import Any

Expand All @@ -17,11 +18,11 @@ def __init__(self, logger: structlog.stdlib.BoundLogger):
self.logger = logger

async def create_execution(self, create_data: DomainExecutionCreate) -> DomainExecution:
doc = ExecutionDocument(**create_data.model_dump())
doc = ExecutionDocument(**dataclasses.asdict(create_data))
self.logger.info("Inserting execution into MongoDB", execution_id=doc.execution_id)
await doc.insert()
self.logger.info("Inserted execution", execution_id=doc.execution_id)
return DomainExecution.model_validate(doc)
return DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__)))

async def get_execution(self, execution_id: str) -> DomainExecution | None:
self.logger.info("Searching for execution in MongoDB", execution_id=execution_id)
Expand All @@ -31,7 +32,7 @@ async def get_execution(self, execution_id: str) -> DomainExecution | None:
return None

self.logger.info("Found execution in MongoDB", execution_id=execution_id)
return DomainExecution.model_validate(doc)
return DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__)))

async def write_terminal_result(self, result: ExecutionResultDomain) -> bool:
doc = await ExecutionDocument.find_one(ExecutionDocument.execution_id == result.execution_id)
Expand Down Expand Up @@ -63,7 +64,7 @@ async def get_executions(
]
find_query = find_query.sort(beanie_sort)
docs = await find_query.skip(skip).limit(limit).to_list()
return [DomainExecution.model_validate(doc) for doc in docs]
return [DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__))) for doc in docs]

async def count_executions(self, query: dict[str, Any]) -> int:
return await ExecutionDocument.find(query).count()
Expand Down
Loading
Loading