Skip to content

Commit 2b9eb3f

Browse files
committed
feat(entities): don't control uniqueness
1 parent c4c876e commit 2b9eb3f

File tree

7 files changed

+70
-65
lines changed

7 files changed

+70
-65
lines changed

src/app_name_snake_case/application/register_user.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from app_name_snake_case.application.ports.user_views import UserViews
1414
from app_name_snake_case.application.ports.users import Users
1515
from app_name_snake_case.entities.core.user import (
16-
RegisteredUserForRegisteredUserError,
1716
registered_user_when,
1817
)
1918

@@ -24,12 +23,14 @@ class Output[SignedUserIDT, UserViewT]:
2423
user_view: UserViewT
2524

2625

26+
class RegisteredUserToRegisterUserError(Exception): ...
27+
28+
29+
class TakenUserNameToRegisterUserError(Exception): ...
30+
31+
2732
@dataclass(kw_only=True, frozen=True, slots=True)
2833
class RegisterUser[SignedUserIDT, UserViewT, UserViewWithIDT]:
29-
"""
30-
:raises app_name_snake_case.entities.user.RegisteredUserForRegisteredUserError:
31-
""" # noqa: E501
32-
3334
user_id_signing: UserIDSigning[SignedUserIDT]
3435
users: Users
3536
map: Map
@@ -39,27 +40,26 @@ class RegisterUser[SignedUserIDT, UserViewT, UserViewWithIDT]:
3940
async def __call__(
4041
self, signed_user_id: SignedUserIDT | None, user_name: str
4142
) -> Output[SignedUserIDT, UserViewT]:
42-
if signed_user_id is None:
43-
user_id = None
44-
else:
43+
"""
44+
:raises app_name_snake_case.application.register_user.RegisteredUserToRegisterUserError:
45+
:raises app_name_snake_case.application.register_user.TakenUserNameToRegisterUserError:
46+
""" # noqa: E501
47+
48+
if signed_user_id is not None:
4549
user_id = await self.user_id_signing.user_id_when(
4650
signed_user_id=signed_user_id
4751
)
4852

49-
async with self.transaction:
50-
if user_id is None:
51-
user = None
52-
else:
53-
user = await self.users.user_with_id(user_id)
53+
if user_id is not None:
54+
raise RegisteredUserToRegisterUserError
5455

55-
registered_user = registered_user_when(
56-
user=user, user_name=user_name
57-
)
56+
registered_user = registered_user_when(user_name=user_name)
5857

58+
async with self.transaction:
5959
try:
6060
await self.map(registered_user)
6161
except NotUniqueUserNameError as error:
62-
raise RegisteredUserForRegisteredUserError from error
62+
raise TakenUserNameToRegisterUserError from error
6363

6464
view = await self.user_views.view_of_user(just(registered_user))
6565

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from dataclasses import dataclass
2-
from typing import TypeGuard
32
from uuid import UUID, uuid4
43

54
from effect import IdentifiedValue, New, new
@@ -10,27 +9,5 @@ class User(IdentifiedValue[UUID]):
109
name: str
1110

1211

13-
type RegisteredUser = User
14-
type UnregisteredUser = None
15-
type AnyUser = RegisteredUser | UnregisteredUser
16-
17-
18-
unregistered_user: UnregisteredUser = None
19-
20-
21-
def is_registered(user: AnyUser) -> TypeGuard[RegisteredUser]:
22-
return user is not None
23-
24-
25-
class RegisteredUserForRegisteredUserError(Exception): ...
26-
27-
28-
def registered_user_when(*, user: AnyUser, user_name: str) -> New[User]:
29-
"""
30-
:raises app_name_snake_case.entities.user.RegisteredUserForRegisteredUserError:
31-
""" # noqa: E501
32-
33-
if is_registered(user):
34-
raise RegisteredUserForRegisteredUserError
35-
12+
def registered_user_when(*, user_name: str) -> New[User]:
3613
return new(User(id=uuid4(), name=user_name))

