Skip to content

Commit 2295fe0

Browse files
committed
feat: implement cursor paginantion to role & permission
1 parent a16aaaa commit 2295fe0

11 files changed

Lines changed: 427 additions & 33 deletions

File tree

src/core/authorization/domain/service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from abc import ABC, abstractmethod
2+
from datetime import datetime
23
from uuid import UUID
34

5+
from src.core.utils.cursor import CursorDirection
46
from src.modules.authorization.domain.entities.permission import Permission
57
from src.modules.authorization.domain.entities.role import Role
68

@@ -38,6 +40,16 @@ async def get_role(self, role_id: UUID) -> Role | None:
3840
async def list_roles(self) -> list[Role]:
3941
pass
4042

43+
@abstractmethod
44+
async def list_roles_cursor(
45+
self,
46+
cursor_created_at: datetime | None = None,
47+
cursor_id: UUID | None = None,
48+
limit: int = 10,
49+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
50+
) -> tuple[list[Role], bool]:
51+
pass
52+
4153
@abstractmethod
4254
async def create_permission(self, permission: Permission) -> Permission:
4355
pass
@@ -58,6 +70,16 @@ async def get_permission(self, permission_id: UUID) -> Permission | None:
5870
async def list_permissions(self) -> list[Permission]:
5971
pass
6072

