diff --git a/autotests/clients/rest/models.py b/autotests/clients/rest/models.py index 14ff3f28..5af69a39 100644 --- a/autotests/clients/rest/models.py +++ b/autotests/clients/rest/models.py @@ -1,9 +1,7 @@ -from pydantic import BaseModel, NonNegativeInt, PositiveInt +from pydantic import BaseModel, NaiveDatetime, PositiveInt class PaginatedResponse(BaseModel): data: list - page: PositiveInt + next_cursor: NaiveDatetime | None per_page: PositiveInt - total_pages: NonNegativeInt | None - total_items: NonNegativeInt | None diff --git a/autotests/clients/rest/projects/client.py b/autotests/clients/rest/projects/client.py index c0d079ee..de5131d1 100644 --- a/autotests/clients/rest/projects/client.py +++ b/autotests/clients/rest/projects/client.py @@ -47,7 +47,7 @@ async def get_projects( position_skill_ids: list[uuid.UUID] | Type[Empty] = Empty, position_specialization_ids: list[uuid.UUID] | Type[Empty] = Empty, participant_user_ids: list[uuid.UUID] | Type[Empty] = Empty, - page: int = 1, + cursor: datetime | Type[Empty] = Empty, per_page: int = 10, ) -> ProjectListResponse: path = "/api/rest/projects/" @@ -63,7 +63,7 @@ async def get_projects( "position_skill_ids": position_skill_ids, "position_specialization_ids": position_specialization_ids, "participant_user_ids": participant_user_ids, - "page": page, + "cursor": cursor, "per_page": per_page, } params = {key: value for key, value in params.items() if value is not Empty} @@ -146,7 +146,7 @@ async def get_positions( project_deadline_ge: datetime | Type[Empty] = Empty, project_deadline_le: datetime | Type[Empty] = Empty, project_status: ProjectStatusEnum | Type[Empty] = Empty, - page: int = 1, + cursor: datetime | Type[Empty] = Empty, per_page: int = 10, ) -> PositionListResponse: path = f"/api/rest/positions/" @@ -161,7 +161,7 @@ async def get_positions( "project_deadline_ge": project_deadline_ge, "project_deadline_le": project_deadline_le, "project_status": Empty if project_status is Empty else project_status.value, - "page": page, + "cursor": cursor, "per_page": per_page, } params = {key: value for key, value in params.items() if value is not Empty} diff --git a/sapphire/common/api/dependencies/pagination.py b/sapphire/common/api/dependencies/pagination.py index 39c09767..5f6a9573 100644 --- a/sapphire/common/api/dependencies/pagination.py +++ b/sapphire/common/api/dependencies/pagination.py @@ -1,15 +1,20 @@ +from datetime import datetime +from typing import Type + from fastapi import Query from pydantic import BaseModel, conint +from sapphire.common.utils.empty import Empty + class Pagination(BaseModel): - page: int + cursor: datetime | Type[Empty] = Empty per_page: int async def pagination( - page: conint(ge=1) = Query(1, description="Page number"), + cursor: datetime | None = Query(None, description="Cursor"), per_page: conint(ge=1) = Query(10, description="Number of items per page"), ) -> Pagination: - return Pagination(page=page, per_page=per_page) + return Pagination(cursor=cursor if cursor else Empty, per_page=per_page) diff --git a/sapphire/common/api/schemas/paginated.py b/sapphire/common/api/schemas/paginated.py index a19be1b8..ef4f44e4 100644 --- a/sapphire/common/api/schemas/paginated.py +++ b/sapphire/common/api/schemas/paginated.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any from pydantic import BaseModel @@ -5,7 +6,5 @@ class PaginatedResponse(BaseModel): data: list[Any] - page: int + next_cursor: datetime | None per_page: int - total_pages: int | None = None - total_items: int | None = None diff --git a/sapphire/messenger/api/rest/chats/handlers.py b/sapphire/messenger/api/rest/chats/handlers.py index 33603c47..aa5bcf43 100644 --- a/sapphire/messenger/api/rest/chats/handlers.py +++ b/sapphire/messenger/api/rest/chats/handlers.py @@ -23,10 +23,17 @@ async def get_chats( session=session, user_id=jwt_data.user_id, members=filters.member, + cursor=pagination.cursor, + per_page=pagination.per_page, ) + next_cursor = None + if db_chats: + next_cursor = db_chats[-1].created_at + chats = [ChatResponse.from_db_model(chat) for chat in db_chats] - return ChatListResponse(data=chats, page=pagination.page, per_page=pagination.per_page) + + return ChatListResponse(data=chats, next_cursor=next_cursor, per_page=pagination.per_page) async def get_chat(chat: Chat = fastapi.Depends(path_chat_is_member)): diff --git a/sapphire/messenger/api/rest/chats/messages/handlers.py b/sapphire/messenger/api/rest/chats/messages/handlers.py index 81c15108..d8fa69a9 100644 --- a/sapphire/messenger/api/rest/chats/messages/handlers.py +++ b/sapphire/messenger/api/rest/chats/messages/handlers.py @@ -20,10 +20,22 @@ async def get_messages( database_service: MessengerDatabaseService = request.app.service.database async with database_service.transaction() as session: - db_messages = await database_service.get_chat_messages(session=session, chat_id=chat.id) + db_messages = await database_service.get_chat_messages( + session=session, + chat_id=chat.id, + cursor=pagination.cursor, + per_page=pagination.per_page, + ) + + next_cursor = None + if db_messages: + next_cursor = db_messages[-1].created_at messages = [MessageResponse.model_validate(db_message) for db_message in db_messages] - return MessageListResponse(data=messages, page=pagination.page, per_page=pagination.per_page) + + return MessageListResponse( + data=messages, next_cursor=next_cursor, per_page=pagination.per_page + ) async def create_message( diff --git a/sapphire/messenger/database/service.py b/sapphire/messenger/database/service.py index b48f8e25..ee75757e 100644 --- a/sapphire/messenger/database/service.py +++ b/sapphire/messenger/database/service.py @@ -1,8 +1,9 @@ +import datetime import pathlib import uuid from typing import Type -from sqlalchemy import or_, select +from sqlalchemy import desc, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sapphire.common.database.service import BaseDatabaseService @@ -27,13 +28,21 @@ async def get_chats( session: AsyncSession, user_id: uuid.UUID, members: set[uuid.UUID] | Type[Empty] = Empty, + cursor: datetime.datetime | Type[Empty] = Empty, + per_page: int | Type[Empty] = Empty, ) -> list[Chat]: query = select(Chat).where(Member.user_id == user_id, Member.chat_id == Chat.id) filters = [] if members is not Empty and len(members) > 0: filters.append(or_(*(Member.user_id == member for member in members))) - query = query.where(*filters) + + if per_page is not Empty: + query = query.limit(per_page) + if cursor is not Empty: + filters.append(Chat.created_at < cursor) + + query = query.where(*filters).order_by(desc(Chat.created_at)) result = await session.execute(query) @@ -61,10 +70,22 @@ async def get_chat(self, session: AsyncSession, chat_id: uuid.UUID) -> Chat | No return result.unique().scalar_one_or_none() - async def get_chat_messages(self, session: AsyncSession, chat_id: uuid.UUID) -> list[Message]: + async def get_chat_messages( + self, + session: AsyncSession, + chat_id: uuid.UUID, + cursor: datetime.datetime | Type[Empty] = Empty, + per_page: int | Type[Empty] = Empty, + ) -> list[Message]: query = ( select(Message).where(Message.chat_id == chat_id).order_by(Message.created_at.desc()) ) + + if cursor is not Empty: + query = query.where(Message.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) + result = await session.execute(query) return list(result.unique().scalars().all()) diff --git a/sapphire/projects/api/rest/positions/handlers.py b/sapphire/projects/api/rest/positions/handlers.py index 88eeedff..f3bcd121 100644 --- a/sapphire/projects/api/rest/positions/handlers.py +++ b/sapphire/projects/api/rest/positions/handlers.py @@ -1,5 +1,3 @@ -import math - import fastapi from sapphire.common.api.dependencies.pagination import Pagination, pagination @@ -26,23 +24,21 @@ async def get_positions( database_service: ProjectsDatabaseService = request.app.service.database async with database_service.transaction() as session: - positions = await database_service.get_positions( + positions_db = await database_service.get_positions( session=session, + cursor=pagination.cursor, + per_page=pagination.per_page, **filters.model_dump(), ) - total_items = len(positions) - total_pages = int(math.ceil(total_items / pagination.per_page)) - offset = (pagination.page - 1) * pagination.per_page - positions = positions[offset:offset + pagination.per_page] - data = [PositionResponse.from_db_model(position) for position in positions] + next_cursor = None + if positions_db: + next_cursor = positions_db[-1].created_at + + positions = [PositionResponse.model_validate(position) for position in positions_db] return PositionListResponse( - data=data, - total_items=total_items, - total_pages=total_pages, - page=pagination.page, - per_page=pagination.per_page, + data=positions, next_cursor=next_cursor, per_page=pagination.per_page ) diff --git a/sapphire/projects/api/rest/projects/handlers.py b/sapphire/projects/api/rest/projects/handlers.py index 921f2e6e..c15fb040 100644 --- a/sapphire/projects/api/rest/projects/handlers.py +++ b/sapphire/projects/api/rest/projects/handlers.py @@ -1,4 +1,3 @@ -import math import pathlib import aiofiles @@ -57,14 +56,20 @@ async def get_projects( async with database_service.transaction() as session: projects_db = await database_service.get_projects( session=session, - page=pagination.page, + cursor=pagination.cursor, per_page=pagination.per_page, **filters.model_dump(), ) + next_cursor = None + if projects_db: + next_cursor = projects_db[-1].created_at + projects = [ProjectResponse.model_validate(project_db) for project_db in projects_db] - return ProjectListResponse(data=projects, page=pagination.page, per_page=pagination.per_page) + return ProjectListResponse( + data=projects, next_cursor=next_cursor, per_page=pagination.per_page + ) async def get_project( @@ -74,22 +79,28 @@ async def get_project( async def history( + request: fastapi.Request, project: Project = fastapi.Depends(get_path_project), pagination: Pagination = fastapi.Depends(pagination), ) -> ProjectHistoryListResponse: - offset = (pagination.page - 1) * pagination.per_page - history = [ - ProjectHistoryResponse.model_validate(event) - for event in project.history[offset : offset + pagination.per_page] - ] - total_items = len(project.history) - total_pages = int(math.ceil(total_items / pagination.per_page)) + database_service: ProjectsDatabaseService = request.app.service.database + + async with database_service.transaction() as session: + project_history_db = await database_service.get_project_history( + session=session, + project_id=project.id, + cursor=pagination.cursor, + per_page=pagination.per_page, + ) + + next_cursor = None + if project_history_db: + next_cursor = project_history_db[-1].created_at + + history = [ProjectHistoryResponse.model_validate(event) for event in project_history_db] + return ProjectHistoryListResponse( - data=history, - page=pagination.page, - per_page=pagination.per_page, - total_pages=total_pages, - total_items=total_items, + data=history, next_cursor=next_cursor, per_page=pagination.per_page ) diff --git a/sapphire/projects/database/service.py b/sapphire/projects/database/service.py index 8973876d..a1725825 100644 --- a/sapphire/projects/database/service.py +++ b/sapphire/projects/database/service.py @@ -148,7 +148,11 @@ async def get_positions( project_deadline_ge: datetime | Type[Empty] = Empty, project_deadline_le: datetime | Type[Empty] = Empty, project_status: ProjectStatusEnum | Type[Empty] = Empty, + cursor: datetime | Type[Empty] = Empty, + per_page: int | Type[Empty] = Empty, ) -> list[Position]: + statement = select(Position).order_by(Position.created_at.desc()) + filters = [] skill_filters = [] project_filters = [] @@ -201,7 +205,12 @@ async def get_positions( Position.project_id.in_(select(Project.id).where(*project_filters)) ) - statement = select(Position).where(*filters) + if cursor is not Empty: + filters.append(Position.created_at < cursor) + if per_page is not Empty: + statement = statement.limit(per_page) + + statement = statement.where(*filters) result = await session.execute(statement) return list(result.unique().scalars().all()) @@ -337,7 +346,7 @@ async def get_projects( position_skill_ids: list[uuid.UUID] | Type[Empty] = Empty, position_specialization_ids: list[uuid.UUID] | Type[Empty] = Empty, participant_user_ids: list[uuid.UUID] | Type[Empty] = Empty, - page: int | Type[Empty] = Empty, + cursor: datetime | Type[Empty] = Empty, per_page: int | Type[Empty] = Empty, ) -> list[Project]: filters = [] @@ -405,16 +414,38 @@ async def get_projects( Project.id.in_(select(Position.project_id).where(*position_filters)) ) - query = query.where(*filters) + if cursor is not Empty: + filters.append(Project.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) - if page is not Empty and per_page is not Empty: - offset = (page - 1) * per_page - query = query.limit(per_page).offset(offset) + query = query.where(*filters) result = await session.execute(query) return list(result.unique().scalars().all()) + async def get_project_history( + self, + session: AsyncSession, + project_id: uuid.UUID, + cursor: datetime | Type[Empty] = Empty, + per_page: int | Type[Empty] = Empty, + ) -> list[Project]: + query = ( + select(ProjectHistory) + .where(ProjectHistory.project_id == project_id) + .order_by(ProjectHistory.created_at.desc()) + ) + + if cursor is not Empty: + query = query.where(ProjectHistory.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) + + result = await session.execute(query) + return list(result.unique().scalars().all()) + async def get_user_statistic(self, session: AsyncSession, user_id: uuid.UUID) -> UserStatistic: stmt = select( func.count(Project.id), # pylint: disable=not-callable diff --git a/sapphire/storage/api/rest/dependencies.py b/sapphire/storage/api/rest/dependencies.py deleted file mode 100644 index 60041563..00000000 --- a/sapphire/storage/api/rest/dependencies.py +++ /dev/null @@ -1,14 +0,0 @@ -import fastapi -from pydantic import BaseModel, PositiveInt - - -class Pagination(BaseModel): - page: PositiveInt = 1 - per_page: PositiveInt = 100 - - -async def pagination( - page: PositiveInt = fastapi.Query(1, description="Page number"), - per_page: PositiveInt = fastapi.Query(100, description="Number of items per page"), -) -> Pagination: - return Pagination(page=page, per_page=per_page) diff --git a/sapphire/storage/api/rest/skills/handlers.py b/sapphire/storage/api/rest/skills/handlers.py index 80510e88..06996a05 100644 --- a/sapphire/storage/api/rest/skills/handlers.py +++ b/sapphire/storage/api/rest/skills/handlers.py @@ -1,6 +1,6 @@ import fastapi -from sapphire.storage.api.rest.dependencies import Pagination, pagination +from sapphire.common.api.dependencies.pagination import Pagination, pagination from sapphire.storage.api.rest.skills.schemas import ( SkillListResponse, SkillResponse, @@ -15,22 +15,20 @@ async def get_skills( filters: SkillsFiltersRequest = fastapi.Depends(SkillsFiltersRequest), ) -> SkillListResponse: database_service: StorageDatabaseService = request.app.service.database - page = pagination.page - per_page = pagination.per_page async with database_service.transaction() as session: paginated_skills = await database_service.get_skills( session=session, query_text=filters.query_text, skill_ids=filters.id, - page=page, - per_page=per_page, + cursor=pagination.cursor, + per_page=pagination.per_page, ) + next_cursor = None + if paginated_skills: + next_cursor = paginated_skills[-1].created_at + skills = [SkillResponse.model_validate(s) for s in paginated_skills] - return SkillListResponse( - data=skills, - page=page, - per_page=per_page, - ) + return SkillListResponse(data=skills, next_cursor=next_cursor, per_page=pagination.per_page) diff --git a/sapphire/storage/api/rest/specialization_groups/handlers.py b/sapphire/storage/api/rest/specialization_groups/handlers.py index 782d9cb4..e962389c 100644 --- a/sapphire/storage/api/rest/specialization_groups/handlers.py +++ b/sapphire/storage/api/rest/specialization_groups/handlers.py @@ -1,6 +1,6 @@ import fastapi -from sapphire.common.api.dependencies.pagination import pagination +from sapphire.common.api.dependencies.pagination import Pagination, pagination from sapphire.storage.api.schemas.specializations import SpecializationGroupResponse from sapphire.storage.database.service import StorageDatabaseService @@ -9,25 +9,28 @@ async def get_specialization_groups( request: fastapi.Request, - pagination: dict = fastapi.Depends(pagination), + pagination: Pagination = fastapi.Depends(pagination), filters: SpecializationGroupsFilterRequest = fastapi.Depends(SpecializationGroupsFilterRequest), ) -> SpecializationGroupListResponse: database_service: StorageDatabaseService = request.app.service.database - page = pagination.page - per_page = pagination.per_page async with database_service.transaction() as session: paginated_specialization_groups = await database_service.get_specialization_groups( - session=session, page=page, per_page=per_page, **filters.model_dump() + session=session, + cursor=pagination.cursor, + per_page=pagination.per_page, + **filters.model_dump(), ) + next_cursor = None + if paginated_specialization_groups: + next_cursor = paginated_specialization_groups[-1].created_at + specialization_groups = [ SpecializationGroupResponse.model_validate(s) for s in paginated_specialization_groups ] return SpecializationGroupListResponse( - data=specialization_groups, - page=page, - per_page=per_page, + data=specialization_groups, next_cursor=next_cursor, per_page=pagination.per_page ) diff --git a/sapphire/storage/api/rest/specializations/handlers.py b/sapphire/storage/api/rest/specializations/handlers.py index 1c889163..6e916d51 100644 --- a/sapphire/storage/api/rest/specializations/handlers.py +++ b/sapphire/storage/api/rest/specializations/handlers.py @@ -1,6 +1,6 @@ import fastapi -from sapphire.common.api.dependencies.pagination import pagination +from sapphire.common.api.dependencies.pagination import Pagination, pagination from sapphire.storage.api.rest.specializations.schemas import ( SpecializationFiltersRequest, SpecializationListResponse, @@ -11,25 +11,28 @@ async def get_specializations( request: fastapi.Request, - pagination: dict = fastapi.Depends(pagination), + pagination: Pagination = fastapi.Depends(pagination), filters: SpecializationFiltersRequest = fastapi.Depends(SpecializationFiltersRequest), ) -> SpecializationListResponse: database_service: StorageDatabaseService = request.app.service.database - page = pagination.page - per_page = pagination.per_page async with database_service.transaction() as session: paginated_specializations = await database_service.get_specializations( - session=session, page=page, per_page=per_page, **filters.model_dump(), + session=session, + cursor=pagination.cursor, + per_page=pagination.per_page, + **filters.model_dump(), ) + next_cursor = None + if paginated_specializations: + next_cursor = paginated_specializations[-1].created_at + specializations = [ SpecializationResponse.model_validate(s) for s in paginated_specializations ] return SpecializationListResponse( - data=specializations, - page=page, - per_page=per_page, + data=specializations, next_cursor=next_cursor, per_page=pagination.per_page ) diff --git a/sapphire/storage/database/service.py b/sapphire/storage/database/service.py index f8c76958..a6df7883 100644 --- a/sapphire/storage/database/service.py +++ b/sapphire/storage/database/service.py @@ -1,5 +1,6 @@ import pathlib import uuid +from datetime import datetime from typing import Type from sqlalchemy import desc, or_, select @@ -25,7 +26,7 @@ async def get_specializations( self, session: AsyncSession, query_text: str | Type[Empty] = Empty, - page: int | Type[Empty] = Empty, + cursor: datetime | Type[Empty] = Empty, per_page: int | Type[Empty] = Empty, group_id: uuid.UUID | Type[Empty] = Empty, ) -> list[Specialization]: @@ -38,11 +39,13 @@ async def get_specializations( if group_id is not Empty: filters.append(Specialization.group_id == group_id) - query = query.where(*filters) + if cursor is not Empty: + filters.append(Skill.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) + - if page is not None and per_page is not None: - offset = (page - 1) * per_page - query = query.limit(per_page).offset(offset) + query = query.where(*filters) specializations = await session.execute(query) @@ -63,7 +66,7 @@ async def get_specialization_groups( self, session: AsyncSession, query_text: str | Type[Empty] = Empty, - page: int | Type[Empty] = Empty, + cursor: datetime | Type[Empty] = Empty, per_page: int | Type[Empty] = Empty, ) -> list[SpecializationGroup]: query = select(SpecializationGroup).order_by(desc(SpecializationGroup.created_at)) @@ -75,11 +78,12 @@ async def get_specialization_groups( SpecializationGroup.name_en.contains(query_text), )) - query = query.where(*filters) + if cursor is not Empty: + filters.append(SpecializationGroup.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) - if page is not Empty and per_page is not Empty: - offset = (page - 1) * per_page - query = query.limit(per_page).offset(offset) + query = query.where(*filters) specialization_groups = await session.execute(query) @@ -101,7 +105,7 @@ async def get_skills( session: AsyncSession, query_text: str | Type[Empty] = Empty, skill_ids: list[uuid.UUID] | Type[Empty] = Empty, - page: int | Type[Empty] = Empty, + cursor: datetime | Type[Empty] = Empty, per_page: int | Type[Empty] = Empty, ) -> list[Skill]: query = select(Skill).order_by(desc(Skill.created_at)) @@ -112,13 +116,14 @@ async def get_skills( if skill_ids is not Empty: filters.append(or_(*(Skill.id == id_ for id_ in skill_ids))) + if cursor is not Empty: + filters.append(Skill.created_at < cursor) + if per_page is not Empty: + query = query.limit(per_page) + query = query.where(*filters) skills = await session.execute(query) - if page is not Empty and per_page is not Empty: - offset = (page - 1) * per_page - query = query.limit(per_page).offset(offset) - return list(skills.unique().scalars().all()) async def get_skill(self, session: AsyncSession, habr_id: int) -> Skill | None: diff --git a/tests/projects/database/test_projects_database_service.py b/tests/projects/database/test_projects_database_service.py index 968894c6..36e61145 100644 --- a/tests/projects/database/test_projects_database_service.py +++ b/tests/projects/database/test_projects_database_service.py @@ -101,20 +101,19 @@ async def test_get_projects_with_pagination(database_service: ProjectsDatabaseSe result = MagicMock() project_id = uuid.uuid4() expected_projects = [Project(id=project_id, name="test", owner_id=uuid.uuid4())] - page = 1 + cursor = datetime.now() per_page = 10 - offset = (page - 1) * per_page expected_query = ( select(Project) + .where(Project.created_at < cursor) .order_by(desc(Project.created_at)) .limit(per_page) - .offset(offset) ) result.unique.return_value.scalars.return_value.all.return_value = expected_projects session.execute = AsyncMock() session.execute.return_value = result - projects = await database_service.get_projects(session=session, page=page, per_page=per_page) + projects = await database_service.get_projects(session=session, cursor=cursor, per_page=per_page) assert projects == expected_projects @@ -218,7 +217,11 @@ async def test_get_project_positions(database_service: ProjectsDatabaseService): expected_positions = [ Position(id=uuid.uuid4(), specialization_id=specialization_id, project_id=project_id) ] - expected_query = select(Position).where(Position.project_id == project_id) + expected_query = ( + select(Position) + .where(Position.project_id == project_id) + .order_by(Position.created_at.desc()) + ) result.unique().scalars.return_value.all.return_value = expected_positions session.execute = AsyncMock() session.execute.return_value = result diff --git a/tests/storage/database/test_storage_database_service.py b/tests/storage/database/test_storage_database_service.py index c696c00d..8f11969b 100644 --- a/tests/storage/database/test_storage_database_service.py +++ b/tests/storage/database/test_storage_database_service.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest.mock import AsyncMock, MagicMock import pytest @@ -36,9 +37,8 @@ async def test_get_specialization_groups_with_all_filters( ): session = MagicMock() name = "Developer" - page = 1 + cursor = datetime.now() per_page = 10 - offset = (page - 1) * per_page expected_specialization_groups = [SpecializationGroup(name=name)] mock_specialization_group = MagicMock() @@ -51,18 +51,20 @@ async def test_get_specialization_groups_with_all_filters( expected_query = ( select(SpecializationGroup) .order_by(desc(SpecializationGroup.created_at)) - .where(or_( - SpecializationGroup.name.contains(name), - SpecializationGroup.name_en.contains(name), - )) + .where( + or_( + SpecializationGroup.name.contains(name), + SpecializationGroup.name_en.contains(name), + ), + SpecializationGroup.created_at < cursor, + ) .limit(per_page) - .offset(offset) ) specialization_groups = await database_service.get_specialization_groups( session=session, query_text=name, - page=page, + cursor=cursor, per_page=per_page, )