Skip to content

fix: split between domain models nd pydantic schemas, also moved to pydantic schemas stuf fthat should be there - #180

Merged
HardMax71 merged 8 commits into
mainfrom
fix/schemas-consolidation
Feb 14, 2026
Merged

fix: split between domain models nd pydantic schemas, also moved to pydantic schemas stuf fthat should be there#180
HardMax71 merged 8 commits into
mainfrom
fix/schemas-consolidation

Conversation

@HardMax71

@HardMax71 HardMax71 commented Feb 14, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Split internal domain models to Python dataclasses and use Pydantic only for API schemas. Routes, repositories, SSE, replay, notifications, settings, and health are updated to this split, with OpenAPI refreshed and payloads unchanged.

  • Refactors

    • Replaced model_validate/model_dump with dataclass constructors, dataclasses.asdict, and TypeAdapter across routes/services (DLQ, SSE Redis bus/service, Execution idempotency).
    • Moved SSE/notification Redis message models to app/domain/sse; SSE routes/docs now use SSEExecutionEventSchema.
    • Consolidated replay API schemas under app/schemas_pydantic/replays (ReplayFilterSchema adds event_ids and aggregate_id); repositories adapt sessions/config/errors to domain dataclasses.
    • Added Beanie bson_encoders for dataclass fields (EventMetadata, ResourceUsageDomain, SagaContextData, ReplayFilter/Error); DLQ repository uses DomainEventAdapter for event parsing.
    • Cached compiled endpoint patterns in RateLimitService for faster matching.
  • Migration

    • Removed AdminUserRepository; admin user flows now use UserRepository and providers updated.
    • Update imports to app.schemas_pydantic.health, app.schemas_pydantic.replays, app.domain.sse; replace Pydantic validation with dataclass construction/TypeAdapter where needed.
    • OpenAPI/Frontend: event stats include top_users, error_rate, avg_processing_time; count schemas reference domain types.

Written for commit b2ce62a. Summary will update on new commits.

Summary by CodeRabbit

  • Chores

    • Consolidated schema/serialization surfaces and renamed several event count types for consistent API names.
    • Moved/standardized replay/session and SSE/notification schemas and centralized liveness model.
  • New Features

    • Event statistics responses include top users, error rate, and average processing time.
  • Bug Fixes

    • User deletion now returns detailed deletion counts.

@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces many Pydantic BaseModel types with pydantic.dataclasses, adopts Pydantic v2 TypeAdapter for (de)serialization across repos/services, reorganizes replay/health/replay schema modules, relocates SSE message types to domain, and removes AdminUserRepository in favor of UserRepository with updated delete/reset APIs.

Changes

