Skip to content

Commit 9bbf395

Browse files
committed
feat: implement uow
1 parent 8732889 commit 9bbf395

21 files changed

Lines changed: 290 additions & 53 deletions

File tree

src/core/database/session.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
from collections.abc import AsyncGenerator
2+
3+
from fastapi import Depends
14
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
25

36
from src.core.config.setting import settings
7+
from src.shared.database.unit_of_work import SQLAlchemyUnitOfWork
8+
from src.shared.unit_of_work import UnitOfWork
49

510
engine = create_async_engine(settings.DATABASE_URL, echo=False, future=True)
611
AsyncSessionLocal = async_sessionmaker(
712
engine, class_=AsyncSession, expire_on_commit=False, autoflush=False
813
)
914

1015

11-
async def get_db() -> AsyncSession:
16+
async def get_db() -> AsyncGenerator[AsyncSession]:
1217
async with AsyncSessionLocal() as session:
1318
yield session
19+
20+
21+
def get_unit_of_work(db: AsyncSession = Depends(get_db)) -> UnitOfWork:
22+
return SQLAlchemyUnitOfWork(db)

src/modules/todo/application/create_todo/handler.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
from src.modules.todo.application.create_todo.command import CreateTodoCommand
44
from src.modules.todo.domain.entities.todo import Todo
55
from src.modules.todo.domain.repositories.todo_repository import TodoRepository
6+
from src.shared.unit_of_work import UnitOfWork
67

78

89
class CreateTodoHandler:
9-
def __init__(self, todo_repo: TodoRepository):
10+
def __init__(self, todo_repo: TodoRepository, unit_of_work: UnitOfWork):
1011
self.todo_repo = todo_repo
12+
self._unit_of_work = unit_of_work
1113

1214
async def execute(self, command: CreateTodoCommand, user_id: UUID) -> Todo:
1315
todo = Todo.create(
1416
title=command.title, user_id=user_id, description=command.description
1517
)
16-
return await self.todo_repo.save(todo)
18+
try:
19+
saved_todo = await self.todo_repo.save(todo)
20+
await self._unit_of_work.commit()
21+
return saved_todo
22+
except Exception:
23+
await self._unit_of_work.rollback()
24+
raise

src/modules/todo/application/delete_todo/handler.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
UnauthorizedTodoAccessError,
66
)
77
from src.modules.todo.domain.repositories.todo_repository import TodoRepository
8+
from src.shared.unit_of_work import UnitOfWork
89

910

1011
class DeleteTodoHandler:
11-
def __init__(self, todo_repo: TodoRepository):
12+
def __init__(self, todo_repo: TodoRepository, unit_of_work: UnitOfWork):
1213
self.todo_repo = todo_repo
14+
self._unit_of_work = unit_of_work
1315

1416
async def execute(self, todo_id: UUID, user_id: UUID) -> None:
1517
todo = await self.todo_repo.get_by_id(todo_id)
@@ -20,4 +22,9 @@ async def execute(self, todo_id: UUID, user_id: UUID) -> None:
2022
"You do not have permission to delete this todo"
2123
)
2224

23-
await self.todo_repo.delete(todo_id)
25+
try:
26+
await self.todo_repo.delete(todo_id)
27+
await self._unit_of_work.commit()
28+
except Exception:
29+
await self._unit_of_work.rollback()
30+
raise

src/modules/todo/application/update_todo/handler.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
UnauthorizedTodoAccessError,
88
)
99
from src.modules.todo.domain.repositories.todo_repository import TodoRepository
10+
from src.shared.unit_of_work import UnitOfWork
1011

1112

1213
class UpdateTodoHandler:
13-
def __init__(self, todo_repo: TodoRepository):
14+
def __init__(self, todo_repo: TodoRepository, unit_of_work: UnitOfWork):
1415
self.todo_repo = todo_repo
16+
self._unit_of_work = unit_of_work
1517