73+
@abstractmethod
74+
async def list_permissions_cursor(
75+
self,
76+
cursor_created_at: datetime | None = None,
77+
cursor_id: UUID | None = None,
78+
limit: int = 10,
79+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
80+
) -> tuple[list[Permission], bool]:
81+
pass
82+
6183
@abstractmethod
6284
async def assign_permission_to_role(
6385
self,

src/core/authorization/infrastructure/repositories/casbin_policy_repository.py

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from datetime import datetime
12
from uuid import UUID
23

3-
from sqlalchemy import delete, select
4+
from sqlalchemy import and_, delete, or_, select
45
from sqlalchemy.ext.asyncio import AsyncSession
56

7+
from src.core.utils.cursor import CursorDirection
68
from src.core.authorization.infrastructure.models.casbin_rule_model import (
79
CasbinRuleModel,
810
)
@@ -12,10 +14,10 @@
1214
from src.core.authorization.infrastructure.models.resource_model import (
1315
AuthorizationResourceModel,
1416
)
17+
from src.core.authorization.infrastructure.models.role_model import RoleModel
1518
from src.core.authorization.infrastructure.models.role_permission_model import (
1619
RolePermissionModel,
1720
)
18-
from src.core.authorization.infrastructure.models.role_model import RoleModel
1921
from src.core.authorization.infrastructure.models.user_has_role_model import (
2022
UserHasRoleModel,
2123
)
@@ -122,10 +124,7 @@ async def create_resource(
122124

123125
async def list_resources(self) -> list[AuthorizationResource]:
124126
result = await self._db.execute(select(AuthorizationResourceModel))
125-
return [
126-
self._resource_from_model(model)
127-
for model in result.scalars().all()
128-
]
127+
return [self._resource_from_model(model) for model in result.scalars().all()]
129128

130129
async def create_role(self, role: Role) -> Role:
131130
model = RoleModel(
@@ -138,7 +137,9 @@ async def create_role(self, role: Role) -> Role:
138137
return self._role_from_model(model)
139138

140139
async def get_role(self, role_id: UUID) -> Role | None:
141-
result = await self._db.execute(select(RoleModel).where(RoleModel.id == role_id))
140+
result = await self._db.execute(
141+
select(RoleModel).where(RoleModel.id == role_id)
142+
)
142143
model = result.scalar_one_or_none()
143144
if model is None:
144145
return None
@@ -148,8 +149,36 @@ async def list_roles(self) -> list[Role]:
148149
result = await self._db.execute(select(RoleModel))
149150
return [self._role_from_model(model) for model in result.scalars().all()]
150151

152+
async def list_roles_cursor(
153+
self,
154+
cursor_created_at: datetime | None = None,
155+
cursor_id: UUID | None = None,
156+
limit: int = 10,
157+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
158+
) -> tuple[list[Role], bool]:
159+
query = select(RoleModel)
160+
query = self._apply_cursor_pagination(
161+
query,
162+
RoleModel,
163+
cursor_created_at,
164+
cursor_id,
165+
direction,
166+
).limit(limit + 1)
167+
168+
result = await self._db.execute(query)
169+
models = list(result.scalars().all())
170+
has_more = len(models) > limit
171+
models = models[:limit]
172+
173+
if direction == CursorDirection.DIRECTION_PREV:
174+
models = list(reversed(models))
175+
176+
return [self._role_from_model(model) for model in models], has_more
177+
151178
async def update_role(self, role: Role) -> Role | None:
152-
result = await self._db.execute(select(RoleModel).where(RoleModel.id == role.id))
179+
result = await self._db.execute(
180+
select(RoleModel).where(RoleModel.id == role.id)
181+
)
153182
model = result.scalar_one_or_none()
154183
if model is None:
155184
return None
@@ -209,10 +238,33 @@ async def get_permission(self, permission_id: UUID) -> Permission | None:
209238

210239
async def list_permissions(self) -> list[Permission]:
211240
result = await self._db.execute(select(PermissionModel))
212-
return [
213-
self._permission_from_model(model)
214-
for model in result.scalars().all()
215-
]
241+
return [self._permission_from_model(model) for model in result.scalars().all()]
242+
243+
async def list_permissions_cursor(
244+
self,
245+
cursor_created_at: datetime | None = None,
246+
cursor_id: UUID | None = None,
247+
limit: int = 10,
248+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
249+
) -> tuple[list[Permission], bool]:
250+
query = select(PermissionModel)
251+
query = self._apply_cursor_pagination(
252+
query,
253+
PermissionModel,
254+
cursor_created_at,
255+
cursor_id,
256+
direction,
257+
).limit(limit + 1)
258+
259+
result = await self._db.execute(query)
260+
models = list(result.scalars().all())
261+
has_more = len(models) > limit
262+
models = models[:limit]
263+
264+
if direction == CursorDirection.DIRECTION_PREV:
265+
models = list(reversed(models))
266+
267+
return [self._permission_from_model(model) for model in models], has_more
216268

217269
async def update_permission(self, permission: Permission) -> Permission | None:
218270
result = await self._db.execute(
@@ -287,9 +339,13 @@ async def list_role_permissions(self) -> list[tuple[str, str]]:
287339
result = await self._db.execute(
288340
select(RoleModel.name, PermissionModel.key)
289341
.join(RolePermissionModel, RolePermissionModel.role_id == RoleModel.id)
290-
.join(PermissionModel, PermissionModel.id == RolePermissionModel.permission_id)
342+
.join(
343+
PermissionModel, PermissionModel.id == RolePermissionModel.permission_id
344+
)
291345
)
292-
return [(role_name, permission_key) for role_name, permission_key in result.all()]
346+
return [
347+
(role_name, permission_key) for role_name, permission_key in result.all()
348+
]
293349

294350
async def remove_permission_from_role(
295351
self,
@@ -319,7 +375,9 @@ def _to_policy_tuple(self, rule: CasbinRuleModel) -> tuple[str, ...]:
319375
populated = [value for value in values if value is not None]
320376
return (rule.ptype, *populated)
321377

322-
async def _get_or_create_resource(self, resource_key: str) -> AuthorizationResourceModel:
378+
async def _get_or_create_resource(
379+
self, resource_key: str
380+
) -> AuthorizationResourceModel:
323381
result = await self._db.execute(
324382
select(AuthorizationResourceModel).where(
325383
AuthorizationResourceModel.key == resource_key,
@@ -384,6 +442,8 @@ def _role_from_model(self, model: RoleModel) -> Role:
384442
id=model.id,
385443
name=model.name,
386444
description=model.description,
445+
created_at=model.created_at.isoformat(),
446+
updated_at=model.updated_at.isoformat(),
387447
)
388448

389449
def _resource_from_model(
@@ -404,4 +464,40 @@ def _permission_from_model(self, model: PermissionModel) -> Permission:
404464
resource=model.resource,
405465
action=model.action,
406466
description=model.description,
467+
created_at=model.created_at.isoformat(),
468+
updated_at=model.updated_at.isoformat(),
407469
)
470+
471+
def _apply_cursor_pagination(
472+
self,
473+
query,
474+
model,
475+
cursor_created_at: datetime | None,
476+
cursor_id: UUID | None,
477+
direction: CursorDirection,
478+
):
479+
if cursor_created_at and cursor_id:
480+
if direction == CursorDirection.DIRECTION_NEXT:
481+
query = query.where(
482+
or_(
483+
model.created_at < cursor_created_at,
484+
and_(
485+
model.created_at == cursor_created_at,
486+
model.id < cursor_id,
487+
),
488+
)
489+
)
490+
return query.order_by(model.created_at.desc(), model.id.desc())
491+
492+
query = query.where(
493+
or_(
494+
model.created_at > cursor_created_at,
495+
and_(
496+
model.created_at == cursor_created_at,
497+
model.id > cursor_id,
498+
),
499+
)
500+
)
501+
return query.order_by(model.created_at.asc(), model.id.asc())
502+
503+
return query.order_by(model.created_at.desc(), model.id.desc())

src/core/authorization/infrastructure/services/casbin_authorization_service.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from datetime import datetime
12
from uuid import UUID
23

34
from src.core.authorization.domain.service import AuthorizationService
45
from src.core.authorization.infrastructure.repositories.casbin_policy_repository import (
56
SQLAlchemyCasbinPolicyRepository,
67
)
78
from src.core.authorization.permissions import permission_key
9+
from src.core.utils.cursor import CursorDirection
810
from src.modules.authorization.domain.entities.permission import Permission
911
from src.modules.authorization.domain.entities.role import Role
1012

@@ -55,6 +57,20 @@ async def get_role(self, role_id: UUID) -> Role | None:
5557
async def list_roles(self) -> list[Role]:
5658
return await self._policy_repository.list_roles()
5759

60+
async def list_roles_cursor(
61+
self,
62+
cursor_created_at: datetime | None = None,
63+
cursor_id: UUID | None = None,
64+
limit: int = 10,
65+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
66+
) -> tuple[list[Role], bool]:
67+
return await self._policy_repository.list_roles_cursor(
68+
cursor_created_at=cursor_created_at,
69+
cursor_id=cursor_id,
70+
limit=limit,
71+
direction=direction,
72+
)
73+
5874
async def create_permission(self, permission: Permission) -> Permission:
5975
return await self._policy_repository.create_permission(permission)
6076

@@ -70,6 +86,20 @@ async def get_permission(self, permission_id: UUID) -> Permission | None:
7086
async def list_permissions(self) -> list[Permission]:
7187
return await self._policy_repository.list_permissions()
7288

89+
async def list_permissions_cursor(
90+
self,
91+
cursor_created_at: datetime | None = None,
92+
cursor_id: UUID | None = None,
93+
limit: int = 10,
94+
direction: CursorDirection = CursorDirection.DIRECTION_NEXT,
95+
) -> tuple[list[Permission], bool]:
96+
return await self._policy_repository.list_permissions_cursor(
97+
cursor_created_at=cursor_created_at,
98+
cursor_id=cursor_id,
99+
limit=limit,
100+
direction=direction,
101+
)
102+
73103
async def assign_permission_to_role(
74104
self,
75105
role_id: UUID,

src/modules/authorization/domain/entities/permission.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ class Permission:
99
resource: str
1010
action: str
1111
description: str | None = None
12+
created_at: str | None = None
13+
updated_at: str | None = None
1214

1315
@classmethod
1416
def create(
15-
cls, key: str, resource: str, action: str, description: str | None
17+
cls,
18+
key: str,
19+
resource: str,
20+
action: str,
21+
description: str | None,
1622
) -> "Permission":
1723
return cls(
1824
id=uuid4(),

src/modules/authorization/domain/entities/role.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
class Role:
77
id: UUID
88
name: str
9-
description: str | None
9+
description: str | None = None
10+
created_at: str | None = None
11+
updated_at: str | None = None
1012

1113
@classmethod
1214
def create(cls, name: str, description: str | None = None) -> "Role":

0 commit comments

Comments
 (0)