Cohort / File(s) Summary
Event domain & filter conversions
backend/app/domain/events/event_models.py, backend/app/domain/events/__init__.py, backend/app/api/routes/admin/events.py
Converted event models to dataclasses, renamed/adjusted count types, removed some exports, and changed routes to construct/validate filters via a DomainEventFilter alias + TypeAdapter usage (replace model_validate → TypeAdapter.validate_python). Review filter fields and public exports.
Replay schemas & models
backend/app/schemas_pydantic/replay_models.py (deleted), backend/app/schemas_pydantic/replays.py, backend/app/domain/admin/replay_models.py, backend/app/api/routes/replay.py
Moved/removed old replay schema module, added replays.py Pydantic schemas, adjusted domain replay dataclasses (ExecutionResultSummary → dataclass), and updated route imports. Check removed file references and dataclass vs schema mapping.
Health schema and route
backend/app/schemas_pydantic/health.py, backend/app/api/routes/health.py, backend/tests/e2e/test_health_routes.py
Introduced LivenessResponse schema module and switched health route import to use it. Verify response_model import and tests updated accordingly.
SSE domain & serialization relocation
backend/app/domain/sse/models.py, backend/app/domain/sse/__init__.py, backend/app/schemas_pydantic/sse.py, backend/app/services/sse/redis_bus.py, backend/app/services/sse/sse_service.py, backend/app/services/notification_service.py
Moved RedisSSEMessage/RedisNotificationMessage/SSEExecutionEvent types to domain dataclasses; removed/updated their schema counterparts; adopted TypeAdapter.validate_json/dump_json for Redis <-> SSE serialization. Review serialization adapters and updated import paths.
Repository TypeAdapter adoption
backend/app/db/repositories/admin/admin_events_repository.py, backend/app/db/repositories/dlq_repository.py, backend/app/db/repositories/user_repository.py
Introduced module-level TypeAdapter instances and replaced model_validate/model_dump calls with TypeAdapter.validate_python/dump; user_repository delete_user now returns UserDeleteResult and gained reset_user_password. Confirm validation modes and cascade delete behavior.
Removed AdminUserRepository & provider wiring
backend/app/db/repositories/admin/admin_user_repository.py (removed), backend/app/db/repositories/__init__.py, backend/app/db/repositories/admin/__init__.py, backend/app/db/__init__.py, backend/app/core/providers.py
Deleted AdminUserRepository implementation and re-exports; updated providers to pass UserRepository into admin user service constructors. Ensure no remaining references to AdminUserRepository.
Admin user service & API changes
backend/app/services/admin/admin_user_service.py, backend/app/api/routes/admin/users.py
Admin service now depends on UserRepository and security_service; create_user signature changed to explicit fields (username,email,password,role,is_active); route updated to call new signature. Review auth/security usage and error mappings.
Admin events service & admin_events schemas
backend/app/services/admin/admin_events_service.py, backend/app/schemas_pydantic/admin_events.py
Switched export row/filter serialization to TypeAdapter-based dump/validate; admin events schemas now use domain types (EventTypeCount/HourlyEventCount/UserEventCount) and expose ExecutionResultSummary. Check CSV/JSON export serialization.
Schemas renames / OpenAPI / frontend types
backend/app/schemas_pydantic/events.py, docs/reference/openapi.json, frontend/src/lib/api/types.gen.ts, frontend/src/lib/api/index.ts
Renamed schema types (EventTypeCountSchema → EventTypeCount, etc.), expanded EventStatistics API fields, and updated OpenAPI and frontend generated types/exports. Confirm frontend/clients align with renamed types.
SSE & SSE tests adjustments
backend/tests/e2e/notifications/test_notification_sse.py, backend/tests/e2e/services/sse/test_partitioned_event_router.py, backend/tests/e2e/services/sse/test_redis_bus.py, backend/tests/unit/services/sse/test_sse_service.py
Updated tests to import SSE/Redis message types from domain and adjusted helper validation to TypeAdapter usage. Verify test fixtures and TypeAdapter expectations.
Misc domain / minor moves
backend/app/dlq/models.py, backend/app/domain/replay/models.py, backend/app/domain/saga/models.py, backend/app/domain/user/user_models.py, backend/app/domain/user/__init__.py, backend/app/domain/user/settings_models.py, backend/app/domain/execution/models.py
Converted DLQ and various domain models to dataclasses or reordered definitions; removed UserCreation/UserSearchFilter and added UserDeleteResult; added ExecutionResultDomain.lang/lang_version. Check public surface changes and downstream consumers.
Tests & small import changes
backend/tests/e2e/test_replay_routes.py, backend/tests/e2e/services/user_settings/test_user_settings_service.py, other test import edits
Multiple test import paths updated to new schema/module locations and minor helper signature adjustments. Run tests to catch unresolved imports.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through modules, swift and spry,
Dataclasses tucked where BaseModels lie.
TypeAdapters hum, JSON neat and trim,
Schemas reshuffled — a refactor hymn.
Carrot cheers for code that’s tidy and prim!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title contains typos ('nd' instead of 'and', 'stuf fthat' instead of 'stuff that') and is somewhat vague despite mentioning the core intent of splitting domain models and Pydantic schemas. Revise the title to be clear and concise without typos. For example: 'refactor: consolidate domain models and pydantic schemas with TypeAdapter validation' would better convey the primary architectural change.
✅ Passed checks (2 passed)
Check name Status Explanation
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/schemas-consolidation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 35 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/app/schemas_pydantic/replays.py">

<violation number="1" location="backend/app/schemas_pydantic/replays.py:13">
P2: ReplayFilterSchema drops `event_ids` and `aggregate_id` from the domain filter, so session/config schemas built from domain objects will silently omit these filter criteria. Include the missing fields to keep schema parity with ReplayFilter.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread backend/app/schemas_pydantic/replays.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/schemas_pydantic/admin_events.py (1)

12-21: ⚠️ Potential issue | 🟡 Minor