src/app_name_snake_case/infrastructure/adapters/map.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ async def __call__(
3636
await self.session.flush()
3737
except IntegrityError as error:
3838
self._handle_integrity_error(error)
39-
raise error from error
4039

4140
def _handle_integrity_error(self, error: IntegrityError) -> None:
42-
if isinstance(error.orig, UniqueViolation):
43-
table_name = error.orig.diag.table_name
44-
column_name = error.orig.diag.column_name
45-
46-
if table_name == "user_table" and column_name == "name":
47-
raise NotUniqueUserNameError from error
41+
match error.orig:
42+
case UniqueViolation() as unique_error:
43+
constraint_name = unique_error.diag.constraint_name
44+
45+
if constraint_name == "users_name_unique":
46+
raise NotUniqueUserNameError from error
47+
case _:
48+
raise error from error

src/app_name_snake_case/infrastructure/alembic/versions/62dcf52bf2a4_add_user_table.py renamed to src/app_name_snake_case/infrastructure/alembic/versions/e78dd190868f_add_user_table.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
"""add user table
22
3-
Revision ID: 62dcf52bf2a4
3+
Revision ID: e78dd190868f
44
Revises:
5-
Create Date: 2025-03-03 11:02:01.231021
5+
Create Date: 2025-03-30 13:37:27.048793
66
77
"""
8-
98
from collections.abc import Sequence
109

1110
import sqlalchemy as sa
1211
from alembic import op
1312

1413

15-
revision: str = "62dcf52bf2a4"
14+
revision: str = "e78dd190868f"
1615
down_revision: str | None = None
1716
branch_labels: str | Sequence[str] | None = None
1817
depends_on: str | Sequence[str] | None = None
1918

2019

2120
def upgrade() -> None:
2221
# ### commands auto generated by Alembic - please adjust! ###
23-
op.create_table(
24-
"users",
25-
sa.Column("id", sa.Uuid(), nullable=False),
26-
sa.Column("name", sa.VARCHAR(), nullable=False),
27-
sa.PrimaryKeyConstraint("id"),
22+
op.create_table("users",
23+
sa.Column("id", sa.Uuid(), nullable=False),
24+
sa.Column("name", sa.String(), nullable=False),
25+
sa.PrimaryKeyConstraint("id"),
26+
sa.UniqueConstraint("name"),
27+
sa.UniqueConstraint("name", name="users_name_unique")
2828
)
2929
# ### end Alembic commands ###
3030

src/app_name_snake_case/infrastructure/sqlalchemy/tables.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from sqlalchemy import Column, MetaData, String, Table, Uuid
1+
from sqlalchemy import (
2+
Column,
3+
MetaData,
4+
String,
5+
Table,
6+
UniqueConstraint,
7+
Uuid,
8+
)
29

310

411
metadata = MetaData()
@@ -8,4 +15,5 @@
815
metadata,
916
Column("id", Uuid(), primary_key=True, nullable=False),
1017
Column("name", String(), nullable=False, unique=True),
18+
UniqueConstraint("name", name="users_name_unique"),
1119
)

src/app_name_snake_case/presentation/fastapi/routes/register_user.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from fastapi.responses import JSONResponse, Response
44
from pydantic import BaseModel, Field
55

6-
from app_name_snake_case.application.register_user import RegisterUser
7-
from app_name_snake_case.entities.core.user import (
8-
RegisteredUserForRegisteredUserError,
6+
from app_name_snake_case.application.register_user import (
7+
RegisteredUserToRegisterUserError,
8+
RegisterUser,
9+
TakenUserNameToRegisterUserError,
910
)
1011
from app_name_snake_case.presentation.fastapi.cookies import (
1112
UserIDCookie,
@@ -16,6 +17,7 @@
1617
)
1718
from app_name_snake_case.presentation.fastapi.schemas.output import (
1819
AlreadyRegisteredUserSchema,
20+
AlreadyTakenUserNameSchema,
1921
UserSchema,
2022
)
2123
from app_name_snake_case.presentation.fastapi.tags import Tag
@@ -33,7 +35,10 @@ class RegisterUserSchema(BaseModel):
3335
responses={
3436
status.HTTP_201_CREATED: {"model": NoDataSchema},
3537
status.HTTP_409_CONFLICT: {
36-
"model": ErrorListSchema[AlreadyRegisteredUserSchema]
38+
"model": (
39+
ErrorListSchema[AlreadyRegisteredUserSchema]
40+
| ErrorListSchema[AlreadyTakenUserNameSchema]
41+
)
3742
},
3843
},
3944
summary="Register user",
@@ -50,9 +55,19 @@ async def register_user_route(
5055
result = await register_user(
5156
signed_user_id=signed_user_id, user_name=request_body.user_name
5257
)
53-
except RegisteredUserForRegisteredUserError:
54-
response_body_model = AlreadyRegisteredUserSchema().to_list()
55-
response_body = response_body_model.model_dump(by_alias=True)
58+
except RegisteredUserToRegisterUserError:
59+
response_body = (
60+
AlreadyRegisteredUserSchema()
61+
.to_list()
62+
.model_dump(mode="json", by_alias=True)
63+
)
64+
return JSONResponse(response_body, status_code=status.HTTP_409_CONFLICT)
65+
except TakenUserNameToRegisterUserError:
66+
response_body = (
67+
AlreadyTakenUserNameSchema()
68+
.to_list()
69+
.model_dump(mode="json", by_alias=True)
70+
)
5671
return JSONResponse(response_body, status_code=status.HTTP_409_CONFLICT)
5772

5873
response_body = result.user_view.model_dump(mode="json", by_alias=True)

src/app_name_snake_case/presentation/fastapi/schemas/output.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ class UserSchema(BaseModel):
1111

1212
class AlreadyRegisteredUserSchema(ErrorSchema):
1313
type: Literal["alreadyRegisteredUser"] = "alreadyRegisteredUser"
14+
15+
16+
class AlreadyTakenUserNameSchema(ErrorSchema):
17+
type: Literal["alreadyTakenUserName"] = "alreadyTakenUserName"

0 commit comments

Comments
 (0)