Skip to content

Commit

Permalink
Add create mutation for manga branch
Browse files Browse the repository at this point in the history
  • Loading branch information
ThirVondukr committed Feb 27, 2024
1 parent 14b7a71 commit 76909dd
Show file tree
Hide file tree
Showing 24 changed files with 416 additions and 9 deletions.
30 changes: 30 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,36 @@ type Manga {
id: ID!
title: String!
titleSlug: String!
status: MangaStatus!
createdAt: DateTime!
updatedAt: DateTime!
tags: [MangaTag!]!
altTitles: [AltTitle!]!
}

type MangaBranch {
id: ID!
name: String!
language: LanguageEnum!
}

union MangaBranchCreateError = ValidationErrors | RelationshipNotFoundError

input MangaBranchCreateInput {
name: String!
language: LanguageEnum!
mangaId: ID!
}

type MangaBranchCreatePayload {
branch: MangaBranch
error: MangaBranchCreateError
}

type MangaBranchMutationGQL {
create(input: MangaBranchCreateInput!): MangaBranchCreatePayload! @isAuthenticated
}

union MangaCreateError = ValidationErrors

input MangaCreateInput {
Expand Down Expand Up @@ -114,6 +138,7 @@ type Mutation {
auth: AuthMutations!
manga: MangaMutations!
groups: GroupMutations!
branches: MangaBranchMutationGQL!
}

type PagePaginationInfo {
Expand Down Expand Up @@ -143,6 +168,11 @@ type Query {
me: PrivateUser! @isAuthenticated
}

type RelationshipNotFoundError implements Error {
message: String!
id: ID!
}

union SignInErrors = InvalidCredentialsError | ValidationErrors

input SignInInput {
Expand Down
18 changes: 18 additions & 0 deletions src/app/adapters/graphql/apps/branches/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import strawberry

from app.adapters.graphql.types import LanguageGQL
from app.core.domain.branches.dto import MangaBranchCreateDTO


@strawberry.input(name="MangaBranchCreateInput")
class MangaBranchCreateInput:
name: str
language: LanguageGQL
manga_id: strawberry.ID

def to_dto(self) -> MangaBranchCreateDTO:
return MangaBranchCreateDTO(
name=self.name,
language=self.language,
manga_id=self.manga_id, # type: ignore[arg-type]
)
51 changes: 48 additions & 3 deletions src/app/adapters/graphql/apps/branches/mutations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
from typing import Annotated

import strawberry
from aioinject import Inject
from aioinject.ext.strawberry import inject
from result import Err

from app.adapters.graphql.apps.branches.input import MangaBranchCreateInput
from app.adapters.graphql.apps.branches.payload import (
MangaBranchCreatePayloadGQL,
)
from app.adapters.graphql.apps.branches.types import MangaBranchGQL
from app.adapters.graphql.context import Info
from app.adapters.graphql.errors import RelationshipNotFoundErrorGQL
from app.adapters.graphql.extensions import AuthExtension
from app.adapters.graphql.validation import validate_callable
from app.core.domain.branches.commands import MangaBranchCreateCommand
from app.core.errors import RelationshipNotFoundError


@strawberry.type
class MangaBranchMutation:
async def create(self) -> None:
pass
class MangaBranchMutationGQL:
@strawberry.mutation(extensions=[AuthExtension]) # type: ignore[misc]
@inject
async def create(
self,
input: MangaBranchCreateInput,
command: Annotated[MangaBranchCreateCommand, Inject],
info: Info,
) -> MangaBranchCreatePayloadGQL:
dto = validate_callable(input.to_dto)
if isinstance(dto, Err):
return MangaBranchCreatePayloadGQL(error=dto.err_value)

result = await command.execute(
dto=dto.ok_value,
user=await info.context.user,
)

if isinstance(result, Err):
match result.err_value:
case RelationshipNotFoundError(): # pragma: no branch
return MangaBranchCreatePayloadGQL(
error=RelationshipNotFoundErrorGQL.from_err(
result.err_value,
),
)

return MangaBranchCreatePayloadGQL(
branch=MangaBranchGQL.from_dto(result.ok_value),
error=None,
)
18 changes: 18 additions & 0 deletions src/app/adapters/graphql/apps/branches/payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Annotated

import strawberry

from app.adapters.graphql.apps.branches.types import MangaBranchGQL
from app.adapters.graphql.errors import RelationshipNotFoundErrorGQL
from app.adapters.graphql.validation import ValidationErrorsGQL

MangaBranchCreateError = Annotated[
ValidationErrorsGQL | RelationshipNotFoundErrorGQL,
strawberry.union(name="MangaBranchCreateError"),
]


@strawberry.type(name="MangaBranchCreatePayload")
class MangaBranchCreatePayloadGQL:
branch: MangaBranchGQL | None = None
error: MangaBranchCreateError | None
2 changes: 1 addition & 1 deletion src/app/adapters/graphql/apps/branches/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class MangaBranchGQL(DTOMixin[MangaBranch]):
@classmethod
def from_dto(cls, model: MangaBranch) -> Self:
return cls(
id=strawberry.ID(str(model)),
id=strawberry.ID(str(model.id)),
name=model.name,
language=model.language,
)
16 changes: 16 additions & 0 deletions src/app/adapters/graphql/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from typing import Self

import strawberry

from app.core.errors import RelationshipNotFoundError


@strawberry.interface(name="Error")
class ErrorGQL:
Expand All @@ -11,6 +15,18 @@ class EntityAlreadyExistsErrorGQL(ErrorGQL):
message: str = "Entity already exists"


@strawberry.type(name="RelationshipNotFoundError")
class RelationshipNotFoundErrorGQL(ErrorGQL):
entity_id: strawberry.ID
message: str = "Relationship not found"

@classmethod
def from_err(cls, err: RelationshipNotFoundError) -> Self:
return cls(
entity_id=strawberry.ID(err.entity_id),
)


@strawberry.type(name="InvalidCredentialsError")
class InvalidCredentialsErrorGQL(ErrorGQL):
message: str = "Invalid credentials"
5 changes: 5 additions & 0 deletions src/app/adapters/graphql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from strawberry.tools import merge_types

from app.adapters.graphql.apps.auth.mutation import AuthMutationsGQL
from app.adapters.graphql.apps.branches.mutations import MangaBranchMutationGQL
from app.adapters.graphql.apps.groups.mutation import GroupMutationsGQL
from app.adapters.graphql.apps.manga.mutation import MangaMutationsGQL
from app.adapters.graphql.apps.manga.query import MangaQuery
Expand All @@ -34,6 +35,10 @@ async def manga(self) -> MangaMutationsGQL:
async def groups(self) -> GroupMutationsGQL:
return GroupMutationsGQL()

@strawberry.field
async def branches(self) -> MangaBranchMutationGQL:
return MangaBranchMutationGQL()


class CustomNameConverter(NameConverter):
def from_enum_value(
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/di/_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
from app.settings import AuthSettings, DatabaseSettings, SentrySettings
from lib.settings import get_settings

from ._modules import auth, database, groups, manga, users
from ._modules import auth, branches, database, groups, manga, users

modules: Iterable[Iterable[Provider[Any]]] = [
auth.providers,
branches.providers,
database.providers,
groups.providers,
manga.providers,
Expand Down
10 changes: 10 additions & 0 deletions src/app/core/di/_modules/branches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from aioinject import Scoped

from app.core.domain.branches.commands import MangaBranchCreateCommand
from app.core.domain.branches.services import MangaBranchService
from lib.types import Providers

providers: Providers = [
Scoped(MangaBranchService),
Scoped(MangaBranchCreateCommand),
]
Empty file.
33 changes: 33 additions & 0 deletions src/app/core/domain/branches/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from result import Err, Ok, Result

from app.core.domain.branches.dto import MangaBranchCreateDTO
from app.core.domain.branches.services import MangaBranchService
from app.core.domain.manga.repositories import MangaRepository
from app.core.errors import RelationshipNotFoundError
from app.db.models import MangaBranch, User


class MangaBranchCreateCommand:
def __init__(
self,
manga_repository: MangaRepository,
service: MangaBranchService,
) -> None:
self._manga_repository = manga_repository
self._service = service

async def execute(
self,
dto: MangaBranchCreateDTO,
user: User, # noqa: ARG002
) -> Result[MangaBranch, RelationshipNotFoundError]:
manga = await self._manga_repository.get(id=dto.manga_id)
if manga is None:
return Err(
RelationshipNotFoundError(
entity_name="Manga",
entity_id=str(dto.manga_id),
),
)

return Ok(await self._service.create(dto=dto, manga=manga))
13 changes: 13 additions & 0 deletions src/app/core/domain/branches/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from uuid import UUID

from pydantic import Field

from app.core.domain.const import GENERIC_NAME_LENGTH
from lib.dto import BaseDTO
from lib.types import Language


class MangaBranchCreateDTO(BaseDTO):
name: str = Field(max_length=GENERIC_NAME_LENGTH)
manga_id: UUID
language: Language
22 changes: 22 additions & 0 deletions src/app/core/domain/branches/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from app.core.domain.branches.dto import MangaBranchCreateDTO
from app.db.models import Manga, MangaBranch
from lib.db import DBContext


class MangaBranchService:
def __init__(self, db_context: DBContext) -> None:
self._db_context = db_context

async def create(
self,
dto: MangaBranchCreateDTO,
manga: Manga,
) -> MangaBranch:
branch = MangaBranch(
name=dto.name,
language=dto.language,
manga=manga,
)
self._db_context.add(branch)
await self._db_context.flush()
return branch
6 changes: 6 additions & 0 deletions src/app/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
@dataclasses.dataclass
class EntityAlreadyExistsError:
pass


@dataclasses.dataclass
class RelationshipNotFoundError:
entity_name: str
entity_id: str
4 changes: 2 additions & 2 deletions src/app/db/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def process_bind_param(
value: TEnum | None,
dialect: Dialect, # noqa: ARG002
) -> int | None:
if value is None:
if value is None: # pragma: no cover
return None
return value.value

Expand All @@ -27,6 +27,6 @@ def process_result_value(
value: int | None,
dialect: Dialect, # noqa: ARG002
) -> TEnum | None:
if value is None:
if value is None: # pragma: no cover
return value
return self.enum(value)
7 changes: 5 additions & 2 deletions src/app/db/models/_manga.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@ class MangaBranch(PkUUID, HasTimestamps, MappedAsDataclass, Base, kw_only=True):
__tablename__ = "manga_branch"

name: Mapped[str_title]
manga_id: Mapped[UUID] = mapped_column(ForeignKey("manga.id"))
manga_id: Mapped[UUID] = mapped_column(ForeignKey("manga.id"), init=False)
manga: Mapped[Manga] = relationship(back_populates="branches")

language: Mapped[Language]

chapters: Mapped[MangaChapter] = relationship(back_populates="branch")
chapters: Mapped[list[MangaChapter]] = relationship(
back_populates="branch",
default_factory=list,
)


class MangaPage(PkUUID, MappedAsDataclass, Base, kw_only=True):
Expand Down
Empty file.
Empty file.
Loading

0 comments on commit 76909dd

Please sign in to comment.