Duplicate EventFilter class — consolidate or document the architectural split.

Two EventFilter classes exist in the schemas_pydantic package:

  1. admin_events.py (lines 12–21) — no model_config, no status field
  2. events.py (lines 21–33) — has ConfigDict(from_attributes=True) and status field

The route handler (backend/app/api/routes/admin/events.py line 37) explicitly converts between them: EventFilter.model_validate(request.filters) accepts the admin variant and converts it to the events variant, where the status field defaults to None.

While this pattern works, it creates maintenance burden: the two models must be kept in sync when new filter fields are added, and the purpose of the split (API boundary vs. internal service contract) is not documented. The admin variant is only used in EventBrowseRequest and tests; all service layer code imports from events.py.

Either consolidate to a single EventFilter used throughout, or add a comment in admin_events.py explaining why the restricted variant is intentional and when to update it in tandem with events.EventFilter.

🤖 Fix all issues with AI agents
In `@backend/app/domain/admin/replay_models.py`:
- Around line 9-10: The domain module app.domain.admin.replay_models incorrectly
imports Pydantic schema types (ExecutionResultSummary, EventSummary) creating
inverted dependencies; remove these schema imports from
app.domain.admin.replay_models (and similar usage in overview_models.py) and
either (A) move ReplaySessionStatusDetail and ReplaySessionData into
app.schemas_pydantic.admin_events so they can reference ExecutionResultSummary
and EventSummary there, or (B) lift ExecutionResultSummary and EventSummary into
the domain layer and have app.schemas_pydantic re-export/convert them; update
references to ReplaySessionStatusDetail.execution_results and
ReplaySessionData.events_preview to use the relocated types and adjust import
sites accordingly to restore the one-way dependency: domain ← schemas_pydantic.

In `@backend/app/schemas_pydantic/events.py`:
- Around line 69-106: Frontend types will break because backend Pydantic models
were renamed; add backwards-compatible aliases by reintroducing the old class
names as thin subclasses or type aliases that reference the new models (e.g.,
define EventTypeCountSchema, HourlyEventCountSchema, ServiceEventCountSchema,
UserEventCountSchema that inherit from or alias EventTypeCount,
HourlyEventCount, ServiceEventCount, UserEventCount) so the OpenAPI generator
still emits the original schema names; ensure these alias classes live alongside
the renamed models in events.py and carry the same model_config if needed.
🧹 Nitpick comments (12)
backend/app/services/saga/saga_service.py (1)

14-14: Service layer now depends on a Pydantic response schema — consider keeping domain/presentation separation.

The service constructs and returns a SagaCancellationResponse (a Pydantic schema) directly. This couples the service layer to the API presentation layer. Typically, services return plain domain objects or dataclasses and the route/controller maps them to response schemas.

Additionally, the route at backend/app/api/routes/saga.py line 89 calls SagaCancellationResponse.model_validate(result) on an object that is already a SagaCancellationResponse, making the validation redundant.

If keeping the current approach, at least remove the redundant model_validate in the route:

# backend/app/api/routes/saga.py, line 89
return result  # already a SagaCancellationResponse

Also applies to: 134-134, 161-161

backend/app/db/repositories/dlq_repository.py (1)

17-17: Repository now depends on a response schema instead of a domain model.

Having a repository return a *Response Pydantic schema couples the persistence layer to the API contract. Typically, repositories return domain models and the mapping to response schemas happens at the service or route layer. This keeps the repository reusable if the API shape changes independently of the stored data.

That said, if DLQTopicSummaryResponse is identical to the removed DLQTopicSummary and this is a deliberate consolidation to avoid duplication, the trade-off is reasonable — just be aware of the coupling.

backend/app/schemas_pydantic/replays.py (1)

22-41: ReplayConfigSchema and ReplayRequest are near-duplicates — consider reusing one from the other.

These two models share almost every field (replay_type, target, speed_multiplier, preserve_timestamps, batch_size, max_events, target_topics, target_file_path, skip_errors, retry_failed, retry_attempts, enable_progress_tracking). The main differences are:

  1. filter type: ReplayFilterSchema vs ReplayFilter
  2. retry_attempts on ReplayConfigSchema (Line 39) lacks the ge=1, le=10 constraint that ReplayRequest (Line 79) enforces — this means a ReplaySession read-back could contain an invalid retry_attempts value that would be rejected if submitted as a ReplayRequest.

