Skip to content

Commit 0b1e856

Browse files
author
Mario G.
committed
feat(projects): add a new person membership
1 parent 2a98984 commit 0b1e856

File tree

14 files changed

+226
-28
lines changed

14 files changed

+226
-28
lines changed

src/liceo/infra/adapters/migrations/20260103_initial_data.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ INSERT INTO liceo_permissions (id, name, description) VALUES ('33Eccx9gpXqbE9mCB
9696
-- PROJECTS
9797
INSERT INTO liceo_permissions (id, name, description) VALUES ('mgewHChKbk5rAbbc5fAnQU', 'PROJECTS_CREATE', 'allows to create projects');
9898
INSERT INTO liceo_permissions (id, name, description) VALUES ('LGoiyMGdNGPFXhzVP7MXnf', 'PROJECTS_LIST', 'allows to list or filter projects');
99+
INSERT INTO liceo_permissions (id, name, description) VALUES ('KrMBkC2Qg5aBFo2GoTMe7z', 'PROJECTS_ADD_MEMBERSHIP', 'allows to add a person to the project');
100+
99101

100102
-- #######################
101103
-- ### ROLES ###
@@ -184,6 +186,8 @@ INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeG
184186
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'mgewHChKbk5rAbbc5fAnQU');
185187
-- ADMIN/PROJECTS_LIST
186188
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'LGoiyMGdNGPFXhzVP7MXnf');
189+
-- ADMIN/PROJECTS_ADD_MEMBERSHIP
190+
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'KrMBkC2Qg5aBFo2GoTMe7z');
187191

188192

189193
-- #######################

src/liceo/people/projects/adapters/di.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from typing import Annotated
2-
from fastapi import Depends, Body, Query
2+
from fastapi import Depends, Body, Query, Path
33
from liceo.infra.adapters.di import ConnectionFactoryDependency, ConnectionManagerDependency, TransactionManagerDependency, EventStoreDependency
44
from liceo.security.common.adapters.di import UserInfo
5-
from ..application.repository import ProjectRepository
5+
from ..application.repository import ProjectRepository, ProjectMemberRepository
66
from ..application.service import ProjectService
7-
from .repository import SQLProjectRepository
7+
from .repository import SQLProjectRepository, SQLProjectMemberRepository
88
from .service import DatabaseAwareProjectService
9-
from .requests import CreateProjectRequest, CreateProjectDetails, ListProjectsRequest
9+
from .requests import CreateProjectRequest, CreateProjectDetails, ListProjectsRequest, AddMemberToProjectRequest, AddMemberToProjectPath
1010

1111
# ------ REPOSITORIES
1212

@@ -19,17 +19,27 @@ def create_project_repository(factory: ConnectionFactoryDependency) -> ProjectRe
1919
create_project_repository)]
2020

2121

22+
def create_project_members_repository(factory: ConnectionFactoryDependency) -> ProjectMemberRepository:
23+
return SQLProjectMemberRepository(factory=factory)
24+
25+
26+
ProjectMemberRepositoryDependency = Annotated[ProjectMemberRepository, Depends(
27+
create_project_members_repository)]
28+
29+
2230
# ------ SERVICES
2331

2432

