main
Сергей Ванюшкин 2024-04-21 20:46:17 +00:00
parent 0e2ecd3449
commit 2ca7787dcb
17 changed files with 249 additions and 72 deletions

View File

@ -1,5 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession,
async_sessionmaker)
from api.application.abstractions.uow import UnitOfWork from api.application.abstractions.uow import UnitOfWork
from api.application.protocols.date_time import DateTimeProvider from api.application.protocols.date_time import DateTimeProvider
@ -7,35 +8,28 @@ from api.application.protocols.jwt import JwtTokenProcessor
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.auth_user import LoginUser from api.application.usecase.auth.auth_user import LoginUser
from api.application.usecase.auth.create_user import CreateUser from api.application.usecase.auth.create_user import CreateUser
from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail from api.application.usecase.company.create_company import CreateCompany
from api.application.usecase.company.get_users_company import \
GetCompaniesByOwnerEmail
from api.domain.company.repository import CompanyRepository from api.domain.company.repository import CompanyRepository
from api.domain.user.repository import UserRepository from api.domain.user.repository import UserRepository
from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import ( from api.infrastructure.dependencies.adapters import (create_engine,
create_engine, create_session_maker,
create_session_maker, new_session,
new_session, new_unit_of_work)
new_unit_of_work, from api.infrastructure.dependencies.configs import (app_settings,
) get_db_settings,
from api.infrastructure.dependencies.configs import ( get_jwt_settings)
app_settings, from api.infrastructure.dependencies.protocols import (get_date_time_provider,
get_db_settings, get_jwt_token_processor,
get_jwt_settings, get_password_hasher,
) get_user_login)
from api.infrastructure.dependencies.protocols import (
get_date_time_provider,
get_jwt_token_processor,
get_password_hasher,
get_user_login,
)
from api.infrastructure.dependencies.repositories import ( from api.infrastructure.dependencies.repositories import (
get_company_repository, get_company_repository, get_user_repository)
get_user_repository,
)
from api.infrastructure.dependencies.usecases import ( from api.infrastructure.dependencies.usecases import (
provide_create_user, provide_create_company, provide_create_user,
provide_get_companies_by_email, provide_get_companies_by_email)
)
from api.infrastructure.persistence.db_setings import DBSettings from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.settings import Settings from api.infrastructure.settings import Settings
@ -62,3 +56,4 @@ def init_dependencies(app: FastAPI) -> None:
app.dependency_overrides[CreateUser] = provide_create_user app.dependency_overrides[CreateUser] = provide_create_user
app.dependency_overrides[GetCompaniesByOwnerEmail] = provide_get_companies_by_email app.dependency_overrides[GetCompaniesByOwnerEmail] = provide_get_companies_by_email
app.dependency_overrides[CreateCompany] = provide_create_company

View File

@ -8,11 +8,9 @@ from api.app_entrypoint.dependencies import init_dependencies
from api.app_entrypoint.error_handlers import init_exc_handlers from api.app_entrypoint.error_handlers import init_exc_handlers
from api.infrastructure.auth.jwt_settings import JwtSettings from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.dependencies.adapters import create_engine from api.infrastructure.dependencies.adapters import create_engine
from api.infrastructure.dependencies.configs import ( from api.infrastructure.dependencies.configs import (app_settings,
app_settings, get_db_settings,
get_db_settings, get_jwt_settings)
get_jwt_settings,
)
from api.infrastructure.persistence.db_setings import DBSettings from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.persistence.models import Base from api.infrastructure.persistence.models import Base
from api.infrastructure.settings import Settings from api.infrastructure.settings import Settings

View File

@ -4,6 +4,7 @@ from dataclasses import dataclass
@dataclass(frozen=True) @dataclass(frozen=True)
class UserCreateRequest: class UserCreateRequest:
name: str name: str
last_name: str
email: str email: str
password: str password: str

View File