At minimum, add the same constraint to ReplayConfigSchema.retry_attempts:

Proposed fix for missing constraint
-    retry_attempts: int = 3
+    retry_attempts: int = Field(default=3, ge=1, le=10)

Longer term, consider having ReplayRequest inherit from or compose ReplayConfigSchema (or vice-versa) to eliminate the duplication.

Also applies to: 64-80

backend/app/services/event_replay/replay_service.py (2)

26-26: Service layer now directly returns API response schemas.

The service methods now construct and return ReplayResponse / CleanupResponse (pydantic schemas) instead of domain result types. This couples the service layer to the API schema layer — typically services return domain objects and the route layer handles mapping.

That said, since ReplayResponse is a simple DTO with no API-specific concerns (no HTTP status codes, no pagination metadata, etc.), this is pragmatic and avoids unnecessary intermediate types. The tradeoff is acceptable here.

One downstream consequence: the route handlers now do redundant ReplayResponse.model_validate(result) calls on values that are already ReplayResponse instances (see backend/app/api/routes/replay.py). This is harmless but unnecessary.

Also applies to: 48-48, 62-62, 101-101, 114-114, 135-135, 157-157


48-60: create_session_from_config — error message loses context.

On Line 60, the ReplayOperationError is constructed with an empty string "" as the session ID since no session exists yet. This is fine, but note that the f-string log on Line 59 uses string interpolation instead of structured logging kwargs:

self.logger.error(f"Failed to create replay session: {e}")

The rest of the file uses structlog consistently. Consider using structured fields for consistency:

Suggested change
-            self.logger.error(f"Failed to create replay session: {e}")
+            self.logger.error("Failed to create replay session", error=str(e))
backend/app/api/routes/replay.py (1)

28-29: Redundant model_validate calls on service return values.

Since the service methods now return ReplayResponse and CleanupResponse directly, the model_validate calls on lines 29, 39, 49, 56, 63, and 89 are no-ops (re-validating an already-validated instance of the same type). You can return the service result directly:

Example simplification
 async def create_replay_session(...) -> ReplayResponse:
     """Create a new event replay session from a configuration."""
-    result = await service.create_session_from_config(ReplayConfig.model_validate(replay_request))
-    return ReplayResponse.model_validate(result)
+    return await service.create_session_from_config(ReplayConfig.model_validate(replay_request))

Same pattern applies to start_replay_session, pause_replay_session, resume_replay_session, cancel_replay_session, and cleanup_old_sessions.

Note: Lines 73 and 79 (SessionSummary.model_validate(s), ReplaySession.model_validate(...)) are not redundant since they convert from domain types — those should stay.

Also applies to: 38-39, 48-49, 55-56, 62-63, 88-89

backend/app/db/repositories/admin/admin_user_repository.py (1)

76-102: Repository returning an API response schema introduces layer coupling.

DeleteUserResponse is a Pydantic response model (lives in schemas_pydantic). Having the repository construct and return it directly couples the persistence layer to the API contract. Conventionally, repositories return domain objects, and the mapping to response schemas happens in the service or route layer.

Also note that in backend/app/api/routes/admin/users.py (line 154), the route calls DeleteUserResponse.model_validate(result) on a value that is already a DeleteUserResponse—making that validation a no-op pass-through.

backend/app/schemas_pydantic/events.py (2)

36-56: EventListResult and EventBrowseResult are nearly identical — consider consolidation.

EventBrowseResult (lines 48-56) has the same fields as EventListResult (lines 36-45) minus has_more. If EventBrowseResult is truly a subset, consider reusing EventListResult (with has_more defaulting to False) or having EventBrowseResult extend EventListResult. This reduces surface area in a PR explicitly about schema consolidation.


21-34: Duplicate EventFilter — consider consolidating with the one in events.py.

backend/app/schemas_pydantic/admin_events.py (lines 12-21) defines its own EventFilter with seven fields. However, backend/app/schemas_pydantic/events.py (lines 21-34) defines a nearly identical EventFilter with eight fields (adding status). The service and repository layers (admin_events_service.py, admin_events_repository.py) all import from events.py, while the local EventFilter in admin_events.py is only used by EventBrowseRequest (line 27).