1618
async def execute(
1719
self, todo_id: UUID, command: UpdateTodoCommand, user_id: UUID
@@ -38,4 +40,10 @@ async def execute(
3840
else:
3941
todo.is_completed = False
4042

41-
return await self.todo_repo.save(todo)
43+
try:
44+
saved_todo = await self.todo_repo.save(todo)
45+
await self._unit_of_work.commit()
46+
return saved_todo
47+
except Exception:
48+
await self._unit_of_work.rollback()
49+
raise

src/modules/todo/infrastructure/repositories/todo_repository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async def save(self, todo: Todo) -> Todo:
5050
user_id=todo.user_id,
5151
)
5252
model = await self.db.merge(model)
53-
await self.db.commit()
53+
await self.db.flush()
5454
await self.db.refresh(model)
5555
return Todo(
5656
id=model.id,
@@ -62,4 +62,4 @@ async def save(self, todo: Todo) -> Todo:
6262

6363
async def delete(self, todo_id: UUID) -> None:
6464
await self.db.execute(delete(TodoModel).where(TodoModel.id == todo_id))
65-
await self.db.commit()
65+
await self.db.flush()

src/modules/todo/presentation/dependency.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import Depends
22
from sqlalchemy.ext.asyncio import AsyncSession
33

4+
from src.core.database.session import get_db, get_unit_of_work
45
from src.modules.todo.application.delete_todo.handler import DeleteTodoHandler
56
from src.modules.todo.application.create_todo.handler import CreateTodoHandler
67
from src.modules.todo.application.list_todo.handler import GetTodosQueryHandler
@@ -9,7 +10,7 @@
910
from src.modules.todo.infrastructure.repositories.todo_repository import (
1011
SQLAlchemyTodoRepository,
1112
)
12-
from src.core.database.session import get_db
13+
from src.shared.unit_of_work import UnitOfWork
1314

1415

1516
def get_todo_repository(db: AsyncSession = Depends(get_db)) -> TodoRepository:
@@ -18,20 +19,23 @@ def get_todo_repository(db: AsyncSession = Depends(get_db)) -> TodoRepository:
1819

1920
def get_create_todo_handler(
2021
repo: TodoRepository = Depends(get_todo_repository),
22+
unit_of_work: UnitOfWork = Depends(get_unit_of_work),
2123
) -> CreateTodoHandler:
22-
return CreateTodoHandler(repo)
24+
return CreateTodoHandler(repo, unit_of_work)
2325

2426

2527
def get_update_todo_handler(
2628
repo: TodoRepository = Depends(get_todo_repository),
29+
unit_of_work: UnitOfWork = Depends(get_unit_of_work),
2730
) -> UpdateTodoHandler:
28-
return UpdateTodoHandler(repo)
31+
return UpdateTodoHandler(repo, unit_of_work)
2932

3033

3134
def get_delete_todo_handler(
3235
repo: TodoRepository = Depends(get_todo_repository),
36+
unit_of_work: UnitOfWork = Depends(get_unit_of_work),
3337
) -> DeleteTodoHandler:
34-
return DeleteTodoHandler(repo)
38+
return DeleteTodoHandler(repo, unit_of_work)
3539

3640

3741
def get_get_todos_query_handler(

src/modules/user/application/login_user/handler.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@
1212
)
1313
from src.modules.user.domain.repositories.user_repository import UserRepository
1414
from src.shared.exceptions.credential_exception import InvalidCredentialsError
15+
from src.shared.unit_of_work import UnitOfWork
1516

1617

1718
class LoginUserCommandHandler:
1819
def __init__(
1920
self,
2021
user_repository: UserRepository,
2122
refresh_token_repository: RefreshTokenRepository,
23+
unit_of_work: UnitOfWork,
2224
):
2325
self._user_repository = user_repository
2426
self._refresh_token_repository = refresh_token_repository
27+
self._unit_of_work = unit_of_work
2528

2629
async def execute(self, command: LoginUserCommand) -> dict[str, str]:
2730
user = await self._user_repository.get_by_email(command.username)
@@ -41,10 +44,15 @@ async def execute(self, command: LoginUserCommand) -> dict[str, str]:
4144
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES
4245
)
4346

44-
new_rt = RefreshToken.create(
45-
user_id=user.id, token_hash=token_hash, expires_at=expires_at
46-
)
47-
await self._refresh_token_repository.save(new_rt)
47+
try:
48+
new_rt = RefreshToken.create(
49+
user_id=user.id, token_hash=token_hash, expires_at=expires_at
50+
)
51+
await self._refresh_token_repository.save(new_rt)
52+
await self._unit_of_work.commit()
53+
except Exception:
54+
await self._unit_of_work.rollback()
55+
raise
4856

4957
return {
5058
"access_token": access_token,

src/modules/user/application/refresh_token/handler.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
RefreshTokenRepository,
1010
)
1111
from src.shared.exceptions.credential_exception import InvalidRefreshTokenError
12+
from src.shared.unit_of_work import UnitOfWork
1213

1314

1415
class RefreshTokenCommandHandler:
15-
def __init__(self, refresh_token_repo: RefreshTokenRepository):
16+
def __init__(
17+
self,
18+
refresh_token_repo: RefreshTokenRepository,
19+
unit_of_work: UnitOfWork,
20+
):
1621
self._refresh_token_repo = refresh_token_repo
22+
self._unit_of_work = unit_of_work
1723

1824
def _hash_token(self, token: str) -> str:
1925
return hashlib.sha256(token.encode()).hexdigest()
@@ -29,30 +35,35 @@ async def execute(self, command: RefreshTokenCommand) -> dict:
2935
if stored_token.expires_at < datetime.now(timezone.utc):
3036
raise InvalidRefreshTokenError("Refresh token has expired")
3137

32-
stored_token.revoke()
33-
await self._refresh_token_repo.save(stored_token)
38+
try:
39+
stored_token.revoke()
40+
await self._refresh_token_repo.save(stored_token)
3441

35-
new_access_token = JWTService.create_access_token(
36-
data={"sub": str(stored_token.user_id)}
37-
)
42+
new_access_token = JWTService.create_access_token(
43+
data={"sub": str(stored_token.user_id)}
44+
)
3845

39-
new_refresh_token_str = JWTService.create_refresh_token(
40-
data={"sub": str(stored_token.user_id)}
41-
)
46+
new_refresh_token_str = JWTService.create_refresh_token(
47+
data={"sub": str(stored_token.user_id)}
48+
)
4249

43-
new_token_hash = self._hash_token(new_refresh_token_str)
50+
new_token_hash = self._hash_token(new_refresh_token_str)
4451

45-
expires_at = datetime.now(timezone.utc) + timedelta(
46-
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES,
47-
)
52+
expires_at = datetime.now(timezone.utc) + timedelta(
53+
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES,
54+
)
4855

49-
new_refresh_token_entity = RefreshToken.create(
50-
user_id=stored_token.user_id,
51-
token_hash=new_token_hash,
52-
expires_at=expires_at,
53-
)
56+
new_refresh_token_entity = RefreshToken.create(
57+
user_id=stored_token.user_id,
58+
token_hash=new_token_hash,
59+
expires_at=expires_at,
60+
)
5461

55-
await self._refresh_token_repo.save(new_refresh_token_entity)
62+
await self._refresh_token_repo.save(new_refresh_token_entity)
63+
await self._unit_of_work.commit()
64+
except Exception:
65+
await self._unit_of_work.rollback()
66+
raise
5667

5768
return {
5869
"access_token": new_access_token,

src/modules/user/application/register_user/handler.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from src.modules.user.domain.entities.user import User
44
from src.modules.user.domain.exceptions.user_exception import UserAlreadyExistsError
55
from src.modules.user.domain.repositories.user_repository import UserRepository
6+
from src.shared.unit_of_work import UnitOfWork
67

78

89
class RegisterUserCommandHandler:
9-
def __init__(self, user_repository: UserRepository):
10+
def __init__(self, user_repository: UserRepository, unit_of_work: UnitOfWork):
1011
self._user_repository = user_repository
12+
self._unit_of_work = unit_of_work
1113

1214
async def execute(self, command: RegisterUserCommand) -> User:
1315
existing = await self._user_repository.get_by_email(command.email)
@@ -19,4 +21,10 @@ async def execute(self, command: RegisterUserCommand) -> User:
1921
command.email,
2022
password=hashed_password,
2123
)
22-
return await self._user_repository.save(user=user)
24+
try:
25+
saved_user = await self._user_repository.save(user=user)
26+
await self._unit_of_work.commit()
27+
return saved_user
28+
except Exception:
29+
await self._unit_of_work.rollback()
30+
raise

src/modules/user/infrastructure/repositories/refresh_token_repository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async def save(self, refresh_token: RefreshToken) -> RefreshToken:
3737
is_revoked=refresh_token.is_revoked,
3838
)
3939
model = await self.db.merge(model)
40-
await self.db.commit()
40+
await self.db.flush()
4141
await self.db.refresh(model)
4242
return RefreshToken(
4343
id=model.id,
@@ -56,4 +56,4 @@ async def revoke_by_user_id(self, user_id: UUID) -> None:
5656
)
5757
.values(is_revoked=True)
5858
)
59-
await self.db.commit()
59+
await self.db.flush()

0 commit comments

Comments
 (0)