@ -1,4 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from uuid import UUID
@dataclass(frozen=True)
class CreateNewCompany:
name: str
email: str
address: str
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@ -1,6 +1,9 @@
from sqlalchemy.exc import IntegrityError
from api.application.abstractions import UnitOfWork from api.application.abstractions import UnitOfWork
from api.application.contracts.auth.auth_request import UserCreateRequest from api.application.contracts.auth.auth_request import UserCreateRequest
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.domain.user.error import UserAlreadyExistsError
from api.domain.user.model import User from api.domain.user.model import User
from api.domain.user.repository import UserRepository from api.domain.user.repository import UserRepository
@ -19,8 +22,18 @@ class CreateUser:
async def execute(self, request: UserCreateRequest) -> None: async def execute(self, request: UserCreateRequest) -> None:
user = User.create( user = User.create(
name=request.name, name=request.name,
last_name=request.last_name,
email=request.email, email=request.email,
hashed_password=self.hasher.hash_password(request.password), hashed_password=self.hasher.hash_password(request.password),
) )
await self.user_repository.create_user(user=user)
await self.uow.commit() try:
await self.user_repository.create_user(user=user)
await self.uow.commit()
except IntegrityError as e:
msg = e.args[0].split("\n")[-1]
msg = msg.split(":")[1].strip()
if "email" in msg:
raise UserAlreadyExistsError(message=msg)

View File

@ -0,0 +1,12 @@
from api.application.contracts.company.company_request import CreateNewCompany
from api.application.contracts.company.company_response import CompanyBaseResponse
from api.domain.company.repository import CompanyRepository
class CreateCompany:
def __init__(self, company_repository: CompanyRepository) -> None:
self.company_repository = company_repository
async def execute(self, request: CreateNewCompany) -> CompanyBaseResponse:
# companies = await self.company_repository.
return CompanyBaseResponse(name=request.name, email=request.email)

View File

@ -4,6 +4,7 @@ from uuid import UUID, uuid4
from api.domain import DomainValidationError from api.domain import DomainValidationError
from api.domain.entity import DomainEntity from api.domain.entity import DomainEntity
from api.domain.user.model import UserId
from api.domain.value_obj import DomainValueObject from api.domain.value_obj import DomainValueObject
@ -15,7 +16,9 @@ class CompanyEmail(DomainValueObject):
pattern = r"^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$" pattern = r"^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, self.value): if not re.match(pattern, self.value):
raise DomainValidationError("Invalid email format. Email must be in the format 'example@example.com'.") raise DomainValidationError(
"Invalid email format. Email must be in the format 'example@example.com'."
)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -26,7 +29,9 @@ class CompanyName(DomainValueObject):
if len(self.value) < 1: if len(self.value) < 1:
raise DomainValidationError("First name must be at least 1 character long.") raise DomainValidationError("First name must be at least 1 character long.")
if len(self.value) > 100: if len(self.value) > 100:
raise DomainValidationError("First name must be at most 100 characters long.") raise DomainValidationError(
"First name must be at most 100 characters long."
)
if not self.value.isalpha(): if not self.value.isalpha():
raise DomainValidationError("First name must only contain letters.") raise DomainValidationError("First name must only contain letters.")
@ -36,15 +41,32 @@ class CompanyId(DomainValueObject):
value: UUID value: UUID
@dataclass(frozen=True)
class CompanyAddress(DomainValueObject):
value: str
def __post_init__(self) -> None:
if len(self.value) < 1:
raise DomainValidationError("Address must be at least 1 character long.")
if len(self.value) > 100:
raise DomainValidationError("Address must be at most 100 characters long.")
if not self.value.isalpha():
raise DomainValidationError("Address must only contain letters.")
@dataclass @dataclass
class Company(DomainEntity[CompanyId]): class Company(DomainEntity[CompanyId]):
name: CompanyName name: CompanyName
email: CompanyEmail email: CompanyEmail
address: CompanyAddress
owner_id: UserId
@staticmethod @staticmethod
def create(name: str, email: str) -> "Company": def create(name: str, email: str, address: str, owner_id: str) -> "Company":
return Company( return Company(
id=CompanyId(uuid4()), id=CompanyId(uuid4()),
name=CompanyName(name), name=CompanyName(name),
email=CompanyEmail(email), email=CompanyEmail(email),
address=CompanyAddress(address),
owner_id=UserId(UUID(owner_id)),
) )

View File