Consolidate by removing the EventFilter from admin_events.py and importing it from events.py instead. Since status is optional, EventBrowseRequest can use the unified definition without issues.

backend/app/domain/admin/overview_models.py (1)

7-9: Domain model now depends on schemas_pydantic — inverted dependency direction.

AdminUserOverviewDomain (a domain-layer dataclass) now imports EventStatistics from app.schemas_pydantic.events. This creates a domain → schemas_pydantic dependency, which inverts the conventional layering (domain should be self-contained; schemas depend on domain, not vice versa).

This is fine if schemas_pydantic is treated as a shared "contract" layer, but be mindful of circular dependency risk if schemas_pydantic modules ever import from app.domain.admin.

backend/app/domain/admin/replay_models.py (1)

13-17: Mixing pydantic.Field and dataclasses.field in one module — be explicit about which is used where.

Line 1 imports field from dataclasses and line 4 imports Field from pydantic. ReplaySessionStatusDetail (line 17) uses pydantic.Field, while ReplaySessionData (line 46) uses dataclasses.field. The casing difference (field vs Field) is easy to confuse. This is fine for now since the usages are correct, but adding a brief comment or aliasing could prevent future mix-ups.

backend/app/api/routes/admin/events.py (1)

34-42: The EventFilter conversion between two different classes is implicit — worth a clarifying comment.

request.filters is typed as admin_events.EventFilter (from EventBrowseRequest), but EventFilter.model_validate(request.filters) on line 37 converts it to events.EventFilter (which has an additional status field). This works but silently coerces between two subtly different models. A brief inline comment would help future readers understand this is intentional and tied to the duplicate EventFilter classes.

Comment thread backend/app/domain/admin/replay_models.py Outdated
Comment thread backend/app/schemas_pydantic/events.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 37 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/app/services/sse/sse_service.py">

<violation number="1" location="backend/app/services/sse/sse_service.py:163">
P2: `TypeAdapter.dump_json` returns bytes, but SSE event data should be a string. Decode the bytes (or switch to a string serializer) to avoid emitting `b'...'` or encoding errors in the SSE stream.</violation>

<violation number="2" location="backend/app/services/sse/sse_service.py:171">
P2: `TypeAdapter.dump_json` returns bytes; decode to a string before returning SSE data so the event payload remains valid JSON text.</violation>
</file>

<file name="backend/app/schemas_pydantic/sse.py">

<violation number="1" location="backend/app/schemas_pydantic/sse.py:33">
P2: The SSE API schema now embeds the internal `DomainExecution` model, which includes fields like `script`, `user_id`, and timestamps that weren’t part of the public execution result. This changes the API payload shape and can expose internal data. Use the public execution result schema instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread backend/app/services/sse/sse_service.py Outdated
Comment thread backend/app/services/sse/sse_service.py Outdated
Comment thread backend/app/schemas_pydantic/sse.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/schemas_pydantic/replays.py (1)

66-82: ⚠️ Potential issue | 🟡 Minor

Inconsistent filter type in ReplayRequest: uses domain ReplayFilter instead of schema ReplayFilterSchema.

Line 71 uses filter: ReplayFilter (the domain model with to_mongo_query() and is_empty() methods), while ReplayConfigSchema at line 29 correctly uses filter: ReplayFilterSchema. Since ReplayRequest is the API input schema, it should use the schema-layer ReplayFilterSchema to maintain clean separation between domain and schema concerns. Both models have compatible field structures, so changing this will not affect the downstream conversion via ReplayConfig.model_validate(replay_request).

Proposed fix
 class ReplayRequest(BaseModel):
     """Request schema for creating replay sessions"""
 
     replay_type: ReplayType
     target: ReplayTarget = ReplayTarget.KAFKA
-    filter: ReplayFilter = Field(default_factory=ReplayFilter)
+    filter: ReplayFilterSchema = Field(default_factory=ReplayFilterSchema)
backend/app/services/sse/sse_service.py (1)

151-163: ⚠️ Potential issue | 🔴 Critical

Decode dump_json() output to str — currently produces malformed SSE events.

_notif_payload_ta.dump_json(payload) returns bytes, which sse-starlette converts to a string via str() in ServerSentEvent.encode(). This produces a literal "b'{...}'" representation instead of the JSON content. Decode to string explicitly:

yield {"event": "notification", "data": _notif_payload_ta.dump_json(payload).decode("utf-8")}

Same issue exists in _format_sse_event() (line 171) — apply the same fix there.

🤖 Fix all issues with AI agents
In `@backend/app/api/routes/admin/users.py`:
- Around line 69-78: The route currently misuses getattr for non-existent
UserCreate fields and inconsistently hashes passwords across layers; remove the
redundant getattr usage when building DomainUserCreate in the route (omit role
and is_active so DomainUserCreate defaults apply) and unify password-hashing by
moving hashing responsibility to a single layer—either always hash in the
service or always hash in the route. Specifically, adjust the route code that
constructs DomainUserCreate (remove getattr for role and is_active) and refactor
admin_user_service.create_user, update_user, and reset_user_password (and usages
of security_service.get_password_hash) so only one of those layers performs
hashing consistently across create/update/reset paths.

In `@backend/app/services/admin/admin_user_service.py`:
- Around line 129-144: The username collision check in
AdminUserService.create_user is unreliable; replace the list_users/loop with an
exact lookup using the repository's get_user(username) (call
_users.get_user(create_data.username)) and raise ConflictError if it returns a
user, then wrap the call to _users.create_user(create_data) in a try/except that
catches DuplicateKeyError (the same DB error handled in UserRepository), and on
DuplicateKeyError raise ConflictError("Username already exists"); keep existing
logging (logger.info) and preserve the method signature.
🧹 Nitpick comments (7)
backend/app/domain/sse/models.py (1)

60-81: Duplication: SSEExecutionEventData exists both here (dataclass) and in schemas_pydantic/sse.py (BaseModel).

Both define identical fields. The schema version adds Field(description=...) for OpenAPI docs, but the structural duplication means field additions/removals must be synchronized in two places. Consider having the schema inherit from or delegate to the domain model, or use a shared field list.

backend/app/schemas_pydantic/sse.py (1)

8-11: Re-exporting domain models (RedisSSEMessage, RedisNotificationMessage) through the schema package may blur the boundary this PR aims to establish.

These are imported from app.domain.sse.models and then re-exported via __all__. Consumers could end up importing domain types from app.schemas_pydantic.sse, which contradicts the separation goal. Consider either dropping them from __all__ here or adding a comment clarifying this is intentional for backward compatibility.

Also applies to: 36-40

backend/app/services/sse/redis_bus.py (1)

27-40: Cache TypeAdapter instances in the subscription hot loop.

TypeAdapter(model) is recreated on every get() call in line 33, but Pydantic's documentation explicitly recommends creating TypeAdapter once and reusing it—this operation is expensive because it requires schema analysis and building. The get() method is called in a tight polling loop (0.5s timeout) with only two types in practice (RedisSSEMessage and RedisNotificationMessage), making the repeated instantiation avoidable overhead. The module-level adapters at lines 15–16 follow the correct pattern; extend it to the generic method.

♻️ Suggested approach: cache TypeAdapters by type
+from functools import lru_cache
+
+@lru_cache(maxsize=8)
+def _get_adapter(model: type) -> TypeAdapter:  # type: ignore[type-arg]
+    return TypeAdapter(model)
+
 class SSERedisSubscription:
     ...
     async def get(self, model: Type[T]) -> T | None:
         ...
         try:
-            return TypeAdapter(model).validate_json(msg["data"])
+            return _get_adapter(model).validate_json(msg["data"])
backend/app/schemas_pydantic/admin_events.py (1)

5-5: Remove unused import and re-export of ExecutionResultSummary from this module.

ExecutionResultSummary is imported from the domain module (line 5) and listed in __all__ (line 134), but no other module imports it from app.schemas_pydantic.admin_events. Consumers import it directly from app.domain.admin, making this re-export pattern unnecessary.

backend/app/schemas_pydantic/events.py (1)

23-56: Dual EventStatistics definitions may cause import confusion.

There's now an EventStatistics dataclass in app.domain.events.event_models and a BaseModel version here with the same name. Since this module re-exports many other types from that domain module, a consumer doing from app.schemas_pydantic.events import EventStatistics gets the BaseModel, but from app.domain.events.event_models import EventStatistics gets the dataclass. This is presumably intentional, but consider adding a brief comment (or a domain-side rename like DomainEventStatistics) to make the distinction explicit and prevent accidental mis-imports.