2533
def create_project_service(
26-
repository: ProjectRepositoryDependency,
34+
projects: ProjectRepositoryDependency,
35+
project_members: ProjectMemberRepositoryDependency,
2736
transaction_manager_factory: TransactionManagerDependency,
2837
connection_manager_factory: ConnectionManagerDependency,
2938
event_store: EventStoreDependency
3039
) -> ProjectService:
3140
return DatabaseAwareProjectService(
32-
repository=repository,
41+
projects=projects,
42+
project_members=project_members,
3343
transaction_manager_factory=transaction_manager_factory,
3444
connection_manager_factory=connection_manager_factory,
3545
event_store=event_store
@@ -53,3 +63,14 @@ def create_project_request(
5363

5464

5565
ListProjectsRequestDependency = Annotated[ListProjectsRequest, Query()]
66+
67+
68+
def created_add_project_member_request(
69+
user_info: UserInfo,
70+
details: AddMemberToProjectPath = Path()
71+
):
72+
return AddMemberToProjectRequest(created_by=user_info, details=details)
73+
74+
75+
AddMemberToProjectRequestDependency = Annotated[AddMemberToProjectRequest, Depends(
76+
created_add_project_member_request)]

src/liceo/people/projects/adapters/endpoints.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,18 @@ def list(
3434
def create(
3535
request: di.CreateProjectRequestDependency,
3636
service: di.ProjectServiceDependency
37-
) -> responses.SimpleProjectResponse:
38-
return responses.SimpleProjectResponse.from_project(service.create_project(request.to_dto()))
37+
) -> responses.ProjectResponse:
38+
return responses.ProjectResponse.from_project(service.create_project(request.to_dto()))
39+
40+
41+
@router.post(
42+
path="/{id}/membership/{person_id}",
43+
summary="Adds a person membership to the project",
44+
dependencies=[has_permission(permissions.PROJECTS_ADD_MEMBERSHIP)],
45+
openapi_extra={**open_api_permissions([permissions.PROJECTS_ADD_MEMBERSHIP])}
46+
)
47+
def add_member(
48+
request: di.AddMemberToProjectRequestDependency,
49+
service: di.ProjectServiceDependency
50+
) -> responses.ProjectMembershipResponse | None:
51+
return responses.ProjectMembershipResponse.from_project_membership(service.add_new_member(request.to_dto()))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
PROJECTS_CREATE: str = "PROJECTS_CREATE"
22
PROJECTS_LIST: str = "PROJECTS_LIST"
3+
PROJECTS_ADD_MEMBERSHIP: str = "PROJECTS_ADD_MEMBERSHIP"

src/liceo/people/projects/adapters/repository.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from shortuuid import uuid
22
from typing import override
33
from liceo.infra.domain.vo import Paged, Pagination
4-
from liceo.people.projects.domain.entities import Project
5-
from liceo.labs.db.sql import SQLRepository
6-
from liceo.people.projects.domain.vo import ProjectId
7-
from ..application.repository import ProjectRepository
4+
from liceo.people.projects.domain.entities import Project, ProjectMembership
5+
from liceo.labs.db.sql import SQLRepository, sql
6+
from liceo.people.projects.domain.vo import ProjectId, ProjectMembershipId
7+
from ..application.repository import ProjectRepository, ProjectMemberRepository
88
from .mappers import from_row_to_project
99

1010

@@ -13,6 +13,10 @@ class SQLProjectRepository(ProjectRepository, SQLRepository):
1313
def generate_id(self) -> ProjectId:
1414
return ProjectId(uuid())
1515

16+
@sql(from_row_to_project)
17+
def find_project_by_id(self, id: str) -> Project | None:
18+
return None
19+
1620
@override
1721
def save_project(self, project: Project) -> Project:
1822
self._get_connection().execute(
@@ -47,3 +51,22 @@ def filter_projects_by_name(self, name: str | None, pagination: Pagination) -> P
4751
total=result[0]["total_count"] if result else 0,
4852
data=list(map(from_row_to_project, result))
4953
)
54+
55+
56+
class SQLProjectMemberRepository(ProjectMemberRepository, SQLRepository):
57+
def generate_id(self) -> ProjectMembershipId:
58+
return ProjectMembershipId(uuid())
59+
60+
def save_membership(self, membership: ProjectMembership) -> ProjectMembership:
61+
self._get_connection().execute(
62+
sql=self.resolve_sql(self.save_membership),
63+
params={
64+
"id": membership.id,
65+
"version": membership._version,
66+
"project_id": membership.project.id,
67+
"person_id": membership.person.id,
68+
"created_by": membership.created_by,
69+
"created_at": membership.created_at
70+
}
71+
)
72+
return membership

src/liceo/people/projects/adapters/requests.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,20 @@ def to_dto(self) -> dtos.FilterProjectsDTO:
3232
name=self.name,
3333
pagination=Pagination(max=self.max, page=self.page)
3434
)
35+
36+
37+
class AddMemberToProjectPath(BaseModel):
38+
id: str
39+
person_id: str
40+
41+
42+
class AddMemberToProjectRequest(BaseModel):
43+
created_by: UserContextModel
44+
details: AddMemberToProjectPath
45+
46+
def to_dto(self) -> dtos.AddMemberToProjectDTO:
47+
return dtos.AddMemberToProjectDTO(
48+
project_id=self.details.id,
49+
person_id=self.details.person_id,
50+
created_by=self.created_by.id
51+
)
Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
from pydantic import BaseModel
22
from liceo.infra.domain.vo import Paged
3-
from ..domain.entities import Project
3+
from ..domain.entities import Project, ProjectMembership
44

55

6-
class SimpleProjectResponse(BaseModel):
6+
class MinimalProjectResponse(BaseModel):
7+
id: str
8+
version: int
9+
10+
@staticmethod
11+
def from_project(project: Project | None):
12+
if project:
13+
return MinimalProjectResponse(id=project.id.id, version=project._version)
14+
15+
16+
class ProjectResponse(BaseModel):
717
id: str
818
version: int
919
name: str
1020
description: str
1121

1222
@staticmethod
1323
def from_project(project: Project):
14-
return SimpleProjectResponse(
24+
return ProjectResponse(
1525
id=project.id.id,
1626
version=project._version,
1727
name=project.name,
@@ -20,12 +30,29 @@ def from_project(project: Project):
2030

2131

2232
class ListProjectsResponse(BaseModel):
23-
data: list[SimpleProjectResponse]
33+
data: list[ProjectResponse]
2434
total: int
2535

2636
@staticmethod
2737
def from_paged(paged: Paged[Project]) -> "ListProjectsResponse":
2838
return ListProjectsResponse(
29-
data=paged.map(SimpleProjectResponse.from_project).data,
39+
data=paged.map(ProjectResponse.from_project).data,
3040
total=paged.total
3141
)
42+
43+
44+
class ProjectMembershipResponse(BaseModel):
45+
id: str
46+
version: int
47+
person_id: str
48+
project_id: str
49+
50+
@staticmethod
51+
def from_project_membership(membership: ProjectMembership | None):
52+
if membership:
53+
return ProjectMembershipResponse(
54+
id=membership.id.id,
55+
version=membership._version,
56+
person_id=membership.person.id,
57+
project_id=membership.project.id
58+
)

src/liceo/people/projects/adapters/service.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,52 @@
22
from liceo.infra.domain.vo import Paged
33
from liceo.labs.db.core import AbstractService, managed_service, transactional
44
from liceo.labs.sherlock.application.service import EventStoreService
5-
from liceo.people.projects.application.dtos import CreateProjectDTO, FilterProjectsDTO
6-
from ..application.repository import ProjectRepository
5+
from liceo.people.projects.application.dtos import AddMemberToProjectDTO, CreateProjectDTO, FilterProjectsDTO
6+
from ..application.repository import ProjectRepository, ProjectMemberRepository
77
from ..application.service import ProjectService
88
from ..domain import entities
99

1010

1111
@dataclass
1212
@managed_service
1313
class DatabaseAwareProjectService(ProjectService, AbstractService):
14-
repository: ProjectRepository
14+
projects: ProjectRepository
15+
project_members: ProjectMemberRepository
1516
event_store: EventStoreService
1617

1718
@transactional()
1819
def create_project(self, dto: CreateProjectDTO) -> entities.Project:
1920
project = entities.Project.create(
2021
entities.Project.CreateProjectCommand(
21-
id=self.repository.generate_id(),
22+
id=self.projects.generate_id(),
2223
name=dto.name,
2324
description=dto.description,
2425
created_by=dto.created_by
2526
)
2627
)
2728

28-
self.repository.save_project(project)
29+
self.projects.save_project(project)
2930
self.event_store.append(project)
3031
return project
3132

3233
def filter_projects(self, dto: FilterProjectsDTO) -> Paged[entities.Project]:
33-
return self.repository.filter_projects_by_name(name=dto.name, pagination=dto.pagination)
34+
return self.projects.filter_projects_by_name(name=dto.name, pagination=dto.pagination)
35+
36+
@transactional()
37+
def add_new_member(self, dto: AddMemberToProjectDTO) -> entities.ProjectMembership | None:
38+
project = self.projects.find_project_by_id(dto.project_id)
39+
40+
if not project:
41+
return
42+
43+
project_membership = entities.ProjectMembership.create(
44+
entities.ProjectMembership.CreateMembershipCommand(
45+
id=self.project_members.generate_id(),
46+
project_id=dto.project_id,
47+
person_id=dto.person_id,
48+
created_by=dto.created_by
49+
)
50+
)
51+
self.project_members.save_membership(project_membership)
52+
self.event_store.append(project_membership)
53+
return project_membership
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SELECT p.*
2+
FROM liceo_projects p
3+
WHERE p.id = :id;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
INSERT INTO liceo_projects_people
2+
(
3+
id,
4+
version,
5+
project_id,
6+
person_id
7+
)
8+
VALUES
9+
(
10+
:id,
11+
:version,
12+
:project_id,
13+
:person_id
14+
) RETURNING id;

0 commit comments

Comments
 (0)