@ -15,7 +15,9 @@ class UserEmail(DomainValueObject):
pattern = r"^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$" pattern = r"^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, self.value): if not re.match(pattern, self.value):
raise DomainValidationError("Invalid email format. Email must be in the format 'example@example.com'.") raise DomainValidationError(
"Invalid email format. Email must be in the format 'example@example.com'."
)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -26,7 +28,9 @@ class UserFirstName(DomainValueObject):
if len(self.value) < 1: if len(self.value) < 1:
raise DomainValidationError("First name must be at least 1 character long.") raise DomainValidationError("First name must be at least 1 character long.")
if len(self.value) > 100: if len(self.value) > 100:
raise DomainValidationError("First name must be at most 100 characters long.") raise DomainValidationError(
"First name must be at most 100 characters long."
)
if not self.value.isalpha(): if not self.value.isalpha():
raise DomainValidationError("First name must only contain letters.") raise DomainValidationError("First name must only contain letters.")
@ -39,7 +43,9 @@ class UserLastName(DomainValueObject):
if len(self.value) < 1: if len(self.value) < 1:
raise DomainValidationError("Last name must be at least 1 character long.") raise DomainValidationError("Last name must be at least 1 character long.")
if len(self.value) > 100: if len(self.value) > 100:
raise DomainValidationError("Last name must be at most 100 characters long.") raise DomainValidationError(
"Last name must be at most 100 characters long."
)
if not self.value.isalpha(): if not self.value.isalpha():
raise DomainValidationError("Last name must only contain letters.") raise DomainValidationError("Last name must only contain letters.")
@ -52,14 +58,16 @@ class UserId(DomainValueObject):
@dataclass @dataclass
class User(DomainEntity[UserId]): class User(DomainEntity[UserId]):
name: UserFirstName name: UserFirstName
last_name: UserLastName
email: UserEmail email: UserEmail
hashed_password: str hashed_password: str
@staticmethod @staticmethod
def create(name: str, email: str, hashed_password: str) -> "User": def create(name: str, last_name: str, email: str, hashed_password: str) -> "User":
return User( return User(
id=UserId(uuid4()), id=UserId(uuid4()),
name=UserFirstName(name), name=UserFirstName(name),
last_name=UserLastName(last_name),
email=UserEmail(email), email=UserEmail(email),
hashed_password=hashed_password, hashed_password=hashed_password,
) )

View File

@ -5,7 +5,9 @@ from fastapi import Depends
from api.application.abstractions.uow import UnitOfWork from api.application.abstractions.uow import UnitOfWork
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.create_user import CreateUser from api.application.usecase.auth.create_user import CreateUser
from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail from api.application.usecase.company.create_company import CreateCompany
from api.application.usecase.company.get_users_company import \
GetCompaniesByOwnerEmail
from api.domain.company.repository import CompanyRepository from api.domain.company.repository import CompanyRepository
from api.domain.user.repository import UserRepository from api.domain.user.repository import UserRepository
from api.infrastructure.dependencies.stub import Stub from api.infrastructure.dependencies.stub import Stub
@ -16,10 +18,18 @@ def provide_create_user(
uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))], uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))],
password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))], password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))],
) -> CreateUser: ) -> CreateUser:
return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher) return CreateUser(
uow=uow, user_repository=user_repository, password_hasher=password_hasher
)
def provide_get_companies_by_email( def provide_get_companies_by_email(
company_repository: Annotated[CompanyRepository, Depends(Stub(CompanyRepository))], company_repository: Annotated[CompanyRepository, Depends(Stub(CompanyRepository))],
) -> GetCompaniesByOwnerEmail: ) -> GetCompaniesByOwnerEmail:
return GetCompaniesByOwnerEmail(company_repository=company_repository) return GetCompaniesByOwnerEmail(company_repository=company_repository)
def provide_create_company(
company_repository: Annotated[CompanyRepository, Depends(Stub(CompanyRepository))]
) -> CreateCompany:
return CreateCompany(company_repository=company_repository)

View File

@ -0,0 +1,4 @@
class TransactionContextManagerError(Exception):
def __init__(self, message: str, *args: object) -> None:
self.message = message
super().__init__(*args)

View File