backend/app/domain/admin/replay_models.py (2)

36-62: Inconsistent @dataclass configuration across sibling models.

ExecutionResultSummary (line 12) uses @dataclass(config=ConfigDict(from_attributes=True)), while ReplaySessionStatusInfo (line 36) and ReplaySessionData (line 54) use bare @dataclass without config. If these models may be constructed from ORM objects or attribute-bearing instances (e.g., via .from_orm() or model_validate), they'll need the same config. Consider making the configuration consistent.


29-33: ReplaySessionStatusDetail is not converted to a dataclass.

This class extends ReplaySessionState (presumably a BaseModel or dataclass from app.domain.replay) and remains a plain class with pydantic.Field. This is likely necessary if ReplaySessionState is itself a BaseModel, but it creates an inconsistency with the rest of this file's dataclass pattern. Worth a brief comment explaining why it's kept as-is.

Comment thread backend/app/api/routes/admin/users.py Outdated
Comment thread backend/app/services/admin/admin_user_service.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/admin/admin_user_service.py (1)

169-183: ⚠️ Potential issue | 🟡 Minor

Service method signature does not match what the route passes.

The route (line 132 in users.py) converts the schema UserUpdate to DomainUserUpdate via model_validate, then passes DomainUserUpdate to the service (line 135). However, the service's update_user method signature declares update: UserUpdate (the domain model without hashed password), and line 175 attempts to access update.password. Since the route passes DomainUserUpdate (which contains hashed_password, not password), this causes a type mismatch—the service would fail at runtime trying to access a non-existent field.

Fix by changing the service signature to accept DomainUserUpdate directly and skip the internal reconstruction on lines 176–182:

async def update_user(self, *, admin_user_id: str, user_id: str, update: DomainUserUpdate) -> User | None:
    self.logger.info(
        "Admin updating user",
        admin_user_id=admin_user_id,
        target_user_id=user_id,
    )
    return await self._users.update_user(user_id, update)
🧹 Nitpick comments (6)
docs/reference/openapi.json (2)

7175-7249: Document defaults for EventStatistics arrays if they’re always returned.

If events_by_* and top_users are always present, consider adding default: [] and/or marking them required to avoid clients treating them as optional.


11521-11550: Avoid drift between ResourceUsageDomain and ResourceUsage.

If these schemas are identical, consider referencing a single schema (or clearly documenting differences) to prevent future divergence in SSE payloads.

backend/app/api/routes/admin/users.py (1)

123-130: Route bypasses service layer for the existence check.

The update_user route injects UserRepository directly to check existence (line 128), while every other endpoint in this file operates exclusively through AdminUserService. The service's update_user already returns None when the user isn't found, so the pre-check is redundant and introduces an unnecessary repository dependency in the route.

Consider removing the repo injection and relying on the service's return value (or raising NotFoundError inside the service):

Proposed simplification
 async def update_user(
     admin: Annotated[User, Depends(admin_user)],
     user_id: str,
     user_update: UserUpdate,
-    user_repo: FromDishka[UserRepository],
     admin_user_service: FromDishka[AdminUserService],
 ) -> UserResponse:
     """Update a user's profile fields."""
-    # Get existing user (explicit 404), then update
-    existing_user = await user_repo.get_user_by_id(user_id)
-    if not existing_user:
-        raise HTTPException(status_code=404, detail="User not found")
-
     domain_update = DomainUserUpdate.model_validate(user_update)
 
     updated_user = await admin_user_service.update_user(
         admin_user_id=admin.user_id, user_id=user_id, update=domain_update
     )
     if not updated_user:
-        raise HTTPException(status_code=500, detail="Failed to update user")
+        raise HTTPException(status_code=404, detail="User not found")
 
     return UserResponse.model_validate(updated_user)
backend/app/schemas_pydantic/sse.py (2)

14-33: Two classes named SSEExecutionEventData exist — one here (API schema) and one in domain/sse/models.py (domain dataclass).

Both are structurally similar but use different types for result (ExecutionResult vs ExecutionResultDomain). The service imports from app.domain.sse, so this API-schema version is only used for OpenAPI documentation. This works, but the identical name across modules is a maintenance hazard — a wrong import silently changes serialization behavior.

Consider renaming one to disambiguate (e.g., SSEExecutionEventSchema here).


