From 6f44443a8927f2e42c2555ca664ccd87fc60e063 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Wed, 16 Jul 2025 19:28:21 +0300 Subject: [PATCH 01/95] feat: add id for recipe publication request table --- src/main/scala/db/tables/DbRecipePublicationRequest.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/db/tables/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/DbRecipePublicationRequest.scala index 4e8b576a..3f10ae6d 100644 --- a/src/main/scala/db/tables/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/DbRecipePublicationRequest.scala @@ -22,6 +22,7 @@ object DbRecipePublicationRequest: val createTable: String = """ CREATE TABLE IF NOT EXISTS recipe_publication_requests( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), recipe_id UUID NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, From 9df241bb8921e6f11670c4e310077e00ef8ee283 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Wed, 16 Jul 2025 21:52:19 +0300 Subject: [PATCH 02/95] feat: add id for recipe & ingredient publication request table --- .../DbIngredientPublicationRequest.scala | 19 +++++++++++-------- .../DbRecipePublicationRequest.scala | 6 ++++-- .../domain/IngredientPublicationRequest.scala | 2 ++ .../domain/RecipePublicationRequest.scala | 2 ++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala index 715f2d47..d3bdc46b 100644 --- a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala @@ -3,24 +3,27 @@ package db.tables.publication import domain.{IngredientId, IngredientPublicationRequest} import java.time.OffsetDateTime +import java.util.UUID final case class DbIngredientPublicationRequest( - ingredientId: IngredientId, - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - status: DbPublicationRequestStatus, - reason: Option[String], - ): + id: UUID, + ingredientId: IngredientId, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: DbPublicationRequestStatus, + reason: Option[String], +): def toDomain: IngredientPublicationRequest = - IngredientPublicationRequest(ingredientId, createdAt, updatedAt, status.toDomain(reason)) + IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason)) object DbIngredientPublicationRequest: def fromDomain(req: IngredientPublicationRequest): DbIngredientPublicationRequest = val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbIngredientPublicationRequest(req.ingredientId, req.createdAt, req.updatedAt, status, reason) + DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason) val createTable: String = """ CREATE TABLE IF NOT EXISTS ingredient_publication_requests( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ingredient_id UUID NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala index ada80541..7c6c3a31 100644 --- a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala @@ -3,8 +3,10 @@ package db.tables.publication import domain.{RecipeId, RecipePublicationRequest} import java.time.OffsetDateTime +import java.util.UUID final case class DbRecipePublicationRequest( + id: UUID, recipeId: RecipeId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, @@ -12,12 +14,12 @@ final case class DbRecipePublicationRequest( reason: Option[String], ): def toDomain: RecipePublicationRequest = - RecipePublicationRequest(recipeId, createdAt, updatedAt, status.toDomain(reason)) + RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status.toDomain(reason)) object DbRecipePublicationRequest: def fromDomain(req: RecipePublicationRequest): DbRecipePublicationRequest = val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbRecipePublicationRequest(req.recipeId, req.createdAt, req.updatedAt, status, reason) + DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason) val createTable: String = """ CREATE TABLE IF NOT EXISTS recipe_publication_requests( diff --git a/src/main/scala/domain/IngredientPublicationRequest.scala b/src/main/scala/domain/IngredientPublicationRequest.scala index 9e7874d5..cf4aeb87 100644 --- a/src/main/scala/domain/IngredientPublicationRequest.scala +++ b/src/main/scala/domain/IngredientPublicationRequest.scala @@ -1,8 +1,10 @@ package domain import java.time.OffsetDateTime +import java.util.UUID final case class IngredientPublicationRequest( + id: UUID, ingredientId: IngredientId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, diff --git a/src/main/scala/domain/RecipePublicationRequest.scala b/src/main/scala/domain/RecipePublicationRequest.scala index 16921aea..58db8ba8 100644 --- a/src/main/scala/domain/RecipePublicationRequest.scala +++ b/src/main/scala/domain/RecipePublicationRequest.scala @@ -1,8 +1,10 @@ package domain import java.time.OffsetDateTime +import java.util.UUID final case class RecipePublicationRequest( + id: UUID, recipeId: RecipeId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, From b874e0e3e0134e913c41b5e3e6c0138f41ac8afd Mon Sep 17 00:00:00 2001 From: Leropsis Date: Wed, 16 Jul 2025 21:59:16 +0300 Subject: [PATCH 03/95] chore: add retrieving of all pending requests from publication requests --- .../db/repositories/IngredientPublicationRequestRepo.scala | 4 ++++ .../scala/db/repositories/RecipePublicationRequestsRepo.scala | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index fb64931a..aecc8b24 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -11,6 +11,7 @@ import javax.sql.DataSource trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] + def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] private inline def ingredientPublicationRequests = query[DbIngredientPublicationRequest] @@ -25,6 +26,9 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] = run(requestPublicationQ(lift(ingredientId))).unit.provideDS + override def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] = + run(allPendingQ).provideDS + object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* inline def requestPublicationQ(inline ingredientId: IngredientId): Insert[DbIngredientPublicationRequest] = diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index a600f1ee..207db53f 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -11,6 +11,7 @@ import javax.sql.DataSource trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] + def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest ]] private inline def recipePublicationRequests = query[DbRecipePublicationRequest] @@ -25,6 +26,9 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(recipeId: RecipeId): IO[DbError, Unit] = run(requestPublicationQ(lift(recipeId))).unit.provideDS + override def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] = + run(allPendingQ).provideDS + object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* From 209d1bf3bb3a6fa2b23526732f18948459dcc782 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Wed, 16 Jul 2025 23:15:09 +0300 Subject: [PATCH 04/95] chore: add GET /moderation/publication-requests -> List[PublicationRequest] endpoint --- src/main/scala/api/moderation/Endpoints.scala | 13 ++++ .../moderation/pubrequests/Endpoints.scala | 14 +++++ .../pubrequests/GetSomePending.scala | 63 +++++++++++++++++++ .../PublicationRequestSummary.scala | 15 +++++ 4 files changed, 105 insertions(+) create mode 100644 src/main/scala/api/moderation/Endpoints.scala create mode 100644 src/main/scala/api/moderation/pubrequests/Endpoints.scala create mode 100644 src/main/scala/api/moderation/pubrequests/GetSomePending.scala create mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala diff --git a/src/main/scala/api/moderation/Endpoints.scala b/src/main/scala/api/moderation/Endpoints.scala new file mode 100644 index 00000000..ab09f7a1 --- /dev/null +++ b/src/main/scala/api/moderation/Endpoints.scala @@ -0,0 +1,13 @@ +package api.moderation + +import sttp.tapir.Endpoint +import sttp.tapir.ztapir.* + +import api.TapirExtensions.superTag + +val moderationEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = + endpoint + .superTag("Moderation") + .in("moderation") + +val moderationEndpoints = ??? diff --git a/src/main/scala/api/moderation/pubrequests/Endpoints.scala b/src/main/scala/api/moderation/pubrequests/Endpoints.scala new file mode 100644 index 00000000..ec68505e --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/Endpoints.scala @@ -0,0 +1,14 @@ +package api.moderation.pubrequests + +import api.moderation.moderationEndpoint +import sttp.tapir.Endpoint +import sttp.tapir.ztapir.* + +import api.TapirExtensions.subTag + +val publicationRequestEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = + moderationEndpoint + .subTag("publication-requests") + .in("publication-requests") + +val publicationRequestEndpoints = ??? diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala new file mode 100644 index 00000000..366c953e --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -0,0 +1,63 @@ +package api.moderation.pubrequests + +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.EndpointErrorVariants.serverErrorVariant +import api.common.search.PaginationParams +import api.moderation.pubrequests.PublicationRequestType.* +import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} +import domain.{IngredientPublicationRequest, InternalServerError, RecipePublicationRequest} +import io.circe.generic.auto.* +import sttp.model.StatusCode.NoContent +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.ztapir.* +import zio.ZIO + +private type GetSomePendingEnv + = RecipePublicationRequestsRepo + & IngredientPublicationRequestsRepo + & RecipesRepo + & IngredientsRepo + +private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest + +private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = + publicationRequestEndpoint + .get + .in(PaginationParams.query) + .out(statusCode(NoContent)) + .out(jsonBody[Seq[PublicationRequestSummary]]) + .errorOut(oneOf(serverErrorVariant)) + .zSecuredServerLogic(getSomePendingHandler) + +private def getSomePendingHandler(paginationParams: PaginationParams): + ZIO[AuthenticatedUser & GetSomePendingEnv & RecipesRepo & IngredientsRepo, InternalServerError, Seq[PublicationRequestSummary]] = { + + def toPublicationRequest(req: PublicationRequest) = req match // some shitty code... I will fix this later + case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _) => + ZIO.serviceWithZIO[RecipesRepo](_.getRecipe(entityId)) + .someOrFail(InternalServerError()) + .map(recipe => PublicationRequestSummary(id, Recipe, recipe.name, createdAt)) + case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _) => + ZIO.serviceWithZIO[IngredientsRepo](_.get(entityId)) + .someOrFail(InternalServerError()) + .map(recipe => PublicationRequestSummary(id, Ingredient, recipe.name, createdAt)) + + paginationParams match + case PaginationParams(count, offset) => { + for + pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) + .flatMap { reqs => + ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + } + pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) + .flatMap { reqs => + ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + } + yield (pendingRecipeReqs ++ pendingIngredientReqs) + .sortWith((sum1, sum2) => sum1.createdAt.toEpochSecond < sum2.createdAt.toEpochSecond) + .slice(offset, offset + count) + }.mapError(_ => InternalServerError()) +} + + diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala new file mode 100644 index 00000000..5985493f --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala @@ -0,0 +1,15 @@ +package api.moderation.pubrequests + +import java.time.OffsetDateTime +import java.util.UUID + +enum PublicationRequestType: + case Ingredient extends PublicationRequestType + case Recipe extends PublicationRequestType + +case class PublicationRequestSummary( + id: UUID, + requestType: PublicationRequestType, + entityName: String, + createdAt: OffsetDateTime +) From 5ddd2955655fe4ac8e0ed91f818d5415bd58b975 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Thu, 17 Jul 2025 00:31:53 +0300 Subject: [PATCH 05/95] chore: add GET /moderation/publication-requests/{id} -> PublicationRequest endpoint --- .../scala/api/EndpointErrorVariants.scala | 2 + .../api/moderation/pubrequests/Get.scala | 95 +++++++++++++++++++ .../pubrequests/GetSomePending.scala | 11 ++- .../PublicationRequestResponse.scala | 17 ++++ .../IngredientPublicationRequestRepo.scala | 10 +- .../RecipePublicationRequestsRepo.scala | 9 +- .../DbIngredientPublicationRequest.scala | 5 +- .../DbRecipePublicationRequest.scala | 5 +- src/main/scala/domain/Errors.scala | 5 + .../domain/IngredientPublicationRequest.scala | 1 + .../domain/RecipePublicationRequest.scala | 1 + 11 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 src/main/scala/api/moderation/pubrequests/Get.scala create mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala diff --git a/src/main/scala/api/EndpointErrorVariants.scala b/src/main/scala/api/EndpointErrorVariants.scala index d66264e0..32005059 100644 --- a/src/main/scala/api/EndpointErrorVariants.scala +++ b/src/main/scala/api/EndpointErrorVariants.scala @@ -9,6 +9,7 @@ import domain.{ StorageAccessForbidden, StorageNotFound, UserNotFound, + PublicationRequestNotFound } import io.circe.{Decoder, Encoder} @@ -28,6 +29,7 @@ extension (sc: StatusCode) object EndpointErrorVariants: val ingredientNotFoundVariant = NotFound.variantJson[IngredientNotFound] val storageNotFoundVariant = NotFound.variantJson[StorageNotFound] + val publicationRequestNotFound = NotFound.variantJson[PublicationRequestNotFound] val storageAccessForbiddenVariant = Forbidden.variantJson[StorageAccessForbidden] val recipeAccessForbiddenVariant = NotFound.variantJson[RecipeAccessForbidden] val userNotFoundVariant = NotFound.variantJson[UserNotFound] diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala new file mode 100644 index 00000000..ac09c220 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -0,0 +1,95 @@ +package api.moderation.pubrequests + +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.moderation.pubrequests.PublicationRequestType.* +import db.DbError.{DbNotRespondingError, FailedDbQuery} +import db.repositories.{ + IngredientPublicationRequestsRepo, + IngredientsRepo, + RecipePublicationRequestsRepo, + RecipesRepo +} +import domain.{ + IngredientPublicationRequest, + InternalServerError, + PublicationRequestNotFound, + RecipePublicationRequest +} +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.ztapir.* +import zio.ZIO + +import java.util.UUID + +private type GetReq + = RecipePublicationRequestsRepo + & IngredientPublicationRequestsRepo + & RecipesRepo + & IngredientsRepo + +private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest + +private val getRequest: ZServerEndpoint[GetReq, Any] = + publicationRequestEndpoint + .get + .in(query[UUID]("id")) + .out(jsonBody[PublicationRequestResponse]) + .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) + .zSecuredServerLogic(getRequestHandler) + +private def getRequestHandler(reqId: UUID): + ZIO[AuthenticatedUser & GetReq, + InternalServerError | PublicationRequestNotFound, + PublicationRequestResponse] = + + def getIngredientRequest = + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) + .flatMap { + _.map { dbEntity => + dbEntity.toDomain match + case IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status, comment) => + ZIO.serviceWithZIO[IngredientsRepo] { + _.get(ingredientId).some.map { ingredient => + PublicationRequestResponse( + reqId, + Ingredient, + ingredientId, + ingredient.name, + createdAt, + updatedAt, + comment, + status + ) + } + } + }.getOrElse(ZIO.fail(PublicationRequestNotFound(reqId.toString))) + } + + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(reqId)) + .flatMap { + _.map { dbEntity => + dbEntity.toDomain match + case RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status, comment) => + ZIO.serviceWithZIO[RecipesRepo] { + _.getRecipe(recipeId).some.map { recipe => + PublicationRequestResponse( + reqId, + Recipe, + recipeId, + recipe.name, + createdAt, + updatedAt, + comment, + status + ) + } + } + }.getOrElse(getIngredientRequest) + }.mapError { + case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId.toString) + case _: DbNotRespondingError => InternalServerError() + case x: PublicationRequestNotFound => x + } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 366c953e..c76fd9b6 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -4,7 +4,12 @@ import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.serverErrorVariant import api.common.search.PaginationParams import api.moderation.pubrequests.PublicationRequestType.* -import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} +import db.repositories.{ + IngredientPublicationRequestsRepo, + IngredientsRepo, + RecipePublicationRequestsRepo, + RecipesRepo +} import domain.{IngredientPublicationRequest, InternalServerError, RecipePublicationRequest} import io.circe.generic.auto.* import sttp.model.StatusCode.NoContent @@ -34,11 +39,11 @@ private def getSomePendingHandler(paginationParams: PaginationParams): ZIO[AuthenticatedUser & GetSomePendingEnv & RecipesRepo & IngredientsRepo, InternalServerError, Seq[PublicationRequestSummary]] = { def toPublicationRequest(req: PublicationRequest) = req match // some shitty code... I will fix this later - case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _) => + case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _, _) => ZIO.serviceWithZIO[RecipesRepo](_.getRecipe(entityId)) .someOrFail(InternalServerError()) .map(recipe => PublicationRequestSummary(id, Recipe, recipe.name, createdAt)) - case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _) => + case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _, _) => ZIO.serviceWithZIO[IngredientsRepo](_.get(entityId)) .someOrFail(InternalServerError()) .map(recipe => PublicationRequestSummary(id, Ingredient, recipe.name, createdAt)) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala new file mode 100644 index 00000000..4d530678 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala @@ -0,0 +1,17 @@ +package api.moderation.pubrequests + +import domain.PublicationRequestStatus + +import java.time.OffsetDateTime +import java.util.UUID + +case class PublicationRequestResponse( + id: UUID, + requestType: PublicationRequestType, + entityId: UUID, + entityName: String, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + comment: String, + status: PublicationRequestStatus +) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index aecc8b24..1828d409 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -7,11 +7,13 @@ import domain.IngredientId import io.getquill.* import zio.{IO, RLayer, ZLayer} +import java.util.UUID import javax.sql.DataSource trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] + def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] private inline def ingredientPublicationRequests = query[DbIngredientPublicationRequest] @@ -29,13 +31,19 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] = run(allPendingQ).provideDS + override def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] = + run(getQ(id)).map(_.headOption).provideDS + object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* inline def requestPublicationQ(inline ingredientId: IngredientId): Insert[DbIngredientPublicationRequest] = ingredientPublicationRequests.insert(_.ingredientId -> ingredientId) inline def allPendingQ = ingredientPublicationRequests.filter(_.status == lift(Pending)) - inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) + inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) + + inline def getQ(inline id: UUID) = + ingredientPublicationRequests.filter(_.id == lift(id)).take(1) object IngredientPublicationRequestsRepo: def layer: RLayer[DataSource, IngredientPublicationRequestsRepo] = diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 207db53f..fe5ee9af 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -7,11 +7,13 @@ import domain.RecipeId import io.getquill.* import zio.{IO, RLayer, ZLayer} +import java.util.UUID import javax.sql.DataSource trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] - def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest ]] + def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] + def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] private inline def recipePublicationRequests = query[DbRecipePublicationRequest] @@ -29,6 +31,9 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] = run(allPendingQ).provideDS + override def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] = + run(getQ(id)).map(_.headOption).provideDS + object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* @@ -37,6 +42,8 @@ object RecipePublicationRequestsQueries: inline def allPendingQ = recipePublicationRequests.filter(_.status == lift(Pending)) inline def pendingRequestsByIdQ(inline recipeId: RecipeId) = allPendingQ.filter(_.recipeId == recipeId) + inline def getQ(inline id: UUID) = + recipePublicationRequests.filter(_.id == lift(id)).take(1) object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = diff --git a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala index d3bdc46b..3505936b 100644 --- a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala @@ -12,14 +12,15 @@ final case class DbIngredientPublicationRequest( updatedAt: OffsetDateTime, status: DbPublicationRequestStatus, reason: Option[String], + comment: String ): def toDomain: IngredientPublicationRequest = - IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason)) + IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason), comment) object DbIngredientPublicationRequest: def fromDomain(req: IngredientPublicationRequest): DbIngredientPublicationRequest = val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason) + DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason, req.comment) val createTable: String = """ CREATE TABLE IF NOT EXISTS ingredient_publication_requests( diff --git a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala index 7c6c3a31..05acde59 100644 --- a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala @@ -12,14 +12,15 @@ final case class DbRecipePublicationRequest( updatedAt: OffsetDateTime, status: DbPublicationRequestStatus, reason: Option[String], + comment: String ): def toDomain: RecipePublicationRequest = - RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status.toDomain(reason)) + RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status.toDomain(reason), comment) object DbRecipePublicationRequest: def fromDomain(req: RecipePublicationRequest): DbRecipePublicationRequest = val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason) + DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason, req.comment) val createTable: String = """ CREATE TABLE IF NOT EXISTS recipe_publication_requests( diff --git a/src/main/scala/domain/Errors.scala b/src/main/scala/domain/Errors.scala index 06593d5c..a202f21e 100644 --- a/src/main/scala/domain/Errors.scala +++ b/src/main/scala/domain/Errors.scala @@ -36,3 +36,8 @@ final case class InvalidInvitationHash( hash: String, message: String = "Invalid invitation hash", ) + +final case class PublicationRequestNotFound( + requestId: String, + message: String = "Publication request not found" +) diff --git a/src/main/scala/domain/IngredientPublicationRequest.scala b/src/main/scala/domain/IngredientPublicationRequest.scala index cf4aeb87..22dc75d4 100644 --- a/src/main/scala/domain/IngredientPublicationRequest.scala +++ b/src/main/scala/domain/IngredientPublicationRequest.scala @@ -9,5 +9,6 @@ final case class IngredientPublicationRequest( createdAt: OffsetDateTime, updatedAt: OffsetDateTime, status: PublicationRequestStatus, + comment: String ) diff --git a/src/main/scala/domain/RecipePublicationRequest.scala b/src/main/scala/domain/RecipePublicationRequest.scala index 58db8ba8..04d7ffd3 100644 --- a/src/main/scala/domain/RecipePublicationRequest.scala +++ b/src/main/scala/domain/RecipePublicationRequest.scala @@ -9,5 +9,6 @@ final case class RecipePublicationRequest( createdAt: OffsetDateTime, updatedAt: OffsetDateTime, status: PublicationRequestStatus, + comment: String ) From d20dd6beded7c3c695ff7152e701fe26d4290913 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Thu, 17 Jul 2025 01:54:05 +0300 Subject: [PATCH 06/95] chore: add PATCH /moderation/publication-requests/{id} endpoint --- src/main/scala/api/Endpoints.scala | 3 +- src/main/scala/api/moderation/Endpoints.scala | 4 +- .../moderation/pubrequests/Endpoints.scala | 6 ++- .../api/moderation/pubrequests/Get.scala | 8 ++-- .../pubrequests/GetSomePending.scala | 2 - .../PublicationRequestUpdate.scala | 8 ++++ .../api/moderation/pubrequests/Update.scala | 46 +++++++++++++++++++ .../IngredientPublicationRequestRepo.scala | 25 ++++++++-- .../RecipePublicationRequestsRepo.scala | 28 +++++++++-- 9 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala create mode 100644 src/main/scala/api/moderation/pubrequests/Update.scala diff --git a/src/main/scala/api/Endpoints.scala b/src/main/scala/api/Endpoints.scala index 0a884fa1..00286de7 100644 --- a/src/main/scala/api/Endpoints.scala +++ b/src/main/scala/api/Endpoints.scala @@ -6,7 +6,7 @@ import api.users.usersEndpoints import api.recipes.recipeEndpoints import api.shoppinglist.shoppingListEndpoints import api.invitations.invitationEndpoints - +import api.moderation.moderationEndpoints import sttp.tapir.ztapir.ZServerEndpoint import sttp.tapir.ztapir.RichZServerEndpoint @@ -18,3 +18,4 @@ object AppEndpoints: ++ recipeEndpoints.map(_.widen) ++ shoppingListEndpoints.map(_.widen) ++ invitationEndpoints.map(_.widen) + ++ moderationEndpoints.map(_.widen) diff --git a/src/main/scala/api/moderation/Endpoints.scala b/src/main/scala/api/moderation/Endpoints.scala index ab09f7a1..ad80b159 100644 --- a/src/main/scala/api/moderation/Endpoints.scala +++ b/src/main/scala/api/moderation/Endpoints.scala @@ -2,12 +2,12 @@ package api.moderation import sttp.tapir.Endpoint import sttp.tapir.ztapir.* - import api.TapirExtensions.superTag +import api.moderation.pubrequests.publicationRequestEndpoints val moderationEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = endpoint .superTag("Moderation") .in("moderation") -val moderationEndpoints = ??? +val moderationEndpoints = publicationRequestEndpoints diff --git a/src/main/scala/api/moderation/pubrequests/Endpoints.scala b/src/main/scala/api/moderation/pubrequests/Endpoints.scala index ec68505e..2b64ce0e 100644 --- a/src/main/scala/api/moderation/pubrequests/Endpoints.scala +++ b/src/main/scala/api/moderation/pubrequests/Endpoints.scala @@ -11,4 +11,8 @@ val publicationRequestEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = .subTag("publication-requests") .in("publication-requests") -val publicationRequestEndpoints = ??? +val publicationRequestEndpoints = List( + getSomePending.widen, + getRequest.widen, + updatePublicationRequest.widen +) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index ac09c220..f45c7fd2 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -24,7 +24,7 @@ import zio.ZIO import java.util.UUID -private type GetReq +private type GetReqEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo & RecipesRepo @@ -32,16 +32,16 @@ private type GetReq private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest -private val getRequest: ZServerEndpoint[GetReq, Any] = +private val getRequest: ZServerEndpoint[GetReqEnv, Any] = publicationRequestEndpoint .get - .in(query[UUID]("id")) + .in(path[UUID]("id")) .out(jsonBody[PublicationRequestResponse]) .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) .zSecuredServerLogic(getRequestHandler) private def getRequestHandler(reqId: UUID): - ZIO[AuthenticatedUser & GetReq, + ZIO[AuthenticatedUser & GetReqEnv, InternalServerError | PublicationRequestNotFound, PublicationRequestResponse] = diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index c76fd9b6..5f8dec9b 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -64,5 +64,3 @@ private def getSomePendingHandler(paginationParams: PaginationParams): .slice(offset, offset + count) }.mapError(_ => InternalServerError()) } - - diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala new file mode 100644 index 00000000..d44d5534 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala @@ -0,0 +1,8 @@ +package api.moderation.pubrequests + +import domain.PublicationRequestStatus + +case class PublicationRequestUpdate( + comment: String, + status: PublicationRequestStatus +) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala new file mode 100644 index 00000000..2fa07582 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -0,0 +1,46 @@ +package api.moderation.pubrequests + +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.moderation.pubrequests.PublicationRequestType.* +import db.DbError +import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} +import db.tables.publication.DbPublicationRequestStatus +import domain.{InternalServerError, PublicationRequestNotFound} +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.ztapir.* +import zio.ZIO +import sttp.model.StatusCode.NoContent + +import java.util.UUID + +private type UpdateReqEnv + = RecipePublicationRequestsRepo + & IngredientPublicationRequestsRepo + + +private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = + publicationRequestEndpoint + .patch + .in(query[UUID]("id")) + .in(jsonBody[PublicationRequestUpdate]) + .out(statusCode(NoContent)) + .errorOut(oneOf(publicationRequestNotFound, serverErrorVariant)) + .zSecuredServerLogic(updatePublicationRequestHandler) + +private def updatePublicationRequestHandler(id: UUID, reqBody: PublicationRequestUpdate): + ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = + + reqBody match + case PublicationRequestUpdate(comment, status) => + val (_, dbStatus) = DbPublicationRequestStatus.fromDomain(status) + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, comment, dbStatus)) + .catchSome { + case _: PublicationRequestNotFound => + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, comment, dbStatus)) + }.mapError { + case x: PublicationRequestNotFound => x + case _: DbError => InternalServerError() + } diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 1828d409..b0f22d20 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -1,11 +1,11 @@ package db.repositories import db.DbError -import db.tables.publication.DbIngredientPublicationRequest +import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} import db.tables.publication.DbPublicationRequestStatus.Pending -import domain.IngredientId +import domain.{IngredientId, PublicationRequestNotFound} import io.getquill.* -import zio.{IO, RLayer, ZLayer} +import zio.{IO, RLayer, ZLayer, ZIO} import java.util.UUID import javax.sql.DataSource @@ -14,6 +14,8 @@ trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] + def update(id: UUID, comment: String, status: DbPublicationRequestStatus): + IO[DbError | PublicationRequestNotFound, Unit] private inline def ingredientPublicationRequests = query[DbIngredientPublicationRequest] @@ -34,6 +36,14 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS + override def update(id: IngredientId, comment: String, status: DbPublicationRequestStatus): + IO[DbError | PublicationRequestNotFound, Unit] = + + run(updateQ(id, comment, status)).provideDS.flatMap { + case 0 => ZIO.fail(PublicationRequestNotFound(id.toString)) + case _ => ZIO.unit + } + object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* inline def requestPublicationQ(inline ingredientId: IngredientId): Insert[DbIngredientPublicationRequest] = @@ -45,6 +55,13 @@ object IngredientPublicationRequestsQueries: inline def getQ(inline id: UUID) = ingredientPublicationRequests.filter(_.id == lift(id)).take(1) + inline def updateQ(inline id: UUID, inline comment: String, inline status: DbPublicationRequestStatus) = + ingredientPublicationRequests.filter(_.id == lift(id)) + .update( + _.comment -> lift(comment), + _.status -> lift(status) + ) + object IngredientPublicationRequestsRepo: def layer: RLayer[DataSource, IngredientPublicationRequestsRepo] = - ZLayer.fromFunction(IngredientPublicationRequestsRepoLive.apply) \ No newline at end of file + ZLayer.fromFunction(IngredientPublicationRequestsRepoLive.apply) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index fe5ee9af..d4704c53 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -1,11 +1,12 @@ package db.repositories import db.DbError -import db.tables.publication.DbPublicationRequestStatus.Pending -import db.tables.publication.DbRecipePublicationRequest -import domain.RecipeId +import db.QuillConfig.ctx +import db.tables.publication.DbPublicationRequestStatus.{Pending, createType} +import db.tables.publication.{DbPublicationRequestStatus, DbRecipePublicationRequest} +import domain.{RecipeId, PublicationRequestNotFound} import io.getquill.* -import zio.{IO, RLayer, ZLayer} +import zio.{IO, RLayer, ZLayer, ZIO} import java.util.UUID import javax.sql.DataSource @@ -14,6 +15,8 @@ trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] + def update(id: UUID, comment: String, status: DbPublicationRequestStatus): + IO[DbError | PublicationRequestNotFound, Unit] private inline def recipePublicationRequests = query[DbRecipePublicationRequest] @@ -34,6 +37,14 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS + override def update(id: RecipeId, comment: String, status: DbPublicationRequestStatus): + IO[DbError | PublicationRequestNotFound, Unit] = + + run(updateQ(id, comment, status)).provideDS.flatMap { + case 0 => ZIO.fail(PublicationRequestNotFound(id.toString)) + case _ => ZIO.unit + } + object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* @@ -41,9 +52,18 @@ object RecipePublicationRequestsQueries: recipePublicationRequests.insert(_.recipeId -> recipeId) inline def allPendingQ = recipePublicationRequests.filter(_.status == lift(Pending)) + inline def pendingRequestsByIdQ(inline recipeId: RecipeId) = allPendingQ.filter(_.recipeId == recipeId) + inline def getQ(inline id: UUID) = recipePublicationRequests.filter(_.id == lift(id)).take(1) + + inline def updateQ(inline id: UUID, inline comment: String, inline status: DbPublicationRequestStatus) = + recipePublicationRequests.filter(_.id == lift(id)) + .update( + _.comment -> lift(comment), + _.status -> lift(status) + ) object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = From bb75620395617175bfbf48f7319ddfc66631dd6a Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 10:48:37 +0300 Subject: [PATCH 07/95] refactor: moved things around for more clarity --- .../api/ingredients/RequestPublication.scala | 18 +++++----- src/main/scala/api/moderation/Endpoints.scala | 6 ++-- .../moderation/pubrequests/Endpoints.scala | 6 ++-- .../api/moderation/pubrequests/Get.scala | 17 ++++++++-- .../pubrequests/GetSomePending.scala | 12 ++++++- .../PublicationRequestResponse.scala | 17 ---------- .../PublicationRequestSummary.scala | 15 -------- .../pubrequests/PublicationRequestType.scala | 4 +++ .../PublicationRequestUpdate.scala | 8 ----- .../api/moderation/pubrequests/Update.scala | 34 ++++++++++--------- 10 files changed, 63 insertions(+), 74 deletions(-) delete mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala delete mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala create mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala delete mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index b53184a3..75395646 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -4,17 +4,17 @@ import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.variantJson import api.EndpointErrorVariants.{ingredientNotFoundVariant, serverErrorVariant} import db.repositories.{ - IngredientPublicationRequestsRepo, IngredientsQueries, + IngredientPublicationRequestsRepo, IngredientsRepo, IngredientPublicationRequestsQueries } -import domain.{IngredientId, IngredientNotFound, InternalServerError, RecipeId} +import domain.{IngredientId, IngredientNotFound, InternalServerError} import db.tables.publication.DbPublicationRequestStatus.given import db.QuillConfig.provideDS import db.QuillConfig.ctx.* +import io.circe.generic.auto.* import io.getquill.* import javax.sql.DataSource -import io.circe.generic.auto.* import sttp.model.StatusCode.BadRequest import sttp.tapir.generic.auto.* import sttp.tapir.ztapir.* @@ -44,19 +44,19 @@ private val requestPublication: ZServerEndpoint[RequestPublicationEnv, Any] = IngredientAlreadyPending.variant, ingredientNotFoundVariant )) .zSecuredServerLogic(requestPublicationHandler) - -def requestPublicationHandler(ingredientId: IngredientId): + +def requestPublicationHandler(ingredientId: IngredientId): ZIO[ AuthenticatedUser & RequestPublicationEnv, InternalServerError | IngredientAlreadyPublished | IngredientAlreadyPending | IngredientNotFound, Unit ] = - for + for ingredient <- ZIO.serviceWithZIO[IngredientsRepo](_ .get(ingredientId) .some.orElseFail(IngredientNotFound(ingredientId.toString)) ) - + _ <- ZIO.fail(IngredientAlreadyPublished(ingredientId)) .when(ingredient.isPublished) @@ -73,5 +73,5 @@ def requestPublicationHandler(ingredientId: IngredientId): _ <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ .requestPublication(ingredientId) .orElseFail(InternalServerError()) - ) - yield () \ No newline at end of file + ) + yield () diff --git a/src/main/scala/api/moderation/Endpoints.scala b/src/main/scala/api/moderation/Endpoints.scala index ad80b159..40c93c31 100644 --- a/src/main/scala/api/moderation/Endpoints.scala +++ b/src/main/scala/api/moderation/Endpoints.scala @@ -1,13 +1,13 @@ package api.moderation +import api.moderation.pubrequests.publicationRequestEndpoints + import sttp.tapir.Endpoint import sttp.tapir.ztapir.* -import api.TapirExtensions.superTag -import api.moderation.pubrequests.publicationRequestEndpoints val moderationEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = endpoint - .superTag("Moderation") + .tag("Moderation") .in("moderation") val moderationEndpoints = publicationRequestEndpoints diff --git a/src/main/scala/api/moderation/pubrequests/Endpoints.scala b/src/main/scala/api/moderation/pubrequests/Endpoints.scala index 2b64ce0e..6eb38c2a 100644 --- a/src/main/scala/api/moderation/pubrequests/Endpoints.scala +++ b/src/main/scala/api/moderation/pubrequests/Endpoints.scala @@ -1,14 +1,14 @@ package api.moderation.pubrequests import api.moderation.moderationEndpoint +import api.TapirExtensions.subTag + import sttp.tapir.Endpoint import sttp.tapir.ztapir.* -import api.TapirExtensions.subTag - val publicationRequestEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = moderationEndpoint - .subTag("publication-requests") + .subTag("Publication Requests") .in("publication-requests") val publicationRequestEndpoints = List( diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index f45c7fd2..6b22775b 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -14,15 +14,28 @@ import domain.{ IngredientPublicationRequest, InternalServerError, PublicationRequestNotFound, - RecipePublicationRequest + PublicationRequestStatus, + RecipePublicationRequest, } + import io.circe.generic.auto.* +import java.time.OffsetDateTime +import java.util.UUID import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO -import java.util.UUID +final case class PublicationRequestResponse( + id: UUID, + requestType: PublicationRequestType, + entityId: UUID, + entityName: String, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + comment: String, + status: PublicationRequestStatus +) private type GetReqEnv = RecipePublicationRequestsRepo diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 5f8dec9b..f613356c 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -11,13 +11,23 @@ import db.repositories.{ RecipesRepo } import domain.{IngredientPublicationRequest, InternalServerError, RecipePublicationRequest} + import io.circe.generic.auto.* +import java.time.OffsetDateTime +import java.util.UUID import sttp.model.StatusCode.NoContent import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO +final case class PublicationRequestSummary( + id: UUID, + requestType: PublicationRequestType, + entityName: String, + createdAt: OffsetDateTime +) + private type GetSomePendingEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo @@ -46,7 +56,7 @@ private def getSomePendingHandler(paginationParams: PaginationParams): case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _, _) => ZIO.serviceWithZIO[IngredientsRepo](_.get(entityId)) .someOrFail(InternalServerError()) - .map(recipe => PublicationRequestSummary(id, Ingredient, recipe.name, createdAt)) + .map(ingredient => PublicationRequestSummary(id, Ingredient, ingredient.name, createdAt)) paginationParams match case PaginationParams(count, offset) => { diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala deleted file mode 100644 index 4d530678..00000000 --- a/src/main/scala/api/moderation/pubrequests/PublicationRequestResponse.scala +++ /dev/null @@ -1,17 +0,0 @@ -package api.moderation.pubrequests - -import domain.PublicationRequestStatus - -import java.time.OffsetDateTime -import java.util.UUID - -case class PublicationRequestResponse( - id: UUID, - requestType: PublicationRequestType, - entityId: UUID, - entityName: String, - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - comment: String, - status: PublicationRequestStatus -) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala deleted file mode 100644 index 5985493f..00000000 --- a/src/main/scala/api/moderation/pubrequests/PublicationRequestSummary.scala +++ /dev/null @@ -1,15 +0,0 @@ -package api.moderation.pubrequests - -import java.time.OffsetDateTime -import java.util.UUID - -enum PublicationRequestType: - case Ingredient extends PublicationRequestType - case Recipe extends PublicationRequestType - -case class PublicationRequestSummary( - id: UUID, - requestType: PublicationRequestType, - entityName: String, - createdAt: OffsetDateTime -) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala new file mode 100644 index 00000000..73decec7 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala @@ -0,0 +1,4 @@ +package api.moderation.pubrequests + +enum PublicationRequestType: + case Ingredient, Recipe diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala deleted file mode 100644 index d44d5534..00000000 --- a/src/main/scala/api/moderation/pubrequests/PublicationRequestUpdate.scala +++ /dev/null @@ -1,8 +0,0 @@ -package api.moderation.pubrequests - -import domain.PublicationRequestStatus - -case class PublicationRequestUpdate( - comment: String, - status: PublicationRequestStatus -) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 2fa07582..b403bcd7 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -6,21 +6,25 @@ import api.moderation.pubrequests.PublicationRequestType.* import db.DbError import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} import db.tables.publication.DbPublicationRequestStatus -import domain.{InternalServerError, PublicationRequestNotFound} +import domain.{PublicationRequestStatus, InternalServerError, PublicationRequestNotFound} + import io.circe.generic.auto.* +import java.util.UUID +import sttp.model.StatusCode.NoContent import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO -import sttp.model.StatusCode.NoContent -import java.util.UUID +final case class PublicationRequestUpdate( + comment: String, + status: PublicationRequestStatus +) private type UpdateReqEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo - private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = publicationRequestEndpoint .patch @@ -32,15 +36,13 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = private def updatePublicationRequestHandler(id: UUID, reqBody: PublicationRequestUpdate): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = - - reqBody match - case PublicationRequestUpdate(comment, status) => - val (_, dbStatus) = DbPublicationRequestStatus.fromDomain(status) - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, comment, dbStatus)) - .catchSome { - case _: PublicationRequestNotFound => - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, comment, dbStatus)) - }.mapError { - case x: PublicationRequestNotFound => x - case _: DbError => InternalServerError() - } + val PublicationRequestUpdate(comment, status) = reqBody + val (_, dbStatus) = DbPublicationRequestStatus.fromDomain(status) + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, comment, dbStatus)) + .catchSome { + case _: PublicationRequestNotFound => + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, comment, dbStatus)) + }.mapError { + case x: PublicationRequestNotFound => x + case _: DbError => InternalServerError() + } From e642db7e04832fc2f5b5c7e01750435b3175a910 Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 11:12:01 +0300 Subject: [PATCH 08/95] refactor: introduce PublicationRequestId --- .../api/moderation/pubrequests/Get.scala | 75 +++++++++---------- .../pubrequests/GetSomePending.scala | 31 ++++---- .../api/moderation/pubrequests/Update.scala | 8 +- .../IngredientPublicationRequestRepo.scala | 40 +++++----- .../RecipePublicationRequestsRepo.scala | 44 ++++++----- src/main/scala/domain/Errors.scala | 2 +- src/main/scala/domain/Ids.scala | 1 + .../domain/IngredientPublicationRequest.scala | 3 +- .../domain/RecipePublicationRequest.scala | 3 +- 9 files changed, 103 insertions(+), 104 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index 6b22775b..feb9c452 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -26,7 +26,7 @@ import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO -final case class PublicationRequestResponse( +final case class PublicationRequestResp( id: UUID, requestType: PublicationRequestType, entityId: UUID, @@ -49,60 +49,57 @@ private val getRequest: ZServerEndpoint[GetReqEnv, Any] = publicationRequestEndpoint .get .in(path[UUID]("id")) - .out(jsonBody[PublicationRequestResponse]) + .out(jsonBody[PublicationRequestResp]) .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) .zSecuredServerLogic(getRequestHandler) private def getRequestHandler(reqId: UUID): ZIO[AuthenticatedUser & GetReqEnv, - InternalServerError | PublicationRequestNotFound, - PublicationRequestResponse] = - + InternalServerError | PublicationRequestNotFound, + PublicationRequestResp] = def getIngredientRequest = ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) .flatMap { _.map { dbEntity => - dbEntity.toDomain match - case IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status, comment) => - ZIO.serviceWithZIO[IngredientsRepo] { - _.get(ingredientId).some.map { ingredient => - PublicationRequestResponse( - reqId, - Ingredient, - ingredientId, - ingredient.name, - createdAt, - updatedAt, - comment, - status - ) - } - } - }.getOrElse(ZIO.fail(PublicationRequestNotFound(reqId.toString))) + val IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status, comment) = dbEntity.toDomain + ZIO.serviceWithZIO[IngredientsRepo] { + _.get(ingredientId).some.map { ingredient => + PublicationRequestResp( + reqId, + Ingredient, + ingredientId, + ingredient.name, + createdAt, + updatedAt, + comment, + status + ) + } + } + }.getOrElse(ZIO.fail(PublicationRequestNotFound(reqId))) } ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(reqId)) .flatMap { _.map { dbEntity => - dbEntity.toDomain match - case RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status, comment) => - ZIO.serviceWithZIO[RecipesRepo] { - _.getRecipe(recipeId).some.map { recipe => - PublicationRequestResponse( - reqId, - Recipe, - recipeId, - recipe.name, - createdAt, - updatedAt, - comment, - status - ) - } - } + val RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status, comment) = dbEntity.toDomain + ZIO.serviceWithZIO[RecipesRepo] { + _.getRecipe(recipeId).some.map { recipe => + PublicationRequestResp( + reqId, + Recipe, + recipeId, + recipe.name, + createdAt, + updatedAt, + comment, + status + ) + } + } }.getOrElse(getIngredientRequest) }.mapError { - case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId.toString) + case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId) case _: DbNotRespondingError => InternalServerError() case x: PublicationRequestNotFound => x } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index f613356c..084b0030 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -46,7 +46,7 @@ private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = .zSecuredServerLogic(getSomePendingHandler) private def getSomePendingHandler(paginationParams: PaginationParams): - ZIO[AuthenticatedUser & GetSomePendingEnv & RecipesRepo & IngredientsRepo, InternalServerError, Seq[PublicationRequestSummary]] = { + ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError, Seq[PublicationRequestSummary]] = def toPublicationRequest(req: PublicationRequest) = req match // some shitty code... I will fix this later case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _, _) => @@ -58,19 +58,16 @@ private def getSomePendingHandler(paginationParams: PaginationParams): .someOrFail(InternalServerError()) .map(ingredient => PublicationRequestSummary(id, Ingredient, ingredient.name, createdAt)) - paginationParams match - case PaginationParams(count, offset) => { - for - pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) - .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) - } - pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) - .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) - } - yield (pendingRecipeReqs ++ pendingIngredientReqs) - .sortWith((sum1, sum2) => sum1.createdAt.toEpochSecond < sum2.createdAt.toEpochSecond) - .slice(offset, offset + count) - }.mapError(_ => InternalServerError()) -} + val PaginationParams(count, offset) = paginationParams + for + pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) + .flatMap { reqs => + ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + }.orElseFail(InternalServerError()) + pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) + .flatMap { reqs => + ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + }.orElseFail(InternalServerError()) + yield (pendingRecipeReqs ++ pendingIngredientReqs) + .sortWith(_.createdAt.toEpochSecond < _.createdAt.toEpochSecond) + .slice(offset, offset + count) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index b403bcd7..459a7703 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -16,7 +16,7 @@ import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO -final case class PublicationRequestUpdate( +final case class UpdatePublicationRequestReqBody( comment: String, status: PublicationRequestStatus ) @@ -29,14 +29,14 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = publicationRequestEndpoint .patch .in(query[UUID]("id")) - .in(jsonBody[PublicationRequestUpdate]) + .in(jsonBody[UpdatePublicationRequestReqBody]) .out(statusCode(NoContent)) .errorOut(oneOf(publicationRequestNotFound, serverErrorVariant)) .zSecuredServerLogic(updatePublicationRequestHandler) -private def updatePublicationRequestHandler(id: UUID, reqBody: PublicationRequestUpdate): +private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublicationRequestReqBody): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = - val PublicationRequestUpdate(comment, status) = reqBody + val UpdatePublicationRequestReqBody(comment, status) = reqBody val (_, dbStatus) = DbPublicationRequestStatus.fromDomain(status) ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, comment, dbStatus)) .catchSome { diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index b0f22d20..f804e8b8 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -3,22 +3,19 @@ package db.repositories import db.DbError import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} import db.tables.publication.DbPublicationRequestStatus.Pending -import domain.{IngredientId, PublicationRequestNotFound} -import io.getquill.* -import zio.{IO, RLayer, ZLayer, ZIO} +import domain.{PublicationRequestId, IngredientId, PublicationRequestNotFound} -import java.util.UUID +import io.getquill.* import javax.sql.DataSource +import zio.{IO, RLayer, ZLayer, ZIO} trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] - def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] - def update(id: UUID, comment: String, status: DbPublicationRequestStatus): + def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] + def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] -private inline def ingredientPublicationRequests = query[DbIngredientPublicationRequest] - final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) extends IngredientPublicationRequestsRepo: import db.QuillConfig.ctx.* @@ -33,30 +30,35 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] = run(allPendingQ).provideDS - override def get(id: UUID): IO[DbError, Option[DbIngredientPublicationRequest]] = + override def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS - override def update(id: IngredientId, comment: String, status: DbPublicationRequestStatus): + override def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] = - run(updateQ(id, comment, status)).provideDS.flatMap { - case 0 => ZIO.fail(PublicationRequestNotFound(id.toString)) + case 0 => ZIO.fail(PublicationRequestNotFound(id)) case _ => ZIO.unit } object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* + + inline def ingredientPublicationRequestsQ = query[DbIngredientPublicationRequest] + inline def requestPublicationQ(inline ingredientId: IngredientId): Insert[DbIngredientPublicationRequest] = - ingredientPublicationRequests.insert(_.ingredientId -> ingredientId) - - inline def allPendingQ = ingredientPublicationRequests.filter(_.status == lift(Pending)) + ingredientPublicationRequestsQ + .insert(_.ingredientId -> ingredientId) + + inline def allPendingQ = ingredientPublicationRequestsQ.filter(_.status == lift(Pending)) inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) - inline def getQ(inline id: UUID) = - ingredientPublicationRequests.filter(_.id == lift(id)).take(1) + inline def getQ(inline id: PublicationRequestId) = + ingredientPublicationRequestsQ + .filter(_.id == lift(id)) - inline def updateQ(inline id: UUID, inline comment: String, inline status: DbPublicationRequestStatus) = - ingredientPublicationRequests.filter(_.id == lift(id)) + inline def updateQ(inline id: PublicationRequestId, inline comment: String, inline status: DbPublicationRequestStatus) = + ingredientPublicationRequestsQ + .filter(_.id == lift(id)) .update( _.comment -> lift(comment), _.status -> lift(status) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index d4704c53..da88112d 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -2,24 +2,21 @@ package db.repositories import db.DbError import db.QuillConfig.ctx -import db.tables.publication.DbPublicationRequestStatus.{Pending, createType} +import db.tables.publication.DbPublicationRequestStatus.Pending import db.tables.publication.{DbPublicationRequestStatus, DbRecipePublicationRequest} -import domain.{RecipeId, PublicationRequestNotFound} -import io.getquill.* -import zio.{IO, RLayer, ZLayer, ZIO} +import domain.{PublicationRequestId, RecipeId, PublicationRequestNotFound} -import java.util.UUID +import io.getquill.* import javax.sql.DataSource +import zio.{IO, RLayer, ZLayer, ZIO} trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] - def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] - def update(id: UUID, comment: String, status: DbPublicationRequestStatus): + def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] + def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] -private inline def recipePublicationRequests = query[DbRecipePublicationRequest] - final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: import db.QuillConfig.ctx.* @@ -34,37 +31,44 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] = run(allPendingQ).provideDS - override def get(id: UUID): IO[DbError, Option[DbRecipePublicationRequest]] = + override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS override def update(id: RecipeId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] = run(updateQ(id, comment, status)).provideDS.flatMap { - case 0 => ZIO.fail(PublicationRequestNotFound(id.toString)) + case 0 => ZIO.fail(PublicationRequestNotFound(id)) case _ => ZIO.unit } object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* - + + inline def recipePublicationRequestsQ = query[DbRecipePublicationRequest] + inline def requestPublicationQ(inline recipeId: RecipeId) = - recipePublicationRequests.insert(_.recipeId -> recipeId) + recipePublicationRequestsQ + .insert(_.recipeId -> recipeId) - inline def allPendingQ = recipePublicationRequests.filter(_.status == lift(Pending)) + inline def allPendingQ = + recipePublicationRequestsQ + .filter(_.status == lift(Pending)) inline def pendingRequestsByIdQ(inline recipeId: RecipeId) = allPendingQ.filter(_.recipeId == recipeId) - inline def getQ(inline id: UUID) = - recipePublicationRequests.filter(_.id == lift(id)).take(1) + inline def getQ(inline id: PublicationRequestId) = + recipePublicationRequestsQ + .filter(_.id == lift(id)).take(1) - inline def updateQ(inline id: UUID, inline comment: String, inline status: DbPublicationRequestStatus) = - recipePublicationRequests.filter(_.id == lift(id)) + inline def updateQ(inline id: PublicationRequestId, inline comment: String, inline status: DbPublicationRequestStatus) = + recipePublicationRequestsQ + .filter(_.id == lift(id)) .update( _.comment -> lift(comment), - _.status -> lift(status) + _.status -> lift(status), ) - + object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = ZLayer.fromFunction(RecipePublicationRequestsRepoLive.apply) diff --git a/src/main/scala/domain/Errors.scala b/src/main/scala/domain/Errors.scala index a202f21e..cf77e15c 100644 --- a/src/main/scala/domain/Errors.scala +++ b/src/main/scala/domain/Errors.scala @@ -38,6 +38,6 @@ final case class InvalidInvitationHash( ) final case class PublicationRequestNotFound( - requestId: String, + requestId: PublicationRequestId, message: String = "Publication request not found" ) diff --git a/src/main/scala/domain/Ids.scala b/src/main/scala/domain/Ids.scala index 1e327bb8..0f896a5f 100644 --- a/src/main/scala/domain/Ids.scala +++ b/src/main/scala/domain/Ids.scala @@ -6,3 +6,4 @@ type UserId = Long type StorageId = UUID type IngredientId = UUID type RecipeId = UUID +type PublicationRequestId = UUID diff --git a/src/main/scala/domain/IngredientPublicationRequest.scala b/src/main/scala/domain/IngredientPublicationRequest.scala index 22dc75d4..cbe3fcb8 100644 --- a/src/main/scala/domain/IngredientPublicationRequest.scala +++ b/src/main/scala/domain/IngredientPublicationRequest.scala @@ -1,10 +1,9 @@ package domain import java.time.OffsetDateTime -import java.util.UUID final case class IngredientPublicationRequest( - id: UUID, + id: PublicationRequestId, ingredientId: IngredientId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, diff --git a/src/main/scala/domain/RecipePublicationRequest.scala b/src/main/scala/domain/RecipePublicationRequest.scala index 04d7ffd3..81286088 100644 --- a/src/main/scala/domain/RecipePublicationRequest.scala +++ b/src/main/scala/domain/RecipePublicationRequest.scala @@ -1,10 +1,9 @@ package domain import java.time.OffsetDateTime -import java.util.UUID final case class RecipePublicationRequest( - id: UUID, + id: PublicationRequestId, recipeId: RecipeId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, From 6b00f9c2cd5267dfe1f63fca7cd1b714d7185c1b Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 16:44:50 +0300 Subject: [PATCH 09/95] test: Create ingredient tests --- .../ingredients/CreateIngredientTests.scala | 51 +++++++++++++++++++ .../invitations/CreateInvitationTests.scala | 1 - 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/integration/api/ingredients/CreateIngredientTests.scala diff --git a/src/test/scala/integration/api/ingredients/CreateIngredientTests.scala b/src/test/scala/integration/api/ingredients/CreateIngredientTests.scala new file mode 100644 index 00000000..6f382559 --- /dev/null +++ b/src/test/scala/integration/api/ingredients/CreateIngredientTests.scala @@ -0,0 +1,51 @@ +package integration.api.ingredients + +import api.Authentication.AuthenticatedUser +import api.ingredients.CreateIngredientReqBody +import db.repositories.IngredientsRepo +import integration.common.Utils.* +import integration.common.ZIOIntegrationTestSpec + +import io.circe.generic.auto.* +import zio.http.* +import zio.* +import zio.test.* + +object CreateIngredientTests extends ZIOIntegrationTestSpec: + private def endpointPath: URL = + URL(Path.root / "ingredients") + + private def createIngredient(user: AuthenticatedUser, reqBody: CreateIngredientReqBody): + RIO[Client, Response] = + Client.batched( + post(endpointPath) + .withJsonBody(reqBody) + .addAuthorization(user) + ) + + override def spec: Spec[TestEnvironment & Scope, Any] = suite("Create invitation tests")( + test("When unauthorized should get 401") { + for + resp <- Client.batched(post(endpointPath)) + yield assertTrue(resp.status == Status.Unauthorized) + }, + test("When create ingredient should get 204 and custom ingredient should be created"){ + for + user <- registerUser + ingredientName <- randomString + + resp <- createIngredient(user, CreateIngredientReqBody(ingredientName)) + + bodyStr <- resp.body.asString + ingredientId <- ZIO.fromOption(bodyStr.toUUID) + .orElse(failed(Cause.fail(s"Could not parse response ingredientId $bodyStr"))) + + ingredient <- ZIO.serviceWithZIO[IngredientsRepo](_ + .get(ingredientId) + .provideUser(user) + ) + yield assertTrue(resp.status == Status.Created) + && assertTrue(ingredient.is(_.some).ownerId.is(_.some) == user.userId) + && assertTrue(ingredient.is(_.some).name == ingredientName) + }, + ).provideLayer(testLayer) diff --git a/src/test/scala/integration/api/invitations/CreateInvitationTests.scala b/src/test/scala/integration/api/invitations/CreateInvitationTests.scala index 5f436cc2..bd5120cb 100644 --- a/src/test/scala/integration/api/invitations/CreateInvitationTests.scala +++ b/src/test/scala/integration/api/invitations/CreateInvitationTests.scala @@ -1,6 +1,5 @@ package integration.api.invitations - import api.Authentication.AuthenticatedUser import db.repositories.StorageMembersRepo import db.tables.storageInvitationTable From 3e00a1c97153467751de6b02f36d7107681cc9b0 Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 16:58:10 +0300 Subject: [PATCH 10/95] chore: modified recipes publication requests repo with getWithRecipe methods --- .../db/repositories/RecipePublicationRequestsRepo.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index da88112d..00b08dfd 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -9,11 +9,13 @@ import domain.{PublicationRequestId, RecipeId, PublicationRequestNotFound} import io.getquill.* import javax.sql.DataSource import zio.{IO, RLayer, ZLayer, ZIO} +import db.tables.DbRecipe trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] + def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] @@ -34,6 +36,13 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS + override def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] = + run( + getQ(id) + .join(RecipesQueries.recipesQ) + .on((rpq, r) => rpq.recipeId == r.id) + ).map(_.headOption).provideDS + override def update(id: RecipeId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] = From 307d17e7667c2534ae1485904f140e625c0c1405 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Thu, 17 Jul 2025 17:52:46 +0300 Subject: [PATCH 11/95] refactor: change "get all pending" method for publication request repos, more concise moderation interface endpoint implementation --- .../api/moderation/pubrequests/Get.scala | 100 ++++++++++-------- .../pubrequests/GetSomePending.scala | 83 ++++++++------- .../IngredientPublicationRequestRepo.scala | 30 ++++-- .../RecipePublicationRequestsRepo.scala | 8 +- 4 files changed, 127 insertions(+), 94 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index feb9c452..d244f4f3 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -40,8 +40,6 @@ final case class PublicationRequestResp( private type GetReqEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo - & RecipesRepo - & IngredientsRepo private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest @@ -57,49 +55,61 @@ private def getRequestHandler(reqId: UUID): ZIO[AuthenticatedUser & GetReqEnv, InternalServerError | PublicationRequestNotFound, PublicationRequestResp] = + def getIngredientRequest = - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) - .flatMap { - _.map { dbEntity => - val IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status, comment) = dbEntity.toDomain - ZIO.serviceWithZIO[IngredientsRepo] { - _.get(ingredientId).some.map { ingredient => - PublicationRequestResp( - reqId, - Ingredient, - ingredientId, - ingredient.name, - createdAt, - updatedAt, - comment, - status - ) - } - } - }.getOrElse(ZIO.fail(PublicationRequestNotFound(reqId))) + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getWithIngredient(reqId)) + .someOrFail(PublicationRequestNotFound(reqId)) + .map { + case (dbPubReq, dbIngredient) => + val IngredientPublicationRequest( + id, + ingredientId, + createdAt, + updatedAt, + status, + comment + ) = dbPubReq.toDomain + + PublicationRequestResp( + reqId, + Ingredient, + ingredientId, + dbIngredient.name, + createdAt, + updatedAt, + comment, + status + ) + } + + def getRecipeRequestOrIngredientRequest = + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(reqId)) + .someOrElseZIO(getIngredientRequest) + .map { + case (dbPubReq, dbRecipe) => + val RecipePublicationRequest( + id, + recipeId, + createdAt, + updatedAt, + status, + comment + ) = dbPubReq.toDomain + + PublicationRequestResp( + reqId, + Recipe, + recipeId, + dbRecipe.name, + createdAt, + updatedAt, + comment, + status + ) } - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(reqId)) - .flatMap { - _.map { dbEntity => - val RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status, comment) = dbEntity.toDomain - ZIO.serviceWithZIO[RecipesRepo] { - _.getRecipe(recipeId).some.map { recipe => - PublicationRequestResp( - reqId, - Recipe, - recipeId, - recipe.name, - createdAt, - updatedAt, - comment, - status - ) - } - } - }.getOrElse(getIngredientRequest) - }.mapError { - case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId) - case _: DbNotRespondingError => InternalServerError() - case x: PublicationRequestNotFound => x - } + getRecipeRequestOrIngredientRequest.mapError { + case _: FailedDbQuery => PublicationRequestNotFound(reqId) + case _: DbNotRespondingError => InternalServerError() + case x: PublicationRequestNotFound => x + } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 084b0030..00c5906d 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -1,18 +1,15 @@ package api.moderation.pubrequests +import api.common.search.paginate import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} -import api.EndpointErrorVariants.serverErrorVariant +import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} import api.common.search.PaginationParams import api.moderation.pubrequests.PublicationRequestType.* -import db.repositories.{ - IngredientPublicationRequestsRepo, - IngredientsRepo, - RecipePublicationRequestsRepo, - RecipesRepo -} -import domain.{IngredientPublicationRequest, InternalServerError, RecipePublicationRequest} - +import db.DbError +import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} +import domain.{IngredientPublicationRequest, InternalServerError, PublicationRequestNotFound, RecipePublicationRequest} import io.circe.generic.auto.* + import java.time.OffsetDateTime import java.util.UUID import sttp.model.StatusCode.NoContent @@ -31,8 +28,6 @@ final case class PublicationRequestSummary( private type GetSomePendingEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo - & RecipesRepo - & IngredientsRepo private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest @@ -41,33 +36,45 @@ private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = .get .in(PaginationParams.query) .out(statusCode(NoContent)) - .out(jsonBody[Seq[PublicationRequestSummary]]) - .errorOut(oneOf(serverErrorVariant)) + .out(jsonBody[Vector[PublicationRequestSummary]]) + .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) .zSecuredServerLogic(getSomePendingHandler) private def getSomePendingHandler(paginationParams: PaginationParams): - ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError, Seq[PublicationRequestSummary]] = - - def toPublicationRequest(req: PublicationRequest) = req match // some shitty code... I will fix this later - case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _, _) => - ZIO.serviceWithZIO[RecipesRepo](_.getRecipe(entityId)) - .someOrFail(InternalServerError()) - .map(recipe => PublicationRequestSummary(id, Recipe, recipe.name, createdAt)) - case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _, _) => - ZIO.serviceWithZIO[IngredientsRepo](_.get(entityId)) - .someOrFail(InternalServerError()) - .map(ingredient => PublicationRequestSummary(id, Ingredient, ingredient.name, createdAt)) - - val PaginationParams(count, offset) = paginationParams - for - pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) - .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) - }.orElseFail(InternalServerError()) - pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) - .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) - }.orElseFail(InternalServerError()) - yield (pendingRecipeReqs ++ pendingIngredientReqs) - .sortWith(_.createdAt.toEpochSecond < _.createdAt.toEpochSecond) - .slice(offset, offset + count) + ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError | PublicationRequestNotFound, Vector[PublicationRequestSummary]] = + + { + for + pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPendingIds) + .flatMap { ids => + ZIO.collectAll { + ids.map { id => + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getWithIngredient(id)) + .someOrFail(PublicationRequestNotFound(id)) + .map { + case (dbPubReq, dbIngredient) => + PublicationRequestSummary(id, Ingredient, dbIngredient.name, dbPubReq.createdAt) + } + } + } + } + pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPendingIds) + .flatMap { ids => + ZIO.collectAll { + ids.map { id => + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(id)) + .someOrFail(PublicationRequestNotFound(id)) + .map { + case (dbPubReq, dbRecipe) => + PublicationRequestSummary(id, Recipe, dbRecipe.name, dbPubReq.createdAt) + } + } + } + } + yield (pendingRecipeReqs ++ pendingIngredientReqs) + .sortWith(_.createdAt.toEpochSecond < _.createdAt.toEpochSecond) + .paginate(paginationParams) + }.mapError { + case _: DbError => InternalServerError() + case x: PublicationRequestNotFound => x + } diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index f804e8b8..9bf61254 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -1,18 +1,20 @@ package db.repositories import db.DbError +import db.tables.DbIngredient import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} import db.tables.publication.DbPublicationRequestStatus.Pending -import domain.{PublicationRequestId, IngredientId, PublicationRequestNotFound} - +import domain.{IngredientId, PublicationRequestId, PublicationRequestNotFound} import io.getquill.* + import javax.sql.DataSource -import zio.{IO, RLayer, ZLayer, ZIO} +import zio.{IO, RLayer, ZIO, ZLayer} trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] - def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] + def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] + def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): IO[DbError | PublicationRequestNotFound, Unit] @@ -27,8 +29,9 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] = run(requestPublicationQ(lift(ingredientId))).unit.provideDS - override def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] = - run(allPendingQ).provideDS + override def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] = { + run(allPendingQ.map(_.id)).provideDS.map(Vector.from) + } override def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS @@ -40,6 +43,15 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) case _ => ZIO.unit } + override def getWithIngredient(id: PublicationRequestId): + IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] = + + run( + getQ(id) + .join(IngredientsQueries.ingredientsQ) + .on((rpq, r) => rpq.ingredientId == r.id) + ).map(_.headOption).provideDS + object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* @@ -49,12 +61,16 @@ object IngredientPublicationRequestsQueries: ingredientPublicationRequestsQ .insert(_.ingredientId -> ingredientId) - inline def allPendingQ = ingredientPublicationRequestsQ.filter(_.status == lift(Pending)) + inline def allPendingQ = + ingredientPublicationRequestsQ + .filter(_.status == lift(Pending)) + inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) inline def getQ(inline id: PublicationRequestId) = ingredientPublicationRequestsQ .filter(_.id == lift(id)) + .take(1) inline def updateQ(inline id: PublicationRequestId, inline comment: String, inline status: DbPublicationRequestStatus) = ingredientPublicationRequestsQ diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 00b08dfd..e36e9a6f 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -13,7 +13,7 @@ import db.tables.DbRecipe trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] - def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] + def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): @@ -30,8 +30,8 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(recipeId: RecipeId): IO[DbError, Unit] = run(requestPublicationQ(lift(recipeId))).unit.provideDS - override def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] = - run(allPendingQ).provideDS + override def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] = + run(allPendingQ.map(_.id)).provideDS.map(Vector.from) override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS @@ -75,7 +75,7 @@ object RecipePublicationRequestsQueries: .filter(_.id == lift(id)) .update( _.comment -> lift(comment), - _.status -> lift(status), + _.status -> lift(status) ) object RecipePublicationRequestsRepo: From 98b35d2194bbf2b77a324c5125ab7fdd15ea6317 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Thu, 17 Jul 2025 17:56:29 +0300 Subject: [PATCH 12/95] fix: patch up non-existing table --- src/main/scala/db/CreateTables.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/db/CreateTables.scala b/src/main/scala/db/CreateTables.scala index 54d9d2fd..2ef9b63c 100644 --- a/src/main/scala/db/CreateTables.scala +++ b/src/main/scala/db/CreateTables.scala @@ -97,6 +97,7 @@ def createTables(xa: Transactor) = ) """, sql(DbRecipePublicationRequest.createTable), + sql(DbIngredientPublicationRequest.createTable) ) tableList.map(_.update.run()) From b6b589d2df4b90a182e33e38a260ae64c14c59cf Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Thu, 17 Jul 2025 01:02:57 +0300 Subject: [PATCH 13/95] feat: add name to inStorages --- src/main/scala/api/recipes/Get.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index c36b8927..9448cbba 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -27,7 +27,12 @@ import zio.ZIO final case class IngredientResp( id: IngredientId, name: String, - inStorages: Vector[StorageId], + inStorages: Vector[StorageSummary], +) + +final case class StorageSummary( + id: StorageId, + name: String ) final case class RecipeCreatorResp( @@ -109,8 +114,9 @@ private inline def rawRecipeQuery( 'name', i.${ingredientsTable.name}, 'inStorages', COALESCE( ( - SELECT JSON_AGG(DISTINCT si.storage_id) + SELECT JSON_AGG(DISTINCT si.storage_id si) FROM $storageIngredientsTable si + JOIN $storagesTable AS s ON si.${storageIngredientsTable.storageId} = s.${storagesTable.id} WHERE si.${storageIngredientsTable.ingredientId} = i.${ingredientsTable.id} AND si.${storageIngredientsTable.storageId} IN ( SELECT ${storageMembersTable.storageId} FROM $storageMembersTable @@ -120,6 +126,7 @@ private inline def rawRecipeQuery( FROM $storagesTable WHERE ${storagesTable.ownerId} = $userId ) + ), '[]'::json ) From f08f3b52f2ee8ed1b9286abd04ade6ced15cbac1 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Thu, 17 Jul 2025 01:18:15 +0300 Subject: [PATCH 14/95] fix: query --- src/main/scala/api/recipes/Get.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index 9448cbba..dd5a4cc2 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -114,7 +114,10 @@ private inline def rawRecipeQuery( 'name', i.${ingredientsTable.name}, 'inStorages', COALESCE( ( - SELECT JSON_AGG(DISTINCT si.storage_id si) + SELECT JSON_AGG(JSON_BUILD_OBJECT( + 'id', si.${storageIngredientsTable.storageId}, + 'name', s.${storagesTable.name} + )) FROM $storageIngredientsTable si JOIN $storagesTable AS s ON si.${storageIngredientsTable.storageId} = s.${storagesTable.id} WHERE si.${storageIngredientsTable.ingredientId} = i.${ingredientsTable.id} From 2bf50c20a7db546fdce603a38eaa96d5ef42736d Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Thu, 17 Jul 2025 01:26:16 +0300 Subject: [PATCH 15/95] fix: adjust tests according to new changes --- .../scala/integration/api/recipes/GetRecipeTests.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/scala/integration/api/recipes/GetRecipeTests.scala b/src/test/scala/integration/api/recipes/GetRecipeTests.scala index 013b086a..da1e4a30 100644 --- a/src/test/scala/integration/api/recipes/GetRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/GetRecipeTests.scala @@ -58,7 +58,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: recipeResp <- ZIO.fromEither(decode[RecipeResp](strBody)) yield assertTrue(resp.status == Status.Ok) && assertTrue(recipeResp.ingredients.map(_.id) hasSameElementsAs ingredientIds) - && assertTrue(recipeResp.ingredients.forall(_.inStorages == Vector(storageId))) + && assertTrue(recipeResp.ingredients.forall(_.inStorages.map(_.id) == Vector(storageId))) }, test("1 user with 2 storages") { for @@ -91,7 +91,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Ok) && assertTrue(recipeRespIngredientsIds hasSameElementsAs recipeIngredientsIds) && assertTrue(recipeResp.ingredients.forall(ingredient => - ingredient.inStorages == ( + ingredient.inStorages.map(_.id) == ( if storage1UsedIngredientIds.contains(ingredient.id) then Vector(storage1Id) else if storage2UsedIngredientIds.contains(ingredient.id) @@ -152,7 +152,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Ok) && assertTrue(recipeRespIngredientsIds hasSameElementsAs recipeIngredientsIds) && assertTrue(recipeResp.ingredients.forall( ingredient => - ingredient.inStorages == ( + ingredient.inStorages.map(_.id) == ( if (user1StorageIngredientIds.contains(ingredient.id)) Vector(user1StorageId) else if (sharedStorageIngredientIds.contains(ingredient.id)) @@ -174,7 +174,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Ok) && assertTrue(recipeRespIngredientsIds.hasSameElementsAs(recipeIngredientsIds)) && assertTrue(recipeResp.ingredients.forall( ingredient => - ingredient.inStorages == ( + ingredient.inStorages.map(_.id) == ( if (user2StorageIngredientIds.contains(ingredient.id)) Vector(user2StorageId) else if (sharedStorageIngredientIds.contains(ingredient.id)) From 0fd60ceb1712e37aed6797fbb42dff6069afd398 Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 19:17:43 +0300 Subject: [PATCH 16/95] chore: resolve merge conflict --- .../api/ingredients/RequestPublication.scala | 13 +++++----- .../DbPublicationRequestStatus.scala | 24 +++++++++---------- .../DbRecipePublicationRequest.scala | 8 ------- .../db/tables/publication/SqlFunctions.scala | 5 ++-- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index 75395646..f1cbd3e5 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -28,9 +28,9 @@ object IngredientAlreadyPublished: val variant = BadRequest.variantJson[IngredientAlreadyPublished] private final case class IngredientAlreadyPending( - ingredientId: IngredientId, - message: String = "Ingredient already pending" - ) + ingredientId: IngredientId, + message: String = "Ingredient already pending" +) object IngredientAlreadyPending: val variant = BadRequest.variantJson[IngredientAlreadyPending] @@ -64,14 +64,13 @@ def requestPublicationHandler(ingredientId: IngredientId): alreadyPending <- run( IngredientPublicationRequestsQueries .pendingRequestsByIdQ(lift(ingredientId)).nonEmpty - ) - .provideDS(using dataSource) + ).provideDS(using dataSource) .orElseFail(InternalServerError()) _ <- ZIO.fail(IngredientAlreadyPending(ingredientId)) .when(alreadyPending) _ <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ - .requestPublication(ingredientId) - .orElseFail(InternalServerError()) + .requestPublication(ingredientId) + .orElseFail(InternalServerError()) ) yield () diff --git a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala index 45a21098..8a24775a 100644 --- a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala +++ b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala @@ -21,16 +21,16 @@ object DbPublicationRequestStatus: case PublicationRequestStatus.Rejected(reason) => (Some(reason), Rejected) val createType: String = """ - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'publication_request_status') THEN - CREATE TYPE publication_request_status AS ENUM ( - 'pending', - 'accepted', - 'rejected' - ); - END IF; - END $$; + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'publication_request_status') THEN + CREATE TYPE publication_request_status AS ENUM ( + 'pending', + 'accepted', + 'rejected' + ); + END IF; + END $$; """ given JdbcDecoder[DbPublicationRequestStatus]( @@ -43,11 +43,11 @@ object DbPublicationRequestStatus: ) given JdbcEncoder[DbPublicationRequestStatus] = encoder( - Types.VARCHAR, + Types.OTHER, (index: Int, value: DbPublicationRequestStatus, row: PreparedStatement) => val statusString = value match case DbPublicationRequestStatus.Pending => "pending" case DbPublicationRequestStatus.Accepted => "accepted" case DbPublicationRequestStatus.Rejected => "rejected" row.setString(index, statusString) - ) \ No newline at end of file + ) diff --git a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala index 05acde59..53cb9d45 100644 --- a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala @@ -33,14 +33,6 @@ object DbRecipePublicationRequest: FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE ); - CREATE OR REPLACE FUNCTION trigger_set_updated_at() - RETURNS TRIGGER AS $$ - BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - CREATE OR REPLACE TRIGGER update_timestamp BEFORE UPDATE ON recipe_publication_requests FOR EACH ROW diff --git a/src/main/scala/db/tables/publication/SqlFunctions.scala b/src/main/scala/db/tables/publication/SqlFunctions.scala index cb8b55e2..f30e1f1c 100644 --- a/src/main/scala/db/tables/publication/SqlFunctions.scala +++ b/src/main/scala/db/tables/publication/SqlFunctions.scala @@ -1,8 +1,7 @@ package db.tables.publication object SqlFunctions: - val triggerSetUpdatedAt = - """ + val triggerSetUpdatedAt: String = """ CREATE OR REPLACE FUNCTION trigger_set_updated_at() RETURNS TRIGGER AS $$ BEGIN @@ -10,4 +9,4 @@ object SqlFunctions: RETURN NEW; END; $$ LANGUAGE plpgsql; - """ \ No newline at end of file + """ From 20940619840af1f45d4f30700a4d0bdd1834d8ac Mon Sep 17 00:00:00 2001 From: danielambda Date: Thu, 17 Jul 2025 19:11:59 +0300 Subject: [PATCH 17/95] test: basic request recipe publication tests --- .../api/recipes/GetRecipeTests.scala | 8 ++-- .../recipes/GetSuggestedRecipesTests.scala | 14 +++--- .../RequestRecipePublicationTests.scala | 44 +++++++++++++++++++ .../AddIngredientToRecipeTests.scala | 24 +++++----- .../RemoveIngredientFromRecipeTests.scala | 24 +++++----- src/test/scala/integration/common/Utils.scala | 2 +- 6 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala diff --git a/src/test/scala/integration/api/recipes/GetRecipeTests.scala b/src/test/scala/integration/api/recipes/GetRecipeTests.scala index da1e4a30..d4f0f3d5 100644 --- a/src/test/scala/integration/api/recipes/GetRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/GetRecipeTests.scala @@ -50,7 +50,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: _extraIngredientIds <- createNIngredients(defaultIngredientAmount) _ <- addIngredientsToStorage(storageId, ingredientIds) - recipeId <- createRecipe(user, ingredientIds) + recipeId <- createCustomRecipe(user, ingredientIds) resp <- getRecipe(user, recipeId) @@ -72,7 +72,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: recipeIngredientsIds = storage1UsedIngredientIds ++ storage2UsedIngredientIds - recipeId <- createRecipe(user, recipeIngredientsIds) + recipeId <- createCustomRecipe(user, recipeIngredientsIds) _ <- addIngredientsToStorage(storage1Id, storage1UsedIngredientIds) _ <- addIngredientsToStorage(storage2Id, storage2UsedIngredientIds) @@ -130,7 +130,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: ++ user2StorageIngredientIds ++ sharedStorageIngredientIds ).distinct - recipeId <- createRecipe(user1, recipeIngredientsIds) + recipeId <- createCustomRecipe(user1, recipeIngredientsIds) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) // create some extra ingredients that are not used in the recipe @@ -216,7 +216,7 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: ++ user2StorageIngredientIds ++ sharedStorageIngredientIds ).distinct - recipeId <- createRecipe(user1, recipeIngredientsIds) + recipeId <- createCustomRecipe(user1, recipeIngredientsIds) // create some extra ingredients that are not used in the recipe _ <- createNIngredients(defaultIngredientAmount) diff --git a/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala b/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala index 211abda4..b107ad2c 100644 --- a/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala +++ b/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala @@ -29,9 +29,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: override def spec: Spec[TestEnvironment & Scope, Any] = suite("Get suggested recipes tests")( test("When unauthorized should get 401") { for - resp <- Client.batched( - Request.get(endpointPath) - ) + resp <- Client.batched(get(endpointPath)) yield assertTrue(resp.status == Status.Unauthorized) }, test("When authorized should get 200") { @@ -48,7 +46,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: recipeIds <- ZIO.collectAll( (1 to n).map(i => createNIngredients(i).flatMap( - createRecipe(user, _) + createCustomRecipe(user, _) ) ) ) @@ -86,7 +84,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: = storage1IngredientIds.take(recipe1Storage1Availability) ++ storage2IngredientIds.take(recipe1Storage2Availability) ++ otherIngredientIds - recipe1Id <- createRecipe(user, recipe1IngredientIds) + recipe1Id <- createCustomRecipe(user, recipe1IngredientIds) recipe2Storage1Availability = minStorageIngredientsAmount - 2 recipe2Storage2Availability = minStorageIngredientsAmount @@ -94,7 +92,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: = storage1IngredientIds.takeRight(recipe2Storage1Availability) ++ storage2IngredientIds.takeRight(recipe2Storage2Availability) ++ otherIngredientIds - recipe2Id <- createRecipe(user, recipe2IngredientIds) + recipe2Id <- createCustomRecipe(user, recipe2IngredientIds) resp <- getSuggestedRecipes(user, Seq(storage1Id, storage2Id)) @@ -145,7 +143,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: ++ storage1IngredientIds.take(recipe1Storage1Availability) ++ storage2IngredientIds.take(recipe1Storage2Availability) ++ otherIngredientIds - recipe1Id <- createRecipe(user, recipe1IngredientIds) + recipe1Id <- createCustomRecipe(user, recipe1IngredientIds) recipe2MemberedStorageAvailability = minStorageIngredientsAmount - 1 recipe2Storage1Availability = minStorageIngredientsAmount - 2 @@ -155,7 +153,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: ++ storage1IngredientIds.takeRight(recipe2Storage1Availability) ++ storage2IngredientIds.takeRight(recipe2Storage2Availability) ++ otherIngredientIds - recipe2Id <- createRecipe(user, recipe2IngredientIds) + recipe2Id <- createCustomRecipe(user, recipe2IngredientIds) resp <- getSuggestedRecipes(user, Seq(storage1Id, storage2Id)) diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala new file mode 100644 index 00000000..f88fbe87 --- /dev/null +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -0,0 +1,44 @@ +package integration.api.recipes + +import api.Authentication.AuthenticatedUser +import domain.RecipeId +import integration.common.Utils.* +import integration.common.ZIOIntegrationTestSpec + +import io.circe.generic.auto.* +import io.circe.parser.decode +import zio.* +import zio.http.* +import zio.test.* + +object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: + private def endpointPath(recipeId: RecipeId): URL = + URL(Path.root / "recipes" / recipeId.toString / "request-publication") + + private def requestRecipePublication(user: AuthenticatedUser, recipeId: RecipeId): + RIO[Client, Response] = + Client.batched( + post(endpointPath(recipeId)) + .addAuthorization(user) + ) + + override def spec: Spec[TestEnvironment & Scope, Any] = suite("Request recipe publication tests")( + test("When unauthorized should get 401") { + for + recipeId <- getRandomUUID + resp <- Client.batched( + post(endpointPath(recipeId)) + ) + yield assertTrue(resp.status == Status.Unauthorized) + }, + test("When authorized should get 200") { + for + user <- registerUser + + recipeId <- createCustomRecipe(user, Vector.empty) + + resp <- requestRecipePublication(user, recipeId) + yield assertTrue(resp.status == Status.Ok) + }, + ).provideLayer(testLayer) + diff --git a/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala b/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala index 0cedcb82..33af35de 100644 --- a/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala @@ -44,7 +44,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser recipeId <- createNIngredients(5) - .flatMap(createRecipe(user, _)) + .flatMap(createCustomRecipe(user, _)) ingredientId <- createPublicIngredient resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -57,7 +57,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) ingredientId <- createPublicIngredient resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -77,7 +77,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) ingredientId <- createCustomIngredient(user) resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -99,7 +99,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -119,7 +119,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: otherUser <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(otherUser, initialIngredients) + recipeId <- createCustomRecipe(otherUser, initialIngredients) ingredientId <- createPublicIngredient @@ -143,7 +143,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: otherUser <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(otherUser, initialIngredients) + recipeId <- createCustomRecipe(otherUser, initialIngredients) user <- registerUser ingredientId <- createCustomIngredient(user) @@ -165,7 +165,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- registerUser.flatMap(createRecipe(_, initialIngredients)) + recipeId <- registerUser.flatMap(createCustomRecipe(_, initialIngredients)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) ingredientId <- createPublicIngredient @@ -189,7 +189,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- registerUser.flatMap(createRecipe(_, initialIngredients)) + recipeId <- registerUser.flatMap(createCustomRecipe(_, initialIngredients)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) user <- registerUser @@ -213,7 +213,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) ingredientId <- createPublicIngredient @@ -236,7 +236,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) ingredientId <- createCustomIngredient(user) @@ -262,7 +262,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) .map(_ :+ ingredientId) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -282,7 +282,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: initialIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) .map(_ :+ ingredientId) - recipeId <- createRecipe(user, initialIngredients) + recipeId <- createCustomRecipe(user, initialIngredients) resp <- addIngredientToRecipe(user, recipeId, ingredientId) diff --git a/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala b/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala index 85072f89..c2edf478 100644 --- a/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala @@ -47,7 +47,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) yield assertTrue(resp.status == Status.NoContent) @@ -60,7 +60,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -79,7 +79,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createCustomIngredient(user) otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -97,7 +97,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: otherUser <- registerUser otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(otherUser, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(otherUser, otherIngredientIds :+ ingredientId) user <- registerUser @@ -119,7 +119,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createCustomIngredient(otherUser) otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(otherUser, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(otherUser, otherIngredientIds :+ ingredientId) user <- registerUser @@ -141,7 +141,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- registerUser.flatMap(createRecipe(_, otherIngredientIds :+ ingredientId)) + recipeId <- registerUser.flatMap(createCustomRecipe(_, otherIngredientIds :+ ingredientId)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) user <- registerUser @@ -164,7 +164,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -185,7 +185,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createCustomIngredient(user) otherIngredientIds <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, otherIngredientIds :+ ingredientId) + recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -205,7 +205,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialRecipeIngredients) + recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- getRandomUUID @@ -227,7 +227,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialRecipeIngredients) + recipeId <- createCustomRecipe(user, initialRecipeIngredients) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -244,7 +244,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialRecipeIngredients) + recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- createPublicIngredient @@ -263,7 +263,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some .flatMap(createNIngredients) - recipeId <- createRecipe(user, initialRecipeIngredients) + recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- createCustomIngredient(user) diff --git a/src/test/scala/integration/common/Utils.scala b/src/test/scala/integration/common/Utils.scala index 725deb38..057f7606 100644 --- a/src/test/scala/integration/common/Utils.scala +++ b/src/test/scala/integration/common/Utils.scala @@ -105,7 +105,7 @@ object Utils: (1 to n).map(_ => createPublicIngredient).toVector ) - def createRecipe(user: AuthenticatedUser, ingredientIds: Vector[IngredientId]): ZIO[ + def createCustomRecipe(user: AuthenticatedUser, ingredientIds: Vector[IngredientId]): ZIO[ RecipesRepo, InternalServerError | DbError, RecipeId From 014d9df5c40315532ccf91941a12301df7ebda52 Mon Sep 17 00:00:00 2001 From: danielambda Date: Fri, 18 Jul 2025 14:52:02 +0300 Subject: [PATCH 18/95] refactor: somethings --- .../api/moderation/pubrequests/Update.scala | 28 +++++--- .../api/recipes/RequestPublication.scala | 17 ++--- .../RecipePublicationRequestsRepo.scala | 67 +++++++++++-------- .../DbPublicationRequestStatus.scala | 12 ++-- .../DbRecipePublicationRequest.scala | 7 +- .../domain/RecipePublicationRequest.scala | 1 - 6 files changed, 77 insertions(+), 55 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 459a7703..8caf1aea 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -15,11 +15,24 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO +import io.circe.Encoder + +enum PublicationRequestStatusReq: + case Pending + case Accepted + case Rejected(reason: String) + def toDomain: PublicationRequestStatus = this match + case Pending => PublicationRequestStatus.Pending + case Accepted => PublicationRequestStatus.Accepted + case Rejected(reason) => PublicationRequestStatus.Rejected(reason) + +final case class UpdatePublicationRequestReqBody(status: PublicationRequestStatusReq) +object UpdatePublicationRequestReqBody: + given Encoder[UpdatePublicationRequestReqBody] = Encoder { req => req.status match + case _ => ??? + + } -final case class UpdatePublicationRequestReqBody( - comment: String, - status: PublicationRequestStatus -) private type UpdateReqEnv = RecipePublicationRequestsRepo @@ -36,12 +49,11 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublicationRequestReqBody): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = - val UpdatePublicationRequestReqBody(comment, status) = reqBody - val (_, dbStatus) = DbPublicationRequestStatus.fromDomain(status) - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, comment, dbStatus)) + val status = reqBody.status.toDomain + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, status)) .catchSome { case _: PublicationRequestNotFound => - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, comment, dbStatus)) + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, status)) }.mapError { case x: PublicationRequestNotFound => x case _: DbError => InternalServerError() diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/RequestPublication.scala index e351cf91..a0942c5c 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/RequestPublication.scala @@ -60,10 +60,12 @@ private val requestPublication: ZServerEndpoint[PublishEnv, Any] = .zSecuredServerLogic(requestPublicationHandler) private def requestPublicationHandler(recipeId: RecipeId): - ZIO[AuthenticatedUser & PublishEnv, - InternalServerError | RecipeAlreadyPublished | RecipeAlreadyPending | - CannotPublishRecipeWithCustomIngredients | RecipeNotFound, - Unit] = + ZIO[ + AuthenticatedUser & PublishEnv, + InternalServerError | RecipeAlreadyPublished | RecipeAlreadyPending + | CannotPublishRecipeWithCustomIngredients | RecipeNotFound, + Unit + ] = for recipe <- ZIO.serviceWithZIO[RecipesRepo](_ .getRecipe(recipeId) @@ -75,9 +77,8 @@ private def requestPublicationHandler(recipeId: RecipeId): dataSource <- ZIO.service[DataSource] alreadyPending <- run( RecipePublicationRequestsQueries - .pendingRequestsByIdQ(lift(recipeId)).nonEmpty - ) - .provideDS(using dataSource) + .pendingRequestsOfRecipeQ(lift(recipeId)).nonEmpty + ).provideDS(using dataSource) .orElseFail(InternalServerError()) _ <- ZIO.fail(RecipeAlreadyPending(recipeId)) .when(alreadyPending) @@ -94,7 +95,7 @@ private def requestPublicationHandler(recipeId: RecipeId): .when(customIngredientIdsInRecipe.nonEmpty) _ <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ - .requestPublication(recipeId) + .createPublicationRequest(recipeId) .orElseFail(InternalServerError()) ) yield () diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 00b08dfd..1fa3ef71 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -2,22 +2,23 @@ package db.repositories import db.DbError import db.QuillConfig.ctx -import db.tables.publication.DbPublicationRequestStatus.Pending +import db.tables.DbRecipe import db.tables.publication.{DbPublicationRequestStatus, DbRecipePublicationRequest} -import domain.{PublicationRequestId, RecipeId, PublicationRequestNotFound} +import db.tables.publication.DbPublicationRequestStatus.Pending +import domain.{PublicationRequestStatus, PublicationRequestId, RecipeId} import io.getquill.* import javax.sql.DataSource import zio.{IO, RLayer, ZLayer, ZIO} -import db.tables.DbRecipe +import java.util.UUID trait RecipePublicationRequestsRepo: - def requestPublication(recipeId: RecipeId): IO[DbError, Unit] + def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] - def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] - def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): - IO[DbError | PublicationRequestNotFound, Unit] + def getWithRecipe(id: PublicationRequestId): + IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] + def update(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Long] final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: @@ -27,55 +28,65 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) private given DataSource = dataSource - override def requestPublication(recipeId: RecipeId): IO[DbError, Unit] = - run(requestPublicationQ(lift(recipeId))).unit.provideDS + override def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] = + run( + createPublicationRequestQ(lift(recipeId)) + ).provideDS override def getAllPending: IO[DbError, Seq[DbRecipePublicationRequest]] = - run(allPendingQ).provideDS + run(pendingRequestsQ).provideDS override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = - run(getQ(id)).map(_.headOption).provideDS + run(getQ(id).value).provideDS override def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] = run( getQ(id) .join(RecipesQueries.recipesQ) .on((rpq, r) => rpq.recipeId == r.id) - ).map(_.headOption).provideDS - - override def update(id: RecipeId, comment: String, status: DbPublicationRequestStatus): - IO[DbError | PublicationRequestNotFound, Unit] = + .value + ).provideDS - run(updateQ(id, comment, status)).provideDS.flatMap { - case 0 => ZIO.fail(PublicationRequestNotFound(id)) - case _ => ZIO.unit - } + override def update(id: RecipeId, status: PublicationRequestStatus): + IO[DbError, Long] = + val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) + run(updateQ(id, dbStatus, reason)).provideDS object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* - inline def recipePublicationRequestsQ = query[DbRecipePublicationRequest] + inline def recipePublicationRequestsQ: EntityQuery[DbRecipePublicationRequest] = + query[DbRecipePublicationRequest] - inline def requestPublicationQ(inline recipeId: RecipeId) = + inline def createPublicationRequestQ(inline recipeId: RecipeId): + ActionReturning[DbRecipePublicationRequest, UUID] = recipePublicationRequestsQ .insert(_.recipeId -> recipeId) + .returningGenerated(_.id) - inline def allPendingQ = + inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ .filter(_.status == lift(Pending)) - inline def pendingRequestsByIdQ(inline recipeId: RecipeId) = allPendingQ.filter(_.recipeId == recipeId) + inline def pendingRequestsOfRecipeQ(inline recipeId: RecipeId): + EntityQuery[DbRecipePublicationRequest] = + pendingRequestsQ + .filter(_.recipeId == recipeId) - inline def getQ(inline id: PublicationRequestId) = + inline def getQ(inline id: PublicationRequestId): EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(_.id == lift(id)).take(1) + .filter(_.id == lift(id)) - inline def updateQ(inline id: PublicationRequestId, inline comment: String, inline status: DbPublicationRequestStatus) = + inline def updateQ( + inline id: PublicationRequestId, + inline status: DbPublicationRequestStatus, + inline reason: Option[String], + ): Update[DbRecipePublicationRequest] = recipePublicationRequestsQ .filter(_.id == lift(id)) .update( - _.comment -> lift(comment), - _.status -> lift(status), + _.status -> lift(status), + _.reason -> lift(reason), ) object RecipePublicationRequestsRepo: diff --git a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala index 8a24775a..5ca3ce16 100644 --- a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala +++ b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala @@ -15,10 +15,10 @@ enum DbPublicationRequestStatus: case Rejected => PublicationRequestStatus.Rejected(reason.get) // <- unsafe code here object DbPublicationRequestStatus: - val fromDomain: PublicationRequestStatus => (Option[String], DbPublicationRequestStatus) = - case PublicationRequestStatus.Pending => (None, Pending) - case PublicationRequestStatus.Accepted => (None, Accepted) - case PublicationRequestStatus.Rejected(reason) => (Some(reason), Rejected) + val fromDomain: PublicationRequestStatus => (DbPublicationRequestStatus, Option[String]) = + case PublicationRequestStatus.Pending => (Pending, None) + case PublicationRequestStatus.Accepted => (Accepted, None) + case PublicationRequestStatus.Rejected(reason) => (Rejected, Some(reason)) val createType: String = """ DO $$ @@ -44,9 +44,9 @@ object DbPublicationRequestStatus: given JdbcEncoder[DbPublicationRequestStatus] = encoder( Types.OTHER, - (index: Int, value: DbPublicationRequestStatus, row: PreparedStatement) => + (index, value, row) => val statusString = value match - case DbPublicationRequestStatus.Pending => "pending" + case DbPublicationRequestStatus.Pending => "pending" case DbPublicationRequestStatus.Accepted => "accepted" case DbPublicationRequestStatus.Rejected => "rejected" row.setString(index, statusString) diff --git a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala index 53cb9d45..2fa2a83d 100644 --- a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala @@ -12,15 +12,14 @@ final case class DbRecipePublicationRequest( updatedAt: OffsetDateTime, status: DbPublicationRequestStatus, reason: Option[String], - comment: String ): def toDomain: RecipePublicationRequest = - RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status.toDomain(reason), comment) + RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status.toDomain(reason)) object DbRecipePublicationRequest: def fromDomain(req: RecipePublicationRequest): DbRecipePublicationRequest = - val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason, req.comment) + val (status, reason) = DbPublicationRequestStatus.fromDomain(req.status) + DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason) val createTable: String = """ CREATE TABLE IF NOT EXISTS recipe_publication_requests( diff --git a/src/main/scala/domain/RecipePublicationRequest.scala b/src/main/scala/domain/RecipePublicationRequest.scala index 81286088..3f6cae8b 100644 --- a/src/main/scala/domain/RecipePublicationRequest.scala +++ b/src/main/scala/domain/RecipePublicationRequest.scala @@ -8,6 +8,5 @@ final case class RecipePublicationRequest( createdAt: OffsetDateTime, updatedAt: OffsetDateTime, status: PublicationRequestStatus, - comment: String ) From 0e03a350238b6e5dec464c1450b7d60fbee77684 Mon Sep 17 00:00:00 2001 From: danielambda Date: Fri, 18 Jul 2025 23:06:05 +0300 Subject: [PATCH 19/95] chore: made code compile --- .../api/common/search/PaginationParams.scala | 4 +- .../api/moderation/pubrequests/Get.scala | 11 ++--- .../pubrequests/GetSomePending.scala | 39 +++++++++-------- .../api/moderation/pubrequests/Update.scala | 43 ++++++++----------- .../IngredientPublicationRequestRepo.scala | 25 +++++------ .../RecipePublicationRequestsRepo.scala | 8 ++-- .../DbIngredientPublicationRequest.scala | 7 ++- .../DbPublicationRequestStatus.scala | 6 +-- .../domain/IngredientPublicationRequest.scala | 1 - .../domain/PublicationRequestStatus.scala | 2 +- 10 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/main/scala/api/common/search/PaginationParams.scala b/src/main/scala/api/common/search/PaginationParams.scala index db3069c2..f797a3a3 100644 --- a/src/main/scala/api/common/search/PaginationParams.scala +++ b/src/main/scala/api/common/search/PaginationParams.scala @@ -1,6 +1,6 @@ package api.common.search -import scala.collection.immutable.IndexedSeqOps +import scala.collection.IterableOps import sttp.tapir case class PaginationParams( @@ -15,7 +15,7 @@ object PaginationParams: (PaginationParams.apply.tupled) {case PaginationParams(size, offset) => (size, offset)} -extension[A, CC[_], C](seq: IndexedSeqOps[A, CC, C]) +extension[A, CC[_], C](seq: IterableOps[A, CC, C]) def paginate(paginationParams: PaginationParams): C = seq.slice(paginationParams.offset, paginationParams.offset + paginationParams.size) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index feb9c452..1f7d481b 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -33,7 +33,6 @@ final case class PublicationRequestResp( entityName: String, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, - comment: String, status: PublicationRequestStatus ) @@ -61,7 +60,7 @@ private def getRequestHandler(reqId: UUID): ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) .flatMap { _.map { dbEntity => - val IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status, comment) = dbEntity.toDomain + val IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status) = dbEntity.toDomain ZIO.serviceWithZIO[IngredientsRepo] { _.get(ingredientId).some.map { ingredient => PublicationRequestResp( @@ -71,8 +70,7 @@ private def getRequestHandler(reqId: UUID): ingredient.name, createdAt, updatedAt, - comment, - status + status, ) } } @@ -82,7 +80,7 @@ private def getRequestHandler(reqId: UUID): ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(reqId)) .flatMap { _.map { dbEntity => - val RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status, comment) = dbEntity.toDomain + val RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status) = dbEntity.toDomain ZIO.serviceWithZIO[RecipesRepo] { _.getRecipe(recipeId).some.map { recipe => PublicationRequestResp( @@ -92,8 +90,7 @@ private def getRequestHandler(reqId: UUID): recipe.name, createdAt, updatedAt, - comment, - status + status, ) } } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 084b0030..77c991dc 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -2,7 +2,7 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.serverErrorVariant -import api.common.search.PaginationParams +import api.common.search.{PaginationParams, paginate} import api.moderation.pubrequests.PublicationRequestType.* import db.repositories.{ IngredientPublicationRequestsRepo, @@ -34,8 +34,6 @@ private type GetSomePendingEnv & RecipesRepo & IngredientsRepo -private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest - private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = publicationRequestEndpoint .get @@ -48,26 +46,29 @@ private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = private def getSomePendingHandler(paginationParams: PaginationParams): ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError, Seq[PublicationRequestSummary]] = - def toPublicationRequest(req: PublicationRequest) = req match // some shitty code... I will fix this later - case RecipePublicationRequest(id, entityId, createdAt, updatedAt, _, _) => - ZIO.serviceWithZIO[RecipesRepo](_.getRecipe(entityId)) - .someOrFail(InternalServerError()) - .map(recipe => PublicationRequestSummary(id, Recipe, recipe.name, createdAt)) - case IngredientPublicationRequest(id, entityId, createdAt, updatedAt, _, _) => - ZIO.serviceWithZIO[IngredientsRepo](_.get(entityId)) - .someOrFail(InternalServerError()) - .map(ingredient => PublicationRequestSummary(id, Ingredient, ingredient.name, createdAt)) + def toRecipePublicationRequest(req: RecipePublicationRequest) = + ZIO.serviceWithZIO[RecipesRepo](_ + .getRecipe(req.recipeId) + .someOrFail(InternalServerError()) + .map(recipe => PublicationRequestSummary(req.id, Recipe, recipe.name, req.createdAt)) + ) + + def toIngredientPublicationRequest(req: IngredientPublicationRequest) = + ZIO.serviceWithZIO[IngredientsRepo](_ + .get(req.ingredientId) + .someOrFail(InternalServerError()) + .map(ingredient => PublicationRequestSummary(req.id, Ingredient, ingredient.name, req.createdAt)) + ) - val PaginationParams(count, offset) = paginationParams for - pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) + pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + ZIO.collectAll(reqs.map(req => toRecipePublicationRequest(req.toDomain))) }.orElseFail(InternalServerError()) - pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPending) + pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPending) .flatMap { reqs => - ZIO.collectAll(reqs.map(req => toPublicationRequest(req.toDomain))) + ZIO.collectAll(reqs.map(req => toIngredientPublicationRequest(req.toDomain))) }.orElseFail(InternalServerError()) yield (pendingRecipeReqs ++ pendingIngredientReqs) - .sortWith(_.createdAt.toEpochSecond < _.createdAt.toEpochSecond) - .slice(offset, offset + count) + .sortBy(_.createdAt.toEpochSecond) + .paginate(paginationParams) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 8caf1aea..757dafcf 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -3,11 +3,10 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} import api.moderation.pubrequests.PublicationRequestType.* -import db.DbError import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} -import db.tables.publication.DbPublicationRequestStatus import domain.{PublicationRequestStatus, InternalServerError, PublicationRequestNotFound} +import io.circe.Encoder import io.circe.generic.auto.* import java.util.UUID import sttp.model.StatusCode.NoContent @@ -15,24 +14,20 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* import zio.ZIO -import io.circe.Encoder enum PublicationRequestStatusReq: case Pending case Accepted - case Rejected(reason: String) - def toDomain: PublicationRequestStatus = this match - case Pending => PublicationRequestStatus.Pending - case Accepted => PublicationRequestStatus.Accepted - case Rejected(reason) => PublicationRequestStatus.Rejected(reason) - -final case class UpdatePublicationRequestReqBody(status: PublicationRequestStatusReq) -object UpdatePublicationRequestReqBody: - given Encoder[UpdatePublicationRequestReqBody] = Encoder { req => req.status match - case _ => ??? - - } + case Rejected +final case class UpdatePublicationRequestReqBody( + status: PublicationRequestStatusReq, + reason: Option[String], +): + def getDomainStatus: PublicationRequestStatus = status match + case PublicationRequestStatusReq.Pending => PublicationRequestStatus.Pending + case PublicationRequestStatusReq.Accepted => PublicationRequestStatus.Accepted + case PublicationRequestStatusReq.Rejected => PublicationRequestStatus.Rejected(reason) private type UpdateReqEnv = RecipePublicationRequestsRepo @@ -49,12 +44,12 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublicationRequestReqBody): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = - val status = reqBody.status.toDomain - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.update(id, status)) - .catchSome { - case _: PublicationRequestNotFound => - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.update(id, status)) - }.mapError { - case x: PublicationRequestNotFound => x - case _: DbError => InternalServerError() - } + val status = reqBody.getDomainStatus + for + rowsUpdated <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ + .updateStatus(id, status) + .orElseFail(InternalServerError()) + ) + // case _: PublicationRequestNotFound => + // ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.updateStatus(id, status)) + yield () diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index f804e8b8..5e63e640 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -3,7 +3,7 @@ package db.repositories import db.DbError import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} import db.tables.publication.DbPublicationRequestStatus.Pending -import domain.{PublicationRequestId, IngredientId, PublicationRequestNotFound} +import domain.{PublicationRequestId, IngredientId, PublicationRequestStatus} import io.getquill.* import javax.sql.DataSource @@ -13,8 +13,7 @@ trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] - def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): - IO[DbError | PublicationRequestNotFound, Unit] + def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) extends IngredientPublicationRequestsRepo: @@ -33,12 +32,10 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS - override def update(id: PublicationRequestId, comment: String, status: DbPublicationRequestStatus): - IO[DbError | PublicationRequestNotFound, Unit] = - run(updateQ(id, comment, status)).provideDS.flatMap { - case 0 => ZIO.fail(PublicationRequestNotFound(id)) - case _ => ZIO.unit - } + override def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): + IO[DbError, Boolean] = + val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) + run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* @@ -56,12 +53,16 @@ object IngredientPublicationRequestsQueries: ingredientPublicationRequestsQ .filter(_.id == lift(id)) - inline def updateQ(inline id: PublicationRequestId, inline comment: String, inline status: DbPublicationRequestStatus) = + inline def updateQ( + inline id: PublicationRequestId, + inline status: DbPublicationRequestStatus, + inline reason: Option[String], + ) = ingredientPublicationRequestsQ .filter(_.id == lift(id)) .update( - _.comment -> lift(comment), - _.status -> lift(status) + _.status -> lift(status), + _.reason -> lift(reason), ) object IngredientPublicationRequestsRepo: diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 1fa3ef71..ecbc705e 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -18,7 +18,8 @@ trait RecipePublicationRequestsRepo: def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] - def update(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Long] + def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): + IO[DbError, Boolean] final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: @@ -47,10 +48,9 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) .value ).provideDS - override def update(id: RecipeId, status: PublicationRequestStatus): - IO[DbError, Long] = + override def updateStatus(id: RecipeId, status: PublicationRequestStatus): IO[DbError, Boolean] = val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) - run(updateQ(id, dbStatus, reason)).provideDS + run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* diff --git a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala index 3505936b..f9586fee 100644 --- a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala @@ -12,15 +12,14 @@ final case class DbIngredientPublicationRequest( updatedAt: OffsetDateTime, status: DbPublicationRequestStatus, reason: Option[String], - comment: String ): def toDomain: IngredientPublicationRequest = - IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason), comment) + IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason)) object DbIngredientPublicationRequest: def fromDomain(req: IngredientPublicationRequest): DbIngredientPublicationRequest = - val (reason, status) = DbPublicationRequestStatus.fromDomain(req.status) - DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason, req.comment) + val (status, reason) = DbPublicationRequestStatus.fromDomain(req.status) + DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason) val createTable: String = """ CREATE TABLE IF NOT EXISTS ingredient_publication_requests( diff --git a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala index 5ca3ce16..cba9abd0 100644 --- a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala +++ b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala @@ -3,7 +3,7 @@ package db.tables.publication import db.QuillConfig.ctx.* import domain.PublicationRequestStatus -import java.sql.{PreparedStatement, Types} +import java.sql.Types enum DbPublicationRequestStatus: case Pending @@ -12,13 +12,13 @@ enum DbPublicationRequestStatus: def toDomain(reason: Option[String]): PublicationRequestStatus = this match case Pending => PublicationRequestStatus.Pending case Accepted => PublicationRequestStatus.Accepted - case Rejected => PublicationRequestStatus.Rejected(reason.get) // <- unsafe code here + case Rejected => PublicationRequestStatus.Rejected(reason) object DbPublicationRequestStatus: val fromDomain: PublicationRequestStatus => (DbPublicationRequestStatus, Option[String]) = case PublicationRequestStatus.Pending => (Pending, None) case PublicationRequestStatus.Accepted => (Accepted, None) - case PublicationRequestStatus.Rejected(reason) => (Rejected, Some(reason)) + case PublicationRequestStatus.Rejected(reason) => (Rejected, reason) val createType: String = """ DO $$ diff --git a/src/main/scala/domain/IngredientPublicationRequest.scala b/src/main/scala/domain/IngredientPublicationRequest.scala index cbe3fcb8..a8677fb2 100644 --- a/src/main/scala/domain/IngredientPublicationRequest.scala +++ b/src/main/scala/domain/IngredientPublicationRequest.scala @@ -8,6 +8,5 @@ final case class IngredientPublicationRequest( createdAt: OffsetDateTime, updatedAt: OffsetDateTime, status: PublicationRequestStatus, - comment: String ) diff --git a/src/main/scala/domain/PublicationRequestStatus.scala b/src/main/scala/domain/PublicationRequestStatus.scala index 82f543a8..ebdf0129 100644 --- a/src/main/scala/domain/PublicationRequestStatus.scala +++ b/src/main/scala/domain/PublicationRequestStatus.scala @@ -3,5 +3,5 @@ package domain enum PublicationRequestStatus: case Pending case Accepted - case Rejected(reason: String) + case Rejected(reason: Option[String]) From 2a9a337869f4eee60925bd8a70f1f439b2ac8388 Mon Sep 17 00:00:00 2001 From: danielambda Date: Fri, 18 Jul 2025 23:08:15 +0300 Subject: [PATCH 20/95] fix: added DbIngredientPublicationRequest create table to tables list --- src/main/scala/db/CreateTables.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/scala/db/CreateTables.scala b/src/main/scala/db/CreateTables.scala index 54d9d2fd..9f16f8e5 100644 --- a/src/main/scala/db/CreateTables.scala +++ b/src/main/scala/db/CreateTables.scala @@ -96,7 +96,9 @@ def createTables(xa: Transactor) = FOREIGN KEY (${storageInvitationTable.storageId}) REFERENCES $storagesTable(${storagesTable.id}) ON DELETE CASCADE ) """, + sql(DbRecipePublicationRequest.createTable), + sql(DbIngredientPublicationRequest.createTable), ) tableList.map(_.update.run()) From c43dcd5c87626c70cda3063237ebdb7e6e631339 Mon Sep 17 00:00:00 2001 From: danielambda Date: Fri, 18 Jul 2025 23:56:47 +0300 Subject: [PATCH 21/95] feat: add created publication request id to request publication endpoints --- .../scala/api/ingredients/RequestPublication.scala | 12 +++++++----- src/main/scala/api/recipes/RequestPublication.scala | 12 ++++++------ .../IngredientPublicationRequestRepo.scala | 11 +++++++---- .../api/recipes/RequestRecipePublicationTests.scala | 4 ++-- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index f1cbd3e5..80090de6 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -7,7 +7,7 @@ import db.repositories.{ IngredientPublicationRequestsRepo, IngredientsRepo, IngredientPublicationRequestsQueries } -import domain.{IngredientId, IngredientNotFound, InternalServerError} +import domain.{IngredientId, IngredientNotFound, InternalServerError, PublicationRequestId} import db.tables.publication.DbPublicationRequestStatus.given import db.QuillConfig.provideDS import db.QuillConfig.ctx.* @@ -15,7 +15,7 @@ import db.QuillConfig.ctx.* import io.circe.generic.auto.* import io.getquill.* import javax.sql.DataSource -import sttp.model.StatusCode.BadRequest +import sttp.model.StatusCode.{BadRequest, Created} import sttp.tapir.generic.auto.* import sttp.tapir.ztapir.* import zio.ZIO @@ -35,10 +35,12 @@ object IngredientAlreadyPending: val variant = BadRequest.variantJson[IngredientAlreadyPending] private type RequestPublicationEnv = IngredientPublicationRequestsRepo & IngredientsRepo & DataSource + private val requestPublication: ZServerEndpoint[RequestPublicationEnv, Any] = ingredientsEndpoint .post .in(path[IngredientId]("ingredientId") / "request-publication") + .out(statusCode(Created) and plainBody[PublicationRequestId]) .errorOut(oneOf( serverErrorVariant, IngredientAlreadyPublished.variant, IngredientAlreadyPending.variant, ingredientNotFoundVariant @@ -49,7 +51,7 @@ def requestPublicationHandler(ingredientId: IngredientId): ZIO[ AuthenticatedUser & RequestPublicationEnv, InternalServerError | IngredientAlreadyPublished | IngredientAlreadyPending | IngredientNotFound, - Unit + PublicationRequestId ] = for ingredient <- ZIO.serviceWithZIO[IngredientsRepo](_ @@ -69,8 +71,8 @@ def requestPublicationHandler(ingredientId: IngredientId): _ <- ZIO.fail(IngredientAlreadyPending(ingredientId)) .when(alreadyPending) - _ <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ + reqId <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ .requestPublication(ingredientId) .orElseFail(InternalServerError()) ) - yield () + yield reqId diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/RequestPublication.scala index a0942c5c..1a1949f3 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/RequestPublication.scala @@ -3,7 +3,7 @@ package api.recipes import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} import api.variantJson -import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound} +import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound, PublicationRequestId} import db.repositories.{RecipePublicationRequestsQueries, IngredientsQueries, RecipeIngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} import db.QuillConfig.provideDS import db.QuillConfig.ctx.* @@ -11,7 +11,7 @@ import io.circe.generic.auto.* import io.getquill.* import javax.sql.DataSource -import sttp.model.StatusCode.{BadRequest, NoContent} +import sttp.model.StatusCode.{BadRequest, Created} import sttp.tapir.generic.auto.* import sttp.tapir.ztapir.* import zio.ZIO @@ -49,6 +49,7 @@ private val requestPublication: ZServerEndpoint[PublishEnv, Any] = recipesEndpoint .post .in(path[RecipeId]("recipeId") / "request-publication") + .out(plainBody[PublicationRequestId] and statusCode(Created)) .errorOut(oneOf( serverErrorVariant, recipeNotFoundVariant, @@ -56,7 +57,6 @@ private val requestPublication: ZServerEndpoint[PublishEnv, Any] = RecipeAlreadyPending.variant, RecipeAlreadyPublished.variant, )) - .out(statusCode(NoContent)) .zSecuredServerLogic(requestPublicationHandler) private def requestPublicationHandler(recipeId: RecipeId): @@ -64,7 +64,7 @@ private def requestPublicationHandler(recipeId: RecipeId): AuthenticatedUser & PublishEnv, InternalServerError | RecipeAlreadyPublished | RecipeAlreadyPending | CannotPublishRecipeWithCustomIngredients | RecipeNotFound, - Unit + PublicationRequestId ] = for recipe <- ZIO.serviceWithZIO[RecipesRepo](_ @@ -94,8 +94,8 @@ private def requestPublicationHandler(recipeId: RecipeId): _ <- ZIO.fail(CannotPublishRecipeWithCustomIngredients(customIngredientIdsInRecipe)) .when(customIngredientIdsInRecipe.nonEmpty) - _ <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ + reqId <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ .createPublicationRequest(recipeId) .orElseFail(InternalServerError()) ) - yield () + yield reqId diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 5e63e640..f7a1539d 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -8,9 +8,10 @@ import domain.{PublicationRequestId, IngredientId, PublicationRequestStatus} import io.getquill.* import javax.sql.DataSource import zio.{IO, RLayer, ZLayer, ZIO} +import java.util.UUID trait IngredientPublicationRequestsRepo: - def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] + def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] @@ -23,8 +24,8 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) private given DataSource = dataSource - override def requestPublication(ingredientId: IngredientId): IO[DbError, Unit] = - run(requestPublicationQ(lift(ingredientId))).unit.provideDS + override def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] = + run(requestPublicationQ(lift(ingredientId))).provideDS override def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] = run(allPendingQ).provideDS @@ -42,9 +43,11 @@ object IngredientPublicationRequestsQueries: inline def ingredientPublicationRequestsQ = query[DbIngredientPublicationRequest] - inline def requestPublicationQ(inline ingredientId: IngredientId): Insert[DbIngredientPublicationRequest] = + inline def requestPublicationQ(inline ingredientId: IngredientId): + ActionReturning[DbIngredientPublicationRequest, UUID] = ingredientPublicationRequestsQ .insert(_.ingredientId -> ingredientId) + .returningGenerated(_.id) inline def allPendingQ = ingredientPublicationRequestsQ.filter(_.status == lift(Pending)) inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala index f88fbe87..18bc7353 100644 --- a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -31,14 +31,14 @@ object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: ) yield assertTrue(resp.status == Status.Unauthorized) }, - test("When authorized should get 200") { + test("When authorized should get 201") { for user <- registerUser recipeId <- createCustomRecipe(user, Vector.empty) resp <- requestRecipePublication(user, recipeId) - yield assertTrue(resp.status == Status.Ok) + yield assertTrue(resp.status == Status.Created) }, ).provideLayer(testLayer) From 0a2f99c174ef173c07cecd92f2e3e02fa74dcf0a Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 23:21:51 +0300 Subject: [PATCH 22/95] fix: foreign key --- .../db/tables/publication/DbIngredientPublicationRequest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala index f9586fee..82bec90d 100644 --- a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala @@ -29,7 +29,7 @@ object DbIngredientPublicationRequest: updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, status publication_request_status NOT NULL DEFAULT 'pending', reason TEXT, - FOREIGN KEY (ingredient_id) REFERENCES recipes(id) ON DELETE CASCADE + FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE CASCADE ); CREATE OR REPLACE TRIGGER update_timestamp From ad56ef7b0cf2b5cc4e59a6da8964a55c47501b76 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 23:22:44 +0300 Subject: [PATCH 23/95] fix: use infix filter to parse enum value properly --- .../db/repositories/IngredientPublicationRequestRepo.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index f7a1539d..e9c4b06e 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -49,7 +49,10 @@ object IngredientPublicationRequestsQueries: .insert(_.ingredientId -> ingredientId) .returningGenerated(_.id) - inline def allPendingQ = ingredientPublicationRequestsQ.filter(_.status == lift(Pending)) + inline def allPendingQ: EntityQuery[DbIngredientPublicationRequest] = + ingredientPublicationRequestsQ + .filter(r => infix"${r.status} = 'pending'::publication_request_status".as[Boolean]) + inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) inline def getQ(inline id: PublicationRequestId) = From af67c247940c6560519ea61c88f58c5a61bbe8ed Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 14:47:45 +0300 Subject: [PATCH 24/95] fix: made code compile after resolving conflicts --- src/main/scala/api/ingredients/RequestPublication.scala | 3 +-- src/main/scala/api/moderation/pubrequests/Get.scala | 8 ++++---- .../scala/api/moderation/pubrequests/GetSomePending.scala | 4 ++-- .../repositories/IngredientPublicationRequestRepo.scala | 5 ++--- .../db/repositories/RecipePublicationRequestsRepo.scala | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index be3d308c..6f5cde4d 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -8,7 +8,6 @@ import db.repositories.{ IngredientsRepo, IngredientPublicationRequestsQueries } import domain.{IngredientId, IngredientNotFound, InternalServerError, PublicationRequestId} -import db.tables.publication.DbPublicationRequestStatus.given import db.QuillConfig.provideDS import db.QuillConfig.ctx.* @@ -75,4 +74,4 @@ def requestPublicationHandler(ingredientId: IngredientId): .requestPublication(ingredientId) .orElseFail(InternalServerError()) ) - yield reqId \ No newline at end of file + yield reqId diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index b3577962..31c872e6 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -38,9 +38,9 @@ final case class PublicationRequestResp( private type GetReqEnv = RecipePublicationRequestsRepo + & RecipesRepo & IngredientPublicationRequestsRepo - -private type PublicationRequest = RecipePublicationRequest | IngredientPublicationRequest + & IngredientsRepo private val getRequest: ZServerEndpoint[GetReqEnv, Any] = publicationRequestEndpoint @@ -54,7 +54,7 @@ private def getRequestHandler(reqId: UUID): ZIO[AuthenticatedUser & GetReqEnv, InternalServerError | PublicationRequestNotFound, PublicationRequestResp] = - + def getIngredientRequest = ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) .flatMap { @@ -98,4 +98,4 @@ private def getRequestHandler(reqId: UUID): case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId) case _: DbNotRespondingError => InternalServerError() case x: PublicationRequestNotFound => x - } \ No newline at end of file + } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 96ca6309..c149c1ec 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -6,7 +6,7 @@ import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant import api.moderation.pubrequests.PublicationRequestType.* import db.DbError import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} -import domain.{IngredientPublicationRequest, InternalServerError, PublicationRequestNotFound, RecipePublicationRequest} +import domain.{InternalServerError, PublicationRequestNotFound} import io.circe.generic.auto.* import java.time.OffsetDateTime @@ -73,4 +73,4 @@ private def getSomePendingHandler(paginationParams: PaginationParams): }.mapError { case _: DbError => InternalServerError() case x: PublicationRequestNotFound => x - } \ No newline at end of file + } diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 36f6652d..65548e32 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -3,8 +3,7 @@ package db.repositories import db.DbError import db.tables.DbIngredient import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} -import db.tables.publication.DbPublicationRequestStatus.Pending -import domain.{IngredientId, PublicationRequestId, PublicationRequestNotFound} +import domain.{IngredientId, PublicationRequestId, PublicationRequestStatus} import io.getquill.* import javax.sql.DataSource @@ -13,7 +12,7 @@ import zio.{IO, RLayer, ZLayer, ZIO} trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] - def getAllPending: IO[DbError, Seq[DbIngredientPublicationRequest]] + def getAllPendingIds: IO[DbError, Seq[PublicationRequestId]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 750ade8f..1022fe9b 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -35,7 +35,7 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) ).provideDS override def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] = - run(allPendingQ.map(_.id)).provideDS.map(Vector.from) + run(pendingRequestsQ.map(_.id)).provideDS.map(Vector.from) override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(id).value).provideDS From 004a80a90438b51a39c6ef6c63f7c1876c9dc902 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 15:12:27 +0300 Subject: [PATCH 25/95] refactor: GetSomePending publication requests --- .../api/ingredients/RequestPublication.scala | 2 +- .../pubrequests/GetSomePending.scala | 69 +++++++------------ .../IngredientPublicationRequestRepo.scala | 44 ++++++------ .../RecipePublicationRequestsRepo.scala | 26 +++---- 4 files changed, 56 insertions(+), 85 deletions(-) diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index 6f5cde4d..da9145cc 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -64,7 +64,7 @@ def requestPublicationHandler(ingredientId: IngredientId): dataSource <- ZIO.service[DataSource] alreadyPending <- run( IngredientPublicationRequestsQueries - .pendingRequestsByIdQ(lift(ingredientId)).nonEmpty + .pendingRequestsByIngredientIdQ(lift(ingredientId)).nonEmpty ).provideDS(using dataSource) .orElseFail(InternalServerError()) _ <- ZIO.fail(IngredientAlreadyPending(ingredientId)) diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index c149c1ec..7d24f363 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -2,15 +2,13 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.common.search.{PaginationParams, paginate} -import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.EndpointErrorVariants.serverErrorVariant import api.moderation.pubrequests.PublicationRequestType.* -import db.DbError import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} -import domain.{InternalServerError, PublicationRequestNotFound} -import io.circe.generic.auto.* +import domain.{InternalServerError, PublicationRequestId} +import io.circe.generic.auto.* import java.time.OffsetDateTime -import java.util.UUID import sttp.model.StatusCode.NoContent import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody @@ -18,7 +16,7 @@ import sttp.tapir.ztapir.* import zio.ZIO final case class PublicationRequestSummary( - id: UUID, + id: PublicationRequestId, requestType: PublicationRequestType, entityName: String, createdAt: OffsetDateTime @@ -33,44 +31,27 @@ private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = .get .in(PaginationParams.query) .out(statusCode(NoContent)) - .out(jsonBody[Vector[PublicationRequestSummary]]) - .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) + .out(jsonBody[Seq[PublicationRequestSummary]]) + .errorOut(oneOf(serverErrorVariant)) .zSecuredServerLogic(getSomePendingHandler) private def getSomePendingHandler(paginationParams: PaginationParams): - ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError | PublicationRequestNotFound, Vector[PublicationRequestSummary]] = - { - for - pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllPendingIds) - .flatMap { ids => - ZIO.collectAll { - ids.map { id => - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getWithIngredient(id)) - .someOrFail(PublicationRequestNotFound(id)) - .map { - case (dbPubReq, dbIngredient) => - PublicationRequestSummary(id, Ingredient, dbIngredient.name, dbPubReq.createdAt) - } - } - } - } - pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllPendingIds) - .flatMap { ids => - ZIO.collectAll { - ids.map { id => - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(id)) - .someOrFail(PublicationRequestNotFound(id)) - .map { - case (dbPubReq, dbRecipe) => - PublicationRequestSummary(id, Recipe, dbRecipe.name, dbPubReq.createdAt) - } - } - } - } - yield (pendingRecipeReqs ++ pendingIngredientReqs) - .sortBy(_.createdAt.toEpochSecond) - .paginate(paginationParams) - }.mapError { - case _: DbError => InternalServerError() - case x: PublicationRequestNotFound => x - } + ZIO[AuthenticatedUser & GetSomePendingEnv, InternalServerError, Seq[PublicationRequestSummary]] = + for + pendingIngredientReqs <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ + .getPendingRequestsWithIngredients + .orElseFail(InternalServerError()) + .map(_.map { case (req, ingredient) => + PublicationRequestSummary(req.id, Ingredient, ingredient.name, req.createdAt) + }) + ) + pendingRecipeReqs <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ + .getPendingRequestsWithRecipes + .orElseFail(InternalServerError()) + .map(_.map { case (req, recipe) => + PublicationRequestSummary(req.id, Recipe, recipe.name, req.createdAt) + }) + ) + yield (pendingRecipeReqs ++ pendingIngredientReqs) + .sortBy(_.createdAt.toEpochSecond) + .paginate(paginationParams) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 65548e32..94115df4 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -12,9 +12,8 @@ import zio.{IO, RLayer, ZLayer, ZIO} trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] - def getAllPendingIds: IO[DbError, Seq[PublicationRequestId]] + def getPendingRequestsWithIngredients: IO[DbError, Seq[(DbIngredientPublicationRequest, DbIngredient)]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] - def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) @@ -28,9 +27,13 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] = run(requestPublicationQ(lift(ingredientId))).provideDS - override def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] = { - run(allPendingQ.map(_.id)).provideDS.map(Vector.from) - } + override def getPendingRequestsWithIngredients: + IO[DbError, Seq[(DbIngredientPublicationRequest, DbIngredient)]] = + run( + pendingRequestsQ + .join(IngredientsQueries.ingredientsQ) + .on(_.ingredientId == _.id) + ).provideDS override def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] = run(getQ(id)).map(_.headOption).provideDS @@ -40,43 +43,36 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS - override def getWithIngredient(id: PublicationRequestId): - IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] = - - run( - getQ(id) - .join(IngredientsQueries.ingredientsQ) - .on((rpq, r) => rpq.ingredientId == r.id) - ).map(_.headOption).provideDS - object IngredientPublicationRequestsQueries: import db.QuillConfig.ctx.* - inline def ingredientPublicationRequestsQ = query[DbIngredientPublicationRequest] + inline def requestsQ = query[DbIngredientPublicationRequest] inline def requestPublicationQ(inline ingredientId: IngredientId): ActionReturning[DbIngredientPublicationRequest, UUID] = - ingredientPublicationRequestsQ + requestsQ .insert(_.ingredientId -> ingredientId) .returningGenerated(_.id) - inline def allPendingQ: EntityQuery[DbIngredientPublicationRequest] = - ingredientPublicationRequestsQ + inline def pendingRequestsQ: EntityQuery[DbIngredientPublicationRequest] = + requestsQ .filter(r => infix"${r.status} = 'pending'::publication_request_status".as[Boolean]) - inline def pendingRequestsByIdQ(inline ingredientId: IngredientId) = allPendingQ.filter(_.ingredientId == ingredientId) + inline def pendingRequestsByIngredientIdQ(inline ingredientId: IngredientId): + EntityQuery[DbIngredientPublicationRequest] = + pendingRequestsQ + .filter(_.ingredientId == ingredientId) - inline def getQ(inline id: PublicationRequestId) = - ingredientPublicationRequestsQ + inline def getQ(inline id: PublicationRequestId): EntityQuery[DbIngredientPublicationRequest] = + requestsQ .filter(_.id == lift(id)) - .take(1) inline def updateQ( inline id: PublicationRequestId, inline status: DbPublicationRequestStatus, inline reason: Option[String], - ) = - ingredientPublicationRequestsQ + ): Update[DbIngredientPublicationRequest] = + requestsQ .filter(_.id == lift(id)) .update( _.status -> lift(status), diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 1022fe9b..202ffd29 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -4,20 +4,17 @@ import db.DbError import db.QuillConfig.ctx import db.tables.DbRecipe import db.tables.publication.{DbPublicationRequestStatus, DbRecipePublicationRequest} -import db.tables.publication.DbPublicationRequestStatus.Pending import domain.{PublicationRequestStatus, PublicationRequestId, RecipeId} import io.getquill.* +import java.util.UUID import javax.sql.DataSource import zio.{IO, RLayer, ZLayer, ZIO} -import java.util.UUID trait RecipePublicationRequestsRepo: def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] - def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] + def getPendingRequestsWithRecipes: IO[DbError, Seq[(DbRecipePublicationRequest, DbRecipe)]] def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] - def getWithRecipe(id: PublicationRequestId): - IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] @@ -34,20 +31,17 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) createPublicationRequestQ(lift(recipeId)) ).provideDS - override def getAllPendingIds: IO[DbError, Vector[PublicationRequestId]] = - run(pendingRequestsQ.map(_.id)).provideDS.map(Vector.from) - - override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = - run(getQ(id).value).provideDS - - override def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] = + override def getPendingRequestsWithRecipes: + IO[DbError, List[(DbRecipePublicationRequest, DbRecipe)]] = run( - getQ(id) + pendingRequestsQ .join(RecipesQueries.recipesQ) - .on((rpq, r) => rpq.recipeId == r.id) - .value + .on(_.recipeId == _.id) ).provideDS + override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = + run(getQ(id).value).provideDS + override def updateStatus(id: RecipeId, status: PublicationRequestStatus): IO[DbError, Boolean] = val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS @@ -66,7 +60,7 @@ object RecipePublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(_.status == lift(Pending)) + .filter(_.status == lift(DbPublicationRequestStatus.Pending)) inline def pendingRequestsOfRecipeQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = From 35f5e3093c72b642f70e7012d2a948a90d944c9b Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 15:35:51 +0300 Subject: [PATCH 26/95] fix: use infix in recipes publication requsts repo to make things work --- .../IngredientPublicationRequestRepo.scala | 14 ++++++------- .../RecipePublicationRequestsRepo.scala | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 94115df4..188f9185 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -36,16 +36,14 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) ).provideDS override def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] = - run(getQ(id)).map(_.headOption).provideDS + run(getQ(lift(id))).map(_.headOption).provideDS override def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] = val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) - run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS + run(updateQ(lift(id), lift(dbStatus), lift(reason))).map(_ > 0).provideDS object IngredientPublicationRequestsQueries: - import db.QuillConfig.ctx.* - inline def requestsQ = query[DbIngredientPublicationRequest] inline def requestPublicationQ(inline ingredientId: IngredientId): @@ -65,7 +63,7 @@ object IngredientPublicationRequestsQueries: inline def getQ(inline id: PublicationRequestId): EntityQuery[DbIngredientPublicationRequest] = requestsQ - .filter(_.id == lift(id)) + .filter(_.id == id) inline def updateQ( inline id: PublicationRequestId, @@ -73,10 +71,10 @@ object IngredientPublicationRequestsQueries: inline reason: Option[String], ): Update[DbIngredientPublicationRequest] = requestsQ - .filter(_.id == lift(id)) + .filter(_.id == id) .update( - _.status -> lift(status), - _.reason -> lift(reason), + _.status -> status, + _.reason -> reason, ) object IngredientPublicationRequestsRepo: diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 202ffd29..dbe09b17 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -40,15 +40,16 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) ).provideDS override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = - run(getQ(id).value).provideDS + run(getQ(lift(id)).value).provideDS - override def updateStatus(id: RecipeId, status: PublicationRequestStatus): IO[DbError, Boolean] = + override def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): + IO[DbError, Boolean] = val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) - run(updateQ(id, dbStatus, reason)).map(_ > 0).provideDS + run( + updateQ(lift(id), lift(dbStatus), lift(reason)) + ).map(_ > 0).provideDS object RecipePublicationRequestsQueries: - import db.QuillConfig.ctx.* - inline def recipePublicationRequestsQ: EntityQuery[DbRecipePublicationRequest] = query[DbRecipePublicationRequest] @@ -60,7 +61,7 @@ object RecipePublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(_.status == lift(DbPublicationRequestStatus.Pending)) + .filter(r => infix"${r.status} = 'pending'::publication_request_status".as[Boolean]) inline def pendingRequestsOfRecipeQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = @@ -69,7 +70,7 @@ object RecipePublicationRequestsQueries: inline def getQ(inline id: PublicationRequestId): EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(_.id == lift(id)) + .filter(_.id == id) inline def updateQ( inline id: PublicationRequestId, @@ -77,10 +78,10 @@ object RecipePublicationRequestsQueries: inline reason: Option[String], ): Update[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(_.id == lift(id)) + .filter(_.id == id) .update( - _.status -> lift(status), - _.reason -> lift(reason), + _.status -> status, + _.reason -> reason, ) object RecipePublicationRequestsRepo: From ce038f9da005c04fd3b29e33fa063c8b50f2e2b1 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 16:04:02 +0300 Subject: [PATCH 27/95] fix: write proper encoder for publication request status enum --- .../DbPublicationRequestStatus.scala | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala index cba9abd0..d04b6d48 100644 --- a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala +++ b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala @@ -4,6 +4,7 @@ import db.QuillConfig.ctx.* import domain.PublicationRequestStatus import java.sql.Types +import org.postgresql.util.PGobject enum DbPublicationRequestStatus: case Pending @@ -14,40 +15,46 @@ enum DbPublicationRequestStatus: case Accepted => PublicationRequestStatus.Accepted case Rejected => PublicationRequestStatus.Rejected(reason) + def postgresValue: String = this match + case Pending => "pending" + case Accepted => "accepted" + case Rejected => "rejected" + object DbPublicationRequestStatus: val fromDomain: PublicationRequestStatus => (DbPublicationRequestStatus, Option[String]) = case PublicationRequestStatus.Pending => (Pending, None) case PublicationRequestStatus.Accepted => (Accepted, None) case PublicationRequestStatus.Rejected(reason) => (Rejected, reason) - val createType: String = """ - DO $$ + val postgresTypeName: String = "publication_request_status" + + val createType: String = s""" + DO $$$$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'publication_request_status') THEN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '$postgresTypeName') THEN CREATE TYPE publication_request_status AS ENUM ( 'pending', 'accepted', 'rejected' ); END IF; - END $$; + END $$$$; """ - given JdbcDecoder[DbPublicationRequestStatus]( - (index, row, _) => - val statusType = row.getString(index) - statusType match + given Decoder[DbPublicationRequestStatus] = + decoder(row => index => + row.getObject(index).toString match case "pending" => DbPublicationRequestStatus.Pending case "accepted" => DbPublicationRequestStatus.Accepted case "rejected" => DbPublicationRequestStatus.Rejected - ) + ) - given JdbcEncoder[DbPublicationRequestStatus] = encoder( + given Encoder[DbPublicationRequestStatus] = encoder( Types.OTHER, - (index, value, row) => - val statusString = value match - case DbPublicationRequestStatus.Pending => "pending" - case DbPublicationRequestStatus.Accepted => "accepted" - case DbPublicationRequestStatus.Rejected => "rejected" - row.setString(index, statusString) + (index, value, row) => { + val pgObj = new PGobject() + pgObj.setType(postgresTypeName) + pgObj.setValue(value.postgresValue) + row.setObject(index, pgObj, Types.OTHER) + } ) From 2db6bd2954ed0db56d11fe3bdc88d83531c5abc3 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 16:10:11 +0300 Subject: [PATCH 28/95] refactor: pull publication_request_status typename to inline val --- .../db/repositories/IngredientPublicationRequestRepo.scala | 5 ++++- .../db/repositories/RecipePublicationRequestsRepo.scala | 5 ++++- .../db/tables/publication/DbPublicationRequestStatus.scala | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 188f9185..b55ef47b 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -54,7 +54,10 @@ object IngredientPublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbIngredientPublicationRequest] = requestsQ - .filter(r => infix"${r.status} = 'pending'::publication_request_status".as[Boolean]) + .filter(r => + infix"${r.status} = 'pending'::${DbPublicationRequestStatus.postgresTypeName}" + .asCondition + ) inline def pendingRequestsByIngredientIdQ(inline ingredientId: IngredientId): EntityQuery[DbIngredientPublicationRequest] = diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index dbe09b17..40593d38 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -61,7 +61,10 @@ object RecipePublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ - .filter(r => infix"${r.status} = 'pending'::publication_request_status".as[Boolean]) + .filter(r => + infix"${r.status} = 'pending'::${DbPublicationRequestStatus.postgresTypeName}" + .asCondition + ) inline def pendingRequestsOfRecipeQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = diff --git a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala index d04b6d48..decb5dca 100644 --- a/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala +++ b/src/main/scala/db/tables/publication/DbPublicationRequestStatus.scala @@ -26,7 +26,8 @@ object DbPublicationRequestStatus: case PublicationRequestStatus.Accepted => (Accepted, None) case PublicationRequestStatus.Rejected(reason) => (Rejected, reason) - val postgresTypeName: String = "publication_request_status" + inline val postgresTypeName: "publication_request_status" = + "publication_request_status" val createType: String = s""" DO $$$$ From c427ad1fd1771c89aca797f7c6387ec25d9b55d6 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 17:27:44 +0300 Subject: [PATCH 29/95] feat: ignore on insert conflict --- src/main/scala/db/repositories/ShoppingListsRepo.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/db/repositories/ShoppingListsRepo.scala b/src/main/scala/db/repositories/ShoppingListsRepo.scala index 215cc62f..1ab4c699 100644 --- a/src/main/scala/db/repositories/ShoppingListsRepo.scala +++ b/src/main/scala/db/repositories/ShoppingListsRepo.scala @@ -71,6 +71,7 @@ object ShoppingListsQueries: inline def addIngredientQ(inline userId: UserId, inline ingredientId: IngredientId) = query[DbShoppingList] .insertValue(DbShoppingList(userId, ingredientId)) + .onConflictIgnore inline def deleteIngredientQ(inline userId: UserId, inline ingredientId: IngredientId): Delete[DbShoppingList] = query[DbShoppingList] From d56d34f6275d3ac54cbdf012a677702c897128aa Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 18:12:53 +0300 Subject: [PATCH 30/95] feat: make creator id optional in models --- src/main/scala/db/tables/DbRecipe.scala | 2 +- src/main/scala/domain/Recipe.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/db/tables/DbRecipe.scala b/src/main/scala/db/tables/DbRecipe.scala index 9cb85ee0..a02e8d37 100644 --- a/src/main/scala/db/tables/DbRecipe.scala +++ b/src/main/scala/db/tables/DbRecipe.scala @@ -9,7 +9,7 @@ import com.augustnagro.magnum.* final case class DbRecipe( @Id id: RecipeId, name: String, - creatorId: UserId, + creatorId: Option[UserId], isPublished: Boolean, sourceLink: Option[String], ) derives DbCodec diff --git a/src/main/scala/domain/Recipe.scala b/src/main/scala/domain/Recipe.scala index 271b5d18..b55e4f70 100644 --- a/src/main/scala/domain/Recipe.scala +++ b/src/main/scala/domain/Recipe.scala @@ -3,7 +3,7 @@ package domain case class Recipe( id: RecipeId, name: String, - creatorId: UserId, + creatorId: Option[UserId], isPublished: Boolean, ingredients: List[IngredientId], sourceLink: Option[String], From 9ac0aef04d874b26a2ce6d5b4081c6cb4563096d Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 18:13:44 +0300 Subject: [PATCH 31/95] feat: change decoding logic to make creator name & id optional --- src/main/scala/api/recipes/Get.scala | 33 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index dd5a4cc2..7fe9df2b 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -44,7 +44,7 @@ final case class RecipeResp( ingredients: Vector[IngredientResp], name: String, sourceLink: Option[String], - creator: RecipeCreatorResp, + creator: Option[RecipeCreatorResp], ) private type GetEnv = Transactor @@ -61,8 +61,8 @@ private val get: ZServerEndpoint[GetEnv, Any] = private case class RawRecipeResult( name: String, sourceLink: Option[String], - creatorId: UserId, - creatorFullName: String, + creatorId: Option[UserId], + creatorFullName: Option[String], ingredients: String, // JSON string from PostgreSQL ) @@ -79,15 +79,24 @@ private def getHandler(recipeId: RecipeId): }.someOrFail(RecipeNotFound(recipeId)).flatMap { rawResult => // Parse the JSON ingredients string ZIO.fromEither(decode[Vector[IngredientResp]](rawResult.ingredients)) - .map(RecipeResp( - _, - rawResult.name, - rawResult.sourceLink, - RecipeCreatorResp( - rawResult.creatorId, - rawResult.creatorFullName, - ), - )) + .map { + (rawResult.creatorId, rawResult.creatorFullName) match + case (Some(creatorId), Some(creatorFullName)) => RecipeResp( + _, + rawResult.name, + rawResult.sourceLink, + Some(RecipeCreatorResp( + creatorId, + creatorFullName, + )), + ) + case _ => RecipeResp( + _, + rawResult.name, + rawResult.sourceLink, + None + ) + } .orElseFail(InternalServerError(s"Failed to parse ingredients JSON: ${rawResult.ingredients}")) }.mapError { case e: DbError.FailedDbQuery => handleFailedSqlQuery(e) From ce7b83246e0bf0a3a4be2cd5731110a80e924e09 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 18:14:38 +0300 Subject: [PATCH 32/95] feat: wrap the remaining creator related code in options --- src/main/scala/api/recipes/Delete.scala | 2 +- src/main/scala/db/CreateTables.scala | 2 +- src/main/scala/db/repositories/RecipesRepo.scala | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/api/recipes/Delete.scala b/src/main/scala/api/recipes/Delete.scala index b5d475ee..22ca1eb2 100644 --- a/src/main/scala/api/recipes/Delete.scala +++ b/src/main/scala/api/recipes/Delete.scala @@ -30,7 +30,7 @@ private def deleteHandler(recipeId: RecipeId): _ <- mRecipe match case None => ZIO.unit case Some(recipe) => - if recipe.creatorId == userId then + if recipe.creatorId.contains(userId) then ZIO.serviceWithZIO[RecipesRepo](_ .deleteRecipe(recipeId) .orElseFail(InternalServerError()) diff --git a/src/main/scala/db/CreateTables.scala b/src/main/scala/db/CreateTables.scala index 2ef9b63c..dace94a0 100644 --- a/src/main/scala/db/CreateTables.scala +++ b/src/main/scala/db/CreateTables.scala @@ -61,7 +61,7 @@ def createTables(xa: Transactor) = CREATE TABLE IF NOT EXISTS $recipesTable( ${recipesTable.id} UUID PRIMARY KEY DEFAULT gen_random_uuid(), ${recipesTable.name} VARCHAR(255) NOT NULL, - ${recipesTable.creatorId} BIGINT NOT NULL, + ${recipesTable.creatorId} BIGINT, ${recipesTable.isPublished} BOOLEAN NOT NULL, ${recipesTable.sourceLink} VARCHAR(255), FOREIGN KEY (${recipesTable.creatorId}) REFERENCES $usersTable(${usersTable.id}) ON DELETE CASCADE diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index a7dc31d0..cc6919b0 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -39,7 +39,7 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: creatorId <- ZIO.serviceWith[AuthenticatedUser](_.userId) recipeId <- run( recipesQ - .insertValue(lift(DbRecipe(id=null, name, creatorId, isPublished=false, sourceLink))) + .insertValue(lift(DbRecipe(id=null, name, Some(creatorId), isPublished=false, sourceLink))) .returningGenerated(r => r.id) // null is safe here because of returningGenerated ) _ <- run( @@ -113,10 +113,10 @@ object RecipesQueries: recipesQ.filter(_.isPublished) inline def visibleRecipesQ(inline userId: UserId): EntityQuery[DbRecipe] = - recipesQ.filter(r => r.isPublished || r.creatorId == userId) + recipesQ.filter(r => r.isPublished || r.creatorId.contains(userId)) inline def customRecipesQ(inline userId: UserId): EntityQuery[DbRecipe] = - recipesQ.filter(r => r.creatorId == userId) + recipesQ.filter(r => r.creatorId.contains(userId)) inline def getRecipeQ(inline recipeId: RecipeId): EntityQuery[DbRecipe] = recipesQ.filter(r => r.id == recipeId) From ab70e176f4efd017e699c429aa994cb63fc14277 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 18:47:13 +0300 Subject: [PATCH 33/95] test: When requesting publication of recipe with public ingredients, pending request should be created --- .../api/recipes/GetRecipeTests.scala | 40 +++++++++---------- .../recipes/GetSuggestedRecipesTests.scala | 10 ++--- .../RequestRecipePublicationTests.scala | 19 +++++++++ .../AddIngredientToRecipeTests.scala | 24 +++++------ .../RemoveIngredientFromRecipeTests.scala | 24 +++++------ src/test/scala/integration/common/Utils.scala | 3 +- 6 files changed, 70 insertions(+), 50 deletions(-) diff --git a/src/test/scala/integration/api/recipes/GetRecipeTests.scala b/src/test/scala/integration/api/recipes/GetRecipeTests.scala index d4f0f3d5..6b4c0483 100644 --- a/src/test/scala/integration/api/recipes/GetRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/GetRecipeTests.scala @@ -46,8 +46,8 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser storageId <- createStorage(user) - ingredientIds <- createNIngredients(defaultIngredientAmount) - _extraIngredientIds <- createNIngredients(defaultIngredientAmount) + ingredientIds <- createNPublicIngredients(defaultIngredientAmount) + _extraIngredientIds <- createNPublicIngredients(defaultIngredientAmount) _ <- addIngredientsToStorage(storageId, ingredientIds) recipeId <- createCustomRecipe(user, ingredientIds) @@ -67,8 +67,8 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: storage1Id <- createStorage(user) storage2Id <- createStorage(user) - storage1UsedIngredientIds <- createNIngredients(defaultIngredientAmount) - storage2UsedIngredientIds <- createNIngredients(defaultIngredientAmount) + storage1UsedIngredientIds <- createNPublicIngredients(defaultIngredientAmount) + storage2UsedIngredientIds <- createNPublicIngredients(defaultIngredientAmount) recipeIngredientsIds = storage1UsedIngredientIds ++ storage2UsedIngredientIds @@ -78,9 +78,9 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: _ <- addIngredientsToStorage(storage2Id, storage2UsedIngredientIds) // create some extra ingredients that are not used in the recipe - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(storage1Id, _)) - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(storage2Id, _)) resp <- getRecipe(user, recipeId) @@ -113,16 +113,16 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: user2StorageId <- createStorage(user2) temp <- for - commonIngredientIds <- createNIngredients(defaultIngredientAmount) - user1OnlyStorageIngredientIds <- createNIngredients(defaultIngredientAmount) - user2OnlyStorageIngredientIds <- createNIngredients(defaultIngredientAmount) + commonIngredientIds <- createNPublicIngredients(defaultIngredientAmount) + user1OnlyStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) + user2OnlyStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) yield (user1OnlyStorageIngredientIds ++ commonIngredientIds, user2OnlyStorageIngredientIds ++ commonIngredientIds) (user1StorageIngredientIds, user2StorageIngredientIds) = temp _ <- addIngredientsToStorage(user1StorageId, user1StorageIngredientIds) _ <- addIngredientsToStorage(user2StorageId, user2StorageIngredientIds) - sharedStorageIngredientIds <- createNIngredients(defaultIngredientAmount) + sharedStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) _ <- addIngredientsToStorage(sharedStorageId, sharedStorageIngredientIds) recipeIngredientsIds = @@ -134,11 +134,11 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) // create some extra ingredients that are not used in the recipe - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(user1StorageId, _)) - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(user2StorageId, _)) - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(sharedStorageId, _)) // case 1: sending request as a 1st user @@ -199,16 +199,16 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: user2StorageId <- createStorage(user2) temp <- for - commonIngredientIds <- createNIngredients(defaultIngredientAmount) - user1OnlyStorageIngredientIds <- createNIngredients(defaultIngredientAmount) - user2OnlyStorageIngredientIds <- createNIngredients(defaultIngredientAmount) + commonIngredientIds <- createNPublicIngredients(defaultIngredientAmount) + user1OnlyStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) + user2OnlyStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) yield (user1OnlyStorageIngredientIds ++ commonIngredientIds, user2OnlyStorageIngredientIds ++ commonIngredientIds) (user1StorageIngredientIds, user2StorageIngredientIds) = temp _ <- addIngredientsToStorage(user1StorageId, user1StorageIngredientIds) _ <- addIngredientsToStorage(user2StorageId, user2StorageIngredientIds) - sharedStorageIngredientIds <- createNIngredients(defaultIngredientAmount) + sharedStorageIngredientIds <- createNPublicIngredients(defaultIngredientAmount) _ <- addIngredientsToStorage(sharedStorageId, sharedStorageIngredientIds) recipeIngredientsIds = @@ -219,11 +219,11 @@ object GetRecipeTests extends ZIOIntegrationTestSpec: recipeId <- createCustomRecipe(user1, recipeIngredientsIds) // create some extra ingredients that are not used in the recipe - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(user1StorageId, _)) - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(user2StorageId, _)) - _ <- createNIngredients(defaultIngredientAmount) + _ <- createNPublicIngredients(defaultIngredientAmount) .flatMap(addIngredientsToStorage(sharedStorageId, _)) resp <- getRecipe(user2, recipeId) diff --git a/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala b/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala index b107ad2c..cd75cfa9 100644 --- a/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala +++ b/src/test/scala/integration/api/recipes/GetSuggestedRecipesTests.scala @@ -45,7 +45,7 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: n <- Gen.int(2, 10).runHead.some recipeIds <- ZIO.collectAll( (1 to n).map(i => - createNIngredients(i).flatMap( + createNPublicIngredients(i).flatMap( createCustomRecipe(user, _) ) ) @@ -70,13 +70,13 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: tmp <- ZIO.foreach(Seq(storage1Id, storage2Id))(storageId => for n <- Gen.int(3, 10).runHead.some - ingredientIds <- createNIngredients(n) + ingredientIds <- createNPublicIngredients(n) _ <- addIngredientsToStorage(storage1Id, ingredientIds) yield ingredientIds) Seq(storage1IngredientIds, storage2IngredientIds) = tmp n <- Gen.int(2, 10).runHead.some - otherIngredientIds <- createNIngredients(n) + otherIngredientIds <- createNPublicIngredients(n) recipe1Storage1Availability = minStorageIngredientsAmount recipe1Storage2Availability = minStorageIngredientsAmount - 1 @@ -127,13 +127,13 @@ object GetSuggestedRecipesTests extends ZIOIntegrationTestSpec: tmp <- ZIO.foreach(Seq(memberedStorageId, storage1Id, storage2Id))(storageId => for n <- Gen.int(3, 10).runHead.some - ingredientIds <- createNIngredients(n) + ingredientIds <- createNPublicIngredients(n) _ <- addIngredientsToStorage(storage1Id, ingredientIds) yield ingredientIds) Seq(memberedStorageIngredientIds, storage1IngredientIds, storage2IngredientIds) = tmp n <- Gen.int(2, 10).runHead.some - otherIngredientIds <- createNIngredients(n) + otherIngredientIds <- createNPublicIngredients(n) recipe1MemberedStorageAvailability = minStorageIngredientsAmount - 2 recipe1Storage1Availability = minStorageIngredientsAmount diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala index 18bc7353..a08b17d8 100644 --- a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -1,6 +1,8 @@ package integration.api.recipes import api.Authentication.AuthenticatedUser +import db.repositories.RecipePublicationRequestsRepo +import db.tables.publication.DbPublicationRequestStatus import domain.RecipeId import integration.common.Utils.* import integration.common.ZIOIntegrationTestSpec @@ -40,5 +42,22 @@ object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: resp <- requestRecipePublication(user, recipeId) yield assertTrue(resp.status == Status.Created) }, + test("When requesting publication of recipe with public ingredients, pending request should be created") { + for + user <- registerUser + + n <- Gen.int(2, 8).runHead.some + ingredientIds <- createNPublicIngredients(n) + recipeId <- createCustomRecipe(user, ingredientIds) + + resp <- requestRecipePublication(user, recipeId) + + bodyStr <- resp.body.asString + requestId <- ZIO.fromOption(bodyStr.toUUID) + request <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(requestId)) + yield assertTrue(resp.status == Status.Created) + && assertTrue(request.is(_.some).recipeId == recipeId) + && assertTrue(request.is(_.some).status == DbPublicationRequestStatus.Pending) + } ).provideLayer(testLayer) diff --git a/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala b/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala index 33af35de..63a7e7b2 100644 --- a/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/ingredients/AddIngredientToRecipeTests.scala @@ -43,7 +43,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser - recipeId <- createNIngredients(5) + recipeId <- createNPublicIngredients(5) .flatMap(createCustomRecipe(user, _)) ingredientId <- createPublicIngredient @@ -56,7 +56,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialIngredients) ingredientId <- createPublicIngredient @@ -76,7 +76,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialIngredients) ingredientId <- createCustomIngredient(user) @@ -98,7 +98,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialIngredients) resp <- addIngredientToRecipe(user, recipeId, ingredientId) @@ -118,7 +118,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for otherUser <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(otherUser, initialIngredients) ingredientId <- createPublicIngredient @@ -142,7 +142,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for otherUser <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(otherUser, initialIngredients) user <- registerUser @@ -164,7 +164,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: should get 403 cannot modify published recipe and ingredient should NOT be added to the recipe""") { for initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- registerUser.flatMap(createCustomRecipe(_, initialIngredients)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -188,7 +188,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: should get 403 cannot modify published recipe and ingredient should NOT be added to the recipe""") { for initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- registerUser.flatMap(createCustomRecipe(_, initialIngredients)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -212,7 +212,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialIngredients) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -235,7 +235,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialIngredients) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -260,7 +260,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) .map(_ :+ ingredientId) recipeId <- createCustomRecipe(user, initialIngredients) @@ -280,7 +280,7 @@ object AddIngredientToRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createCustomIngredient(user) initialIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) .map(_ :+ ingredientId) recipeId <- createCustomRecipe(user, initialIngredients) diff --git a/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala b/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala index c2edf478..98dd8039 100644 --- a/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala +++ b/src/test/scala/integration/api/recipes/ingredients/RemoveIngredientFromRecipeTests.scala @@ -46,7 +46,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -59,7 +59,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -78,7 +78,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createCustomIngredient(user) otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -96,7 +96,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherUser <- registerUser otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(otherUser, otherIngredientIds :+ ingredientId) user <- registerUser @@ -118,7 +118,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: otherUser <- registerUser ingredientId <- createCustomIngredient(otherUser) otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(otherUser, otherIngredientIds :+ ingredientId) user <- registerUser @@ -140,7 +140,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- registerUser.flatMap(createCustomRecipe(_, otherIngredientIds :+ ingredientId)) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -163,7 +163,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: ingredientId <- createPublicIngredient user <- registerUser otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -184,7 +184,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser ingredientId <- createCustomIngredient(user) otherIngredientIds <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, otherIngredientIds :+ ingredientId) _ <- ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) @@ -204,7 +204,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- getRandomUUID @@ -226,7 +226,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialRecipeIngredients) resp <- removeIngredientFromRecipe(user, recipeId, ingredientId) @@ -243,7 +243,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- createPublicIngredient @@ -262,7 +262,7 @@ object RemoveIngredientFromRecipeTests extends ZIOIntegrationTestSpec: for user <- registerUser initialRecipeIngredients <- Gen.int(0, 5).runHead.some - .flatMap(createNIngredients) + .flatMap(createNPublicIngredients) recipeId <- createCustomRecipe(user, initialRecipeIngredients) ingredientId <- createCustomIngredient(user) diff --git a/src/test/scala/integration/common/Utils.scala b/src/test/scala/integration/common/Utils.scala index 057f7606..22e65d36 100644 --- a/src/test/scala/integration/common/Utils.scala +++ b/src/test/scala/integration/common/Utils.scala @@ -100,7 +100,8 @@ object Utils: ) ) - def createNIngredients(n: Int): ZIO[IngredientsRepo, InternalServerError, Vector[IngredientId]] = + def createNPublicIngredients(n: Int): + ZIO[IngredientsRepo, InternalServerError, Vector[IngredientId]] = ZIO.collectAll( (1 to n).map(_ => createPublicIngredient).toVector ) From 44bc882414a127db3d64d337f0ebfd9bdee60695 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 18:55:33 +0300 Subject: [PATCH 34/95] refactor: use for comprehension instead of manual option matching --- src/main/scala/api/recipes/Get.scala | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index 7fe9df2b..f8981e9d 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -79,23 +79,17 @@ private def getHandler(recipeId: RecipeId): }.someOrFail(RecipeNotFound(recipeId)).flatMap { rawResult => // Parse the JSON ingredients string ZIO.fromEither(decode[Vector[IngredientResp]](rawResult.ingredients)) - .map { - (rawResult.creatorId, rawResult.creatorFullName) match - case (Some(creatorId), Some(creatorFullName)) => RecipeResp( - _, - rawResult.name, - rawResult.sourceLink, - Some(RecipeCreatorResp( - creatorId, - creatorFullName, - )), - ) - case _ => RecipeResp( - _, - rawResult.name, - rawResult.sourceLink, - None - ) + .map { ingredients => + val recipeCreatorResp = for + creatorId <- rawResult.creatorId + creatorFullName <- rawResult.creatorFullName + yield RecipeCreatorResp(creatorId, creatorFullName) + RecipeResp( + ingredients, + rawResult.name, + rawResult.sourceLink, + recipeCreatorResp, + ) } .orElseFail(InternalServerError(s"Failed to parse ingredients JSON: ${rawResult.ingredients}")) }.mapError { From 8294c7c4b9f7af5e44221b3299a0535e1c99e167 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sat, 19 Jul 2025 20:13:06 +0300 Subject: [PATCH 35/95] fix: quill infix interpolation was surrounding everything in ' which is invalid syntax in postgres --- .../api/ingredients/RequestPublication.scala | 11 ++++++----- .../scala/api/recipes/RequestPublication.scala | 12 +++++------- .../IngredientPublicationRequestRepo.scala | 5 +++-- .../RecipePublicationRequestsRepo.scala | 2 +- .../DbIngredientPublicationRequest.scala | 18 +++++++++--------- .../DbRecipePublicationRequest.scala | 6 +++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/scala/api/ingredients/RequestPublication.scala b/src/main/scala/api/ingredients/RequestPublication.scala index da9145cc..80fd362d 100644 --- a/src/main/scala/api/ingredients/RequestPublication.scala +++ b/src/main/scala/api/ingredients/RequestPublication.scala @@ -19,14 +19,14 @@ import sttp.tapir.generic.auto.* import sttp.tapir.ztapir.* import zio.ZIO -private final case class IngredientAlreadyPublished( +final case class IngredientAlreadyPublished( ingredientId: IngredientId, message: String = "Ingredient already published", ) object IngredientAlreadyPublished: val variant = BadRequest.variantJson[IngredientAlreadyPublished] -private final case class IngredientAlreadyPending( +final case class IngredientAlreadyPending( ingredientId: IngredientId, message: String = "Ingredient already pending" ) @@ -46,10 +46,11 @@ private val requestPublication: ZServerEndpoint[RequestPublicationEnv, Any] = )) .zSecuredServerLogic(requestPublicationHandler) -def requestPublicationHandler(ingredientId: IngredientId): +private def requestPublicationHandler(ingredientId: IngredientId): ZIO[ AuthenticatedUser & RequestPublicationEnv, - InternalServerError | IngredientAlreadyPublished | IngredientAlreadyPending | IngredientNotFound, + InternalServerError | IngredientAlreadyPublished + | IngredientAlreadyPending | IngredientNotFound, PublicationRequestId ] = for @@ -59,7 +60,7 @@ def requestPublicationHandler(ingredientId: IngredientId): ) _ <- ZIO.fail(IngredientAlreadyPublished(ingredientId)) - .when(ingredient.isPublished) + .when(ingredient.isPublished) dataSource <- ZIO.service[DataSource] alreadyPending <- run( diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/RequestPublication.scala index 1a1949f3..5b0c8d7a 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/RequestPublication.scala @@ -3,13 +3,13 @@ package api.recipes import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} import api.variantJson -import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound, PublicationRequestId} -import db.repositories.{RecipePublicationRequestsQueries, IngredientsQueries, RecipeIngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} -import db.QuillConfig.provideDS import db.QuillConfig.ctx.* +import db.QuillConfig.provideDS +import db.repositories.{RecipePublicationRequestsQueries, IngredientsQueries, RecipeIngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} +import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound, PublicationRequestId} + import io.circe.generic.auto.* import io.getquill.* - import javax.sql.DataSource import sttp.model.StatusCode.{BadRequest, Created} import sttp.tapir.generic.auto.* @@ -30,12 +30,10 @@ final case class RecipeAlreadyPublished( object RecipeAlreadyPublished: val variant = BadRequest.variantJson[RecipeAlreadyPublished] - -private final case class RecipeAlreadyPending( +final case class RecipeAlreadyPending( recipeId: RecipeId, message: String = "Recipe already pending" ) - object RecipeAlreadyPending: val variant = BadRequest.variantJson[RecipeAlreadyPending] diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index b55ef47b..5af66026 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -44,7 +44,8 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) run(updateQ(lift(id), lift(dbStatus), lift(reason))).map(_ > 0).provideDS object IngredientPublicationRequestsQueries: - inline def requestsQ = query[DbIngredientPublicationRequest] + inline def requestsQ: EntityQuery[DbIngredientPublicationRequest] = + query[DbIngredientPublicationRequest] inline def requestPublicationQ(inline ingredientId: IngredientId): ActionReturning[DbIngredientPublicationRequest, UUID] = @@ -55,7 +56,7 @@ object IngredientPublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbIngredientPublicationRequest] = requestsQ .filter(r => - infix"${r.status} = 'pending'::${DbPublicationRequestStatus.postgresTypeName}" + infix"${r.status} = 'pending'::publication_request_status" .asCondition ) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 40593d38..bdf76a25 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -62,7 +62,7 @@ object RecipePublicationRequestsQueries: inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = recipePublicationRequestsQ .filter(r => - infix"${r.status} = 'pending'::${DbPublicationRequestStatus.postgresTypeName}" + infix"${r.status} = 'pending'::publication_request_status" .asCondition ) diff --git a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala index 82bec90d..0ad17a07 100644 --- a/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbIngredientPublicationRequest.scala @@ -6,12 +6,12 @@ import java.time.OffsetDateTime import java.util.UUID final case class DbIngredientPublicationRequest( - id: UUID, - ingredientId: IngredientId, - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - status: DbPublicationRequestStatus, - reason: Option[String], + id: UUID, + ingredientId: IngredientId, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: DbPublicationRequestStatus, + reason: Option[String], ): def toDomain: IngredientPublicationRequest = IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status.toDomain(reason)) @@ -21,18 +21,18 @@ object DbIngredientPublicationRequest: val (status, reason) = DbPublicationRequestStatus.fromDomain(req.status) DbIngredientPublicationRequest(req.id, req.ingredientId, req.createdAt, req.updatedAt, status, reason) - val createTable: String = """ + val createTable: String = s""" CREATE TABLE IF NOT EXISTS ingredient_publication_requests( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ingredient_id UUID NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - status publication_request_status NOT NULL DEFAULT 'pending', + status ${DbPublicationRequestStatus.postgresTypeName} NOT NULL DEFAULT 'pending', reason TEXT, FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE CASCADE ); - CREATE OR REPLACE TRIGGER update_timestamp + CREATE OR REPLACE TRIGGER ingredient_publication_requests_update_timestamp BEFORE UPDATE ON ingredient_publication_requests FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); diff --git a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala index 2fa2a83d..e7b1934e 100644 --- a/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala +++ b/src/main/scala/db/tables/publication/DbRecipePublicationRequest.scala @@ -21,18 +21,18 @@ object DbRecipePublicationRequest: val (status, reason) = DbPublicationRequestStatus.fromDomain(req.status) DbRecipePublicationRequest(req.id, req.recipeId, req.createdAt, req.updatedAt, status, reason) - val createTable: String = """ + val createTable: String = s""" CREATE TABLE IF NOT EXISTS recipe_publication_requests( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), recipe_id UUID NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - status publication_request_status NOT NULL DEFAULT 'pending', + status ${DbPublicationRequestStatus.postgresTypeName} NOT NULL DEFAULT 'pending', reason TEXT, FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE ); - CREATE OR REPLACE TRIGGER update_timestamp + CREATE OR REPLACE TRIGGER recipe_publication_requests_update_timestamp BEFORE UPDATE ON recipe_publication_requests FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); From f5e2cac8afb40dfea5df3786aa1db1dc2409db82 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 21:25:48 +0300 Subject: [PATCH 36/95] refactor: change GET /moderation/publication-requests/{id} and PATCH /moderation/publication-requests/{id} implementation --- .../api/moderation/pubrequests/Get.scala | 102 ++++++++---------- .../pubrequests/GetSomePending.scala | 11 +- .../PublicationRequestStatusResp.scala | 15 +++ ...scala => PublicationRequestTypeResp.scala} | 2 +- .../api/moderation/pubrequests/Update.scala | 9 +- .../api/recipes/RequestPublication.scala | 2 +- .../IngredientPublicationRequestRepo.scala | 10 ++ .../RecipePublicationRequestsRepo.scala | 26 +++-- 8 files changed, 100 insertions(+), 77 deletions(-) create mode 100644 src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala rename src/main/scala/api/moderation/pubrequests/{PublicationRequestType.scala => PublicationRequestTypeResp.scala} (65%) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index 31c872e6..53ed6a4a 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -2,23 +2,16 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} -import api.moderation.pubrequests.PublicationRequestType.* -import db.DbError.{DbNotRespondingError, FailedDbQuery} +import api.moderation.pubrequests.PublicationRequestTypeResp.* +import db.DbError +import domain.{IngredientPublicationRequest, RecipePublicationRequest} import db.repositories.{ IngredientPublicationRequestsRepo, - IngredientsRepo, - RecipePublicationRequestsRepo, - RecipesRepo + RecipePublicationRequestsRepo } -import domain.{ - IngredientPublicationRequest, - InternalServerError, - PublicationRequestNotFound, - PublicationRequestStatus, - RecipePublicationRequest, -} - +import domain.{InternalServerError, PublicationRequestNotFound} import io.circe.generic.auto.* + import java.time.OffsetDateTime import java.util.UUID import sttp.tapir.generic.auto.* @@ -27,20 +20,18 @@ import sttp.tapir.ztapir.* import zio.ZIO final case class PublicationRequestResp( - id: UUID, - requestType: PublicationRequestType, - entityId: UUID, - entityName: String, - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - status: PublicationRequestStatus + id: UUID, + requestType: PublicationRequestTypeResp, + entityId: UUID, + entityName: String, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: PublicationRequestStatusResp ) private type GetReqEnv = RecipePublicationRequestsRepo - & RecipesRepo & IngredientPublicationRequestsRepo - & IngredientsRepo private val getRequest: ZServerEndpoint[GetReqEnv, Any] = publicationRequestEndpoint @@ -56,46 +47,41 @@ private def getRequestHandler(reqId: UUID): PublicationRequestResp] = def getIngredientRequest = - ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.get(reqId)) - .flatMap { - _.map { dbEntity => - val IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status) = dbEntity.toDomain - ZIO.serviceWithZIO[IngredientsRepo] { - _.get(ingredientId).some.map { ingredient => - PublicationRequestResp( - reqId, - Ingredient, - ingredientId, - ingredient.name, - createdAt, - updatedAt, - status, - ) - } - } - }.getOrElse(ZIO.fail(PublicationRequestNotFound(reqId))) + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getWithIngredient(reqId)) + .someOrFail(PublicationRequestNotFound(reqId)) + .map { + case (dbReq, dbIngredient) => dbReq.toDomain match + case IngredientPublicationRequest(id, ingredientId, createdAt, updatedAt, status) => + PublicationRequestResp( + reqId, + Ingredient, + ingredientId, + dbIngredient.name, + createdAt, + updatedAt, + PublicationRequestStatusResp.fromDomain(status) + ) } - ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(reqId)) - .flatMap { - _.map { dbEntity => - val RecipePublicationRequest(id, recipeId, createdAt, updatedAt, status) = dbEntity.toDomain - ZIO.serviceWithZIO[RecipesRepo] { - _.getRecipe(recipeId).some.map { recipe => + def getRecipeRequestOrIngredientRequest = + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(reqId)) + .someOrElseZIO(getIngredientRequest) + .map { + case (dbReq, dbRecipe) => dbReq.toDomain match + case RecipePublicationRequest(id, ingredientId, createdAt, updatedAt, status) => PublicationRequestResp( reqId, - Recipe, - recipeId, - recipe.name, + Ingredient, + ingredientId, + dbRecipe.name, createdAt, updatedAt, - status, + PublicationRequestStatusResp.fromDomain(status) ) - } - } - }.getOrElse(getIngredientRequest) - }.mapError { - case _: (Option[_] | FailedDbQuery) => PublicationRequestNotFound(reqId) - case _: DbNotRespondingError => InternalServerError() - case x: PublicationRequestNotFound => x - } + case response: PublicationRequestResp => response + } + + getRecipeRequestOrIngredientRequest.mapError { + case _: DbError => InternalServerError() + case x: PublicationRequestNotFound => x + } diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 7d24f363..79844fe7 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -3,7 +3,7 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.common.search.{PaginationParams, paginate} import api.EndpointErrorVariants.serverErrorVariant -import api.moderation.pubrequests.PublicationRequestType.* +import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} import domain.{InternalServerError, PublicationRequestId} @@ -16,10 +16,10 @@ import sttp.tapir.ztapir.* import zio.ZIO final case class PublicationRequestSummary( - id: PublicationRequestId, - requestType: PublicationRequestType, - entityName: String, - createdAt: OffsetDateTime + id: PublicationRequestId, + requestType: PublicationRequestTypeResp, + entityName: String, + createdAt: OffsetDateTime ) private type GetSomePendingEnv @@ -30,7 +30,6 @@ private val getSomePending: ZServerEndpoint[GetSomePendingEnv, Any] = publicationRequestEndpoint .get .in(PaginationParams.query) - .out(statusCode(NoContent)) .out(jsonBody[Seq[PublicationRequestSummary]]) .errorOut(oneOf(serverErrorVariant)) .zSecuredServerLogic(getSomePendingHandler) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala new file mode 100644 index 00000000..e1cb0602 --- /dev/null +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala @@ -0,0 +1,15 @@ +package api.moderation.pubrequests + +import domain.PublicationRequestStatus + +enum PublicationRequestStatusResp: + case Pending + case Accepted + case Rejected(reason: Option[String]) + +object PublicationRequestStatusResp: + def fromDomain(domainModel: PublicationRequestStatus): PublicationRequestStatusResp = + domainModel match + case PublicationRequestStatus.Accepted => Accepted + case PublicationRequestStatus.Pending => Pending + case PublicationRequestStatus.Rejected(reason) => Rejected(reason) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala b/src/main/scala/api/moderation/pubrequests/PublicationRequestTypeResp.scala similarity index 65% rename from src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala rename to src/main/scala/api/moderation/pubrequests/PublicationRequestTypeResp.scala index 73decec7..3a6fbbff 100644 --- a/src/main/scala/api/moderation/pubrequests/PublicationRequestType.scala +++ b/src/main/scala/api/moderation/pubrequests/PublicationRequestTypeResp.scala @@ -1,4 +1,4 @@ package api.moderation.pubrequests -enum PublicationRequestType: +enum PublicationRequestTypeResp: case Ingredient, Recipe diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 757dafcf..6da34c70 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -2,7 +2,7 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} -import api.moderation.pubrequests.PublicationRequestType.* +import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} import domain.{PublicationRequestStatus, InternalServerError, PublicationRequestNotFound} @@ -50,6 +50,9 @@ private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublication .updateStatus(id, status) .orElseFail(InternalServerError()) ) - // case _: PublicationRequestNotFound => - // ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.updateStatus(id, status)) + rowsUpdated <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ + .updateStatus(id, status) + .orElseFail(InternalServerError()) + ) + _ <- ZIO.fail(PublicationRequestNotFound(id)).unless(rowsUpdated) yield () diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/RequestPublication.scala index 5b0c8d7a..dbfd7f1a 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/RequestPublication.scala @@ -75,7 +75,7 @@ private def requestPublicationHandler(recipeId: RecipeId): dataSource <- ZIO.service[DataSource] alreadyPending <- run( RecipePublicationRequestsQueries - .pendingRequestsOfRecipeQ(lift(recipeId)).nonEmpty + .pendingRequestsByRecipeIdQ(lift(recipeId)).nonEmpty ).provideDS(using dataSource) .orElseFail(InternalServerError()) _ <- ZIO.fail(RecipeAlreadyPending(recipeId)) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 5af66026..a56bac50 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -14,6 +14,7 @@ trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] def getPendingRequestsWithIngredients: IO[DbError, Seq[(DbIngredientPublicationRequest, DbIngredient)]] def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] + def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) @@ -43,6 +44,15 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) run(updateQ(lift(id), lift(dbStatus), lift(reason))).map(_ > 0).provideDS + override def getWithIngredient(id: PublicationRequestId): + IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] = + + run( + requestsQ + .join(IngredientsQueries.ingredientsQ) + .on(_.ingredientId == _.id) + ).map(_.headOption).provideDS + object IngredientPublicationRequestsQueries: inline def requestsQ: EntityQuery[DbIngredientPublicationRequest] = query[DbIngredientPublicationRequest] diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index bdf76a25..8fdebdb5 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -15,6 +15,7 @@ trait RecipePublicationRequestsRepo: def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] def getPendingRequestsWithRecipes: IO[DbError, Seq[(DbRecipePublicationRequest, DbRecipe)]] def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] + def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] @@ -28,7 +29,7 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] = run( - createPublicationRequestQ(lift(recipeId)) + requestPublicationQ(lift(recipeId)) ).provideDS override def getPendingRequestsWithRecipes: @@ -42,6 +43,15 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def get(id: PublicationRequestId): IO[DbError, Option[DbRecipePublicationRequest]] = run(getQ(lift(id)).value).provideDS + override def getWithRecipe(id: PublicationRequestId): + IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] = + + run( + requestsQ + .join(RecipesQueries.recipesQ) + .on(_.recipeId == _.id) + ).map(_.headOption).provideDS + override def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] = val (dbStatus, reason) = DbPublicationRequestStatus.fromDomain(status) @@ -50,29 +60,29 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) ).map(_ > 0).provideDS object RecipePublicationRequestsQueries: - inline def recipePublicationRequestsQ: EntityQuery[DbRecipePublicationRequest] = + inline def requestsQ: EntityQuery[DbRecipePublicationRequest] = query[DbRecipePublicationRequest] - inline def createPublicationRequestQ(inline recipeId: RecipeId): + inline def requestPublicationQ(inline recipeId: RecipeId): ActionReturning[DbRecipePublicationRequest, UUID] = - recipePublicationRequestsQ + requestsQ .insert(_.recipeId -> recipeId) .returningGenerated(_.id) inline def pendingRequestsQ: EntityQuery[DbRecipePublicationRequest] = - recipePublicationRequestsQ + requestsQ .filter(r => infix"${r.status} = 'pending'::publication_request_status" .asCondition ) - inline def pendingRequestsOfRecipeQ(inline recipeId: RecipeId): + inline def pendingRequestsByRecipeIdQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = pendingRequestsQ .filter(_.recipeId == recipeId) inline def getQ(inline id: PublicationRequestId): EntityQuery[DbRecipePublicationRequest] = - recipePublicationRequestsQ + requestsQ .filter(_.id == id) inline def updateQ( @@ -80,7 +90,7 @@ object RecipePublicationRequestsQueries: inline status: DbPublicationRequestStatus, inline reason: Option[String], ): Update[DbRecipePublicationRequest] = - recipePublicationRequestsQ + requestsQ .filter(_.id == id) .update( _.status -> status, From 0673e90a0db3be1a3ccf84ddadd369d27fa32250 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 21:27:34 +0300 Subject: [PATCH 37/95] refactor: cosmetic changes --- .../scala/api/moderation/pubrequests/GetSomePending.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 79844fe7..1f0b2d4a 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -16,10 +16,10 @@ import sttp.tapir.ztapir.* import zio.ZIO final case class PublicationRequestSummary( - id: PublicationRequestId, - requestType: PublicationRequestTypeResp, - entityName: String, - createdAt: OffsetDateTime + id: PublicationRequestId, + requestType: PublicationRequestTypeResp, + entityName: String, + createdAt: OffsetDateTime ) private type GetSomePendingEnv From b1b9b939c6babd079fba76d58bbea11faa91c410 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 21:45:00 +0300 Subject: [PATCH 38/95] refactor: a little bit of rearrangement --- .../scala/api/moderation/pubrequests/Get.scala | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index 53ed6a4a..05e1034e 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -4,12 +4,8 @@ import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.DbError -import domain.{IngredientPublicationRequest, RecipePublicationRequest} -import db.repositories.{ - IngredientPublicationRequestsRepo, - RecipePublicationRequestsRepo -} -import domain.{InternalServerError, PublicationRequestNotFound} +import domain.{IngredientPublicationRequest, InternalServerError, PublicationRequestId, PublicationRequestNotFound, RecipePublicationRequest} +import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} import io.circe.generic.auto.* import java.time.OffsetDateTime @@ -41,7 +37,7 @@ private val getRequest: ZServerEndpoint[GetReqEnv, Any] = .errorOut(oneOf(serverErrorVariant, publicationRequestNotFound)) .zSecuredServerLogic(getRequestHandler) -private def getRequestHandler(reqId: UUID): +private def getRequestHandler(reqId: PublicationRequestId): ZIO[AuthenticatedUser & GetReqEnv, InternalServerError | PublicationRequestNotFound, PublicationRequestResp] = @@ -63,9 +59,9 @@ private def getRequestHandler(reqId: UUID): ) } - def getRecipeRequestOrIngredientRequest = + def getRecipeRequest = ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(reqId)) - .someOrElseZIO(getIngredientRequest) + .someOrFail(PublicationRequestNotFound(reqId)) .map { case (dbReq, dbRecipe) => dbReq.toDomain match case RecipePublicationRequest(id, ingredientId, createdAt, updatedAt, status) => @@ -78,10 +74,9 @@ private def getRequestHandler(reqId: UUID): updatedAt, PublicationRequestStatusResp.fromDomain(status) ) - case response: PublicationRequestResp => response } - getRecipeRequestOrIngredientRequest.mapError { + getIngredientRequest.orElse(getRecipeRequest).mapError { case _: DbError => InternalServerError() case x: PublicationRequestNotFound => x } From 1487e6bfc3710f5e0b79cf4d3681794e10fd5539 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 21:56:06 +0300 Subject: [PATCH 39/95] fix: patch up ignoring "recipe publication request" update operation's result --- src/main/scala/api/moderation/pubrequests/Update.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 6da34c70..3bfcd702 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -4,10 +4,10 @@ import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} -import domain.{PublicationRequestStatus, InternalServerError, PublicationRequestNotFound} - +import domain.{InternalServerError, PublicationRequestId, PublicationRequestNotFound, PublicationRequestStatus} import io.circe.Encoder import io.circe.generic.auto.* + import java.util.UUID import sttp.model.StatusCode.NoContent import sttp.tapir.generic.auto.* @@ -42,7 +42,7 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = .errorOut(oneOf(publicationRequestNotFound, serverErrorVariant)) .zSecuredServerLogic(updatePublicationRequestHandler) -private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublicationRequestReqBody): +private def updatePublicationRequestHandler(id: PublicationRequestId, reqBody: UpdatePublicationRequestReqBody): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = val status = reqBody.getDomainStatus for @@ -53,6 +53,6 @@ private def updatePublicationRequestHandler(id: UUID, reqBody: UpdatePublication rowsUpdated <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ .updateStatus(id, status) .orElseFail(InternalServerError()) - ) + ).unless(rowsUpdated).someOrElse(false) _ <- ZIO.fail(PublicationRequestNotFound(id)).unless(rowsUpdated) yield () From 3f5270caa244832158ddb1379c8120a01c2f6da5 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:03:36 +0300 Subject: [PATCH 40/95] feat: add isUserOwner method --- src/main/scala/db/repositories/RecipesRepo.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index cc6919b0..e9625e33 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -18,7 +18,8 @@ trait RecipesRepo: def getAll: ZIO[AuthenticatedUser, DbError, List[DbRecipe]] def getAllCustom: ZIO[AuthenticatedUser, DbError, List[DbRecipe]] def getAllPublic: IO[DbError, List[DbRecipe]] - + + def isUserOwner(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] def isVisible(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] def isPublic(recipeId: RecipeId): IO[DbError, Boolean] @@ -77,6 +78,12 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: override def getAllPublic: IO[DbError, List[DbRecipe]] = run(publicRecipesQ).provideDS + override def isUserOwner(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] = + for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + res <- run(getByUserIdAndRecipeIdQ(lift(userId), lift(recipeId)).nonEmpty).provideDS + yield res + override def isVisible(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] = ZIO.serviceWithZIO[AuthenticatedUser](user => run( @@ -123,7 +130,10 @@ object RecipesQueries: inline def getVisibleRecipeQ(inline userId: UserId, inline recipeId: RecipeId): EntityQuery[DbRecipe] = visibleRecipesQ(userId).filter(r => r.id == recipeId) - + + inline def getByUserIdAndRecipeIdQ(inline userId: UserId, inline recipeId: RecipeId): EntityQuery[DbRecipe] = + recipesQ.filter(r => r.id == recipeId && r.creatorId.contains(userId)) + object RecipesRepo: def layer: RLayer[DataSource, RecipesRepo] = ZLayer.fromFunction(RecipesRepoLive.apply) From e1490a375ecc70258c3c4d7757cf1428c7b5207d Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:01 +0300 Subject: [PATCH 41/95] feat: add getAllByRecipeId --- .../db/repositories/RecipePublicationRequestsRepo.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index a600f1ee..8369c8fc 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -11,6 +11,7 @@ import javax.sql.DataSource trait RecipePublicationRequestsRepo: def requestPublication(recipeId: RecipeId): IO[DbError, Unit] + def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] private inline def recipePublicationRequests = query[DbRecipePublicationRequest] @@ -25,6 +26,9 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def requestPublication(recipeId: RecipeId): IO[DbError, Unit] = run(requestPublicationQ(lift(recipeId))).unit.provideDS + override def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] = + run(getAllByRecipeIdQ(lift(recipeId))).provideDS + object RecipePublicationRequestsQueries: import db.QuillConfig.ctx.* @@ -34,6 +38,9 @@ object RecipePublicationRequestsQueries: inline def allPendingQ = recipePublicationRequests.filter(_.status == lift(Pending)) inline def pendingRequestsByIdQ(inline recipeId: RecipeId) = allPendingQ.filter(_.recipeId == recipeId) + inline def getAllByRecipeIdQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = + recipePublicationRequests.filter(_.recipeId == recipeId) + object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = ZLayer.fromFunction(RecipePublicationRequestsRepoLive.apply) From 972f1e02ffab6068cc43fe02a6e2596cbdf7124e Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:29 +0300 Subject: [PATCH 42/95] feat: add isPublished check to customIngredientsQ --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 1b579f3f..04fb4b37 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -98,7 +98,7 @@ object IngredientsQueries: ingredientsQ.filter(_.ownerId.isEmpty) inline def customIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(_.ownerId == Some(userId)) + ingredientsQ.filter(i => !i.isPublished && i.ownerId == Some(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = ingredientsQ.filter(i => i.ownerId == None || i.ownerId == Some(userId)) From 403c8a20124c2cad8b2bf8b3323954bac48587ca Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:50 +0300 Subject: [PATCH 43/95] feat: moderationHistory endpoint --- src/main/scala/api/recipes/Endpoints.scala | 1 + .../scala/api/recipes/ModerationHistory.scala | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/scala/api/recipes/ModerationHistory.scala diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index ea201d10..5fa20c50 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -20,4 +20,5 @@ val recipeEndpoints = List( searchAll.widen, delete.widen, requestPublication.widen, + moderationHistory.widen ) ++ recipesIngredientsEndpoints.map(_.widen) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/ModerationHistory.scala new file mode 100644 index 00000000..db06c013 --- /dev/null +++ b/src/main/scala/api/recipes/ModerationHistory.scala @@ -0,0 +1,45 @@ +package api.recipes + +import domain.{InternalServerError, RecipeNotFound, RecipeId} +import api.EndpointErrorVariants.{serverErrorVariant, recipeNotFoundVariant} +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import db.tables.publication.DbPublicationRequestStatus +import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} + +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO +import java.time.OffsetDateTime + +final case class ModerationHistoryResponse( + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: DbPublicationRequestStatus, + reason: Option[String] +) + +private type ModerationHistoryEnv = RecipePublicationRequestsRepo & RecipesRepo +val moderationHistory: ZServerEndpoint[ModerationHistoryEnv, Any] = + recipesEndpoint + .get + .in(path[RecipeId]("recipe-id") / "moderation-history") + .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) + .out(jsonBody[List[ModerationHistoryResponse]]) + .zSecuredServerLogic(moderationHistoryHandler) + +def moderationHistoryHandler(recipeId: RecipeId): + ZIO[AuthenticatedUser & ModerationHistoryEnv, InternalServerError | RecipeNotFound, List[ModerationHistoryResponse]] = + for + isUserOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) + .orElseFail(InternalServerError()) + _ <- ZIO.fail(RecipeNotFound(recipeId)).unless(isUserOwner) + + dbRequests <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllByRecipeId(recipeId)) + .orElseFail(InternalServerError()) + res = dbRequests + .map(dbReq => ModerationHistoryResponse(dbReq.createdAt, dbReq.updatedAt, dbReq.status, dbReq.reason)) + .sortBy(_.updatedAt) + + yield res \ No newline at end of file From b0ea213e37c594513387070cb564377a39caf013 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 22:34:55 +0300 Subject: [PATCH 44/95] chore: add optional field "last publication request status" for GET /recipes/{id} response --- src/main/scala/api/recipes/Get.scala | 78 ++++++++++--------- .../RecipePublicationRequestsRepo.scala | 6 ++ 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index f8981e9d..596a1386 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -1,22 +1,15 @@ package api.recipes import api.{handleFailedSqlQuery, toUserNotFound} -import api.Authentication.{zSecuredServerLogic, AuthenticatedUser} +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant, userNotFoundVariant} +import api.moderation.pubrequests.PublicationRequestStatusResp import db.{DbError, handleDbError} -import db.tables.{usersTable, ingredientsTable, recipeIngredientsTable, recipesTable, storageIngredientsTable, storageMembersTable, storagesTable} -import domain.{ - IngredientId, - InternalServerError, - RecipeId, - RecipeNotFound, - StorageId, - UserId, - UserNotFound, -} - +import db.tables.{ingredientsTable, recipeIngredientsTable, recipesTable, storageIngredientsTable, storageMembersTable, storagesTable, usersTable} +import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound, StorageId, UserId, UserNotFound} import com.augustnagro.magnum.magzio.* import com.augustnagro.magnum.Query +import db.repositories.RecipePublicationRequestsRepo import io.circe.generic.auto.* import io.circe.parser.decode import sttp.tapir.generic.auto.* @@ -45,9 +38,10 @@ final case class RecipeResp( name: String, sourceLink: Option[String], creator: Option[RecipeCreatorResp], + moderationStatus: Option[PublicationRequestStatusResp] ) -private type GetEnv = Transactor +private type GetEnv = Transactor & RecipePublicationRequestsRepo private val get: ZServerEndpoint[GetEnv, Any] = recipesEndpoint @@ -69,29 +63,42 @@ private case class RawRecipeResult( private def getHandler(recipeId: RecipeId): ZIO[AuthenticatedUser & GetEnv, InternalServerError | RecipeNotFound | UserNotFound, - RecipeResp] = - ZIO.serviceWithZIO[AuthenticatedUser] { authenticatedUser => - val userId = authenticatedUser.userId - ZIO.serviceWithZIO[Transactor](_ - .transact(rawRecipeQuery(userId, recipeId).run().headOption) - .mapError(handleDbError) - ) - }.someOrFail(RecipeNotFound(recipeId)).flatMap { rawResult => - // Parse the JSON ingredients string - ZIO.fromEither(decode[Vector[IngredientResp]](rawResult.ingredients)) - .map { ingredients => - val recipeCreatorResp = for - creatorId <- rawResult.creatorId - creatorFullName <- rawResult.creatorFullName - yield RecipeCreatorResp(creatorId, creatorFullName) - RecipeResp( - ingredients, - rawResult.name, - rawResult.sourceLink, - recipeCreatorResp, + RecipeResp] = { + + def getLastPublicationRequestStatus = + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllRequestsForRecipe(recipeId)) + .map( + _.sortBy(_.updatedAt).headOption.map( + req => PublicationRequestStatusResp.fromDomain(req.status.toDomain(req.reason)) ) - } - .orElseFail(InternalServerError(s"Failed to parse ingredients JSON: ${rawResult.ingredients}")) + ) + + { + for + rawResult <- ZIO.serviceWithZIO[AuthenticatedUser] { authenticatedUser => + val userId = authenticatedUser.userId + ZIO.serviceWithZIO[Transactor](_ + .transact(rawRecipeQuery(userId, recipeId).run().headOption) + .mapError(handleDbError) + ) + }.someOrFail(RecipeNotFound(recipeId)) + status <- getLastPublicationRequestStatus + result <- ZIO.fromEither(decode[Vector[IngredientResp]](rawResult.ingredients)) + // Parse the JSON ingredients string + .map { ingredients => + val recipeCreatorResp = for + creatorId <- rawResult.creatorId + creatorFullName <- rawResult.creatorFullName + yield RecipeCreatorResp(creatorId, creatorFullName) + RecipeResp( + ingredients, + rawResult.name, + rawResult.sourceLink, + recipeCreatorResp, + status + ) + }.orElseFail(InternalServerError(s"Failed to parse ingredients JSON: ${rawResult.ingredients}")) + yield result }.mapError { case e: DbError.FailedDbQuery => handleFailedSqlQuery(e) .flatMap(toUserNotFound) @@ -99,6 +106,7 @@ private def getHandler(recipeId: RecipeId): case _: DbError => InternalServerError() case e: (InternalServerError | RecipeNotFound | UserNotFound) => e } +} private inline def rawRecipeQuery( inline userId: UserId, diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 8fdebdb5..06b08257 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -18,6 +18,7 @@ trait RecipePublicationRequestsRepo: def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] + def getAllRequestsForRecipe(id: RecipeId): IO[DbError, Seq[DbRecipePublicationRequest]] final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: @@ -59,6 +60,11 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) updateQ(lift(id), lift(dbStatus), lift(reason)) ).map(_ > 0).provideDS + override def getAllRequestsForRecipe(id: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] = + run( + requestsQ.filter(_.recipeId == lift(id)) + ).provideDS + object RecipePublicationRequestsQueries: inline def requestsQ: EntityQuery[DbRecipePublicationRequest] = query[DbRecipePublicationRequest] From d020d5c365590c6625f59bfca528ac848cd7aa74 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:03:36 +0300 Subject: [PATCH 45/95] feat: add isUserOwner method --- src/main/scala/db/repositories/RecipesRepo.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index cc6919b0..e9625e33 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -18,7 +18,8 @@ trait RecipesRepo: def getAll: ZIO[AuthenticatedUser, DbError, List[DbRecipe]] def getAllCustom: ZIO[AuthenticatedUser, DbError, List[DbRecipe]] def getAllPublic: IO[DbError, List[DbRecipe]] - + + def isUserOwner(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] def isVisible(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] def isPublic(recipeId: RecipeId): IO[DbError, Boolean] @@ -77,6 +78,12 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: override def getAllPublic: IO[DbError, List[DbRecipe]] = run(publicRecipesQ).provideDS + override def isUserOwner(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] = + for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + res <- run(getByUserIdAndRecipeIdQ(lift(userId), lift(recipeId)).nonEmpty).provideDS + yield res + override def isVisible(recipeId: RecipeId): ZIO[AuthenticatedUser, DbError, Boolean] = ZIO.serviceWithZIO[AuthenticatedUser](user => run( @@ -123,7 +130,10 @@ object RecipesQueries: inline def getVisibleRecipeQ(inline userId: UserId, inline recipeId: RecipeId): EntityQuery[DbRecipe] = visibleRecipesQ(userId).filter(r => r.id == recipeId) - + + inline def getByUserIdAndRecipeIdQ(inline userId: UserId, inline recipeId: RecipeId): EntityQuery[DbRecipe] = + recipesQ.filter(r => r.id == recipeId && r.creatorId.contains(userId)) + object RecipesRepo: def layer: RLayer[DataSource, RecipesRepo] = ZLayer.fromFunction(RecipesRepoLive.apply) From c51f29639b2c1e9ce56136ea7018709203e7f850 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:01 +0300 Subject: [PATCH 46/95] feat: add getAllByRecipeId # Conflicts: # src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala --- .../db/repositories/RecipePublicationRequestsRepo.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 8fdebdb5..b49c2323 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -18,6 +18,9 @@ trait RecipePublicationRequestsRepo: def getWithRecipe(id: PublicationRequestId): IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] + def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] + +private inline def recipePublicationRequests = query[DbRecipePublicationRequest] final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: @@ -59,6 +62,9 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) updateQ(lift(id), lift(dbStatus), lift(reason)) ).map(_ > 0).provideDS + override def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] = + run(getAllByRecipeIdQ(lift(recipeId))).provideDS + object RecipePublicationRequestsQueries: inline def requestsQ: EntityQuery[DbRecipePublicationRequest] = query[DbRecipePublicationRequest] @@ -97,6 +103,9 @@ object RecipePublicationRequestsQueries: _.reason -> reason, ) + inline def getAllByRecipeIdQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = + recipePublicationRequests.filter(_.recipeId == recipeId) + object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = ZLayer.fromFunction(RecipePublicationRequestsRepoLive.apply) From 6fb4fd929538d9d792a03fed2a75f89d2c64894f Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:29 +0300 Subject: [PATCH 47/95] feat: add isPublished check to customIngredientsQ --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 1b579f3f..04fb4b37 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -98,7 +98,7 @@ object IngredientsQueries: ingredientsQ.filter(_.ownerId.isEmpty) inline def customIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(_.ownerId == Some(userId)) + ingredientsQ.filter(i => !i.isPublished && i.ownerId == Some(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = ingredientsQ.filter(i => i.ownerId == None || i.ownerId == Some(userId)) From 0c9b4c0ba4f16243110d596363591aa4c9cccdb2 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 22:04:50 +0300 Subject: [PATCH 48/95] feat: moderationHistory endpoint --- src/main/scala/api/recipes/Endpoints.scala | 1 + .../scala/api/recipes/ModerationHistory.scala | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/scala/api/recipes/ModerationHistory.scala diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index ea201d10..5fa20c50 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -20,4 +20,5 @@ val recipeEndpoints = List( searchAll.widen, delete.widen, requestPublication.widen, + moderationHistory.widen ) ++ recipesIngredientsEndpoints.map(_.widen) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/ModerationHistory.scala new file mode 100644 index 00000000..db06c013 --- /dev/null +++ b/src/main/scala/api/recipes/ModerationHistory.scala @@ -0,0 +1,45 @@ +package api.recipes + +import domain.{InternalServerError, RecipeNotFound, RecipeId} +import api.EndpointErrorVariants.{serverErrorVariant, recipeNotFoundVariant} +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import db.tables.publication.DbPublicationRequestStatus +import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} + +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO +import java.time.OffsetDateTime + +final case class ModerationHistoryResponse( + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: DbPublicationRequestStatus, + reason: Option[String] +) + +private type ModerationHistoryEnv = RecipePublicationRequestsRepo & RecipesRepo +val moderationHistory: ZServerEndpoint[ModerationHistoryEnv, Any] = + recipesEndpoint + .get + .in(path[RecipeId]("recipe-id") / "moderation-history") + .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) + .out(jsonBody[List[ModerationHistoryResponse]]) + .zSecuredServerLogic(moderationHistoryHandler) + +def moderationHistoryHandler(recipeId: RecipeId): + ZIO[AuthenticatedUser & ModerationHistoryEnv, InternalServerError | RecipeNotFound, List[ModerationHistoryResponse]] = + for + isUserOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) + .orElseFail(InternalServerError()) + _ <- ZIO.fail(RecipeNotFound(recipeId)).unless(isUserOwner) + + dbRequests <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllByRecipeId(recipeId)) + .orElseFail(InternalServerError()) + res = dbRequests + .map(dbReq => ModerationHistoryResponse(dbReq.createdAt, dbReq.updatedAt, dbReq.status, dbReq.reason)) + .sortBy(_.updatedAt) + + yield res \ No newline at end of file From 7133a06a875b4fdf3d5a9af5024bfaccb8df2601 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 23:13:37 +0300 Subject: [PATCH 49/95] chore: add optional field "last publication request status" for GET /ingredients?filter=custom response --- .../api/ingredients/IngredientResp.scala | 7 +++- src/main/scala/api/ingredients/Search.scala | 33 ++++++++++++++----- .../IngredientPublicationRequestRepo.scala | 8 +++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/main/scala/api/ingredients/IngredientResp.scala b/src/main/scala/api/ingredients/IngredientResp.scala index a472cd89..4d42fa47 100644 --- a/src/main/scala/api/ingredients/IngredientResp.scala +++ b/src/main/scala/api/ingredients/IngredientResp.scala @@ -1,10 +1,15 @@ package api.ingredients import api.common.search.Searchable +import api.moderation.pubrequests.PublicationRequestStatusResp import domain.IngredientId import db.tables.DbIngredient -final case class IngredientResp(id: IngredientId, name: String) extends Searchable +final case class IngredientResp( + id: IngredientId, + name: String, + moderationStatus: Option[PublicationRequestStatusResp] = None +) extends Searchable object IngredientResp: def fromDb(dbIngredient: DbIngredient): IngredientResp = diff --git a/src/main/scala/api/ingredients/Search.scala b/src/main/scala/api/ingredients/Search.scala index 058b42bd..2dcd771e 100644 --- a/src/main/scala/api/ingredients/Search.scala +++ b/src/main/scala/api/ingredients/Search.scala @@ -1,13 +1,14 @@ package api.ingredients -import api.Authentication.{zSecuredServerLogic, AuthenticatedUser} -import api.common.search.{PaginationParams, paginate, SearchParams, Searchable} +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.common.search.{PaginationParams, SearchParams, Searchable, paginate} import api.EndpointErrorVariants.serverErrorVariant -import db.repositories.{IngredientsRepo, StorageIngredientsRepo} +import api.ingredients.SearchIngredientsFilter.Custom +import api.moderation.pubrequests.PublicationRequestStatusResp +import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, StorageIngredientsRepo} import domain.{IngredientId, InternalServerError} - import io.circe.generic.auto.* -import sttp.tapir.{Codec, Schema, Validator, EndpointInput} +import sttp.tapir.{Codec, EndpointInput, Schema, Validator} import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* @@ -25,7 +26,7 @@ object SearchIngredientsFilter: given Codec.PlainCodec[SearchIngredientsFilter] = Codec.derivedEnumeration.defaultStringBased -private type SearchEnv = IngredientsRepo & StorageIngredientsRepo +private type SearchEnv = IngredientsRepo & StorageIngredientsRepo & IngredientPublicationRequestsRepo private val search: ZServerEndpoint[SearchEnv, Any] = ingredientsEndpoint @@ -41,7 +42,17 @@ private def searchHandler( searchParams: SearchParams, paginationParams: PaginationParams, filter: SearchIngredientsFilter, -): ZIO[AuthenticatedUser & SearchEnv, InternalServerError, SearchResp[IngredientResp]] = +): ZIO[AuthenticatedUser & SearchEnv, InternalServerError, SearchResp[IngredientResp]] = { + + def getLastPublicationRequestStatus(ingredientId: IngredientId) = + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo]( + _.getAllRequestsForIngredient(ingredientId).map( + _.sortBy(_.updatedAt).reverse.headOption.map( + req => PublicationRequestStatusResp.fromDomain(req.status.toDomain(req.reason)) + ) + ) + ) + for getIngredients <- ZIO.serviceWith[IngredientsRepo](filter match case SearchIngredientsFilter.Custom => _.getAllCustom @@ -52,4 +63,10 @@ private def searchHandler( .map(_.map(IngredientResp.fromDb)) .orElseFail(InternalServerError()) res = Searchable.search(Vector.from(allIngredients), searchParams) - yield SearchResp(res.paginate(paginationParams), res.length) \ No newline at end of file + res <- ZIO.foreach(res) { resp => + getLastPublicationRequestStatus(resp.id) + .map(status => resp.copy(moderationStatus = status)) + .orElseFail(InternalServerError()) + }.when(filter == Custom).someOrElse(res) + yield SearchResp(res.paginate(paginationParams), res.length) +} \ No newline at end of file diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index a56bac50..2b94604d 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -16,6 +16,7 @@ trait IngredientPublicationRequestsRepo: def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] + def getAllRequestsForIngredient(id: IngredientId): IO[DbError, Seq[DbIngredientPublicationRequest]] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) extends IngredientPublicationRequestsRepo: @@ -53,6 +54,13 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) .on(_.ingredientId == _.id) ).map(_.headOption).provideDS + override def getAllRequestsForIngredient(id: IngredientId): + IO[DbError, List[DbIngredientPublicationRequest]] = + + run( + requestsQ.filter(_.ingredientId == lift(id)) + ).provideDS + object IngredientPublicationRequestsQueries: inline def requestsQ: EntityQuery[DbIngredientPublicationRequest] = query[DbIngredientPublicationRequest] From 76c281c3a00bc728fdfd51ab818f890fd67f27f5 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 23:14:17 +0300 Subject: [PATCH 50/95] fix: increase inlining limit for better support of auto json derivation --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index 0f8d48c4..85b5b381 100644 --- a/build.sbt +++ b/build.sbt @@ -16,6 +16,7 @@ val circeVersion = "0.14.14" lazy val root = (project in file(".")) .settings( + scalacOptions ++= Seq("-Xmax-inlines", "64"), name := "CookCookHnya-backend", scalacOptions ++= Seq( "-Wunused:imports" From 142a612c795a2d5407da4554401ef7c673d70d9d Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 23:15:40 +0300 Subject: [PATCH 51/95] fix: add missing reversing for recipe publication requests sorting --- src/main/scala/api/recipes/Get.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index 596a1386..7b6f135e 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -68,7 +68,7 @@ private def getHandler(recipeId: RecipeId): def getLastPublicationRequestStatus = ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllRequestsForRecipe(recipeId)) .map( - _.sortBy(_.updatedAt).headOption.map( + _.sortBy(_.updatedAt).reverse.headOption.map( req => PublicationRequestStatusResp.fromDomain(req.status.toDomain(req.reason)) ) ) From f7fa48a200b349f8499614fb4a94bdf2edd9b5c7 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sat, 19 Jul 2025 23:24:37 +0300 Subject: [PATCH 52/95] refactor: remove unnecessary reversing --- src/main/scala/api/ingredients/Search.scala | 2 +- src/main/scala/api/recipes/Get.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/api/ingredients/Search.scala b/src/main/scala/api/ingredients/Search.scala index 2dcd771e..d9d98440 100644 --- a/src/main/scala/api/ingredients/Search.scala +++ b/src/main/scala/api/ingredients/Search.scala @@ -47,7 +47,7 @@ private def searchHandler( def getLastPublicationRequestStatus(ingredientId: IngredientId) = ZIO.serviceWithZIO[IngredientPublicationRequestsRepo]( _.getAllRequestsForIngredient(ingredientId).map( - _.sortBy(_.updatedAt).reverse.headOption.map( + _.sortBy(_.updatedAt).lastOption.map( req => PublicationRequestStatusResp.fromDomain(req.status.toDomain(req.reason)) ) ) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index 7b6f135e..465e34a0 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -68,7 +68,7 @@ private def getHandler(recipeId: RecipeId): def getLastPublicationRequestStatus = ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllRequestsForRecipe(recipeId)) .map( - _.sortBy(_.updatedAt).reverse.headOption.map( + _.sortBy(_.updatedAt).lastOption.map( req => PublicationRequestStatusResp.fromDomain(req.status.toDomain(req.reason)) ) ) From 97b11e56648f023a936e1ef996aa0f9edad477cc Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 23:38:26 +0300 Subject: [PATCH 53/95] feat: encoder --- .../PublicationRequestStatusResp.scala | 12 +++++++++++- src/main/scala/api/moderation/pubrequests/Get.scala | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) rename src/main/scala/api/{moderation/pubrequests => }/PublicationRequestStatusResp.scala (57%) diff --git a/src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala b/src/main/scala/api/PublicationRequestStatusResp.scala similarity index 57% rename from src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala rename to src/main/scala/api/PublicationRequestStatusResp.scala index e1cb0602..5e7d1f73 100644 --- a/src/main/scala/api/moderation/pubrequests/PublicationRequestStatusResp.scala +++ b/src/main/scala/api/PublicationRequestStatusResp.scala @@ -1,6 +1,7 @@ -package api.moderation.pubrequests +package api import domain.PublicationRequestStatus +import io.circe.{Encoder, Json} enum PublicationRequestStatusResp: case Pending @@ -13,3 +14,12 @@ object PublicationRequestStatusResp: case PublicationRequestStatus.Accepted => Accepted case PublicationRequestStatus.Pending => Pending case PublicationRequestStatus.Rejected(reason) => Rejected(reason) + + given Encoder[PublicationRequestStatusResp] = Encoder.instance { + case Pending => Json.fromString("pending") + case Accepted => Json.fromString("accepted") + case Rejected(reason) => + reason match + case Some(r) => Json.fromString(s"rejected: $r") + case None => Json.fromString("rejected") + } \ No newline at end of file diff --git a/src/main/scala/api/moderation/pubrequests/Get.scala b/src/main/scala/api/moderation/pubrequests/Get.scala index 05e1034e..850623b0 100644 --- a/src/main/scala/api/moderation/pubrequests/Get.scala +++ b/src/main/scala/api/moderation/pubrequests/Get.scala @@ -2,6 +2,7 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.PublicationRequestStatusResp import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.DbError import domain.{IngredientPublicationRequest, InternalServerError, PublicationRequestId, PublicationRequestNotFound, RecipePublicationRequest} From 702c1d64cbc54f15a8109f4cb2b600d6baaef5be Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sat, 19 Jul 2025 23:38:51 +0300 Subject: [PATCH 54/95] feat: new model --- .../scala/api/recipes/ModerationHistory.scala | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/ModerationHistory.scala index db06c013..5eabf3e6 100644 --- a/src/main/scala/api/recipes/ModerationHistory.scala +++ b/src/main/scala/api/recipes/ModerationHistory.scala @@ -1,22 +1,25 @@ package api.recipes -import domain.{InternalServerError, RecipeNotFound, RecipeId} -import api.EndpointErrorVariants.{serverErrorVariant, recipeNotFoundVariant} +import domain.{InternalServerError, RecipeId, RecipeNotFound} +import api.PublicationRequestStatusResp +import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} -import db.tables.publication.DbPublicationRequestStatus import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} +import io.circe.derivation.Configuration +import io.circe.Decoder import io.circe.generic.auto.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO + import java.time.OffsetDateTime final case class ModerationHistoryResponse( createdAt: OffsetDateTime, updatedAt: OffsetDateTime, - status: DbPublicationRequestStatus, + status: PublicationRequestStatusResp, reason: Option[String] ) @@ -39,7 +42,13 @@ def moderationHistoryHandler(recipeId: RecipeId): dbRequests <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllByRecipeId(recipeId)) .orElseFail(InternalServerError()) res = dbRequests - .map(dbReq => ModerationHistoryResponse(dbReq.createdAt, dbReq.updatedAt, dbReq.status, dbReq.reason)) + .map( + dbReq => ModerationHistoryResponse( + dbReq.createdAt, dbReq.updatedAt, + PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), + dbReq.reason + ) + ) .sortBy(_.updatedAt) yield res \ No newline at end of file From a4a09f66844b687d977d5ab60bca162499b405d2 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sun, 20 Jul 2025 00:14:35 +0300 Subject: [PATCH 55/95] fix: add missing publication action when updating publication request --- .../api/moderation/pubrequests/Update.scala | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 3bfcd702..a18305fa 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -2,8 +2,9 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.moderation.pubrequests.PublicationRequestStatusReq.Accepted import api.moderation.pubrequests.PublicationRequestTypeResp.* -import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} +import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} import domain.{InternalServerError, PublicationRequestId, PublicationRequestNotFound, PublicationRequestStatus} import io.circe.Encoder import io.circe.generic.auto.* @@ -32,6 +33,8 @@ final case class UpdatePublicationRequestReqBody( private type UpdateReqEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo + & IngredientsRepo + & RecipesRepo private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = publicationRequestEndpoint @@ -45,6 +48,25 @@ private val updatePublicationRequest: ZServerEndpoint[UpdateReqEnv, Any] = private def updatePublicationRequestHandler(id: PublicationRequestId, reqBody: UpdatePublicationRequestReqBody): ZIO[AuthenticatedUser & UpdateReqEnv, InternalServerError | PublicationRequestNotFound, Unit] = val status = reqBody.getDomainStatus + + def publishIngredient = + ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getWithIngredient(id)) + .some + .flatMap { + case (_, dbIngredient) => + val ingredientId = dbIngredient.id + ZIO.serviceWithZIO[IngredientsRepo](_.publish(ingredientId)) + }.orElseFail(InternalServerError()) + + def publishRecipe = + ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getWithRecipe(id)) + .some + .flatMap { + case (_, dbRecipe) => + val recipeId = dbRecipe.id + ZIO.serviceWithZIO[RecipesRepo](_.publish(recipeId)) + }.orElseFail(InternalServerError()) + for rowsUpdated <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ .updateStatus(id, status) @@ -55,4 +77,7 @@ private def updatePublicationRequestHandler(id: PublicationRequestId, reqBody: U .orElseFail(InternalServerError()) ).unless(rowsUpdated).someOrElse(false) _ <- ZIO.fail(PublicationRequestNotFound(id)).unless(rowsUpdated) + _ <- ZIO.when(reqBody.status == Accepted) { + publishRecipe.orElse(publishIngredient) + } yield () From 0306ad39564aab4aad4008d459cf2137b203ea42 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sun, 20 Jul 2025 00:16:08 +0300 Subject: [PATCH 56/95] feat: POST admin/recipes/ --- .../api/recipes/CreateRecipeReqBody.scala | 10 ++++ .../api/recipes/admin/CreatePublic.scala | 52 +++++++++++++++++++ .../scala/api/recipes/admin/Endpoints.scala | 16 ++++++ .../scala/db/repositories/RecipesRepo.scala | 18 +++++++ 4 files changed, 96 insertions(+) create mode 100644 src/main/scala/api/recipes/CreateRecipeReqBody.scala create mode 100644 src/main/scala/api/recipes/admin/CreatePublic.scala create mode 100644 src/main/scala/api/recipes/admin/Endpoints.scala diff --git a/src/main/scala/api/recipes/CreateRecipeReqBody.scala b/src/main/scala/api/recipes/CreateRecipeReqBody.scala new file mode 100644 index 00000000..7dea42b2 --- /dev/null +++ b/src/main/scala/api/recipes/CreateRecipeReqBody.scala @@ -0,0 +1,10 @@ +package api.recipes + +import domain.IngredientId + +final case class CreateRecipeReqBody( + name: String, + sourceLink: Option[String], + ingredients: List[IngredientId], +) + diff --git a/src/main/scala/api/recipes/admin/CreatePublic.scala b/src/main/scala/api/recipes/admin/CreatePublic.scala new file mode 100644 index 00000000..5d5bdd27 --- /dev/null +++ b/src/main/scala/api/recipes/admin/CreatePublic.scala @@ -0,0 +1,52 @@ +package api.recipes.admin + +import api.EndpointErrorVariants.{ingredientNotFoundVariant, serverErrorVariant} +import api.recipes.CreateRecipeReqBody +import api.{toIngredientNotFound, handleFailedSqlQuery} +import db.DbError.{DbNotRespondingError, FailedDbQuery} +import db.QuillConfig.ctx.* +import db.QuillConfig.provideDS +import db.repositories.{RecipeIngredientsRepo, RecipesRepo, IngredientsQueries} +import domain.{IngredientNotFound, IngredientId, InternalServerError, RecipeId} + +import io.circe.generic.auto.* +import io.getquill.* +import javax.sql.DataSource +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + +private type CreatePublicEnv = RecipesRepo & RecipeIngredientsRepo & DataSource + +private val createPublic: ZServerEndpoint[CreatePublicEnv, Any] = + adminRecipesEndpoint + .post + .in(jsonBody[CreateRecipeReqBody]) + .out(plainBody[RecipeId]) + .errorOut(oneOf(serverErrorVariant, ingredientNotFoundVariant)) + .zServerLogic(createPublicHandler) + +private def createPublicHandler(recipe: CreateRecipeReqBody): + ZIO[CreatePublicEnv, InternalServerError | IngredientNotFound, RecipeId] = for + dataSource <- ZIO.service[DataSource] + existingIngredientIds <- run( + IngredientsQueries.publicIngredientsQ + .map(_.id) + .filter(id => liftQuery(recipe.ingredients).contains(id)) + ).provideDS(using dataSource) + .orElseFail(InternalServerError()) + unknownIngredientIds = recipe.ingredients.diff(existingIngredientIds) + _ <- ZIO.fail(IngredientNotFound(unknownIngredientIds.head.toString)) + .when(unknownIngredientIds.nonEmpty) + + recipeId <- ZIO.serviceWithZIO[RecipesRepo](_ + .createPublic(recipe.name, recipe.sourceLink, recipe.ingredients) + .mapError { + case _: DbNotRespondingError => InternalServerError() + case e: FailedDbQuery => handleFailedSqlQuery(e) + .flatMap(toIngredientNotFound) + .getOrElse(InternalServerError()) + } + ) +yield recipeId diff --git a/src/main/scala/api/recipes/admin/Endpoints.scala b/src/main/scala/api/recipes/admin/Endpoints.scala new file mode 100644 index 00000000..c1d8004b --- /dev/null +++ b/src/main/scala/api/recipes/admin/Endpoints.scala @@ -0,0 +1,16 @@ +package api.recipes.admin + +import sttp.tapir.Endpoint +import sttp.tapir.ztapir.* + +import api.recipes.recipesEndpoint +import api.TapirExtensions.superTag + +val adminRecipesEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = + recipesEndpoint + .prependIn("admin") + .superTag("Admin") + +val adminRecipesEndpoints = List( + createPublic.widen, +) diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index cc6919b0..9683a219 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -11,6 +11,8 @@ import javax.sql.DataSource import zio.{ZIO, IO, RLayer, ZLayer} trait RecipesRepo: + def createPublic(name: String, sourceLink: Option[String], ingredients: List[IngredientId]): + IO[DbError, RecipeId] def addRecipe(name: String, sourceLink: Option[String], ingredients: List[IngredientId]): ZIO[AuthenticatedUser, DbError, RecipeId] @@ -33,6 +35,22 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: private given DataSource = dataSource + override def createPublic(name: String, sourceLink: Option[String], ingredientIds: List[IngredientId]): + IO[DbError, RecipeId] = transaction { + for + recipeId <- run( + recipesQ + .insertValue(lift(DbRecipe(id=null, name, None, isPublished=false, sourceLink))) + .returningGenerated(r => r.id) // null is safe here because of returningGenerated + ) + _ <- run( + liftQuery(ingredientIds).foreach( + RecipeIngredientsQueries.addIngredientQ(lift(recipeId), _) + ) + ) + yield recipeId + }.provideDS + override def addRecipe(name: String, sourceLink: Option[String], ingredientIds: List[IngredientId]): ZIO[AuthenticatedUser, DbError, RecipeId] = transaction { for From 9458f440fb7e44ac19743389481bde6c067f4e5e Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sun, 20 Jul 2025 00:28:22 +0300 Subject: [PATCH 57/95] fix: patch up listing all public ingredients --- src/main/scala/db/repositories/IngredientsRepo.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 1b579f3f..59f7ddba 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -95,13 +95,13 @@ object IngredientsQueries: ingredientsQ.filter(_.isPublished) inline def publicIngredientsQ: EntityQuery[DbIngredient] = - ingredientsQ.filter(_.ownerId.isEmpty) + ingredientsQ.filter(_.isPublished) inline def customIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(_.ownerId == Some(userId)) + ingredientsQ.filter(i => !i.isPublished && i.ownerId.contains(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(i => i.ownerId == None || i.ownerId == Some(userId)) + ingredientsQ.filter(i => i.ownerId.isEmpty || i.ownerId.contains(userId)) inline def getIngredientsQ(inline ingredientId: IngredientId): EntityQuery[DbIngredient] = ingredientsQ.filter(_.id == ingredientId) From 170a6d4610feeed61d149e40c51160172933f4f8 Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sun, 20 Jul 2025 01:39:11 +0300 Subject: [PATCH 58/95] fix: add filtering by request id in publication request repos method "getWith..." --- src/main/scala/api/moderation/pubrequests/Update.scala | 3 +-- .../db/repositories/IngredientPublicationRequestRepo.scala | 2 +- .../scala/db/repositories/RecipePublicationRequestsRepo.scala | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index a18305fa..7331c65e 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -2,7 +2,6 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} -import api.moderation.pubrequests.PublicationRequestStatusReq.Accepted import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} import domain.{InternalServerError, PublicationRequestId, PublicationRequestNotFound, PublicationRequestStatus} @@ -77,7 +76,7 @@ private def updatePublicationRequestHandler(id: PublicationRequestId, reqBody: U .orElseFail(InternalServerError()) ).unless(rowsUpdated).someOrElse(false) _ <- ZIO.fail(PublicationRequestNotFound(id)).unless(rowsUpdated) - _ <- ZIO.when(reqBody.status == Accepted) { + _ <- ZIO.when(status == PublicationRequestStatus.Accepted) { publishRecipe.orElse(publishIngredient) } yield () diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 2b94604d..e69409aa 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -49,7 +49,7 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] = run( - requestsQ + requestsQ.filter(_.id == lift(id)) .join(IngredientsQueries.ingredientsQ) .on(_.ingredientId == _.id) ).map(_.headOption).provideDS diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 06b08257..cceab4ab 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -48,7 +48,7 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) IO[DbError, Option[(DbRecipePublicationRequest, DbRecipe)]] = run( - requestsQ + requestsQ.filter(_.id == lift(id)) .join(RecipesQueries.recipesQ) .on(_.recipeId == _.id) ).map(_.headOption).provideDS From bed6d5ba309e13687fa19f81fd62f9c1a50e71a5 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sun, 20 Jul 2025 02:36:00 +0300 Subject: [PATCH 59/95] fix: add admin endpoint to endpoints list --- src/main/scala/api/recipes/Create.scala | 6 ------ src/main/scala/api/recipes/Endpoints.scala | 2 ++ src/main/scala/db/repositories/RecipesRepo.scala | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/scala/api/recipes/Create.scala b/src/main/scala/api/recipes/Create.scala index 64635528..e419de53 100644 --- a/src/main/scala/api/recipes/Create.scala +++ b/src/main/scala/api/recipes/Create.scala @@ -18,12 +18,6 @@ import sttp.tapir.ztapir.* import zio.ZIO import db.repositories.IngredientsQueries -final case class CreateRecipeReqBody( - name: String, - sourceLink: Option[String], - ingredients: List[IngredientId] -) - private type CreateEnv = RecipesRepo & RecipeIngredientsRepo & DataSource private val create: ZServerEndpoint[CreateEnv, Any] = diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index ea201d10..45672c05 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -4,6 +4,7 @@ import sttp.tapir.Endpoint import sttp.tapir.ztapir.* import api.recipes.ingredients.recipesIngredientsEndpoints +import api.recipes.admin.adminRecipesEndpoints val recipesEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = recipesEndpoint() @@ -21,3 +22,4 @@ val recipeEndpoints = List( delete.widen, requestPublication.widen, ) ++ recipesIngredientsEndpoints.map(_.widen) + ++ adminRecipesEndpoints.map(_.widen) diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index 9683a219..edc83e63 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -40,7 +40,7 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: for recipeId <- run( recipesQ - .insertValue(lift(DbRecipe(id=null, name, None, isPublished=false, sourceLink))) + .insertValue(lift(DbRecipe(id=null, name, None, isPublished=true, sourceLink))) .returningGenerated(r => r.id) // null is safe here because of returningGenerated ) _ <- run( From a528c903b73befb0a649d80c903e4afc04a07f08 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sun, 20 Jul 2025 02:55:56 +0300 Subject: [PATCH 60/95] fix: sanititize duplicate ingredients when creating recipes --- src/main/scala/api/recipes/Create.scala | 46 ++++++++++--------- .../api/recipes/admin/CreatePublic.scala | 46 ++++++++++--------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/main/scala/api/recipes/Create.scala b/src/main/scala/api/recipes/Create.scala index e419de53..d08e0513 100644 --- a/src/main/scala/api/recipes/Create.scala +++ b/src/main/scala/api/recipes/Create.scala @@ -28,26 +28,28 @@ private val create: ZServerEndpoint[CreateEnv, Any] = .errorOut(oneOf(serverErrorVariant, ingredientNotFoundVariant)) .zSecuredServerLogic(createHandler) -private def createHandler(recipe: CreateRecipeReqBody): - ZIO[AuthenticatedUser & CreateEnv, InternalServerError | IngredientNotFound, RecipeId] = for - userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) - dataSource <- ZIO.service[DataSource] - existingIngredientIds <- run( - IngredientsQueries.visibleIngredientsQ(lift(userId)) - .map(_.id) - .filter(id => liftQuery(recipe.ingredients).contains(id)) - ).provideDS(using dataSource).orElseFail(InternalServerError()) - unknownIngredientIds = recipe.ingredients.diff(existingIngredientIds) - _ <- ZIO.fail(IngredientNotFound(unknownIngredientIds.head.toString)) - .when(unknownIngredientIds.nonEmpty) +private def createHandler(recipeReq: CreateRecipeReqBody): + ZIO[AuthenticatedUser & CreateEnv, InternalServerError | IngredientNotFound, RecipeId] = + val recipe = recipeReq.copy(ingredients=recipeReq.ingredients.distinct) + for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + dataSource <- ZIO.service[DataSource] + existingIngredientIds <- run( + IngredientsQueries.visibleIngredientsQ(lift(userId)) + .map(_.id) + .filter(id => liftQuery(recipe.ingredients).contains(id)) + ).provideDS(using dataSource).orElseFail(InternalServerError()) + unknownIngredientIds = recipe.ingredients.diff(existingIngredientIds) + _ <- ZIO.fail(IngredientNotFound(unknownIngredientIds.head.toString)) + .when(unknownIngredientIds.nonEmpty) - recipeId <- ZIO.serviceWithZIO[RecipesRepo](_ - .addRecipe(recipe.name, recipe.sourceLink, recipe.ingredients) - .mapError { - case DbNotRespondingError(_) => InternalServerError() - case e: FailedDbQuery => handleFailedSqlQuery(e) - .flatMap(toIngredientNotFound) - .getOrElse(InternalServerError()) - } - ) -yield recipeId + recipeId <- ZIO.serviceWithZIO[RecipesRepo](_ + .addRecipe(recipe.name, recipe.sourceLink, recipe.ingredients) + .mapError { + case DbNotRespondingError(_) => InternalServerError() + case e: FailedDbQuery => handleFailedSqlQuery(e) + .flatMap(toIngredientNotFound) + .getOrElse(InternalServerError()) + } + ) + yield recipeId diff --git a/src/main/scala/api/recipes/admin/CreatePublic.scala b/src/main/scala/api/recipes/admin/CreatePublic.scala index 5d5bdd27..ec232a9c 100644 --- a/src/main/scala/api/recipes/admin/CreatePublic.scala +++ b/src/main/scala/api/recipes/admin/CreatePublic.scala @@ -27,26 +27,28 @@ private val createPublic: ZServerEndpoint[CreatePublicEnv, Any] = .errorOut(oneOf(serverErrorVariant, ingredientNotFoundVariant)) .zServerLogic(createPublicHandler) -private def createPublicHandler(recipe: CreateRecipeReqBody): - ZIO[CreatePublicEnv, InternalServerError | IngredientNotFound, RecipeId] = for - dataSource <- ZIO.service[DataSource] - existingIngredientIds <- run( - IngredientsQueries.publicIngredientsQ - .map(_.id) - .filter(id => liftQuery(recipe.ingredients).contains(id)) - ).provideDS(using dataSource) - .orElseFail(InternalServerError()) - unknownIngredientIds = recipe.ingredients.diff(existingIngredientIds) - _ <- ZIO.fail(IngredientNotFound(unknownIngredientIds.head.toString)) - .when(unknownIngredientIds.nonEmpty) +private def createPublicHandler(recipeReq: CreateRecipeReqBody): + ZIO[CreatePublicEnv, InternalServerError | IngredientNotFound, RecipeId] = + val recipe = recipeReq.copy(ingredients=recipeReq.ingredients.distinct) + for + dataSource <- ZIO.service[DataSource] + existingIngredientIds <- run( + IngredientsQueries.publicIngredientsQ + .map(_.id) + .filter(id => liftQuery(recipe.ingredients).contains(id)) + ).provideDS(using dataSource) + .orElseFail(InternalServerError()) + unknownIngredientIds = recipe.ingredients.diff(existingIngredientIds) + _ <- ZIO.fail(IngredientNotFound(unknownIngredientIds.head.toString)) + .when(unknownIngredientIds.nonEmpty) - recipeId <- ZIO.serviceWithZIO[RecipesRepo](_ - .createPublic(recipe.name, recipe.sourceLink, recipe.ingredients) - .mapError { - case _: DbNotRespondingError => InternalServerError() - case e: FailedDbQuery => handleFailedSqlQuery(e) - .flatMap(toIngredientNotFound) - .getOrElse(InternalServerError()) - } - ) -yield recipeId + recipeId <- ZIO.serviceWithZIO[RecipesRepo](_ + .createPublic(recipe.name, recipe.sourceLink, recipe.ingredients) + .mapError { + case _: DbNotRespondingError => InternalServerError() + case e: FailedDbQuery => handleFailedSqlQuery(e) + .flatMap(toIngredientNotFound) + .getOrElse(InternalServerError()) + } + ) + yield recipeId From e3dfa09feb3f12b64a874d307af89ae5f1674e56 Mon Sep 17 00:00:00 2001 From: danielambda Date: Sun, 20 Jul 2025 03:17:48 +0300 Subject: [PATCH 61/95] fix: GET /recipes/{recipe-id} did not work with recipes with no creator --- src/main/scala/api/recipes/Get.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/api/recipes/Get.scala b/src/main/scala/api/recipes/Get.scala index f8981e9d..2f0f4b84 100644 --- a/src/main/scala/api/recipes/Get.scala +++ b/src/main/scala/api/recipes/Get.scala @@ -132,7 +132,6 @@ private inline def rawRecipeQuery( FROM $storagesTable WHERE ${storagesTable.ownerId} = $userId ) - ), '[]'::json ) @@ -145,7 +144,7 @@ private inline def rawRecipeQuery( '[]'::json ) AS "ingredients" FROM $recipesTable r - RIGHT JOIN $usersTable u ON r.${recipesTable.creatorId} = u.${usersTable.id} + LEFT JOIN $usersTable u ON r.${recipesTable.creatorId} = u.${usersTable.id} WHERE r.${recipesTable.id} = $recipeId - AND (r.${recipesTable.isPublished} = true OR r.${recipesTable.creatorId} = $userId); + AND (r.${recipesTable.isPublished} OR r.${recipesTable.creatorId} = $userId); """.query[RawRecipeResult] From a82103bb0f12d299c4d57e280ec2ab4efbc5513e Mon Sep 17 00:00:00 2001 From: danielambda Date: Sun, 20 Jul 2025 04:34:37 +0300 Subject: [PATCH 62/95] feat: public recipes endpoints --- src/main/scala/api/recipes/Endpoints.scala | 2 + .../scala/api/recipes/public/Endpoint.scala | 17 +++++ src/main/scala/api/recipes/public/Get.scala | 70 +++++++++++++++++++ .../scala/api/recipes/public/Search.scala | 38 ++++++++++ .../repositories/RecipeIngredientsRepo.scala | 11 +-- .../scala/db/repositories/RecipesRepo.scala | 5 +- 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/main/scala/api/recipes/public/Endpoint.scala create mode 100644 src/main/scala/api/recipes/public/Get.scala create mode 100644 src/main/scala/api/recipes/public/Search.scala diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index 45672c05..a2e9d29c 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -5,6 +5,7 @@ import sttp.tapir.ztapir.* import api.recipes.ingredients.recipesIngredientsEndpoints import api.recipes.admin.adminRecipesEndpoints +import api.recipes.public.publicRecipesEndpoints val recipesEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = recipesEndpoint() @@ -23,3 +24,4 @@ val recipeEndpoints = List( requestPublication.widen, ) ++ recipesIngredientsEndpoints.map(_.widen) ++ adminRecipesEndpoints.map(_.widen) + ++ publicRecipesEndpoints.map(_.widen) diff --git a/src/main/scala/api/recipes/public/Endpoint.scala b/src/main/scala/api/recipes/public/Endpoint.scala new file mode 100644 index 00000000..78132e18 --- /dev/null +++ b/src/main/scala/api/recipes/public/Endpoint.scala @@ -0,0 +1,17 @@ +package api.recipes.public + +import sttp.tapir.Endpoint +import sttp.tapir.ztapir.* + +import api.recipes.recipesEndpoint +import api.TapirExtensions.superTag + +val publicRecipesEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = + recipesEndpoint + .prependIn("public") + .superTag("Public") + +val publicRecipesEndpoints = List( + getPublic.widen, + searchPublic.widen, +) diff --git a/src/main/scala/api/recipes/public/Get.scala b/src/main/scala/api/recipes/public/Get.scala new file mode 100644 index 00000000..d2adc84d --- /dev/null +++ b/src/main/scala/api/recipes/public/Get.scala @@ -0,0 +1,70 @@ +package api.recipes.public + +import api.ingredients.IngredientResp +import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant, userNotFoundVariant} +import db.QuillConfig.ctx.* +import db.QuillConfig.provideDS +import db.repositories.{IngredientsQueries, RecipeIngredientsQueries, RecipesQueries} +import db.tables.{DbIngredient, DbRecipe, DbUser} +import domain.{IngredientId, InternalServerError, RecipeId, RecipeNotFound, UserId} + +import io.circe.generic.auto.* +import io.getquill.{query => quillQuery, *} +import java.sql.SQLException +import javax.sql.DataSource +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + +final case class RecipeCreatorResp( + id: UserId, + fullName: String +) + +final case class RecipeResp( + ingredients: Seq[IngredientResp], + name: String, + sourceLink: Option[String], + creator: Option[RecipeCreatorResp], +) + +private type GetPublicEnv = DataSource + +private val getPublic: ZServerEndpoint[GetPublicEnv, Any] = + publicRecipesEndpoint + .get + .in(path[RecipeId]("recipeId")) + .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant, userNotFoundVariant)) + .out(jsonBody[RecipeResp]) + .zServerLogic(getPublicHandler) + +private def getPublicHandler(recipeId: RecipeId): + ZIO[GetPublicEnv, InternalServerError | RecipeNotFound, RecipeResp] = for + dataSource <- ZIO.service[DataSource] + recipe <- getRecipe(recipeId).provideDS(using dataSource) + .orElseFail(InternalServerError()) + .someOrFail(RecipeNotFound(recipeId)) + yield recipe + +def getRecipe(recipeId: RecipeId): ZIO[DataSource, Throwable, Option[RecipeResp]] = transaction { + for + mRecipe <- run( + RecipesQueries.getPublicRecipeQ(lift(recipeId)) + .leftJoin(quillQuery[DbUser]) + .on(_.creatorId contains _.id) + .value + ) + mRecipeWithIngredients <- ZIO.foreach(mRecipe) { (recipe, mCreator) => + val DbRecipe(id, name, creatorId, isPublished, sourceLink) = recipe + val creatorResp = mCreator.map(creator => RecipeCreatorResp(creator.id, creator.fullName)) + run( + RecipeIngredientsQueries.getAllIngredientsQ(lift(recipeId)) + .join(IngredientsQueries.publicIngredientsQ) + .on(_ == _.id) + .map(_._2) + ).map(_.map{case DbIngredient(id, _, name, _) => IngredientResp(id, name)}) + .map(RecipeResp(_, name, sourceLink, creatorResp)) + } + yield mRecipeWithIngredients +} diff --git a/src/main/scala/api/recipes/public/Search.scala b/src/main/scala/api/recipes/public/Search.scala new file mode 100644 index 00000000..9dc26433 --- /dev/null +++ b/src/main/scala/api/recipes/public/Search.scala @@ -0,0 +1,38 @@ +package api.recipes.public + +import api.common.search.* +import api.recipes.{SearchAllRecipesResp, RecipeSearchResp} +import api.EndpointErrorVariants.serverErrorVariant +import db.repositories.{RecipesRepo, StorageIngredientsRepo} +import db.tables.DbRecipe +import domain.{RecipeId, InternalServerError} + +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + +private type SearchPublicEnv = RecipesRepo & StorageIngredientsRepo + +private val searchPublic: ZServerEndpoint[SearchPublicEnv, Any] = + publicRecipesEndpoint + .get + .in(SearchParams.query) + .in(PaginationParams.query) + .out(jsonBody[SearchAllRecipesResp]) + .errorOut(oneOf(serverErrorVariant)) + .zServerLogic(searchPublicHandler) + +private def searchPublicHandler( + searchParams: SearchParams, + paginationParams: PaginationParams, +): ZIO[SearchPublicEnv, InternalServerError, SearchAllRecipesResp] = for + allDbRecipes <- ZIO.serviceWithZIO[RecipesRepo](_ + .getAllPublic + .map(Vector.from) + .orElseFail(InternalServerError()) + ) + allRecipes = allDbRecipes.map(RecipeSearchResp.fromDb) + res = Searchable.search(allRecipes, searchParams) +yield SearchAllRecipesResp(res.paginate(paginationParams), res.length) diff --git a/src/main/scala/db/repositories/RecipeIngredientsRepo.scala b/src/main/scala/db/repositories/RecipeIngredientsRepo.scala index a7061039..e70da67e 100644 --- a/src/main/scala/db/repositories/RecipeIngredientsRepo.scala +++ b/src/main/scala/db/repositories/RecipeIngredientsRepo.scala @@ -14,8 +14,6 @@ trait RecipeIngredientsRepo: def addIngredients(recipeId: RecipeId, ingredientIds: List[IngredientId]): IO[DbError, Unit] def removeIngredient(recipeId: RecipeId, ingredientId: IngredientId): IO[DbError, Unit] -private inline def recipeIngredients = query[DbRecipeIngredient] - final case class RecipeIngredientsRepoLive(dataSource: DataSource) extends RecipeIngredientsRepo: import db.QuillConfig.ctx.* import db.QuillConfig.provideDS @@ -50,16 +48,19 @@ final case class RecipeIngredientsRepoLive(dataSource: DataSource) extends Recip object RecipeIngredientsQueries: import db.QuillConfig.ctx.* + inline def recipeIngredientsQ: EntityQuery[DbRecipeIngredient] = + query[DbRecipeIngredient] + inline def getAllIngredientsQ(inline recipeId: RecipeId) = - recipeIngredients + recipeIngredientsQ .filter(_.recipeId == recipeId) .map(_.ingredientId) inline def addIngredientQ(inline recipeId: RecipeId, inline ingredientIds: IngredientId) = - recipeIngredients.insertValue((DbRecipeIngredient(recipeId, ingredientIds))) + recipeIngredientsQ.insertValue((DbRecipeIngredient(recipeId, ingredientIds))) inline def deleteIngredientQ(inline recipeId: RecipeId, inline ingredientId: IngredientId) = - recipeIngredients + recipeIngredientsQ .filter(ri => ri.recipeId == recipeId && ri.ingredientId == ingredientId) .delete diff --git a/src/main/scala/db/repositories/RecipesRepo.scala b/src/main/scala/db/repositories/RecipesRepo.scala index edc83e63..cb4d2fa2 100644 --- a/src/main/scala/db/repositories/RecipesRepo.scala +++ b/src/main/scala/db/repositories/RecipesRepo.scala @@ -72,7 +72,7 @@ final case class RecipesRepoLive(dataSource: DataSource) extends RecipesRepo: ZIO[AuthenticatedUser, DbError, Option[Recipe]] = transaction { for userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) - mRecipe <- run(getVisibleRecipeQ(lift(userId), lift(recipeId))).map(_.headOption) + mRecipe <- run(getVisibleRecipeQ(lift(userId), lift(recipeId)).value) mRecipeWithIngredients <- ZIO.foreach(mRecipe) { recipe => val DbRecipe(id, name, creatorId, isPublished, sourceLink) = recipe run( @@ -142,6 +142,9 @@ object RecipesQueries: inline def getVisibleRecipeQ(inline userId: UserId, inline recipeId: RecipeId): EntityQuery[DbRecipe] = visibleRecipesQ(userId).filter(r => r.id == recipeId) + inline def getPublicRecipeQ(inline recipeId: RecipeId): EntityQuery[DbRecipe] = + publicRecipesQ.filter(r => r.id == recipeId) + object RecipesRepo: def layer: RLayer[DataSource, RecipesRepo] = ZLayer.fromFunction(RecipesRepoLive.apply) From e734349c0d90d4d3d74056309cf6e37462b3bb8e Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 14:10:26 +0300 Subject: [PATCH 63/95] feat: getAllCreatedByMethod --- .../IngredientPublicationRequestRepo.scala | 21 ++++++++++++++--- .../RecipePublicationRequestsRepo.scala | 23 +++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index a56bac50..8609ee5c 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -1,14 +1,15 @@ package db.repositories +import api.Authentication.AuthenticatedUser import db.DbError import db.tables.DbIngredient import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} -import domain.{IngredientId, PublicationRequestId, PublicationRequestStatus} - +import domain.{IngredientId, PublicationRequestId, PublicationRequestStatus, UserId} import io.getquill.* + import javax.sql.DataSource import java.util.UUID -import zio.{IO, RLayer, ZLayer, ZIO} +import zio.{IO, RLayer, ZIO, ZLayer} trait IngredientPublicationRequestsRepo: def requestPublication(ingredientId: IngredientId): IO[DbError, PublicationRequestId] @@ -16,6 +17,7 @@ trait IngredientPublicationRequestsRepo: def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] + def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbIngredientPublicationRequest]] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) extends IngredientPublicationRequestsRepo: @@ -53,6 +55,12 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) .on(_.ingredientId == _.id) ).map(_.headOption).provideDS + override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbIngredientPublicationRequest]] = + for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + res <- run(getAllCreatedByQ(lift(userId))).provideDS + yield res + object IngredientPublicationRequestsQueries: inline def requestsQ: EntityQuery[DbIngredientPublicationRequest] = query[DbIngredientPublicationRequest] @@ -91,6 +99,13 @@ object IngredientPublicationRequestsQueries: _.reason -> reason, ) + inline def getAllCreatedByQ(inline userId: UserId): Query[DbIngredientPublicationRequest] = + query[DbIngredientPublicationRequest] + .join(query[DbIngredientPublicationRequest]) + .on(_.ingredientId == _.id) + .map(_._1) + + object IngredientPublicationRequestsRepo: def layer: RLayer[DataSource, IngredientPublicationRequestsRepo] = ZLayer.fromFunction(IngredientPublicationRequestsRepoLive.apply) diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index 9d192810..53590132 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -1,15 +1,16 @@ package db.repositories +import api.Authentication.AuthenticatedUser import db.DbError import db.QuillConfig.ctx import db.tables.DbRecipe import db.tables.publication.{DbPublicationRequestStatus, DbRecipePublicationRequest} -import domain.{PublicationRequestStatus, PublicationRequestId, RecipeId} - +import domain.{PublicationRequestId, PublicationRequestStatus, RecipeId, UserId} import io.getquill.* + import java.util.UUID import javax.sql.DataSource -import zio.{IO, RLayer, ZLayer, ZIO} +import zio.{IO, RLayer, ZIO, ZLayer} trait RecipePublicationRequestsRepo: def createPublicationRequest(recipeId: RecipeId): IO[DbError, PublicationRequestId] @@ -19,6 +20,7 @@ trait RecipePublicationRequestsRepo: def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] + def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbRecipePublicationRequest]] private inline def recipePublicationRequests = query[DbRecipePublicationRequest] @@ -65,6 +67,13 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] = run(getAllByRecipeIdQ(lift(recipeId))).provideDS + override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbRecipePublicationRequest]] = + for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + res <- run(getAllCreatedByQ(lift(userId))).provideDS + yield res + + object RecipePublicationRequestsQueries: inline def requestsQ: EntityQuery[DbRecipePublicationRequest] = query[DbRecipePublicationRequest] @@ -105,7 +114,13 @@ object RecipePublicationRequestsQueries: inline def getAllByRecipeIdQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = recipePublicationRequests.filter(_.recipeId == recipeId) - + + inline def getAllCreatedByQ(inline userId: UserId): Query[DbRecipePublicationRequest] = + query[DbRecipePublicationRequest] + .join(query[DbRecipe]) + .on(_.recipeId == _.id) + .map(_._1) + object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = ZLayer.fromFunction(RecipePublicationRequestsRepoLive.apply) From ae1bed008030c052dbdeca0ce1d4908e2700878d Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 14:10:42 +0300 Subject: [PATCH 64/95] refactor: mv moderationHistoryResponse to moderation package --- .../api/moderation/ModerationHistoryResponse.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/scala/api/moderation/ModerationHistoryResponse.scala diff --git a/src/main/scala/api/moderation/ModerationHistoryResponse.scala b/src/main/scala/api/moderation/ModerationHistoryResponse.scala new file mode 100644 index 00000000..80feef90 --- /dev/null +++ b/src/main/scala/api/moderation/ModerationHistoryResponse.scala @@ -0,0 +1,12 @@ +package api.moderation + +import api.PublicationRequestStatusResp + +import java.time.OffsetDateTime + +final case class ModerationHistoryResponse( + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: PublicationRequestStatusResp, + reason: Option[String] +) From d5aec344cbf7ba07cba90b40a6e72fdb37f2b772 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 14:10:50 +0300 Subject: [PATCH 65/95] refactor: mv moderationHistoryResponse to moderation package --- .../scala/api/recipes/ModerationHistory.scala | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/ModerationHistory.scala index 5eabf3e6..598ceb99 100644 --- a/src/main/scala/api/recipes/ModerationHistory.scala +++ b/src/main/scala/api/recipes/ModerationHistory.scala @@ -1,27 +1,20 @@ package api.recipes -import domain.{InternalServerError, RecipeId, RecipeNotFound} -import api.PublicationRequestStatusResp -import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} +import api.PublicationRequestStatusResp +import api.moderation.ModerationHistoryResponse import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} +import domain.{InternalServerError, RecipeId, RecipeNotFound} -import io.circe.derivation.Configuration import io.circe.Decoder +import io.circe.derivation.Configuration import io.circe.generic.auto.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO -import java.time.OffsetDateTime - -final case class ModerationHistoryResponse( - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - status: PublicationRequestStatusResp, - reason: Option[String] -) private type ModerationHistoryEnv = RecipePublicationRequestsRepo & RecipesRepo val moderationHistory: ZServerEndpoint[ModerationHistoryEnv, Any] = From 625f077633606b1c05fe8ac39acc00e19c8c8e62 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 14:11:40 +0300 Subject: [PATCH 66/95] feat: full history endpoint --- src/main/scala/api/moderation/Endpoints.scala | 2 +- .../scala/api/moderation/FullHistory.scala | 52 +++++++++++++++++++ src/main/scala/api/recipes/Endpoints.scala | 1 - 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/api/moderation/FullHistory.scala diff --git a/src/main/scala/api/moderation/Endpoints.scala b/src/main/scala/api/moderation/Endpoints.scala index 40c93c31..4dc8673d 100644 --- a/src/main/scala/api/moderation/Endpoints.scala +++ b/src/main/scala/api/moderation/Endpoints.scala @@ -10,4 +10,4 @@ val moderationEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = .tag("Moderation") .in("moderation") -val moderationEndpoints = publicationRequestEndpoints +val moderationEndpoints = publicationRequestEndpoints :+ fullHistory diff --git a/src/main/scala/api/moderation/FullHistory.scala b/src/main/scala/api/moderation/FullHistory.scala new file mode 100644 index 00000000..0651a382 --- /dev/null +++ b/src/main/scala/api/moderation/FullHistory.scala @@ -0,0 +1,52 @@ +package api.moderation + +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.EndpointErrorVariants.serverErrorVariant +import api.PublicationRequestStatusResp +import api.common.search.PaginationParams +import db.repositories.{RecipePublicationRequestsRepo, IngredientPublicationRequestsRepo} +import domain.InternalServerError + +import io.circe.Decoder +import io.circe.derivation.Configuration +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + + +private type FullHistoryEnv = RecipePublicationRequestsRepo & IngredientPublicationRequestsRepo +val fullHistory: ZServerEndpoint[FullHistoryEnv, Any] = + endpoint + .get + .in("publication-requests" / PaginationParams.query) + .errorOut(oneOf(serverErrorVariant)) + .out(jsonBody[List[ModerationHistoryResponse]]) + .zSecuredServerLogic(fullHistoryHandler) + +def fullHistoryHandler(paginationParams: PaginationParams): +ZIO[AuthenticatedUser & FullHistoryEnv, InternalServerError, List[ModerationHistoryResponse]] = + for + dbRecipeRequests <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllCreatedBy) + .orElseFail(InternalServerError()) + dbIngredientRequests <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_.getAllCreatedBy) + .orElseFail(InternalServerError()) + recipeRequests = dbRecipeRequests + .map( + dbReq => ModerationHistoryResponse( + dbReq.createdAt, dbReq.updatedAt, + PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), + dbReq.reason + ) + ) + ingredientRequests = dbIngredientRequests + .map( + dbReq => ModerationHistoryResponse( + dbReq.createdAt, dbReq.updatedAt, + PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), + dbReq.reason + ) + ) + res = (recipeRequests ++ ingredientRequests).sortBy(_.updatedAt) + yield res diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index 5fa20c50..dfa3b5e5 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -2,7 +2,6 @@ package api.recipes import sttp.tapir.Endpoint import sttp.tapir.ztapir.* - import api.recipes.ingredients.recipesIngredientsEndpoints val recipesEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = From 3b679ad21cb4f63687c5e62fd2b97dc9a4676b11 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 21:22:25 +0300 Subject: [PATCH 67/95] fix: rm reason from encoder --- src/main/scala/api/PublicationRequestStatusResp.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/scala/api/PublicationRequestStatusResp.scala b/src/main/scala/api/PublicationRequestStatusResp.scala index 5e7d1f73..3988cd8b 100644 --- a/src/main/scala/api/PublicationRequestStatusResp.scala +++ b/src/main/scala/api/PublicationRequestStatusResp.scala @@ -1,7 +1,7 @@ package api import domain.PublicationRequestStatus -import io.circe.{Encoder, Json} +import io.circe.{Decoder, Encoder, Json} enum PublicationRequestStatusResp: case Pending @@ -18,8 +18,5 @@ object PublicationRequestStatusResp: given Encoder[PublicationRequestStatusResp] = Encoder.instance { case Pending => Json.fromString("pending") case Accepted => Json.fromString("accepted") - case Rejected(reason) => - reason match - case Some(r) => Json.fromString(s"rejected: $r") - case None => Json.fromString("rejected") + case Rejected(reason) => Json.fromString("rejected") } \ No newline at end of file From 3a9b59b59edfd07752c167d630712b711d361352 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Sun, 20 Jul 2025 21:22:50 +0300 Subject: [PATCH 68/95] feat: add decoder --- src/main/scala/api/PublicationRequestStatusResp.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/scala/api/PublicationRequestStatusResp.scala b/src/main/scala/api/PublicationRequestStatusResp.scala index 3988cd8b..42a2b6ec 100644 --- a/src/main/scala/api/PublicationRequestStatusResp.scala +++ b/src/main/scala/api/PublicationRequestStatusResp.scala @@ -19,4 +19,11 @@ object PublicationRequestStatusResp: case Pending => Json.fromString("pending") case Accepted => Json.fromString("accepted") case Rejected(reason) => Json.fromString("rejected") + } + + given Decoder[PublicationRequestStatusResp] = Decoder.decodeString.emap { + case "pending" => Right(Pending) + case "accepted" => Right(Accepted) + case "rejected" => Right(Rejected(None)) + case other => Left(s"Unknown status: $other") } \ No newline at end of file From 29e3efdf5e30fa5f892b2cf21b87434aa1c5230c Mon Sep 17 00:00:00 2001 From: Leropsis Date: Sun, 20 Jul 2025 22:56:09 +0300 Subject: [PATCH 69/95] chore: add search for shopping list --- .../scala/api/ingredients/Endpoints.scala | 1 + .../ingredients/SearchForShoppingList.scala | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/main/scala/api/ingredients/SearchForShoppingList.scala diff --git a/src/main/scala/api/ingredients/Endpoints.scala b/src/main/scala/api/ingredients/Endpoints.scala index 3adcbcde..6fb56090 100644 --- a/src/main/scala/api/ingredients/Endpoints.scala +++ b/src/main/scala/api/ingredients/Endpoints.scala @@ -21,5 +21,6 @@ val ingredientsEndpoints = List( search.widen, searchForRecipe.widen, searchForStorage.widen, + searchForShoppingList.widen, requestPublication.widen ) ++ publicEndpoints.map(_.widen) diff --git a/src/main/scala/api/ingredients/SearchForShoppingList.scala b/src/main/scala/api/ingredients/SearchForShoppingList.scala new file mode 100644 index 00000000..947a47b0 --- /dev/null +++ b/src/main/scala/api/ingredients/SearchForShoppingList.scala @@ -0,0 +1,56 @@ +package api.ingredients + +import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} +import api.common.search.* +import api.EndpointErrorVariants.serverErrorVariant +import db.QuillConfig.provideDS +import db.repositories.IngredientsQueries +import db.tables.DbShoppingList +import domain.{IngredientId, InternalServerError} + +import io.circe.generic.auto.* +import io.getquill.{query => quillQuery, *} +import javax.sql.DataSource +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + +final case class IngredientsForShoppingListResp( + id: IngredientId, + name: String, + isInShopList: Boolean +) extends Searchable + +private type SearchForShopListEnv = DataSource + +private val searchForShoppingList: ZServerEndpoint[SearchForShopListEnv, Any] = + ingredientsEndpoint( + path="ingredients-for-shopping-list" + ).get + .in(SearchParams.query) + .in(PaginationParams.query) + .out(jsonBody[SearchResp[IngredientsForShoppingListResp]]) + .errorOut(oneOf(serverErrorVariant)) + .zSecuredServerLogic(searchForShoppingListHandler) + +private def searchForShoppingListHandler( + searchParams: SearchParams, + paginationParams: PaginationParams, +): ZIO[AuthenticatedUser & SearchForShopListEnv, + InternalServerError, + SearchResp[IngredientsForShoppingListResp]] = + import db.QuillConfig.ctx.* + for + dataSource <- ZIO.service[DataSource] + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) + allIngredientsAvailability <- run( + IngredientsQueries.visibleIngredientsQ(lift(userId)) + .leftJoin(quillQuery[DbShoppingList]) + .on((i, ri) => i.id == ri.ingredientId && ri.ownerId == lift(userId)) + .map((i, ri) => IngredientsForShoppingListResp(i.id, i.name, ri.map(_.ownerId).isDefined)) + ).provideDS(using dataSource) + .map(Vector.from) + .orElseFail(InternalServerError()) + res = Searchable.search(allIngredientsAvailability, searchParams) + yield SearchResp(res.paginate(paginationParams), res.length) From d697e60d522d35f2c5927986850b23b49b624478 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Mon, 21 Jul 2025 01:56:47 +0300 Subject: [PATCH 70/95] refactor: rm ownerId --- src/main/scala/api/storages/StorageSummaryResp.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/api/storages/StorageSummaryResp.scala b/src/main/scala/api/storages/StorageSummaryResp.scala index 2cb814ff..4c31a9c7 100644 --- a/src/main/scala/api/storages/StorageSummaryResp.scala +++ b/src/main/scala/api/storages/StorageSummaryResp.scala @@ -3,8 +3,8 @@ package api.storages import db.tables.DbStorage import domain.{StorageId, UserId} -final case class StorageSummaryResp(id: StorageId, ownerId: UserId, name: String) +final case class StorageSummaryResp(id: StorageId, name: String) object StorageSummaryResp: def fromDb(dbStorage: DbStorage): StorageSummaryResp = - StorageSummaryResp(dbStorage.id, dbStorage.ownerId, dbStorage.name) + StorageSummaryResp(dbStorage.id, dbStorage.name) From 9b7b4f2a240c5b7b7f6690e9f046e3b80f61b21a Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Mon, 21 Jul 2025 01:57:28 +0300 Subject: [PATCH 71/95] feat: make activate endpoint return storage id & name --- src/main/scala/api/invitations/Activate.scala | 10 ++++- .../db/repositories/InvitationsRepo.scala | 41 +++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main/scala/api/invitations/Activate.scala b/src/main/scala/api/invitations/Activate.scala index fb3d5c39..c4b3b03e 100644 --- a/src/main/scala/api/invitations/Activate.scala +++ b/src/main/scala/api/invitations/Activate.scala @@ -2,9 +2,13 @@ package api.invitations import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{invalidInvitationHashVariant, serverErrorVariant} +import api.storages.StorageSummaryResp import db.repositories.{InvitationsRepo, StorageMembersRepo} import domain.{InternalServerError, InvalidInvitationHash} +import io.circe.generic.auto.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO @@ -12,12 +16,14 @@ private type ActivateEnv = InvitationsRepo & StorageMembersRepo val activate: ZServerEndpoint[ActivateEnv, Any] = invitationEndpoint .post .in(path[String]("invitationHash") / "activate") + .out(jsonBody[StorageSummaryResp]) .errorOut(oneOf(invalidInvitationHashVariant, serverErrorVariant)) .zSecuredServerLogic(activateHandler) def activateHandler(invitationHash: String): - ZIO[AuthenticatedUser & ActivateEnv, InvalidInvitationHash | InternalServerError, Unit] = - ZIO.serviceWithZIO[InvitationsRepo](_.activate(invitationHash)).debug + ZIO[AuthenticatedUser & ActivateEnv, InvalidInvitationHash | InternalServerError, StorageSummaryResp] = + ZIO.serviceWithZIO[InvitationsRepo](_.activate(invitationHash)) + .map{case (storageId, storageName) => StorageSummaryResp(storageId, storageName)} .mapError { case e: InvalidInvitationHash => e case _ => InternalServerError() diff --git a/src/main/scala/db/repositories/InvitationsRepo.scala b/src/main/scala/db/repositories/InvitationsRepo.scala index cd86f089..fddf7b3f 100644 --- a/src/main/scala/db/repositories/InvitationsRepo.scala +++ b/src/main/scala/db/repositories/InvitationsRepo.scala @@ -2,23 +2,30 @@ package db.repositories import api.Authentication.AuthenticatedUser import db.{DbError, handleDbError} -import db.tables.{DbStorageInvitation, storageInvitationTable} +import db.tables.{DbStorage, DbStorageInvitation, storageInvitationTable} import domain.{InternalServerError, InvalidInvitationHash, StorageAccessForbidden, StorageId} import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.concurrent.TimeUnit +import io.getquill.* + +import javax.sql.DataSource import com.augustnagro.magnum.magzio.* -import zio.{URLayer, Layer, System, ZIO, ZLayer, Clock} +import zio.{Clock, Layer, System, URLayer, ZIO, ZLayer} trait InvitationsRepo: def create(storageId: StorageId): ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InternalServerError | StorageAccessForbidden, String] def activate(hash: String): - ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InvalidInvitationHash, Unit] + ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InvalidInvitationHash, (StorageId, String)] -private final case class InvitationsRepoLive(xa: Transactor, secretKey: InvitationsSecretKey) +private final case class InvitationsRepoLive(xa: Transactor, dataSource: DataSource, secretKey: InvitationsSecretKey) extends Repo[DbStorageInvitation, DbStorageInvitation, Null] with InvitationsRepo: + private given DataSource = dataSource + import db.QuillConfig.ctx.* + import db.QuillConfig.provideDS + import InvitationQueries.* override def create(storageId: StorageId): ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InternalServerError, String] = @@ -34,13 +41,12 @@ private final case class InvitationsRepoLive(xa: Transactor, secretKey: Invitati yield invitationHash override def activate(invitationHash: String): - ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InvalidInvitationHash, Unit] = + ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InvalidInvitationHash, (StorageId, String)] = for - dbInvitation <- xa.transact { - findAll(Spec[DbStorageInvitation] - .where(sql"${storageInvitationTable.invitation} = $invitationHash") - ).headOption - }.mapError(handleDbError).someOrFail(InvalidInvitationHash(invitationHash)) + dbInvitationWithName <- run(getInvitationWithStorageNameByHashQ(lift(invitationHash)).value) + .provideDS + .someOrFail(InvalidInvitationHash(invitationHash)) + (dbInvitation, storageName) = dbInvitationWithName isMemberOrOwner <- ZIO.serviceWithZIO[StorageMembersRepo](_.checkForMembership(dbInvitation.storageId)) _ <- ZIO.unless(isMemberOrOwner) { for @@ -49,15 +55,24 @@ private final case class InvitationsRepoLive(xa: Transactor, secretKey: Invitati _ <- ZIO.serviceWithZIO[StorageMembersRepo](_.addMemberToStorageById(dbInvitation.storageId, userId)) yield () } - yield () + yield (dbInvitation.storageId, storageName) + +object InvitationQueries: + inline def getInvitationWithStorageNameByHashQ(inline invitationHash: String): Query[(DbStorageInvitation, String)] = + query[DbStorageInvitation] + .filter(_.invitation == invitationHash) + .join(query[DbStorage]) + .on(_.storageId == _.id) + .map((i, s) => (i, s.name)) object InvitationsRepo: - val layer: URLayer[Transactor & InvitationsSecretKey, InvitationsRepoLive] = + val layer: URLayer[Transactor & DataSource & InvitationsSecretKey, InvitationsRepoLive] = ZLayer.fromZIO( for xa <- ZIO.service[Transactor] + dataSource <- ZIO.service[DataSource] secretKey <- ZIO.service[InvitationsSecretKey] - yield InvitationsRepoLive(xa, secretKey) + yield InvitationsRepoLive(xa, dataSource, secretKey) ) final case class InvitationsSecretKey(value: String) From b6a0228ae14eb7cb88487887fe9696ac9bab0ac7 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Mon, 21 Jul 2025 02:03:33 +0300 Subject: [PATCH 72/95] fix: tests --- .../scala/integration/api/storages/GetAllStoragesTests.scala | 1 - .../scala/integration/api/storages/GetStorageSummaryTests.scala | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/test/scala/integration/api/storages/GetAllStoragesTests.scala b/src/test/scala/integration/api/storages/GetAllStoragesTests.scala index 568bb384..12e44cc4 100644 --- a/src/test/scala/integration/api/storages/GetAllStoragesTests.scala +++ b/src/test/scala/integration/api/storages/GetAllStoragesTests.scala @@ -50,7 +50,6 @@ object GetAllStoragesTests extends ZIOIntegrationTestSpec: storages <- ZIO.fromEither(decode[Vector[StorageSummaryResp]](bodyStr)) yield assertTrue(resp.status == Status.Ok) && assertTrue(storages.map(_.name).hasSameElementsAs(storageNames)) - && assertTrue(storages.forall(_.ownerId == user.userId)) }, test("When authorized with membered storages should get 200 and all storages") { for diff --git a/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala b/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala index 55fca6d3..02b6a7ab 100644 --- a/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala +++ b/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala @@ -54,7 +54,6 @@ object GetStorageSummaryTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Ok) && assertTrue(storage.id == storageId) && assertTrue(storage.name == storageName) - && assertTrue(storage.ownerId == user.userId) }, test("When authorized and user is a member of the storage should get 200 and the storage") { for @@ -77,7 +76,6 @@ object GetStorageSummaryTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Ok) && assertTrue(storage.id == storageId) && assertTrue(storage.name == storageName) - && assertTrue(storage.ownerId == creator.userId) }, test("When authorized but user is neither the owner nor a member should get 404") { for From d1f81490b78aad6eabe89d00921efa3119b78816 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Mon, 21 Jul 2025 02:21:17 +0300 Subject: [PATCH 73/95] fix: error --- src/main/scala/db/repositories/InvitationsRepo.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/InvitationsRepo.scala b/src/main/scala/db/repositories/InvitationsRepo.scala index fddf7b3f..1217260d 100644 --- a/src/main/scala/db/repositories/InvitationsRepo.scala +++ b/src/main/scala/db/repositories/InvitationsRepo.scala @@ -43,8 +43,9 @@ private final case class InvitationsRepoLive(xa: Transactor, dataSource: DataSou override def activate(invitationHash: String): ZIO[AuthenticatedUser & StorageMembersRepo, DbError | InvalidInvitationHash, (StorageId, String)] = for - dbInvitationWithName <- run(getInvitationWithStorageNameByHashQ(lift(invitationHash)).value) + dbInvitationWithName <- run(getInvitationWithStorageNameByHashQ(lift(invitationHash))) .provideDS + .map(_.headOption) .someOrFail(InvalidInvitationHash(invitationHash)) (dbInvitation, storageName) = dbInvitationWithName isMemberOrOwner <- ZIO.serviceWithZIO[StorageMembersRepo](_.checkForMembership(dbInvitation.storageId)) From 7d319f0f1b1c3e4e9b53273598e5ebffb85a5ce8 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 17:04:28 +0300 Subject: [PATCH 74/95] feat: POST /admin/recipes --- src/main/scala/api/ingredients/Create.scala | 3 +- .../scala/api/ingredients/Endpoints.scala | 2 ++ .../api/ingredients/admin/CreatePublic.scala | 31 +++++++++++++++++++ .../api/ingredients/admin/Endpoints.scala | 16 ++++++++++ .../pubrequests/GetSomePending.scala | 1 - .../api/recipes/admin/CreatePublic.scala | 3 +- 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/main/scala/api/ingredients/admin/CreatePublic.scala create mode 100644 src/main/scala/api/ingredients/admin/Endpoints.scala diff --git a/src/main/scala/api/ingredients/Create.scala b/src/main/scala/api/ingredients/Create.scala index c9317152..aa1fd318 100644 --- a/src/main/scala/api/ingredients/Create.scala +++ b/src/main/scala/api/ingredients/Create.scala @@ -17,6 +17,7 @@ import sttp.tapir.ztapir.* import zio.ZIO private type CreateEnv = IngredientsRepo & DataSource + private val create: ZServerEndpoint[CreateEnv, Any] = ingredientsEndpoint .post @@ -42,4 +43,4 @@ private def createHandler(reqBody: CreateIngredientReqBody): .map(_.id) .orElseFail(InternalServerError()) ) - yield ingredientId \ No newline at end of file + yield ingredientId diff --git a/src/main/scala/api/ingredients/Endpoints.scala b/src/main/scala/api/ingredients/Endpoints.scala index 3adcbcde..fca6e17c 100644 --- a/src/main/scala/api/ingredients/Endpoints.scala +++ b/src/main/scala/api/ingredients/Endpoints.scala @@ -3,6 +3,7 @@ package api.ingredients import sttp.tapir.Endpoint import sttp.tapir.ztapir.* +import api.ingredients.admin.adminIngredientsEndpoints import api.ingredients.public.publicEndpoints import api.TapirExtensions.subTag @@ -23,3 +24,4 @@ val ingredientsEndpoints = List( searchForStorage.widen, requestPublication.widen ) ++ publicEndpoints.map(_.widen) + ++ adminIngredientsEndpoints.map(_.widen) diff --git a/src/main/scala/api/ingredients/admin/CreatePublic.scala b/src/main/scala/api/ingredients/admin/CreatePublic.scala new file mode 100644 index 00000000..56211c41 --- /dev/null +++ b/src/main/scala/api/ingredients/admin/CreatePublic.scala @@ -0,0 +1,31 @@ +package api.ingredients.admin + +import api.EndpointErrorVariants.serverErrorVariant +import api.ingredients.CreateIngredientReqBody +import db.repositories.{IngredientsRepo} +import domain.{ IngredientId, InternalServerError} + +import io.circe.generic.auto.* +import sttp.model.StatusCode.Created +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.ztapir.* +import zio.ZIO + +private type CreatePublicEnv = IngredientsRepo + +private val createPublic: ZServerEndpoint[CreatePublicEnv, Any] = + adminIngredientsEndpoint + .post + .in(jsonBody[CreateIngredientReqBody]) + .out(plainBody[IngredientId] and statusCode(Created)) + .errorOut(oneOf(serverErrorVariant)) + .zServerLogic(createPublicHandler) + +private def createPublicHandler(req: CreateIngredientReqBody): + ZIO[CreatePublicEnv, InternalServerError, IngredientId] = + ZIO.serviceWithZIO[IngredientsRepo](_ + .addPublic(req.name) + .orElseFail(InternalServerError()) + .map(_.id) + ) diff --git a/src/main/scala/api/ingredients/admin/Endpoints.scala b/src/main/scala/api/ingredients/admin/Endpoints.scala new file mode 100644 index 00000000..0aa56ff2 --- /dev/null +++ b/src/main/scala/api/ingredients/admin/Endpoints.scala @@ -0,0 +1,16 @@ +package api.ingredients.admin + +import sttp.tapir.Endpoint +import sttp.tapir.ztapir.* + +import api.ingredients.ingredientsEndpoint +import api.TapirExtensions.superTag + +val adminIngredientsEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = + ingredientsEndpoint + .prependIn("admin") + .superTag("Admin") + +val adminIngredientsEndpoints = List( + createPublic.widen, +) diff --git a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala index 1f0b2d4a..db26d834 100644 --- a/src/main/scala/api/moderation/pubrequests/GetSomePending.scala +++ b/src/main/scala/api/moderation/pubrequests/GetSomePending.scala @@ -9,7 +9,6 @@ import domain.{InternalServerError, PublicationRequestId} import io.circe.generic.auto.* import java.time.OffsetDateTime -import sttp.model.StatusCode.NoContent import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.ztapir.* diff --git a/src/main/scala/api/recipes/admin/CreatePublic.scala b/src/main/scala/api/recipes/admin/CreatePublic.scala index ec232a9c..fba943f7 100644 --- a/src/main/scala/api/recipes/admin/CreatePublic.scala +++ b/src/main/scala/api/recipes/admin/CreatePublic.scala @@ -12,6 +12,7 @@ import domain.{IngredientNotFound, IngredientId, InternalServerError, RecipeId} import io.circe.generic.auto.* import io.getquill.* import javax.sql.DataSource +import sttp.model.StatusCode.Created import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* @@ -23,7 +24,7 @@ private val createPublic: ZServerEndpoint[CreatePublicEnv, Any] = adminRecipesEndpoint .post .in(jsonBody[CreateRecipeReqBody]) - .out(plainBody[RecipeId]) + .out(plainBody[RecipeId] and statusCode(Created)) .errorOut(oneOf(serverErrorVariant, ingredientNotFoundVariant)) .zServerLogic(createPublicHandler) From 192936ac83bbfcd21e5cf3611e3ada48f8ea1c50 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 17:12:13 +0300 Subject: [PATCH 75/95] refactor: remove unused imports --- src/main/scala/api/storages/StorageSummaryResp.scala | 2 +- src/main/scala/db/repositories/InvitationsRepo.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/api/storages/StorageSummaryResp.scala b/src/main/scala/api/storages/StorageSummaryResp.scala index 4c31a9c7..a04f2da9 100644 --- a/src/main/scala/api/storages/StorageSummaryResp.scala +++ b/src/main/scala/api/storages/StorageSummaryResp.scala @@ -1,7 +1,7 @@ package api.storages import db.tables.DbStorage -import domain.{StorageId, UserId} +import domain.StorageId final case class StorageSummaryResp(id: StorageId, name: String) diff --git a/src/main/scala/db/repositories/InvitationsRepo.scala b/src/main/scala/db/repositories/InvitationsRepo.scala index 1217260d..2d0bd923 100644 --- a/src/main/scala/db/repositories/InvitationsRepo.scala +++ b/src/main/scala/db/repositories/InvitationsRepo.scala @@ -2,7 +2,7 @@ package db.repositories import api.Authentication.AuthenticatedUser import db.{DbError, handleDbError} -import db.tables.{DbStorage, DbStorageInvitation, storageInvitationTable} +import db.tables.{DbStorage, DbStorageInvitation} import domain.{InternalServerError, InvalidInvitationHash, StorageAccessForbidden, StorageId} import java.nio.charset.StandardCharsets From 00c58a05154da137ee36bb6ec7ea637cab02f507 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 18:07:11 +0300 Subject: [PATCH 76/95] fix: IngredientsRepo.addPublic isPublished -> true --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 5af19aff..835aae7d 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -30,7 +30,7 @@ private final case class IngredientsRepoLive(dataSource: DataSource) extends Ing override def addPublic(name: String): IO[DbError, DbIngredient] = run( ingredientsQ - .insert(_.name -> lift(name), _.isPublished -> false) + .insert(_.name -> lift(name), _.isPublished -> true) .returning(ingredient => ingredient) ).provideDS From b58c8db3e3ccf44f7f39fb35d5f4d64a771927f9 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 19:28:52 +0300 Subject: [PATCH 77/95] chore: rename to /recipes/{recipe-id}/publication-requests --- src/main/scala/api/recipes/ModerationHistory.scala | 5 ++--- src/main/scala/api/recipes/RequestPublication.scala | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/ModerationHistory.scala index 598ceb99..9b8534b0 100644 --- a/src/main/scala/api/recipes/ModerationHistory.scala +++ b/src/main/scala/api/recipes/ModerationHistory.scala @@ -15,12 +15,11 @@ import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO - private type ModerationHistoryEnv = RecipePublicationRequestsRepo & RecipesRepo val moderationHistory: ZServerEndpoint[ModerationHistoryEnv, Any] = recipesEndpoint .get - .in(path[RecipeId]("recipe-id") / "moderation-history") + .in(path[RecipeId]("recipe-id") / "publication-requests") .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) .out(jsonBody[List[ModerationHistoryResponse]]) .zSecuredServerLogic(moderationHistoryHandler) @@ -44,4 +43,4 @@ def moderationHistoryHandler(recipeId: RecipeId): ) .sortBy(_.updatedAt) - yield res \ No newline at end of file + yield res diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/RequestPublication.scala index dbfd7f1a..8efc8f32 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/RequestPublication.scala @@ -46,7 +46,7 @@ private type PublishEnv private val requestPublication: ZServerEndpoint[PublishEnv, Any] = recipesEndpoint .post - .in(path[RecipeId]("recipeId") / "request-publication") + .in(path[RecipeId]("recipeId") / "publication-requests") .out(plainBody[PublicationRequestId] and statusCode(Created)) .errorOut(oneOf( serverErrorVariant, From 605abe0053439016b37b60de40106cb34cfe5617 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 19:42:03 +0300 Subject: [PATCH 78/95] refactor: create api/recipes/publicationRequests directory --- src/main/scala/api/recipes/Endpoints.scala | 4 ++-- .../Create.scala} | 15 +++++++-------- .../publicationRequests/Endpoints.scala | 17 +++++++++++++++++ .../GetAll.scala} | 18 +++++++++++------- 4 files changed, 37 insertions(+), 17 deletions(-) rename src/main/scala/api/recipes/{RequestPublication.scala => publicationRequests/Create.scala} (90%) create mode 100644 src/main/scala/api/recipes/publicationRequests/Endpoints.scala rename src/main/scala/api/recipes/{ModerationHistory.scala => publicationRequests/GetAll.scala} (79%) diff --git a/src/main/scala/api/recipes/Endpoints.scala b/src/main/scala/api/recipes/Endpoints.scala index f3924b21..2ffa6cb6 100644 --- a/src/main/scala/api/recipes/Endpoints.scala +++ b/src/main/scala/api/recipes/Endpoints.scala @@ -3,6 +3,7 @@ package api.recipes import sttp.tapir.Endpoint import sttp.tapir.ztapir.* import api.recipes.ingredients.recipesIngredientsEndpoints +import api.recipes.publicationRequests.recipesPublicationRequestsEndpoints import api.recipes.admin.adminRecipesEndpoints import api.recipes.public.publicRecipesEndpoints @@ -20,8 +21,7 @@ val recipeEndpoints = List( get.widen, searchAll.widen, delete.widen, - requestPublication.widen, - moderationHistory.widen ) ++ recipesIngredientsEndpoints.map(_.widen) + ++ recipesPublicationRequestsEndpoints.map(_.widen) ++ adminRecipesEndpoints.map(_.widen) ++ publicRecipesEndpoints.map(_.widen) diff --git a/src/main/scala/api/recipes/RequestPublication.scala b/src/main/scala/api/recipes/publicationRequests/Create.scala similarity index 90% rename from src/main/scala/api/recipes/RequestPublication.scala rename to src/main/scala/api/recipes/publicationRequests/Create.scala index 8efc8f32..b6defd86 100644 --- a/src/main/scala/api/recipes/RequestPublication.scala +++ b/src/main/scala/api/recipes/publicationRequests/Create.scala @@ -1,4 +1,4 @@ -package api.recipes +package api.recipes.publicationRequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} @@ -37,16 +37,15 @@ final case class RecipeAlreadyPending( object RecipeAlreadyPending: val variant = BadRequest.variantJson[RecipeAlreadyPending] -private type PublishEnv +private type CreateEnv = RecipesRepo & RecipeIngredientsRepo & RecipePublicationRequestsRepo & DataSource -private val requestPublication: ZServerEndpoint[PublishEnv, Any] = - recipesEndpoint +private val create: ZServerEndpoint[CreateEnv, Any] = + recipesPublicationRequestsEndpoint .post - .in(path[RecipeId]("recipeId") / "publication-requests") .out(plainBody[PublicationRequestId] and statusCode(Created)) .errorOut(oneOf( serverErrorVariant, @@ -55,11 +54,11 @@ private val requestPublication: ZServerEndpoint[PublishEnv, Any] = RecipeAlreadyPending.variant, RecipeAlreadyPublished.variant, )) - .zSecuredServerLogic(requestPublicationHandler) + .zSecuredServerLogic(createHandler) -private def requestPublicationHandler(recipeId: RecipeId): +private def createHandler(recipeId: RecipeId): ZIO[ - AuthenticatedUser & PublishEnv, + AuthenticatedUser & CreateEnv, InternalServerError | RecipeAlreadyPublished | RecipeAlreadyPending | CannotPublishRecipeWithCustomIngredients | RecipeNotFound, PublicationRequestId diff --git a/src/main/scala/api/recipes/publicationRequests/Endpoints.scala b/src/main/scala/api/recipes/publicationRequests/Endpoints.scala new file mode 100644 index 00000000..ca6e5b24 --- /dev/null +++ b/src/main/scala/api/recipes/publicationRequests/Endpoints.scala @@ -0,0 +1,17 @@ +package api.recipes.publicationRequests + +import sttp.tapir.ztapir.* + +import api.recipes.{recipesEndpoint} +import api.TapirExtensions.subTag +import domain.RecipeId + +val recipesPublicationRequestsEndpoint = + recipesEndpoint + .subTag("Publication Requests") + .in(path[RecipeId]("recipeId") / "publication-requests") + +val recipesPublicationRequestsEndpoints = List( + create.widen, + getAll.widen, +) diff --git a/src/main/scala/api/recipes/ModerationHistory.scala b/src/main/scala/api/recipes/publicationRequests/GetAll.scala similarity index 79% rename from src/main/scala/api/recipes/ModerationHistory.scala rename to src/main/scala/api/recipes/publicationRequests/GetAll.scala index 9b8534b0..058f4aa2 100644 --- a/src/main/scala/api/recipes/ModerationHistory.scala +++ b/src/main/scala/api/recipes/publicationRequests/GetAll.scala @@ -1,4 +1,4 @@ -package api.recipes +package api.recipes.publicationRequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} @@ -15,17 +15,21 @@ import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO -private type ModerationHistoryEnv = RecipePublicationRequestsRepo & RecipesRepo -val moderationHistory: ZServerEndpoint[ModerationHistoryEnv, Any] = - recipesEndpoint +private type GetAllEnv = RecipePublicationRequestsRepo & RecipesRepo + +val getAll: ZServerEndpoint[GetAllEnv, Any] = + recipesPublicationRequestsEndpoint .get - .in(path[RecipeId]("recipe-id") / "publication-requests") - .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) .out(jsonBody[List[ModerationHistoryResponse]]) + .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) .zSecuredServerLogic(moderationHistoryHandler) def moderationHistoryHandler(recipeId: RecipeId): - ZIO[AuthenticatedUser & ModerationHistoryEnv, InternalServerError | RecipeNotFound, List[ModerationHistoryResponse]] = + ZIO[ + AuthenticatedUser & GetAllEnv, + InternalServerError | RecipeNotFound, + List[ModerationHistoryResponse] + ] = for isUserOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) .orElseFail(InternalServerError()) From ce96eee2c9b07b3d8088e133b43b10109488414b Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 19:43:03 +0300 Subject: [PATCH 79/95] fix: add full publication requests endpoint to endpoints list --- src/main/scala/api/Endpoints.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/api/Endpoints.scala b/src/main/scala/api/Endpoints.scala index 00286de7..d22997d2 100644 --- a/src/main/scala/api/Endpoints.scala +++ b/src/main/scala/api/Endpoints.scala @@ -19,3 +19,4 @@ object AppEndpoints: ++ shoppingListEndpoints.map(_.widen) ++ invitationEndpoints.map(_.widen) ++ moderationEndpoints.map(_.widen) + :+ getPublicationRequestsHistory.widen From 7a6e8a49e425d6674a8916cc21c3d6803e3b3766 Mon Sep 17 00:00:00 2001 From: Vadim Ksenofontov Date: Mon, 21 Jul 2025 22:21:55 +0300 Subject: [PATCH 80/95] refactor: a little bit of rearrangement in quill query --- src/main/scala/api/ingredients/SearchForShoppingList.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/api/ingredients/SearchForShoppingList.scala b/src/main/scala/api/ingredients/SearchForShoppingList.scala index 947a47b0..fd43418e 100644 --- a/src/main/scala/api/ingredients/SearchForShoppingList.scala +++ b/src/main/scala/api/ingredients/SearchForShoppingList.scala @@ -46,9 +46,9 @@ private def searchForShoppingListHandler( userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) allIngredientsAvailability <- run( IngredientsQueries.visibleIngredientsQ(lift(userId)) - .leftJoin(quillQuery[DbShoppingList]) - .on((i, ri) => i.id == ri.ingredientId && ri.ownerId == lift(userId)) - .map((i, ri) => IngredientsForShoppingListResp(i.id, i.name, ri.map(_.ownerId).isDefined)) + .leftJoin(quillQuery[DbShoppingList].filter(_.ownerId == lift(userId))) + .on((i, si) => i.id == si.ingredientId) + .map((i, si) => IngredientsForShoppingListResp(i.id, i.name, si.map(_.ownerId).isDefined)) ).provideDS(using dataSource) .map(Vector.from) .orElseFail(InternalServerError()) From da8db70dda5ffe686cfc98f95c215895ef2654b5 Mon Sep 17 00:00:00 2001 From: Maxim Fomin <62051211+Makcal@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:26:39 +0300 Subject: [PATCH 81/95] fix: customness of ingredient depended on publicity --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 835aae7d..e42283f3 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -98,7 +98,7 @@ object IngredientsQueries: ingredientsQ.filter(_.isPublished) inline def customIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(i => !i.isPublished && i.ownerId.contains(userId)) + ingredientsQ.filter(i => i.ownerId.contains(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = ingredientsQ.filter(i => i.ownerId.isEmpty || i.ownerId.contains(userId)) From df897da5fab849af21ed3c3885adf938a94ca38e Mon Sep 17 00:00:00 2001 From: Maxim Fomin <62051211+Makcal@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:28:31 +0300 Subject: [PATCH 82/95] fix: all published ingredients should be visible (not depend on owner) --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index e42283f3..9889b2ce 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -101,7 +101,7 @@ object IngredientsQueries: ingredientsQ.filter(i => i.ownerId.contains(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(i => i.ownerId.isEmpty || i.ownerId.contains(userId)) + ingredientsQ.filter(i => i.ownerId.isEmpty || i.ownerId.contains(userId) || i.isPublished) inline def getIngredientsQ(inline ingredientId: IngredientId): EntityQuery[DbIngredient] = ingredientsQ.filter(_.id == ingredientId) From 802d04a24fe4e1aeeb2664e719f612bfe61ec1f9 Mon Sep 17 00:00:00 2001 From: Maxim Fomin <62051211+Makcal@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:32:02 +0300 Subject: [PATCH 83/95] fix: remove not published ingredients without an owner from visibility --- src/main/scala/db/repositories/IngredientsRepo.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 9889b2ce..95265b3e 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -101,7 +101,7 @@ object IngredientsQueries: ingredientsQ.filter(i => i.ownerId.contains(userId)) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(i => i.ownerId.isEmpty || i.ownerId.contains(userId) || i.isPublished) + ingredientsQ.filter(i => i.isPublished || i.ownerId.contains(userId)) inline def getIngredientsQ(inline ingredientId: IngredientId): EntityQuery[DbIngredient] = ingredientsQ.filter(_.id == ingredientId) From 63e3fd55eb58b3ff60cb2cc25739c7fc97408924 Mon Sep 17 00:00:00 2001 From: danielambda Date: Mon, 21 Jul 2025 22:32:40 +0300 Subject: [PATCH 84/95] fix: update route in RequestRecipePublicationTests --- src/main/scala/api/recipes/publicationRequests/GetAll.scala | 4 ++-- .../api/recipes/RequestRecipePublicationTests.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/api/recipes/publicationRequests/GetAll.scala b/src/main/scala/api/recipes/publicationRequests/GetAll.scala index 058f4aa2..5c4f8004 100644 --- a/src/main/scala/api/recipes/publicationRequests/GetAll.scala +++ b/src/main/scala/api/recipes/publicationRequests/GetAll.scala @@ -22,9 +22,9 @@ val getAll: ZServerEndpoint[GetAllEnv, Any] = .get .out(jsonBody[List[ModerationHistoryResponse]]) .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) - .zSecuredServerLogic(moderationHistoryHandler) + .zSecuredServerLogic(getAllHandler) -def moderationHistoryHandler(recipeId: RecipeId): +private def getAllHandler(recipeId: RecipeId): ZIO[ AuthenticatedUser & GetAllEnv, InternalServerError | RecipeNotFound, diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala index a08b17d8..066bcdfc 100644 --- a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -15,7 +15,7 @@ import zio.test.* object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: private def endpointPath(recipeId: RecipeId): URL = - URL(Path.root / "recipes" / recipeId.toString / "request-publication") + URL(Path.root / "recipes" / recipeId.toString / "publication-requests") private def requestRecipePublication(user: AuthenticatedUser, recipeId: RecipeId): RIO[Client, Response] = From 23fb21dc7082b135bab8cbabd6b8c7ba05c0fdd6 Mon Sep 17 00:00:00 2001 From: danielambda Date: Tue, 22 Jul 2025 01:00:19 +0300 Subject: [PATCH 85/95] fix: bring storage id back to GET /storages/{storageId} --- src/main/scala/api/storages/GetSummary.scala | 39 +++++++++++-------- ...mmaryTests.scala => GetStorageTests.scala} | 12 +++--- 2 files changed, 29 insertions(+), 22 deletions(-) rename src/test/scala/integration/api/storages/{GetStorageSummaryTests.scala => GetStorageTests.scala} (91%) diff --git a/src/main/scala/api/storages/GetSummary.scala b/src/main/scala/api/storages/GetSummary.scala index 5ab1dcf8..eba34478 100644 --- a/src/main/scala/api/storages/GetSummary.scala +++ b/src/main/scala/api/storages/GetSummary.scala @@ -13,26 +13,33 @@ import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO -private type GetSummaryEnv = StoragesRepo & StorageMembersRepo +final case class GetStorageResp( + ownerId: UserId, + name: String, +) +object GetStorageResp: + def fromDb(dbStorage: DbStorage): GetStorageResp = + GetStorageResp(dbStorage.ownerId, dbStorage.name) -private val getSummary: ZServerEndpoint[GetSummaryEnv, Any] = +private type GetEnv = StoragesRepo & StorageMembersRepo + +private val getSummary: ZServerEndpoint[GetEnv, Any] = storagesEndpoint - .get - .in(path[StorageId]("storageId")) - .out(jsonBody[StorageSummaryResp]) - .errorOut(oneOf(serverErrorVariant, storageNotFoundVariant)) - .zSecuredServerLogic(getSummaryHandler) + .get + .in(path[StorageId]("storageId")) + .out(jsonBody[GetStorageResp]) + .errorOut(oneOf(serverErrorVariant, storageNotFoundVariant)) + .zSecuredServerLogic(getHandler) -private def getSummaryHandler(storageId: StorageId): - ZIO[AuthenticatedUser & GetSummaryEnv, - InternalServerError | StorageNotFound, - StorageSummaryResp] = +private def getHandler(storageId: StorageId): + ZIO[ + AuthenticatedUser & GetEnv, + InternalServerError | StorageNotFound, + GetStorageResp + ] = ZIO.serviceWithZIO[StoragesRepo](_ .getById(storageId) + .orElseFail(InternalServerError()) .someOrFail(StorageNotFound(storageId.toString)) - .map(StorageSummaryResp.fromDb) - .mapError { - case e: StorageNotFound => e - case _ => InternalServerError() - } + .map(GetStorageResp.fromDb) ) diff --git a/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala b/src/test/scala/integration/api/storages/GetStorageTests.scala similarity index 91% rename from src/test/scala/integration/api/storages/GetStorageSummaryTests.scala rename to src/test/scala/integration/api/storages/GetStorageTests.scala index 02b6a7ab..1acd0534 100644 --- a/src/test/scala/integration/api/storages/GetStorageSummaryTests.scala +++ b/src/test/scala/integration/api/storages/GetStorageTests.scala @@ -1,6 +1,6 @@ package integration.api.storages -import api.storages.StorageSummaryResp +import api.storages.GetStorageResp import db.repositories.{StorageMembersRepo, StoragesRepo} import domain.StorageId import integration.common.Utils.* @@ -13,7 +13,7 @@ import zio.http.{Client, Status, URL, Path} import zio.test.{Gen, TestEnvironment, assertTrue, Spec} import zio.{Scope, ZIO, ZLayer} -object GetStorageSummaryTests extends ZIOIntegrationTestSpec: +object GetStorageTests extends ZIOIntegrationTestSpec: private def endpointPath(storageId: StorageId): URL = URL(Path.root / "storages" / storageId.toString) @@ -50,9 +50,9 @@ object GetStorageSummaryTests extends ZIOIntegrationTestSpec: ) bodyStr <- resp.body.asString - storage <- ZIO.fromEither(decode[StorageSummaryResp](bodyStr)) + storage <- ZIO.fromEither(decode[GetStorageResp](bodyStr)) yield assertTrue(resp.status == Status.Ok) - && assertTrue(storage.id == storageId) + && assertTrue(storage.ownerId == user.userId) && assertTrue(storage.name == storageName) }, test("When authorized and user is a member of the storage should get 200 and the storage") { @@ -72,9 +72,9 @@ object GetStorageSummaryTests extends ZIOIntegrationTestSpec: ) bodyStr <- resp.body.asString - storage <- ZIO.fromEither(decode[StorageSummaryResp](bodyStr)) + storage <- ZIO.fromEither(decode[GetStorageResp](bodyStr)) yield assertTrue(resp.status == Status.Ok) - && assertTrue(storage.id == storageId) + && assertTrue(storage.ownerId == creator.userId) && assertTrue(storage.name == storageName) }, test("When authorized but user is neither the owner nor a member should get 404") { From d853b2bc396faf8c544d8c349041713bb93ec402 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Tue, 22 Jul 2025 01:57:52 +0300 Subject: [PATCH 86/95] refactor: add name & request type --- .../api/recipes/publicationRequests/GetAll.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/recipes/publicationRequests/GetAll.scala b/src/main/scala/api/recipes/publicationRequests/GetAll.scala index 5c4f8004..300ce443 100644 --- a/src/main/scala/api/recipes/publicationRequests/GetAll.scala +++ b/src/main/scala/api/recipes/publicationRequests/GetAll.scala @@ -6,7 +6,6 @@ import api.PublicationRequestStatusResp import api.moderation.ModerationHistoryResponse import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} import domain.{InternalServerError, RecipeId, RecipeNotFound} - import io.circe.Decoder import io.circe.derivation.Configuration import io.circe.generic.auto.* @@ -15,12 +14,20 @@ import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO +import java.time.OffsetDateTime + +final case class RecipeModerationHistoryResponse( + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: PublicationRequestStatusResp, + reason: Option[String] + ) private type GetAllEnv = RecipePublicationRequestsRepo & RecipesRepo val getAll: ZServerEndpoint[GetAllEnv, Any] = recipesPublicationRequestsEndpoint .get - .out(jsonBody[List[ModerationHistoryResponse]]) + .out(jsonBody[List[RecipeModerationHistoryResponse]]) .errorOut(oneOf(serverErrorVariant, recipeNotFoundVariant)) .zSecuredServerLogic(getAllHandler) @@ -28,7 +35,7 @@ private def getAllHandler(recipeId: RecipeId): ZIO[ AuthenticatedUser & GetAllEnv, InternalServerError | RecipeNotFound, - List[ModerationHistoryResponse] + List[RecipeModerationHistoryResponse] ] = for isUserOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) @@ -39,7 +46,7 @@ private def getAllHandler(recipeId: RecipeId): .orElseFail(InternalServerError()) res = dbRequests .map( - dbReq => ModerationHistoryResponse( + dbReq => RecipeModerationHistoryResponse( dbReq.createdAt, dbReq.updatedAt, PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), dbReq.reason From 78b011e27ca815afa0f3e2976d1c1e2bf9d4040b Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Tue, 22 Jul 2025 01:58:15 +0300 Subject: [PATCH 87/95] feat: make query return request & db entity --- .../api/GetPublicationRequestsHistory.scala | 42 ++++++++++--------- .../IngredientPublicationRequestRepo.scala | 13 +++--- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/scala/api/GetPublicationRequestsHistory.scala b/src/main/scala/api/GetPublicationRequestsHistory.scala index e2de12e3..768cdb2f 100644 --- a/src/main/scala/api/GetPublicationRequestsHistory.scala +++ b/src/main/scala/api/GetPublicationRequestsHistory.scala @@ -4,9 +4,10 @@ import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.serverErrorVariant import api.common.search.PaginationParams import api.moderation.ModerationHistoryResponse -import db.repositories.{RecipePublicationRequestsRepo, IngredientPublicationRequestsRepo} +import db.repositories.{IngredientPublicationRequestsRepo, RecipePublicationRequestsRepo} +import db.tables.{DbIngredient, DbRecipe} +import db.tables.publication.{DbIngredientPublicationRequest, DbRecipePublicationRequest} import domain.InternalServerError - import io.circe.Decoder import io.circe.derivation.Configuration import io.circe.generic.auto.* @@ -38,27 +39,30 @@ def getPublicationRequestsHistoryHandler(paginationParams: PaginationParams): .getAllCreatedBy .orElseFail(InternalServerError()) ) + recipeRequests = dbRecipeRequests.map { + case (dbRecipeReq: DbRecipePublicationRequest, recipe: DbRecipe) => ModerationHistoryResponse( + recipe.name, + "recipe", + dbRecipeReq.createdAt, + dbRecipeReq.updatedAt, + PublicationRequestStatusResp.fromDomain(dbRecipeReq.status.toDomain(dbRecipeReq.reason)), + dbRecipeReq.reason + ) + } + dbIngredientRequests <- ZIO.serviceWithZIO[IngredientPublicationRequestsRepo](_ .getAllCreatedBy .orElseFail(InternalServerError()) ) - recipeRequests = dbRecipeRequests - .map( - dbReq => ModerationHistoryResponse( - dbReq.createdAt, - dbReq.updatedAt, - PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), - dbReq.reason + ingredientRequests = dbIngredientRequests.map { + case (dbRecipeReq: DbIngredientPublicationRequest, ingredient: DbIngredient) => ModerationHistoryResponse( + ingredient.name, + "ingredient", + dbRecipeReq.createdAt, + dbRecipeReq.updatedAt, + PublicationRequestStatusResp.fromDomain(dbRecipeReq.status.toDomain(dbRecipeReq.reason)), + dbRecipeReq.reason ) - ) - ingredientRequests = dbIngredientRequests - .map( - dbReq => ModerationHistoryResponse( - dbReq.createdAt, - dbReq.updatedAt, - PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), - dbReq.reason - ) - ) + } yield (recipeRequests ++ ingredientRequests) .sortBy(_.updatedAt) diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 6e97298d..73619d2a 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -1,6 +1,8 @@ package db.repositories import api.Authentication.AuthenticatedUser +import api.PublicationRequestStatusResp +import api.moderation.ModerationHistoryResponse import db.DbError import db.tables.DbIngredient import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} @@ -17,7 +19,7 @@ trait IngredientPublicationRequestsRepo: def get(id: PublicationRequestId): IO[DbError, Option[DbIngredientPublicationRequest]] def getWithIngredient(id: PublicationRequestId): IO[DbError, Option[(DbIngredientPublicationRequest, DbIngredient)]] def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] - def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbIngredientPublicationRequest]] + def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[(DbIngredientPublicationRequest, DbIngredient)]] def getAllRequestsForIngredient(id: IngredientId): IO[DbError, Seq[DbIngredientPublicationRequest]] final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) @@ -56,7 +58,7 @@ final case class IngredientPublicationRequestsRepoLive(dataSource: DataSource) .on(_.ingredientId == _.id) ).map(_.headOption).provideDS - override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbIngredientPublicationRequest]] = + override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[(DbIngredientPublicationRequest, DbIngredient)]] = for userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) res <- run(getAllCreatedByQ(lift(userId))).provideDS @@ -106,12 +108,11 @@ object IngredientPublicationRequestsQueries: _.reason -> reason, ) - inline def getAllCreatedByQ(inline userId: UserId): Query[DbIngredientPublicationRequest] = + inline def getAllCreatedByQ(inline userId: UserId): Query[(DbIngredientPublicationRequest, DbIngredient)] = query[DbIngredientPublicationRequest] - .join(query[DbIngredientPublicationRequest]) + .join(query[DbIngredient]) .on(_.ingredientId == _.id) - .map(_._1) - + .filter(_._2.ownerId.contains(userId)) object IngredientPublicationRequestsRepo: def layer: RLayer[DataSource, IngredientPublicationRequestsRepo] = From 705d2afbcebfa47c5201c6247de046b53639dcd1 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Tue, 22 Jul 2025 01:58:45 +0300 Subject: [PATCH 88/95] idc --- .../scala/api/moderation/ModerationHistoryResponse.scala | 2 ++ .../db/repositories/RecipePublicationRequestsRepo.scala | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/moderation/ModerationHistoryResponse.scala b/src/main/scala/api/moderation/ModerationHistoryResponse.scala index 80feef90..772a7457 100644 --- a/src/main/scala/api/moderation/ModerationHistoryResponse.scala +++ b/src/main/scala/api/moderation/ModerationHistoryResponse.scala @@ -5,6 +5,8 @@ import api.PublicationRequestStatusResp import java.time.OffsetDateTime final case class ModerationHistoryResponse( + name: String, + requestType: String, createdAt: OffsetDateTime, updatedAt: OffsetDateTime, status: PublicationRequestStatusResp, diff --git a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala index d3ed67df..8ce0cacd 100644 --- a/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala +++ b/src/main/scala/db/repositories/RecipePublicationRequestsRepo.scala @@ -20,7 +20,7 @@ trait RecipePublicationRequestsRepo: def updateStatus(id: PublicationRequestId, status: PublicationRequestStatus): IO[DbError, Boolean] def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] - def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbRecipePublicationRequest]] + def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[(DbRecipePublicationRequest, DbRecipe)]] final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) extends RecipePublicationRequestsRepo: @@ -65,7 +65,7 @@ final case class RecipePublicationRequestsRepoLive(dataSource: DataSource) override def getAllByRecipeId(recipeId: RecipeId): IO[DbError, List[DbRecipePublicationRequest]] = run(getAllByRecipeIdQ(lift(recipeId))).provideDS - override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[DbRecipePublicationRequest]] = + override def getAllCreatedBy: ZIO[AuthenticatedUser, DbError, List[(DbRecipePublicationRequest, DbRecipe)]] = for userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) res <- run(getAllCreatedByQ(lift(userId))).provideDS @@ -113,12 +113,11 @@ object RecipePublicationRequestsQueries: inline def getAllByRecipeIdQ(inline recipeId: RecipeId): EntityQuery[DbRecipePublicationRequest] = requestsQ.filter(_.recipeId == recipeId) - inline def getAllCreatedByQ(inline userId: UserId): Query[DbRecipePublicationRequest] = + inline def getAllCreatedByQ(inline userId: UserId): Query[(DbRecipePublicationRequest, DbRecipe)] = requestsQ .join(query[DbRecipe]) .on(_.recipeId == _.id) .filter(_._2.creatorId.contains(userId)) - .map(_._1) object RecipePublicationRequestsRepo: def layer: RLayer[DataSource, RecipePublicationRequestsRepo] = From 2dbd9bce4d2814f14193e2d0da6df6fc937a75b7 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Tue, 22 Jul 2025 17:05:48 +0300 Subject: [PATCH 89/95] refactor: use domain enum instead of custom enum --- src/main/scala/api/moderation/pubrequests/Update.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/api/moderation/pubrequests/Update.scala b/src/main/scala/api/moderation/pubrequests/Update.scala index 7331c65e..711e25f5 100644 --- a/src/main/scala/api/moderation/pubrequests/Update.scala +++ b/src/main/scala/api/moderation/pubrequests/Update.scala @@ -2,6 +2,7 @@ package api.moderation.pubrequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{publicationRequestNotFound, serverErrorVariant} +import api.PublicationRequestStatusResp import api.moderation.pubrequests.PublicationRequestTypeResp.* import db.repositories.{IngredientPublicationRequestsRepo, IngredientsRepo, RecipePublicationRequestsRepo, RecipesRepo} import domain.{InternalServerError, PublicationRequestId, PublicationRequestNotFound, PublicationRequestStatus} @@ -21,13 +22,12 @@ enum PublicationRequestStatusReq: case Rejected final case class UpdatePublicationRequestReqBody( - status: PublicationRequestStatusReq, - reason: Option[String], + status: PublicationRequestStatusResp, ): def getDomainStatus: PublicationRequestStatus = status match - case PublicationRequestStatusReq.Pending => PublicationRequestStatus.Pending - case PublicationRequestStatusReq.Accepted => PublicationRequestStatus.Accepted - case PublicationRequestStatusReq.Rejected => PublicationRequestStatus.Rejected(reason) + case PublicationRequestStatusResp.Pending => PublicationRequestStatus.Pending + case PublicationRequestStatusResp.Accepted => PublicationRequestStatus.Accepted + case PublicationRequestStatusResp.Rejected(reason) => PublicationRequestStatus.Rejected(reason) private type UpdateReqEnv = RecipePublicationRequestsRepo From 7992e03d802c0e936fc4e3780dc0684f7c2ea2f8 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Tue, 22 Jul 2025 17:06:03 +0300 Subject: [PATCH 90/95] feat: encoder & decoder --- .../api/PublicationRequestStatusResp.scala | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/scala/api/PublicationRequestStatusResp.scala b/src/main/scala/api/PublicationRequestStatusResp.scala index 42a2b6ec..200ac566 100644 --- a/src/main/scala/api/PublicationRequestStatusResp.scala +++ b/src/main/scala/api/PublicationRequestStatusResp.scala @@ -1,7 +1,7 @@ package api import domain.PublicationRequestStatus -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, HCursor, Json} enum PublicationRequestStatusResp: case Pending @@ -16,14 +16,20 @@ object PublicationRequestStatusResp: case PublicationRequestStatus.Rejected(reason) => Rejected(reason) given Encoder[PublicationRequestStatusResp] = Encoder.instance { - case Pending => Json.fromString("pending") - case Accepted => Json.fromString("accepted") - case Rejected(reason) => Json.fromString("rejected") + case Pending => Json.obj("type" -> Json.fromString("pending")) + case Accepted => Json.obj("type" -> Json.fromString("accepted")) + case Rejected(reason) => + val baseObj = Json.obj("type" -> Json.fromString("rejected")) + reason match + case Some(r) => baseObj.deepMerge(Json.obj("reason" -> Json.fromString(r))) + case None => baseObj } - given Decoder[PublicationRequestStatusResp] = Decoder.decodeString.emap { - case "pending" => Right(Pending) - case "accepted" => Right(Accepted) - case "rejected" => Right(Rejected(None)) - case other => Left(s"Unknown status: $other") - } \ No newline at end of file + given Decoder[PublicationRequestStatusResp] = (c: HCursor) => + c.downField("type").as[String].flatMap { + case "pending" => Right(Pending) + case "accepted" => Right(Accepted) + case "rejected" => + c.downField("reason").as[Option[String]].map(Rejected.apply) + case other => Left(DecodingFailure(s"Unknown status type: $other", c.history)) + } \ No newline at end of file From 993e98601cbc918c8dac9a8773f92bff34b5cd16 Mon Sep 17 00:00:00 2001 From: danielambda Date: Tue, 22 Jul 2025 20:45:32 +0300 Subject: [PATCH 91/95] test: empty recipe and recipe with published custom ingredients request publication --- .../RequestRecipePublicationTests.scala | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala index 066bcdfc..e1f7d7a6 100644 --- a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -12,6 +12,7 @@ import io.circe.parser.decode import zio.* import zio.http.* import zio.test.* +import db.repositories.IngredientsRepo object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: private def endpointPath(recipeId: RecipeId): URL = @@ -58,6 +59,42 @@ object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: yield assertTrue(resp.status == Status.Created) && assertTrue(request.is(_.some).recipeId == recipeId) && assertTrue(request.is(_.some).status == DbPublicationRequestStatus.Pending) - } + }, + test("When requesting publication of recipe with no ingredients, pending request should be created") { + for + user <- registerUser + + recipeId <- createCustomRecipe(user, Vector.empty) + + resp <- requestRecipePublication(user, recipeId) + + bodyStr <- resp.body.asString + requestId <- ZIO.fromOption(bodyStr.toUUID) + request <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(requestId)) + yield assertTrue(resp.status == Status.Created) + && assertTrue(request.is(_.some).recipeId == recipeId) + && assertTrue(request.is(_.some).status == DbPublicationRequestStatus.Pending) + }, + test("""When requesting publication of recipe with published custom ingredients, + pending request should be created""") { + for + user <- registerUser + + n <- Gen.int(2, 8).runHead.some + ingredientIds <- ZIO.foreach(1 to n)(_ => for + ingredientId <- createCustomIngredient(user) + _ <- ZIO.serviceWithZIO[IngredientsRepo](_.publish(ingredientId)) + yield ingredientId) + recipeId <- createCustomRecipe(user, ingredientIds.toVector) + + resp <- requestRecipePublication(user, recipeId) + + bodyStr <- resp.body.asString + requestId <- ZIO.fromOption(bodyStr.toUUID) + request <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.get(requestId)) + yield assertTrue(resp.status == Status.Created) + && assertTrue(request.is(_.some).recipeId == recipeId) + && assertTrue(request.is(_.some).status == DbPublicationRequestStatus.Pending) + }, ).provideLayer(testLayer) From e8118547c8772a5bb6c45f9bc442188948e365a5 Mon Sep 17 00:00:00 2001 From: danielambda Date: Tue, 22 Jul 2025 23:56:49 +0300 Subject: [PATCH 92/95] fix: restrict usage of private ingredients when requesting recipe publication (instead of custom) --- .../recipes/publicationRequests/Create.scala | 20 +++++++++---------- .../db/repositories/IngredientsRepo.scala | 5 ++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/scala/api/recipes/publicationRequests/Create.scala b/src/main/scala/api/recipes/publicationRequests/Create.scala index b6defd86..3b445259 100644 --- a/src/main/scala/api/recipes/publicationRequests/Create.scala +++ b/src/main/scala/api/recipes/publicationRequests/Create.scala @@ -16,12 +16,12 @@ import sttp.tapir.generic.auto.* import sttp.tapir.ztapir.* import zio.ZIO -final case class CannotPublishRecipeWithCustomIngredients( +final case class CannotPublishRecipeWithPrivateIngredients( ingredients: Seq[IngredientId], - message: String = "Cannot publish recipe with custom ingredients", + message: String = "Cannot publish recipe with private ingredients", ) -object CannotPublishRecipeWithCustomIngredients: - val variant = BadRequest.variantJson[CannotPublishRecipeWithCustomIngredients] +object CannotPublishRecipeWithPrivateIngredients: + val variant = BadRequest.variantJson[CannotPublishRecipeWithPrivateIngredients] final case class RecipeAlreadyPublished( recipeId: RecipeId, @@ -50,7 +50,7 @@ private val create: ZServerEndpoint[CreateEnv, Any] = .errorOut(oneOf( serverErrorVariant, recipeNotFoundVariant, - CannotPublishRecipeWithCustomIngredients.variant, + CannotPublishRecipeWithPrivateIngredients.variant, RecipeAlreadyPending.variant, RecipeAlreadyPublished.variant, )) @@ -60,7 +60,7 @@ private def createHandler(recipeId: RecipeId): ZIO[ AuthenticatedUser & CreateEnv, InternalServerError | RecipeAlreadyPublished | RecipeAlreadyPending - | CannotPublishRecipeWithCustomIngredients | RecipeNotFound, + | CannotPublishRecipeWithPrivateIngredients | RecipeNotFound, PublicationRequestId ] = for @@ -81,15 +81,15 @@ private def createHandler(recipeId: RecipeId): .when(alreadyPending) userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) - customIngredientIdsInRecipe <- run( - IngredientsQueries.customIngredientsQ(lift(userId)) + privateIngredientIdsInRecipe <- run( + IngredientsQueries.privateIngredientsQ(lift(userId)) .filter(i => liftQuery(recipe.ingredients).contains(i.id)) .map(_.id) ).provideDS(using dataSource) .orElseFail(InternalServerError()) - _ <- ZIO.fail(CannotPublishRecipeWithCustomIngredients(customIngredientIdsInRecipe)) - .when(customIngredientIdsInRecipe.nonEmpty) + _ <- ZIO.fail(CannotPublishRecipeWithPrivateIngredients(privateIngredientIdsInRecipe)) + .when(privateIngredientIdsInRecipe.nonEmpty) reqId <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_ .createPublicationRequest(recipeId) diff --git a/src/main/scala/db/repositories/IngredientsRepo.scala b/src/main/scala/db/repositories/IngredientsRepo.scala index 95265b3e..de8e3634 100644 --- a/src/main/scala/db/repositories/IngredientsRepo.scala +++ b/src/main/scala/db/repositories/IngredientsRepo.scala @@ -98,7 +98,10 @@ object IngredientsQueries: ingredientsQ.filter(_.isPublished) inline def customIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = - ingredientsQ.filter(i => i.ownerId.contains(userId)) + ingredientsQ.filter(_.ownerId.contains(userId)) + + inline def privateIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = + customIngredientsQ(userId).filter(!_.isPublished) inline def visibleIngredientsQ(inline userId: UserId): EntityQuery[DbIngredient] = ingredientsQ.filter(i => i.isPublished || i.ownerId.contains(userId)) From 39d9e15fe2dae9194278423750abaae67bbe64d3 Mon Sep 17 00:00:00 2001 From: danielambda Date: Wed, 23 Jul 2025 00:29:47 +0300 Subject: [PATCH 93/95] test: When requesting publication of recipe with private ingredients, should get 400 Cannot publish recipe with private ingredients --- .../recipes/publicationRequests/GetAll.scala | 22 +++++++------- src/main/scala/api/storages/GetAll.scala | 2 +- .../IngredientPublicationRequestRepo.scala | 4 +-- .../RequestRecipePublicationTests.scala | 29 +++++++++++++++++++ 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/main/scala/api/recipes/publicationRequests/GetAll.scala b/src/main/scala/api/recipes/publicationRequests/GetAll.scala index 300ce443..494d763a 100644 --- a/src/main/scala/api/recipes/publicationRequests/GetAll.scala +++ b/src/main/scala/api/recipes/publicationRequests/GetAll.scala @@ -3,25 +3,24 @@ package api.recipes.publicationRequests import api.Authentication.{AuthenticatedUser, zSecuredServerLogic} import api.EndpointErrorVariants.{recipeNotFoundVariant, serverErrorVariant} import api.PublicationRequestStatusResp -import api.moderation.ModerationHistoryResponse import db.repositories.{RecipePublicationRequestsRepo, RecipesRepo} import domain.{InternalServerError, RecipeId, RecipeNotFound} + import io.circe.Decoder import io.circe.derivation.Configuration import io.circe.generic.auto.* +import java.time.OffsetDateTime import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import sttp.tapir.ztapir.* import zio.ZIO -import java.time.OffsetDateTime - final case class RecipeModerationHistoryResponse( - createdAt: OffsetDateTime, - updatedAt: OffsetDateTime, - status: PublicationRequestStatusResp, - reason: Option[String] - ) + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime, + status: PublicationRequestStatusResp, +) + private type GetAllEnv = RecipePublicationRequestsRepo & RecipesRepo val getAll: ZServerEndpoint[GetAllEnv, Any] = @@ -38,9 +37,10 @@ private def getAllHandler(recipeId: RecipeId): List[RecipeModerationHistoryResponse] ] = for - isUserOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) + userIsOwner <- ZIO.serviceWithZIO[RecipesRepo](_.isUserOwner(recipeId)) .orElseFail(InternalServerError()) - _ <- ZIO.fail(RecipeNotFound(recipeId)).unless(isUserOwner) + _ <- ZIO.fail(RecipeNotFound(recipeId)) + .unless(userIsOwner) dbRequests <- ZIO.serviceWithZIO[RecipePublicationRequestsRepo](_.getAllByRecipeId(recipeId)) .orElseFail(InternalServerError()) @@ -49,9 +49,7 @@ private def getAllHandler(recipeId: RecipeId): dbReq => RecipeModerationHistoryResponse( dbReq.createdAt, dbReq.updatedAt, PublicationRequestStatusResp.fromDomain(dbReq.status.toDomain(dbReq.reason)), - dbReq.reason ) ) .sortBy(_.updatedAt) - yield res diff --git a/src/main/scala/api/storages/GetAll.scala b/src/main/scala/api/storages/GetAll.scala index d1d38eff..7b20694f 100644 --- a/src/main/scala/api/storages/GetAll.scala +++ b/src/main/scala/api/storages/GetAll.scala @@ -3,7 +3,7 @@ package api.storages import api.Authentication.{zSecuredServerLogic, AuthenticatedUser} import api.EndpointErrorVariants.serverErrorVariant import db.repositories.StoragesRepo -import domain.{UserId, StorageId, InternalServerError} +import domain.{StorageId, InternalServerError} import io.circe.generic.auto.* import sttp.tapir.generic.auto.* diff --git a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala index 73619d2a..509654aa 100644 --- a/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala +++ b/src/main/scala/db/repositories/IngredientPublicationRequestRepo.scala @@ -1,14 +1,12 @@ package db.repositories import api.Authentication.AuthenticatedUser -import api.PublicationRequestStatusResp -import api.moderation.ModerationHistoryResponse import db.DbError import db.tables.DbIngredient import db.tables.publication.{DbIngredientPublicationRequest, DbPublicationRequestStatus} import domain.{IngredientId, PublicationRequestId, PublicationRequestStatus, UserId} -import io.getquill.* +import io.getquill.* import javax.sql.DataSource import java.util.UUID import zio.{IO, RLayer, ZIO, ZLayer} diff --git a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala index e1f7d7a6..50bc3e33 100644 --- a/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala +++ b/src/test/scala/integration/api/recipes/RequestRecipePublicationTests.scala @@ -13,6 +13,7 @@ import zio.* import zio.http.* import zio.test.* import db.repositories.IngredientsRepo +import api.recipes.publicationRequests.CannotPublishRecipeWithPrivateIngredients object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: private def endpointPath(recipeId: RecipeId): URL = @@ -96,5 +97,33 @@ object RequestRecipePublicationTests extends ZIOIntegrationTestSpec: && assertTrue(request.is(_.some).recipeId == recipeId) && assertTrue(request.is(_.some).status == DbPublicationRequestStatus.Pending) }, + test("""When requesting publication of recipe with private ingredients, + should get 400 Cannot publish recipe with private ingredients""") { + for + user <- registerUser + + n <- Gen.int(2, 8).runHead.some + publishedCustomIngredients <- ZIO.foreach(1 to n)(_ => for + ingredientId <- createCustomIngredient(user) + _ <- ZIO.serviceWithZIO[IngredientsRepo](_.publish(ingredientId)) + yield ingredientId) + m <- Gen.int(2, 8).runHead.some + publiсIngredients <- createNPublicIngredients(m) + + k <- Gen.int(2, 8).runHead.some + privateIngredients <- ZIO.foreach(1 to n)(_ => createCustomIngredient(user)) + + ingredientIds = publishedCustomIngredients ++ publiсIngredients ++ privateIngredients + + recipeId <- createCustomRecipe(user, ingredientIds.toVector) + + resp <- requestRecipePublication(user, recipeId) + + bodyStr <- resp.body.asString + error = decode[CannotPublishRecipeWithPrivateIngredients](bodyStr) + yield assertTrue(resp.status == Status.BadRequest) + && assertTrue(error.isRight) + && assertTrue(error.forall(_.ingredients hasSameElementsAs privateIngredients)) + }, ).provideLayer(testLayer) From 9e6725e42bcf6a0ce424e4f513fae626a0bb50c7 Mon Sep 17 00:00:00 2001 From: danielambda Date: Wed, 23 Jul 2025 15:02:13 +0300 Subject: [PATCH 94/95] fix: check recipe publicity in /suggested-recipes --- .../scala/db/repositories/RecipesDomainRepo.scala | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/scala/db/repositories/RecipesDomainRepo.scala b/src/main/scala/db/repositories/RecipesDomainRepo.scala index f8978cdb..2ecaf8f1 100644 --- a/src/main/scala/db/repositories/RecipesDomainRepo.scala +++ b/src/main/scala/db/repositories/RecipesDomainRepo.scala @@ -7,22 +7,24 @@ import domain.{RecipeId, StorageId} import com.augustnagro.magnum.magzio.* import zio.{ZIO, ZLayer} +import api.Authentication.AuthenticatedUser trait RecipesDomainRepo: protected type RecipeSummary = (RecipeId, String, Int, Int, Int) def getSuggestedIngredients( paginationParams: PaginationParams, storageIds: Vector[StorageId] - ): ZIO[StorageIngredientsRepo, DbError, Vector[RecipeSummary]] + ): ZIO[AuthenticatedUser & StorageIngredientsRepo, DbError, Vector[RecipeSummary]] private final case class RecipesDomainRepoLive(xa: Transactor) extends RecipesDomainRepo: override def getSuggestedIngredients( paginationParams: PaginationParams, storageIds: Vector[StorageId] - ): ZIO[StorageIngredientsRepo, DbError, Vector[RecipeSummary]] = + ): ZIO[AuthenticatedUser & StorageIngredientsRepo, DbError, Vector[RecipeSummary]] = val PaginationParams(size, offset) = paginationParams val table = recipeIngredientsTable for + userId <- ZIO.serviceWith[AuthenticatedUser](_.userId) allIngredients <- ZIO.collectAll(storageIds.map { storageId => ZIO.serviceWithZIO[StorageIngredientsRepo](_.getAllIngredientsFromStorage(storageId)) }).map(_.flatten) @@ -34,6 +36,8 @@ private final case class RecipesDomainRepoLive(xa: Transactor) extends RecipesDo ${table.recipeId}, r.${recipesTable.name} AS recipe_name, COUNT(*) AS total_ingredients, + r.${recipesTable.isPublished}, + r.${recipesTable.creatorId}, SUM( CASE WHEN ${table.ingredientId} = ANY(${allIngredients.toArray}) THEN 1 @@ -42,7 +46,11 @@ private final case class RecipesDomainRepoLive(xa: Transactor) extends RecipesDo ) AS available_ingredients FROM $table ri JOIN ${recipesTable} r ON r.${recipesTable.id} = ri.${table.recipeId} - GROUP BY ${table.recipeId}, recipe_name + GROUP BY + ${table.recipeId}, + recipe_name, + ${recipesTable.isPublished}, + ${recipesTable.creatorId} ) SELECT ${table.recipeId}, @@ -52,6 +60,7 @@ private final case class RecipesDomainRepoLive(xa: Transactor) extends RecipesDo COUNT(*) OVER() AS recipes_found FROM recipe_stats WHERE total_ingredients > 0 -- Avoid division by zero + AND (${recipesTable.isPublished} OR ${recipesTable.creatorId} = $userId) ORDER BY (available_ingredients::float / total_ingredients) DESC, total_ingredients DESC From e59721a753ebdcb8d2884ce96bf23d349d44d245 Mon Sep 17 00:00:00 2001 From: danielambda Date: Wed, 23 Jul 2025 15:24:24 +0300 Subject: [PATCH 95/95] fix: use MD5 for shorter invitation hashs --- src/main/scala/api/invitations/Create.scala | 11 ++++++++--- src/main/scala/db/repositories/InvitationsRepo.scala | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/scala/api/invitations/Create.scala b/src/main/scala/api/invitations/Create.scala index fe1bfe85..933dcd3a 100644 --- a/src/main/scala/api/invitations/Create.scala +++ b/src/main/scala/api/invitations/Create.scala @@ -18,11 +18,16 @@ val create: ZServerEndpoint[CreateEnv, Any] = invitationEndpoint def createHandler(storageId: StorageId): ZIO[AuthenticatedUser & CreateEnv, InternalServerError | StorageNotFound, String] = for - storageExists <- ZIO.serviceWithZIO[StoragesRepo](_.getById(storageId)) + storageExists <- ZIO.serviceWithZIO[StoragesRepo](_ + .getById(storageId) .map(_.isDefined) .orElseFail(InternalServerError()) - _ <- ZIO.fail(StorageNotFound(storageId.toString)).unless(storageExists) + ) + _ <- ZIO.fail(StorageNotFound(storageId.toString)) + .unless(storageExists) - inviteHash <- ZIO.serviceWithZIO[InvitationsRepo](_.create(storageId)) + inviteHash <- ZIO.serviceWithZIO[InvitationsRepo](_ + .create(storageId) .orElseFail(InternalServerError()) + ) yield inviteHash diff --git a/src/main/scala/db/repositories/InvitationsRepo.scala b/src/main/scala/db/repositories/InvitationsRepo.scala index 2d0bd923..6e761714 100644 --- a/src/main/scala/db/repositories/InvitationsRepo.scala +++ b/src/main/scala/db/repositories/InvitationsRepo.scala @@ -32,7 +32,7 @@ private final case class InvitationsRepoLive(xa: Transactor, dataSource: DataSou for currentTime <- Clock.currentTime(TimeUnit.NANOSECONDS) input = s"$currentTime:$storageId:${secretKey.value}" - digest = MessageDigest.getInstance("SHA-256") + digest = MessageDigest.getInstance("MD5") hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)) invitationHash = hashBytes.map("%02x".format(_)).mkString _ <- xa.transact {