@ -1,13 +1,13 @@
import uuid import uuid
from sqlalchemy import UUID from sqlalchemy import UUID, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from api.infrastructure.persistence.models.base import Base from api.infrastructure.persistence.models.base import Base
class CompanyModel(Base): class CompanyModel(Base):
__tablename__ = "companies" __tablename__ = "company"
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), UUID(as_uuid=True),
@ -15,3 +15,45 @@ class CompanyModel(Base):
) )
name: Mapped[str] name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True) email: Mapped[str] = mapped_column(unique=True)
address: Mapped[str]
owner_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"),
)
class DepartmentModel(Base):
__tablename__ = "department"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
)
name: Mapped[str]
address: Mapped[str]
company_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("company.id", ondelete="CASCADE")
)
class CompanyDepartmentModel(Base):
__tablename__ = "company_department_m2m"
company_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("company.id", ondelete="CASCADE"),
primary_key=True,
)
department_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("department.id", ondelete="CASCADE"),
primary_key=True,
)
class DepartmentUserModel(Base):
__tablename__ = "department_user_m2m"
department_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("department.id", ondelete="CASCADE"),
primary_key=True,
)
user_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id"),
primary_key=True,
)

View File

@ -14,5 +14,6 @@ class UserModel(Base):
primary_key=True, primary_key=True,
) )
name: Mapped[str] name: Mapped[str]
last_name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True) email: Mapped[str] = mapped_column(unique=True)
hashed_password: Mapped[str] hashed_password: Mapped[str]

View File

@ -1,30 +1,33 @@
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.company.model import Company, CompanyEmail, CompanyId, CompanyName from api.domain.company.model import (Company, CompanyAddress, CompanyEmail,
CompanyId, CompanyName)
from api.domain.company.repository import CompanyRepository from api.domain.company.repository import CompanyRepository
from api.domain.user.model import UserId
class SqlAlchemyCompanyRepository(CompanyRepository): class SqlAlchemyCompanyRepository(CompanyRepository):
def __init__(self, session: AsyncSession) -> None: def __init__(self, session: AsyncSession) -> None:
self.session = session self.session = session
# async def create_user(self, user: User) -> None: async def create_company(self, company: Company) -> None:
# stmt = text( stmt = text(
# """INSERT INTO users (id, name, email, hashed_password) """INSERT INTO company (id, name, email, address, owner_id)
# VALUES(:id, :name, :email, :hashed_password) VALUES(:id, :name, :email, :address, :owner_id)
# """ """
# ) )
# await self.session.execute( await self.session.execute(
# stmt, stmt,
# { {
# "id": str(user.id.value), "id": str(company.id.value),
# "name": user.name.value, "name": company.name.value,
# "email": user.email.value, "email": company.email.value,
# "hashed_password": user.hashed_password, "address": company.address.value,
# }, "owner_id": str(company.owner_id.value),
# ) },
# )
async def get_companies_by_owner_email(self, filter: dict) -> list[Company]: async def get_companies_by_owner_email(self, filter: dict) -> list[Company]:
stmt = text("""SELECT * FROM companies WHERE email = :val""") stmt = text("""SELECT * FROM companies WHERE email = :val""")
result = await self.session.execute(stmt, {"val": filter["email"]}) result = await self.session.execute(stmt, {"val": filter["email"]})
@ -36,6 +39,8 @@ class SqlAlchemyCompanyRepository(CompanyRepository):
id=CompanyId(c.id), id=CompanyId(c.id),
name=CompanyName(c.name), name=CompanyName(c.name),
email=CompanyEmail(c.email), email=CompanyEmail(c.email),
address=CompanyAddress(c.address),
owner_id=UserId(c.owner_id),
) )
for c in result for c in result
] ]

View File

@ -2,7 +2,8 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.user import User, UserRepository from api.domain.user import User, UserRepository
from api.domain.user.model import UserEmail, UserFirstName, UserId from api.domain.user.model import (UserEmail, UserFirstName, UserId,
UserLastName)
class SqlAlchemyUserRepository(UserRepository): class SqlAlchemyUserRepository(UserRepository):
@ -11,8 +12,8 @@ class SqlAlchemyUserRepository(UserRepository):
async def create_user(self, user: User) -> None: async def create_user(self, user: User) -> None:
stmt = text( stmt = text(
"""INSERT INTO users (id, name, email, hashed_password) """INSERT INTO users (id, name, last_name, email, hashed_password)
VALUES(:id, :name, :email, :hashed_password) VALUES(:id, :name, :last_name, :email, :hashed_password)
""" """
) )
await self.session.execute( await self.session.execute(
@ -20,6 +21,7 @@ class SqlAlchemyUserRepository(UserRepository):
{ {
"id": str(user.id.value), "id": str(user.id.value),
"name": user.name.value, "name": user.name.value,
"last_name": user.last_name.value,
"email": user.email.value, "email": user.email.value,
"hashed_password": user.hashed_password, "hashed_password": user.hashed_password,
}, },
@ -37,6 +39,7 @@ class SqlAlchemyUserRepository(UserRepository):
return User( return User(
id=UserId(result.id), id=UserId(result.id),
name=UserFirstName(result.name), name=UserFirstName(result.name),
last_name=UserLastName(result.last_name),
email=UserEmail(result.email), email=UserEmail(result.email),
hashed_password=result.hashed_password, hashed_password=result.hashed_password,
) )

View File

@ -0,0 +1,29 @@
from types import TracebackType
from sqlalchemy.ext.asyncio import AsyncSession
from api.infrastructure.persistence.error import TransactionContextManagerError
class SqlalchemyTransactionContextManager:
def __init__(self, session: AsyncSession):
self._session = session
async def __aenter__(self):
return self
async def __aexit__(
self,
exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> None:
if exc_type:
await self.rollback()
raise TransactionContextManagerError(message="Transaction Error")
async def commit(self):
await self._session.commit()
async def rollback(self):
await self._session.rollback()

View File

@ -2,31 +2,42 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from api.application.contracts.company.company_request import CompanyByOwnerEmail from api.application.contracts.company.company_request import (
CompanyByOwnerEmail,
CreateNewCompany,
)
from api.application.contracts.company.company_response import CompanyBaseResponse from api.application.contracts.company.company_response import CompanyBaseResponse
from api.application.protocols.jwt import JwtTokenProcessor from api.application.protocols.jwt import JwtTokenProcessor
from api.application.usecase.company.create_company import CreateCompany
from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail from api.application.usecase.company.get_users_company import GetCompaniesByOwnerEmail
from api.domain.user.error import UserValidationError from api.domain.user.error import UserValidationError
from api.infrastructure.dependencies.stub import Stub from api.infrastructure.dependencies.stub import Stub
from api.presentation.auth.fasapi_auth import auth_required from api.presentation.auth.fasapi_auth import auth_required
company_router = APIRouter(prefix="/company", tags=["Company"]) company_router = APIRouter(
prefix="/me",
tags=["Company"],
dependencies=[Depends(auth_required)],
)
@company_router.get( @company_router.get(
"/", "/companies",
response_model=None, response_model=list[CompanyBaseResponse],
dependencies=[Depends(auth_required)],
) )
async def get_companies( async def get_my_companies(
request: Request, request: Request,
token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))], token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
usecase: Annotated[GetCompaniesByOwnerEmail, Depends(Stub(GetCompaniesByOwnerEmail))], usecase: Annotated[
GetCompaniesByOwnerEmail, Depends(Stub(GetCompaniesByOwnerEmail))
],
) -> list[CompanyBaseResponse]: ) -> list[CompanyBaseResponse]:
token_data = token_processor.validate_token(request.scope["auth"]) token_data = token_processor.validate_token(request.scope["auth"])
if not token_data: if not token_data:
raise UserValidationError("Login required") raise UserValidationError("Login required")
companies = await usecase.execute(request=CompanyByOwnerEmail(email=token_data[1].value)) companies = await usecase.execute(
request=CompanyByOwnerEmail(email=token_data[1].value)
)
return [ return [
CompanyBaseResponse( CompanyBaseResponse(
name=c.name, name=c.name,
@ -34,3 +45,18 @@ async def get_companies(
) )
for c in companies for c in companies
] ]
@company_router.post("/companies/create", response_model=CompanyBaseResponse)
async def create_new_company(
request: Request,
request_data: CreateNewCompany,
token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
usecase: Annotated[CreateCompany, Depends(Stub(CreateCompany))],
) -> CompanyBaseResponse:
token_data = token_processor.validate_token(request.scope["auth"])
if not token_data:
raise UserValidationError("Login required")
company = await usecase.execute(request=request_data)
return company

View File

@ -1,5 +1,5 @@
db: db:
host: "db" host: "localhost"
port: 5432 port: 5432
database: "serviceman_db" database: "serviceman_db"
user: "demo_user" user: "demo_user"