36-40: Re-exporting domain types from a schema module blurs the boundary.

RedisSSEMessage and RedisNotificationMessage are domain types (defined in app.domain.sse.models), but re-exporting them from app.schemas_pydantic.sse makes it look like they're API schemas. Consumers that need these types should import from app.domain.sse directly.

backend/app/services/sse/sse_service.py (1)

125-140: _build_sse_event_from_redis: consider handling the missing-execution case for RESULT_STORED.

When msg.event_type is RESULT_STORED but get_execution returns None (line 130), result stays None and the event is emitted without result data. This is silently swallowed — a warning log would help diagnose why a terminal event arrived with no result payload.

Suggested improvement
         if msg.event_type == EventType.RESULT_STORED:
             execution = await self.repository.get_execution(execution_id)
             if execution:
                 result = ExecutionResultDomain.model_validate(execution)
+            else:
+                self.logger.warning(
+                    "Execution not found for RESULT_STORED event",
+                    execution_id=execution_id,
+                )

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 49 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/app/schemas_pydantic/admin_settings.py">

<violation number="1" location="backend/app/schemas_pydantic/admin_settings.py:7">
P2: The memory_limit validator only allows Ki/Mi/Gi, but Kubernetes quantities also allow Ti/Pi/Ei, decimal SI (M/G), and no suffix. This will reject valid memory limits even though the error message claims K8s compatibility.</violation>

<violation number="2" location="backend/app/schemas_pydantic/admin_settings.py:8">
P2: The cpu_limit validator only accepts millicore values (e.g., "500m"), but Kubernetes CPU quantities also allow plain or decimal cores ("1", "0.5"). This will reject valid CPU limits.</violation>
</file>

<file name="backend/app/db/repositories/saga_repository.py">

<violation number="1" location="backend/app/db/repositories/saga_repository.py:22">
P2: Guard against context_data being None; SagaContextData(**None) will raise TypeError when stored records have null context_data.</violation>
</file>

<file name="backend/app/services/result_processor/processor.py">

<violation number="1" location="backend/app/services/result_processor/processor.py:69">
P2: Convert `resource_usage` dicts to `ResourceUsageDomain` in timeout handling to keep `ExecutionResultDomain.resource_usage` typed consistently.</violation>
</file>

<file name="backend/app/services/sse/sse_service.py">

<violation number="1" location="backend/app/services/sse/sse_service.py:150">
P2: The SSE event builder no longer forwards `resource_usage` from Redis event data, so execution completion/failure/timeout SSE payloads will always omit resource usage even when present in the domain event.</violation>
</file>

<file name="backend/app/domain/admin/settings_models.py">

<violation number="1" location="backend/app/domain/admin/settings_models.py:28">
P2: SystemSettings no longer enforces bounds/pattern validation, so invalid TOML/DB settings (e.g., malformed K8s resource limits or out-of-range timeouts) can propagate to runtime without checks. Consider validating these values at the domain boundary (e.g., use a Pydantic dataclass or validate via SystemSettingsSchema/TypeAdapter when constructing SystemSettings from raw settings).</violation>
</file>

<file name="backend/app/domain/rate_limit/rate_limit_models.py">

<violation number="1" location="backend/app/domain/rate_limit/rate_limit_models.py:45">
P2: `compiled_pattern` is now serialized into the persisted rate-limit config. Since this field holds compiled `re.Pattern` objects and is set at runtime, dumping the config to JSON can fail (or persist non-portable data). Keep it excluded from serialization so runtime-only patterns don’t break `TypeAdapter.dump_json()` or get stored in Redis.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread backend/app/schemas_pydantic/admin_settings.py
Comment thread backend/app/schemas_pydantic/admin_settings.py
Comment thread backend/app/db/repositories/saga_repository.py Outdated
Comment thread backend/app/services/result_processor/processor.py
Comment thread backend/app/services/sse/sse_service.py
Comment thread backend/app/domain/admin/settings_models.py
Comment thread backend/app/domain/rate_limit/rate_limit_models.py Outdated
@sonarqubecloud

Copy link
Copy Markdown

@HardMax71
HardMax71 merged commit 9ffd18f into main Feb 14, 2026
19 checks passed
@HardMax71
HardMax71 deleted the fix/schemas-consolidation branch February 14, 2026 19:33
This was referenced Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant