Skip to content

Commit a201c96

Browse files
committed
unification of api (result -> skip/has more,..)
1 parent 2b615bb commit a201c96

File tree

10 files changed

+149
-47
lines changed

10 files changed

+149
-47
lines changed

backend/app/api/routes/events.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,30 @@ async def get_execution_events(
3838
current_user: Annotated[UserResponse, Depends(current_user)],
3939
event_service: FromDishka[EventService],
4040
include_system_events: bool = Query(False, description="Include system-generated events"),
41+
limit: int = Query(100, ge=1, le=1000),
42+
skip: int = Query(0, ge=0),
4143
) -> EventListResponse:
42-
events = await event_service.get_execution_events(
44+
result = await event_service.get_execution_events(
4345
execution_id=execution_id,
4446
user_id=current_user.user_id,
4547
user_role=current_user.role,
4648
include_system_events=include_system_events,
49+
limit=limit,
50+
skip=skip,
4751
)
4852

49-
if events is None:
53+
if result is None:
5054
raise HTTPException(status_code=403, detail="Access denied")
5155

52-
event_responses = [EventResponse.model_validate(event) for event in events]
56+
event_responses = [EventResponse.model_validate(event) for event in result.events]
5357

54-
return EventListResponse(events=event_responses, total=len(event_responses), limit=1000, skip=0, has_more=False)
58+
return EventListResponse(
59+
events=event_responses,
60+
total=result.total,
61+
limit=limit,
62+
skip=skip,
63+
has_more=result.has_more,
64+
)
5565

5666

5767
@router.get("/user", response_model=EventListResponse)
@@ -126,41 +136,57 @@ async def get_events_by_correlation(
126136
event_service: FromDishka[EventService],
127137
include_all_users: bool = Query(False, description="Include events from all users (admin only)"),
128138
limit: int = Query(100, ge=1, le=1000),
139+
skip: int = Query(0, ge=0),
129140
) -> EventListResponse:
130-
events = await event_service.get_events_by_correlation(
141+
result = await event_service.get_events_by_correlation(
131142
correlation_id=correlation_id,
132143
user_id=current_user.user_id,
133144
user_role=current_user.role,
134145
include_all_users=include_all_users,
135146
limit=limit,
147+
skip=skip,
136148
)
137149

138-
event_responses = [EventResponse.model_validate(event) for event in events]
150+
event_responses = [EventResponse.model_validate(event) for event in result.events]
139151

140-
return EventListResponse(events=event_responses, total=len(event_responses), limit=limit, skip=0, has_more=False)
152+
return EventListResponse(
153+
events=event_responses,
154+
total=result.total,
155+
limit=limit,
156+
skip=skip,
157+
has_more=result.has_more,
158+
)
141159

142160

143161
@router.get("/current-request", response_model=EventListResponse)
144162
async def get_current_request_events(
145163
current_user: Annotated[UserResponse, Depends(current_user)],
146164
event_service: FromDishka[EventService],
147165
limit: int = Query(100, ge=1, le=1000),
166+
skip: int = Query(0, ge=0),
148167
) -> EventListResponse:
149168
correlation_id = CorrelationContext.get_correlation_id()
150169
if not correlation_id:
151-
return EventListResponse(events=[], total=0, limit=limit, skip=0, has_more=False)
170+
return EventListResponse(events=[], total=0, limit=limit, skip=skip, has_more=False)
152171

153-
events = await event_service.get_events_by_correlation(
172+
result = await event_service.get_events_by_correlation(
154173
correlation_id=correlation_id,
155174
user_id=current_user.user_id,
156175
user_role=current_user.role,
157176
include_all_users=False,
158177
limit=limit,
178+
skip=skip,
159179
)
160180

161-
event_responses = [EventResponse.model_validate(event) for event in events]
181+
event_responses = [EventResponse.model_validate(event) for event in result.events]
162182

163-
return EventListResponse(events=event_responses, total=len(event_responses), limit=limit, skip=0, has_more=False)
183+
return EventListResponse(
184+
events=event_responses,
185+
total=result.total,
186+
limit=limit,
187+
skip=skip,
188+
has_more=result.has_more,
189+
)
164190

165191

166192
@router.get("/statistics", response_model=EventStatistics)

backend/app/api/routes/saga.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ async def get_execution_sagas(
5656
saga_service: FromDishka[SagaService],
5757
auth_service: FromDishka[AuthService],
5858
state: SagaState | None = Query(None, description="Filter by saga state"),
59+
limit: int = Query(100, ge=1, le=1000),
60+
skip: int = Query(0, ge=0),
5961
) -> SagaListResponse:
6062
"""Get all sagas for an execution.
6163
@@ -65,9 +67,11 @@ async def get_execution_sagas(
6567
saga_service: Saga service from DI
6668
auth_service: Auth service from DI
6769
state: Optional state filter
70+
limit: Maximum number of results
71+
skip: Number of results to skip
6872
6973
Returns:
70-
List of sagas for the execution
74+
Paginated list of sagas for the execution
7175
7276
Raises:
7377
HTTPException: 403 if access denied
@@ -76,9 +80,15 @@ async def get_execution_sagas(
7680

7781
service_user = User.from_response(current_user)
7882
domain_user = AdminUserMapper.from_pydantic_service_user(service_user)
79-
sagas = await saga_service.get_execution_sagas(execution_id, domain_user, state)
80-
saga_responses = [SagaStatusResponse.from_domain(s) for s in sagas]
81-
return SagaListResponse(sagas=saga_responses, total=len(saga_responses))
83+
result = await saga_service.get_execution_sagas(execution_id, domain_user, state, limit=limit, skip=skip)
84+
saga_responses = [SagaStatusResponse.from_domain(s) for s in result.sagas]
85+
return SagaListResponse(
86+
sagas=saga_responses,
87+
total=result.total,
88+
skip=skip,
89+
limit=limit,
90+
has_more=result.has_more,
91+
)
8292

8393

8494
@router.get("/", response_model=SagaListResponse)
@@ -88,7 +98,7 @@ async def list_sagas(
8898
auth_service: FromDishka[AuthService],
8999
state: SagaState | None = Query(None, description="Filter by saga state"),
90100
limit: int = Query(100, ge=1, le=1000),
91-
offset: int = Query(0, ge=0),
101+
skip: int = Query(0, ge=0),
92102
) -> SagaListResponse:
93103
"""List sagas accessible by the current user.
94104
@@ -98,7 +108,7 @@ async def list_sagas(
98108
auth_service: Auth service from DI
99109
state: Optional state filter
100110
limit: Maximum number of results
101-
offset: Number of results to skip
111+
skip: Number of results to skip
102112
103113
Returns:
104114
Paginated list of sagas
@@ -107,9 +117,15 @@ async def list_sagas(
107117

108118
service_user = User.from_response(current_user)
109119
domain_user = AdminUserMapper.from_pydantic_service_user(service_user)
110-
result = await saga_service.list_user_sagas(domain_user, state, limit, offset)
120+
result = await saga_service.list_user_sagas(domain_user, state, limit, skip)
111121
saga_responses = [SagaStatusResponse.from_domain(s) for s in result.sagas]
112-
return SagaListResponse(sagas=saga_responses, total=result.total)
122+
return SagaListResponse(
123+
sagas=saga_responses,
124+
total=result.total,
125+
skip=skip,
126+
limit=limit,
127+
has_more=result.has_more,
128+
)
113129

114130

115131
@router.post("/{saga_id}/cancel", response_model=SagaCancellationResponse)

backend/app/api/routes/user_settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def get_settings_history(
100100
) -> SettingsHistoryResponse:
101101
history = await settings_service.get_settings_history(current_user.user_id, limit=limit)
102102
entries = [SettingsHistoryEntry.model_validate(entry) for entry in history]
103-
return SettingsHistoryResponse(history=entries, total=len(entries))
103+
return SettingsHistoryResponse(history=entries, limit=limit)
104104

105105

106106
@router.post("/restore", response_model=UserSettings)

backend/app/db/repositories/event_repository.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,26 @@ async def get_events_by_aggregate(
125125
docs = await cursor.to_list(length=limit)
126126
return [self.mapper.from_mongo_document(doc) for doc in docs]
127127

128-
async def get_events_by_correlation(self, correlation_id: str, limit: int = 100) -> list[Event]:
128+
async def get_events_by_correlation(
129+
self, correlation_id: str, limit: int = 100, skip: int = 0
130+
) -> EventListResult:
131+
query = {EventFields.METADATA_CORRELATION_ID: correlation_id}
132+
total_count = await self._collection.count_documents(query)
133+
129134
cursor = (
130-
self._collection.find({EventFields.METADATA_CORRELATION_ID: correlation_id})
135+
self._collection.find(query)
131136
.sort(EventFields.TIMESTAMP, ASCENDING)
137+
.skip(skip)
132138
.limit(limit)
133139
)
134140
docs = await cursor.to_list(length=limit)
135-
return [self.mapper.from_mongo_document(doc) for doc in docs]
141+
return EventListResult(
142+
events=[self.mapper.from_mongo_document(doc) for doc in docs],
143+
total=total_count,
144+
skip=skip,
145+
limit=limit,
146+
has_more=(skip + limit) < total_count,
147+
)
136148

137149
async def get_events_by_user(
138150
self,
@@ -154,12 +166,26 @@ async def get_events_by_user(
154166
docs = await cursor.to_list(length=limit)
155167
return [self.mapper.from_mongo_document(doc) for doc in docs]
156168

157-
async def get_execution_events(self, execution_id: str, limit: int = 100) -> list[Event]:
169+
async def get_execution_events(
170+
self, execution_id: str, limit: int = 100, skip: int = 0
171+
) -> EventListResult:
158172
query = {"$or": [{EventFields.PAYLOAD_EXECUTION_ID: execution_id}, {EventFields.AGGREGATE_ID: execution_id}]}
173+
total_count = await self._collection.count_documents(query)
159174

160-
cursor = self._collection.find(query).sort(EventFields.TIMESTAMP, ASCENDING).limit(limit)
175+
cursor = (
176+
self._collection.find(query)
177+
.sort(EventFields.TIMESTAMP, ASCENDING)
178+
.skip(skip)
179+
.limit(limit)
180+
)
161181
docs = await cursor.to_list(length=limit)
162-
return [self.mapper.from_mongo_document(doc) for doc in docs]
182+
return EventListResult(
183+
events=[self.mapper.from_mongo_document(doc) for doc in docs],
184+
total=total_count,
185+
skip=skip,
186+
limit=limit,
187+
has_more=(skip + limit) < total_count,
188+
)
163189

164190
async def search_events(
165191
self, text_query: str, filters: dict[str, object] | None = None, limit: int = 100, skip: int = 0

backend/app/db/repositories/saga_repository.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,19 @@ async def get_saga(self, saga_id: str) -> Saga | None:
4646
doc = await self.sagas.find_one({"saga_id": saga_id})
4747
return self.mapper.from_mongo(doc) if doc else None
4848

49-
async def get_sagas_by_execution(self, execution_id: str, state: SagaState | None = None) -> list[Saga]:
49+
async def get_sagas_by_execution(
50+
self, execution_id: str, state: SagaState | None = None, limit: int = 100, skip: int = 0
51+
) -> SagaListResult:
5052
query: dict[str, object] = {"execution_id": execution_id}
5153
if state:
5254
query["state"] = state.value
5355

54-
cursor = self.sagas.find(query).sort("created_at", DESCENDING)
55-
docs = await cursor.to_list(length=None)
56-
return [self.mapper.from_mongo(doc) for doc in docs]
56+
total = await self.sagas.count_documents(query)
57+
cursor = self.sagas.find(query).sort("created_at", DESCENDING).skip(skip).limit(limit)
58+
docs = await cursor.to_list(length=limit)
59+
sagas = [self.mapper.from_mongo(doc) for doc in docs]
60+
61+
return SagaListResult(sagas=sagas, total=total, skip=skip, limit=limit)
5762

5863
async def list_sagas(self, saga_filter: SagaFilter, limit: int = 100, skip: int = 0) -> SagaListResult:
5964
query = self.filter_mapper.to_mongodb_query(saga_filter)

backend/app/schemas_pydantic/saga.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class SagaListResponse(BaseModel):
4444

4545
sagas: list[SagaStatusResponse]
4646
total: int
47+
skip: int
48+
limit: int
49+
has_more: bool
4750

4851

4952
class SagaCancellationResponse(BaseModel):

backend/app/schemas_pydantic/user_settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,12 @@ class SettingsHistoryEntry(BaseModel):
110110

111111

112112
class SettingsHistoryResponse(BaseModel):
113-
"""Response model for settings history"""
113+
"""Response model for settings history (limited snapshot of recent changes)"""
114114

115115
model_config = ConfigDict(from_attributes=True)
116116

117117
history: List[SettingsHistoryEntry]
118-
total: int
118+
limit: int
119119

120120

121121
class RestoreSettingsRequest(BaseModel):

backend/app/services/event_service.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ async def get_execution_events(
3434
user_id: str,
3535
user_role: UserRole,
3636
include_system_events: bool = False,
37-
) -> list[Event] | None:
38-
events = await self.repository.get_events_by_aggregate(aggregate_id=execution_id, limit=1000)
39-
if not events:
40-
return []
37+
limit: int = 1000,
38+
skip: int = 0,
39+
) -> EventListResult | None:
40+
result = await self.repository.get_execution_events(
41+
execution_id=execution_id, limit=limit, skip=skip
42+
)
43+
if not result.events:
44+
return EventListResult(events=[], total=0, skip=skip, limit=limit, has_more=False)
4145

4246
owner = None
43-
for e in events:
47+
for e in result.events:
4448
if e.metadata and e.metadata.user_id:
4549
owner = e.metadata.user_id
4650
break
@@ -49,9 +53,17 @@ async def get_execution_events(
4953
return None
5054

5155
if not include_system_events:
52-
events = [e for e in events if not (e.metadata and e.metadata.service_name.startswith("system-"))]
53-
54-
return events
56+
filtered = [e for e in result.events if not (e.metadata and e.metadata.service_name.startswith("system-"))]
57+
# Recalculate has_more based on filtered count
58+
return EventListResult(
59+
events=filtered,
60+
total=result.total,
61+
skip=skip,
62+
limit=limit,
63+
has_more=result.has_more,
64+
)
65+
66+
return result
5567

5668
async def get_user_events_paginated(
5769
self,
@@ -118,11 +130,21 @@ async def get_events_by_correlation(
118130
user_role: UserRole,
119131
include_all_users: bool = False,
120132
limit: int = 100,
121-
) -> list[Event]:
122-
events = await self.repository.get_events_by_correlation(correlation_id=correlation_id, limit=limit)
133+
skip: int = 0,
134+
) -> EventListResult:
135+
result = await self.repository.get_events_by_correlation(
136+
correlation_id=correlation_id, limit=limit, skip=skip
137+
)
123138
if not include_all_users or user_role != UserRole.ADMIN:
124-
events = [e for e in events if (e.metadata and e.metadata.user_id == user_id)]
125-
return events
139+
filtered = [e for e in result.events if (e.metadata and e.metadata.user_id == user_id)]
140+
return EventListResult(
141+
events=filtered,
142+
total=result.total,
143+
skip=skip,
144+
limit=limit,
145+
has_more=result.has_more,
146+
)
147+
return result
126148

127149
async def get_event_statistics(
128150
self,

backend/app/services/saga/saga_service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ async def get_saga_with_access_check(self, saga_id: str, user: User) -> Saga:
6363

6464
return saga
6565

66-
async def get_execution_sagas(self, execution_id: str, user: User, state: SagaState | None = None) -> list[Saga]:
66+
async def get_execution_sagas(
67+
self, execution_id: str, user: User, state: SagaState | None = None, limit: int = 100, skip: int = 0
68+
) -> SagaListResult:
6769
"""Get sagas for an execution with access control."""
6870
# Check access to execution
6971
if not await self.check_execution_access(execution_id, user):
@@ -72,7 +74,7 @@ async def get_execution_sagas(self, execution_id: str, user: User, state: SagaSt
7274
)
7375
raise SagaAccessDeniedError(f"Access denied - no access to execution {execution_id}")
7476

75-
return await self.saga_repo.get_sagas_by_execution(execution_id, state)
77+
return await self.saga_repo.get_sagas_by_execution(execution_id, state, limit=limit, skip=skip)
7678

7779
async def list_user_sagas(
7880
self, user: User, state: SagaState | None = None, limit: int = 100, skip: int = 0

backend/tests/unit/infrastructure/mappers/test_infra_event_mapper.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def test_archived_export_mapper() -> None:
5757
arch = ArchivedEventMapper.from_event(e, deleted_by="admin", deletion_reason="r")
5858
assert arch.deleted_by == "admin"
5959
arch_doc = ArchivedEventMapper.to_mongo_document(arch)
60-
assert "_deleted_at" in arch_doc or "_deletion_reason" in arch_doc or True # enum names vary
60+
assert "_deleted_at" in arch_doc
61+
assert "_deleted_by" in arch_doc
62+
assert "_deletion_reason" in arch_doc
6163

6264
row = type("Row", (), {})()
6365
row.event_id = e.event_id

0 commit comments

Comments
 (0)