From 8c0366dab3219a441dc6420eafde6847f44dd2c5 Mon Sep 17 00:00:00 2001 From: Daniel Bell Date: Fri, 13 Oct 2023 09:42:24 +0100 Subject: [PATCH 1/2] Migrate Organizations to Cats Effect (#4360) --- .../delta/routes/OrganizationsRoutes.scala | 46 ++++++++++----- .../delta/routes/PermissionsRoutes.scala | 24 +++++--- .../nexus/delta/wiring/EventsModule.scala | 5 +- .../delta/wiring/OrganizationsModule.scala | 16 ++---- .../nexus/delta/wiring/ProjectsModule.scala | 12 +++- .../routes/OrganizationsRoutesSpec.scala | 3 +- .../sdk/organizations/Organizations.scala | 56 +++++++++---------- .../sdk/organizations/OrganizationsImpl.scala | 47 ++++++++-------- .../model/OrganizationRejection.scala | 3 +- .../delta/sdk/projects/FetchContext.scala | 8 ++- .../nexus/delta/sdk/projects/Projects.scala | 6 +- .../OrganizationDeleterSuite.scala | 33 +++++------ .../organizations/OrganizationsImplSpec.scala | 2 +- .../sdk/organizations/OrganizationsSpec.scala | 9 ++- 14 files changed, 156 insertions(+), 114 deletions(-) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala index 000e2860ed..7c15e3a810 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala @@ -3,6 +3,7 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{Directive1, Route} +import cats.effect.IO import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder @@ -10,14 +11,13 @@ import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.routes.OrganizationsRoutes.OrganizationInput import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck -import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives.{emit => emitCE} +import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives} import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF} import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.OrganizationSearchParams import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._ import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults} @@ -29,7 +29,6 @@ import io.circe.Decoder import io.circe.generic.extras.Configuration import io.circe.generic.extras.semiauto.deriveConfiguredDecoder import kamon.instrumentation.akka.http.TracingDirectives.operationName -import monix.execution.Scheduler import scala.annotation.nowarn @@ -54,7 +53,6 @@ final class OrganizationsRoutes( )(implicit baseUri: BaseUri, paginationConfig: PaginationConfig, - s: Scheduler, cr: RemoteContextResolution, ordering: JsonKeyOrdering ) extends AuthDirectives(identities, aclCheck) @@ -76,6 +74,14 @@ final class OrganizationsRoutes( ) } + private def emitMetadata(value: IO[OrganizationResource]) = { + emit( + value + .mapValue(_.metadata) + .attemptNarrow[OrganizationRejection] + ) + } + def routes: Route = baseUriPrefix(baseUri.prefix) { pathPrefix("orgs") { @@ -88,7 +94,12 @@ final class OrganizationsRoutes( implicit val searchJsonLdEncoder: JsonLdEncoder[SearchResults[OrganizationResource]] = searchResultsJsonLdEncoder(Organization.context, pagination, uri) - emit(organizations.list(pagination, params, order).widen[SearchResults[OrganizationResource]]) + emit( + organizations + .list(pagination, params, order) + .widen[SearchResults[OrganizationResource]] + .attemptNarrow[OrganizationRejection] + ) } }, (resolveOrg & pathEndOrSingleSlash) { id => @@ -99,7 +110,10 @@ final class OrganizationsRoutes( authorizeFor(id, orgs.write).apply { // Update organization entity(as[OrganizationInput]) { case OrganizationInput(description) => - emit(organizations.update(id, description, rev).mapValue(_.metadata)) + emitMetadata( + organizations + .update(id, description, rev) + ) } } } @@ -108,9 +122,9 @@ final class OrganizationsRoutes( authorizeFor(id, orgs.read).apply { parameter("rev".as[Int].?) { case Some(rev) => // Fetch organization at specific revision - emit(organizations.fetchAt(id, rev).leftWiden[OrganizationRejection]) + emit(organizations.fetchAt(id, rev).attemptNarrow[OrganizationRejection]) case None => // Fetch organization - emit(organizations.fetch(id).leftWiden[OrganizationRejection]) + emit(organizations.fetch(id).attemptNarrow[OrganizationRejection]) } } @@ -120,12 +134,14 @@ final class OrganizationsRoutes( concat( parameter("rev".as[Int]) { rev => authorizeFor(id, orgs.write).apply { - emit(organizations.deprecate(id, rev).mapValue(_.metadata)) + emitMetadata( + organizations.deprecate(id, rev) + ) } }, parameter("prune".requiredValue(true)) { _ => authorizeFor(id, orgs.delete).apply { - emitCE(orgDeleter.delete(id).attemptNarrow[OrganizationRejection]) + emit(orgDeleter.delete(id).attemptNarrow[OrganizationRejection]) } } ) @@ -138,7 +154,12 @@ final class OrganizationsRoutes( (put & authorizeFor(label, orgs.create)) { // Create organization entity(as[OrganizationInput]) { case OrganizationInput(description) => - emit(StatusCodes.Created, organizations.create(label, description).mapValue(_.metadata)) + val response: IO[Either[OrganizationRejection, ResourceF[Organization.Metadata]]] = + organizations.create(label, description).mapValue(_.metadata).attemptNarrow[OrganizationRejection] + emit( + StatusCodes.Created, + response + ) } } } @@ -171,7 +192,6 @@ object OrganizationsRoutes { )(implicit baseUri: BaseUri, paginationConfig: PaginationConfig, - s: Scheduler, cr: RemoteContextResolution, ordering: JsonKeyOrdering ): Route = diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/PermissionsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/PermissionsRoutes.scala index 16ebc2ee80..d71eb70061 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/PermissionsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/PermissionsRoutes.scala @@ -2,6 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{MalformedRequestContentRejection, Route} +import cats.effect.IO import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords @@ -10,6 +11,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.routes.PermissionsRoutes.PatchPermissions._ import ch.epfl.bluebrain.nexus.delta.routes.PermissionsRoutes._ +import ch.epfl.bluebrain.nexus.delta.sdk.PermissionsResource import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ @@ -20,7 +22,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{permissions => permissionsPerms} -import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.{Permission, PermissionsRejection} import io.circe.generic.extras.Configuration import io.circe.generic.extras.semiauto.deriveConfiguredDecoder import io.circe.syntax._ @@ -63,8 +65,8 @@ final class PermissionsRoutes(identities: Identities, permissions: Permissions, get { authorizeFor(AclAddress.Root, permissionsPerms.read).apply { parameter("rev".as[Int].?) { - case Some(rev) => emit(permissions.fetchAt(rev)) - case None => emit(permissions.fetch) + case Some(rev) => emitPermissions(permissions.fetchAt(rev)) + case None => emitPermissions(permissions.fetch) } } }, @@ -72,7 +74,7 @@ final class PermissionsRoutes(identities: Identities, permissions: Permissions, (put & parameter("rev" ? 0)) { rev => authorizeFor(AclAddress.Root, permissionsPerms.write).apply { entity(as[PatchPermissions]) { - case Replace(set) => emit(permissions.replace(set, rev).map(_.void)) + case Replace(set) => emitVoid(permissions.replace(set, rev)) case _ => reject( malformedContent(s"Value for field '${keywords.tpe}' must be 'Replace' when using 'PUT'.") @@ -84,8 +86,8 @@ final class PermissionsRoutes(identities: Identities, permissions: Permissions, (patch & parameter("rev" ? 0)) { rev => authorizeFor(AclAddress.Root, permissionsPerms.write).apply { entity(as[PatchPermissions]) { - case Append(set) => emit(permissions.append(set, rev).map(_.void)) - case Subtract(set) => emit(permissions.subtract(set, rev).map(_.void)) + case Append(set) => emitVoid(permissions.append(set, rev)) + case Subtract(set) => emitVoid(permissions.subtract(set, rev)) case _ => reject( malformedContent( @@ -99,7 +101,7 @@ final class PermissionsRoutes(identities: Identities, permissions: Permissions, delete { authorizeFor(AclAddress.Root, permissionsPerms.write).apply { parameter("rev".as[Int]) { rev => - emit(permissions.delete(rev).map(_.void)) + emitVoid(permissions.delete(rev)) } } } @@ -111,6 +113,14 @@ final class PermissionsRoutes(identities: Identities, permissions: Permissions, } } + private def emitVoid(value: IO[PermissionsResource]) = { + emit(value.map(_.void).attemptNarrow[PermissionsRejection]) + } + + private def emitPermissions(value: IO[PermissionsResource]) = { + emit(value.attemptNarrow[PermissionsRejection]) + } + private def malformedContent(field: String) = MalformedRequestContentRejection(field, new IllegalArgumentException()) } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/EventsModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/EventsModule.scala index 1d49b0ba49..0eec5f9fec 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/EventsModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/EventsModule.scala @@ -2,16 +2,17 @@ package ch.epfl.bluebrain.nexus.delta.wiring import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.routes.{ElemRoutes, EventsRoutes} -import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection import ch.epfl.bluebrain.nexus.delta.sdk.projects.Projects import ch.epfl.bluebrain.nexus.delta.sdk.sse.{SseElemStream, SseEncoder, SseEventLog} import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors @@ -36,7 +37,7 @@ object EventsModule extends ModuleDef { toCatsIO( SseEventLog( sseEncoders, - organizations.fetch(_).void, + organizations.fetch(_).void.toBIO[OrganizationRejection], projects.fetch(_).map { p => (p.value.organizationUuid, p.value.uuid) }, config.sse, xas diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/OrganizationsModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/OrganizationsModule.scala index 611f5eb5d5..7486054330 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/OrganizationsModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/OrganizationsModule.scala @@ -1,6 +1,6 @@ package ch.epfl.bluebrain.nexus.delta.wiring -import cats.effect.Clock +import cats.effect.{Clock, ContextShift, IO} import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -14,13 +14,10 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, MetadataContextValue} import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationEvent -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.{Organizations, OrganizationsImpl} +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.{OrganizationDeleter, Organizations, OrganizationsImpl} import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors import izumi.distage.model.definition.{Id, ModuleDef} -import monix.bio.UIO -import monix.execution.Scheduler -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter /** * Organizations module wiring config. @@ -33,15 +30,16 @@ object OrganizationsModule extends ModuleDef { ( config: AppConfig, scopeInitializations: Set[ScopeInitialization], - clock: Clock[UIO], + clock: Clock[IO], uuidF: UUIDF, - xas: Transactors + xas: Transactors, + contextShift: ContextShift[IO] ) => OrganizationsImpl( scopeInitializations, config.organizations, xas - )(clock, uuidF) + )(clock, uuidF, contextShift) } make[OrganizationDeleter].from { (xas: Transactors) => @@ -56,14 +54,12 @@ object OrganizationsModule extends ModuleDef { cfg: AppConfig, aclCheck: AclCheck, schemeDirectives: DeltaSchemeDirectives, - s: Scheduler, cr: RemoteContextResolution @Id("aggregate"), ordering: JsonKeyOrdering ) => new OrganizationsRoutes(identities, organizations, orgDeleter, aclCheck, schemeDirectives)( cfg.http.baseUri, cfg.organizations.pagination, - s, cr, ordering ) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala index c12698757b..b71a50dc00 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala @@ -1,8 +1,10 @@ package ch.epfl.bluebrain.nexus.delta.wiring import cats.effect.{Clock, IO} +import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.toMonixBIOOps import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} @@ -19,10 +21,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount import ch.epfl.bluebrain.nexus.delta.sdk.model._ import ch.epfl.bluebrain.nexus.delta.sdk.model.metrics.ScopedEventMetricEncoder import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext.ContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.projects._ import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.WrappedOrganizationRejection -import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, Project, ProjectEvent} +import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, Project, ProjectEvent, ProjectRejection} import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.ProjectProvisioning import ch.epfl.bluebrain.nexus.delta.sdk.quotas.Quotas import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder @@ -61,7 +64,12 @@ object ProjectsModule extends ModuleDef { ) => IO.pure( ProjectsImpl( - organizations.fetchActiveOrganization(_).mapError(WrappedOrganizationRejection), + organizations + .fetchActiveOrganization(_) + .adaptError { case e: OrganizationRejection => + WrappedOrganizationRejection(e) + } + .toBIO[ProjectRejection], ValidateProjectDeletion(xas, config.projects.deletion.enabled), scopeInitializations, mappings.merge, diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala index 339dfca586..c55bec576f 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala @@ -29,10 +29,11 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import io.circe.Json import cats.effect.IO +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsIOValues, IOFixedClock} import java.util.UUID -class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap { +class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap with IOFixedClock with CatsIOValues { private val fixedUuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(fixedUuid) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/Organizations.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/Organizations.scala index f659111b0a..021b24d549 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/Organizations.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/Organizations.scala @@ -1,24 +1,23 @@ package ch.epfl.bluebrain.nexus.delta.sdk.organizations -import cats.effect.Clock -import ch.epfl.bluebrain.nexus.delta.kernel.Mapper -import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOUtils.instant -import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF +import cats.effect.{Clock, IO} +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ +import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination +import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOInstant.now +import ch.epfl.bluebrain.nexus.delta.kernel.utils.{IOInstant, UUIDF} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceUris -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.OrganizationSearchParams import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.UnscoredSearchResults import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationEvent._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model._ +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, Label} import ch.epfl.bluebrain.nexus.delta.sourcing.{GlobalEntityDefinition, StateMachine} -import monix.bio.{IO, UIO} /** * Operations pertaining to managing organizations. @@ -38,7 +37,7 @@ trait Organizations { def create( label: Label, description: Option[String] - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] + )(implicit caller: Subject): IO[OrganizationResource] /** * Updates an existing organization description. @@ -56,7 +55,7 @@ trait Organizations { label: Label, description: Option[String], rev: Int - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] + )(implicit caller: Subject): IO[OrganizationResource] /** * Deprecate an organization. @@ -71,7 +70,7 @@ trait Organizations { def deprecate( label: Label, rev: Int - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] + )(implicit caller: Subject): IO[OrganizationResource] /** * Fetch an organization at the current revision by label. @@ -81,7 +80,7 @@ trait Organizations { * @return * the organization in a Resource representation, None otherwise */ - def fetch(label: Label): IO[OrganizationNotFound, OrganizationResource] + def fetch(label: Label): IO[OrganizationResource] /** * Fetch an organization at the passed revision by label. @@ -93,14 +92,14 @@ trait Organizations { * @return * the organization in a Resource representation, None otherwise */ - def fetchAt(label: Label, rev: Int): IO[OrganizationRejection.NotFound, OrganizationResource] + def fetchAt(label: Label, rev: Int): IO[OrganizationResource] /** * Fetches the current active organization, rejecting if the organization does not exists or if it is deprecated */ def fetchActiveOrganization( label: Label - ): IO[OrganizationRejection, Organization] = + ): IO[Organization] = fetch(label) .flatMap { case resource if resource.deprecated => @@ -108,14 +107,6 @@ trait Organizations { case resource => IO.pure(resource.value) } - /** - * Fetches the current organization, rejecting if the organization does not exists - */ - def fetchOrganization[R]( - label: Label - )(implicit rejectionMapper: Mapper[OrganizationRejection, R]): IO[R, Organization] = - fetch(label).bimap(rejectionMapper.to, _.value) - /** * Lists all organizations. * @@ -132,7 +123,7 @@ trait Organizations { pagination: FromPagination, params: OrganizationSearchParams, ordering: Ordering[OrganizationResource] - ): UIO[UnscoredSearchResults[OrganizationResource]] + ): IO[UnscoredSearchResults[OrganizationResource]] } object Organizations { @@ -162,16 +153,16 @@ object Organizations { } private[delta] def evaluate(state: Option[OrganizationState], command: OrganizationCommand)(implicit - clock: Clock[UIO] = IO.clock, + clock: Clock[IO], uuidf: UUIDF - ): IO[OrganizationRejection, OrganizationEvent] = { + ): IO[OrganizationEvent] = { def create(c: CreateOrganization) = state match { case None => for { - uuid <- uuidf() - now <- instant + uuid <- uuidf().toCatsIO + now <- IOInstant.now } yield OrganizationCreated(c.label, uuid, 1, c.description, now, c.subject) case _ => IO.raiseError(OrganizationAlreadyExists(c.label)) } @@ -182,7 +173,7 @@ object Organizations { case Some(s) if c.rev != s.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) case Some(s) if s.deprecated => IO.raiseError(OrganizationIsDeprecated(s.label)) //remove this check if we want to allow un-deprecate - case Some(s) => instant.map(OrganizationUpdated(s.label, s.uuid, s.rev + 1, c.description, _, c.subject)) + case Some(s) => now.map(OrganizationUpdated(s.label, s.uuid, s.rev + 1, c.description, _, c.subject)) } def deprecate(c: DeprecateOrganization) = @@ -190,7 +181,7 @@ object Organizations { case None => IO.raiseError(OrganizationNotFound(c.label)) case Some(s) if c.rev != s.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) case Some(s) if s.deprecated => IO.raiseError(OrganizationIsDeprecated(s.label)) - case Some(s) => instant.map(OrganizationDeprecated(s.label, s.uuid, s.rev + 1, _, c.subject)) + case Some(s) => now.map(OrganizationDeprecated(s.label, s.uuid, s.rev + 1, _, c.subject)) } command match { @@ -204,12 +195,17 @@ object Organizations { * Entity definition for [[Organization]] */ def definition(implicit - clock: Clock[UIO] = IO.clock, + clock: Clock[IO], uuidf: UUIDF ): GlobalEntityDefinition[Label, OrganizationState, OrganizationCommand, OrganizationEvent, OrganizationRejection] = GlobalEntityDefinition( entityType, - StateMachine(None, evaluate, next), + StateMachine( + None, + (state: Option[OrganizationState], command: OrganizationCommand) => + evaluate(state, command).toBIO[OrganizationRejection], + next + ), OrganizationEvent.serializer, OrganizationState.serializer, onUniqueViolation = (id: Label, c: OrganizationCommand) => diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImpl.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImpl.scala index 02b97091f4..336c5cd621 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImpl.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImpl.scala @@ -1,45 +1,45 @@ package ch.epfl.bluebrain.nexus.delta.sdk.organizations -import cats.effect.Clock +import cats.effect.IO._ +import cats.effect.{Clock, ContextShift, IO} +import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF -import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource -import ch.epfl.bluebrain.nexus.delta.sdk.ScopeInitialization -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults +import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.ScopeInitializationFailed +import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{SearchParams, SearchResults} import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations.entityType import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsImpl.OrganizationsLog -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand._ -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationEvent -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._ -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationState +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{OrganizationCommand, OrganizationEvent, OrganizationRejection, OrganizationState} import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import ch.epfl.bluebrain.nexus.delta.sdk.{OrganizationResource, ScopeInitialization} import ch.epfl.bluebrain.nexus.delta.sourcing._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label -import monix.bio.IO -import monix.bio.UIO final class OrganizationsImpl private ( log: OrganizationsLog, scopeInitializations: Set[ScopeInitialization] -) extends Organizations { +)(implicit contextShift: ContextShift[IO]) + extends Organizations { implicit private val kamonComponent: KamonMetricComponent = KamonMetricComponent(entityType.value) override def create( label: Label, description: Option[String] - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] = + )(implicit caller: Subject): IO[OrganizationResource] = for { resource <- eval(CreateOrganization(label, description, caller)).span("createOrganization") - _ <- IO.parTraverseUnordered(scopeInitializations)(_.onOrganizationCreation(resource.value, caller).toUIO) + _ <- scopeInitializations + .parUnorderedTraverse(_.onOrganizationCreation(resource.value, caller)) .void - .mapError(OrganizationInitializationFailed) + .adaptError { case e: ScopeInitializationFailed => + OrganizationInitializationFailed(e) + } .span("initializeOrganization") } yield resource @@ -47,33 +47,33 @@ final class OrganizationsImpl private ( label: Label, description: Option[String], rev: Int - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] = + )(implicit caller: Subject): IO[OrganizationResource] = eval(UpdateOrganization(label, rev, description, caller)).span("updateOrganization") override def deprecate( label: Label, rev: Int - )(implicit caller: Subject): IO[OrganizationRejection, OrganizationResource] = + )(implicit caller: Subject): IO[OrganizationResource] = eval(DeprecateOrganization(label, rev, caller)).span("deprecateOrganization") - override def fetch(label: Label): IO[OrganizationNotFound, OrganizationResource] = + override def fetch(label: Label): IO[OrganizationResource] = log.stateOr(label, OrganizationNotFound(label)).map(_.toResource).span("fetchOrganization") - override def fetchAt(label: Label, rev: Int): IO[OrganizationRejection.NotFound, OrganizationResource] = { + override def fetchAt(label: Label, rev: Int): IO[OrganizationResource] = { log .stateOr(label, rev, OrganizationNotFound(label), RevisionNotFound) .map(_.toResource) .span("fetchOrganizationAt") } - private def eval(cmd: OrganizationCommand): IO[OrganizationRejection, OrganizationResource] = + private def eval(cmd: OrganizationCommand): IO[OrganizationResource] = log.evaluate(cmd.label, cmd).map(_._2.toResource) override def list( pagination: Pagination.FromPagination, params: SearchParams.OrganizationSearchParams, ordering: Ordering[OrganizationResource] - ): UIO[SearchResults.UnscoredSearchResults[OrganizationResource]] = + ): IO[SearchResults.UnscoredSearchResults[OrganizationResource]] = SearchResults( log .currentStates(_.toResource) @@ -93,8 +93,9 @@ object OrganizationsImpl { config: OrganizationsConfig, xas: Transactors )(implicit - clock: Clock[UIO] = IO.clock, - uuidf: UUIDF + clock: Clock[IO], + uuidf: UUIDF, + contextShift: ContextShift[IO] ): Organizations = new OrganizationsImpl( GlobalEventLog(Organizations.definition, config.eventLog, xas), diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala index 68710c7ff4..185cbe5580 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala @@ -9,6 +9,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.ScopeInitializationFailed import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.HttpResponseFields import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label +import ch.epfl.bluebrain.nexus.delta.sourcing.rejection.Rejection import io.circe.syntax._ import io.circe.{Encoder, JsonObject} @@ -18,7 +19,7 @@ import io.circe.{Encoder, JsonObject} * @param reason * a descriptive message as to why the rejection occurred */ -sealed abstract class OrganizationRejection(val reason: String) extends Exception(reason) with Product with Serializable +sealed abstract class OrganizationRejection(val reason: String) extends Rejection object OrganizationRejection { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/FetchContext.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/FetchContext.scala index c221859a00..a81447f5a3 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/FetchContext.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/FetchContext.scala @@ -14,6 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import io.circe.{Encoder, JsonObject} import monix.bio.{IO, UIO} +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import scala.collection.concurrent @@ -144,7 +145,12 @@ object FetchContext { * Create a fetch context instance from an [[Organizations]], [[Projects]] and [[Quotas]] instances */ def apply(organizations: Organizations, projects: Projects, quotas: Quotas): FetchContext[ContextRejection] = - apply(organizations.fetchActiveOrganization(_).void, projects.defaultApiMappings, projects.fetch, quotas) + apply( + organizations.fetchActiveOrganization(_).void.toBIO[OrganizationRejection], + projects.defaultApiMappings, + projects.fetch, + quotas + ) def apply( fetchActiveOrganization: Label => IO[OrganizationRejection, Unit], diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/Projects.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/Projects.scala index aaeea056de..d2c3736243 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/Projects.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/projects/Projects.scala @@ -5,12 +5,13 @@ import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOUtils.instant import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sdk.ProjectResource +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceUris import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.ProjectSearchParams import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.UnscoredSearchResults import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.Organization +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{Organization, OrganizationRejection} import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectCommand.{CreateProject, DeleteProject, DeprecateProject, UpdateProject} import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectEvent.{ProjectCreated, ProjectDeprecated, ProjectMarkedForDeletion, ProjectUpdated} import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.{IncorrectRev, ProjectAlreadyExists, ProjectIsDeprecated, ProjectIsMarkedForDeletion, ProjectNotFound, WrappedOrganizationRejection} @@ -183,7 +184,8 @@ object Projects { clock: Clock[UIO], uuidF: UUIDF ): IO[ProjectRejection, ProjectEvent] = { - val f: FetchOrganization = label => orgs.fetchActiveOrganization(label).mapError(WrappedOrganizationRejection(_)) + val f: FetchOrganization = label => + orgs.fetchActiveOrganization(label).toBIO[OrganizationRejection].mapError(WrappedOrganizationRejection(_)) evaluate(f, validateDeletion)(state, command) } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleterSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleterSuite.scala index fe129c61c8..9b1968d927 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleterSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleterSuite.scala @@ -1,6 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.organizations +import cats.effect.IO import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.sdk.ConfigFixtures import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclsImpl @@ -16,23 +18,22 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.{ProjectsConfig, ProjectsFixtu import ch.epfl.bluebrain.nexus.delta.sourcing.PartitionInit import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef} -import ch.epfl.bluebrain.nexus.testkit.IOFixedClock -import ch.epfl.bluebrain.nexus.testkit.bio.BioSuite +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsEffectSuite, IOFixedClock} import doobie.implicits._ -import monix.bio.{IO, Task, UIO} +import monix.bio.{IO => BIO} import munit.AnyFixture import java.util.UUID -class OrganizationDeleterSuite extends BioSuite with IOFixedClock with ConfigFixtures { +class OrganizationDeleterSuite extends CatsEffectSuite with IOFixedClock with ConfigFixtures { private val org1 = Label.unsafe("org1") private val org2 = Label.unsafe("org2") private def fetchOrg: FetchOrganization = { - case `org1` => UIO.pure(Organization(org1, UUID.randomUUID(), None)) - case `org2` => UIO.pure(Organization(org2, UUID.randomUUID(), None)) - case other => IO.raiseError(WrappedOrganizationRejection(OrganizationNotFound(other))) + case `org1` => BIO.pure(Organization(org1, UUID.randomUUID(), None)) + case `org2` => BIO.pure(Organization(org2, UUID.randomUUID(), None)) + case other => BIO.raiseError(WrappedOrganizationRejection(OrganizationNotFound(other))) } private val config = ProjectsConfig(eventLogConfig, pagination, cacheConfig, deletionConfig) @@ -47,7 +48,7 @@ class OrganizationDeleterSuite extends BioSuite with IOFixedClock with ConfigFix private val fields = ProjectFields(None, ApiMappings.empty, None, None) private lazy val orgs = OrganizationsImpl(Set(), orgConfig, xas) private val permission = Permissions.resources.read - private lazy val acls = AclsImpl(UIO.pure(Set(permission)), _ => IO.unit, Set(), aclsConfig, xas) + private lazy val acls = AclsImpl(BIO.pure(Set(permission)), _ => BIO.unit, Set(), aclsConfig, xas) implicit val subject: Subject = Identity.User("Bob", Label.unsafe("realm")) implicit val uuidF: UUIDF = UUIDF.fixed(UUID.randomUUID()) @@ -69,20 +70,20 @@ class OrganizationDeleterSuite extends BioSuite with IOFixedClock with ConfigFix } yield () } - def createOrgAndAcl(org: Label) = for { - _ <- acls.replace(Acl(AclAddress.fromOrg(org), subject -> Set(permission)), 0) + def createOrgAndAcl(org: Label): IO[Unit] = for { + _ <- acls.replace(Acl(AclAddress.fromOrg(org), subject -> Set(permission)), 0).toCatsIO _ <- orgs.create(org, None) } yield () - def createProj() = projects.create(projRef, fields) + def createProj() = projects.create(projRef, fields).toCatsIO - def deleteOrg(org: Label): UIO[Either[OrganizationNonEmpty, Unit]] = - IO.from(orgDeleter.delete(org).attemptNarrow[OrganizationNonEmpty]).hideErrors + def deleteOrg(org: Label): IO[Either[OrganizationNonEmpty, Unit]] = + orgDeleter.delete(org).attemptNarrow[OrganizationNonEmpty] def assertDeletionFailed(result: Either[OrganizationNonEmpty, Unit]) = for { eventPartitionDeleted <- orgPartitionIsDeleted("scoped_events", org1) statePartitionDeleted <- orgPartitionIsDeleted("scoped_states", org1) - fetchedProject <- projects.fetch(projRef) + fetchedProject <- projects.fetch(projRef).toCatsIO orgResult <- orgs.fetch(org1).map(_.value.label) aclExists <- acls.fetch(AclAddress.fromOrg(org1)).attempt.map(_.isRight) } yield { @@ -107,10 +108,10 @@ class OrganizationDeleterSuite extends BioSuite with IOFixedClock with ConfigFix assertEquals(aclDeleted, true) } - def orgPartitionIsDeleted(table: String, org: Label): Task[Boolean] = + def orgPartitionIsDeleted(table: String, org: Label): IO[Boolean] = queryPartitions(table).map(!_.contains(PartitionInit.orgPartition(table, org))) - def queryPartitions(table: String): Task[List[String]] = + def queryPartitions(table: String): IO[List[String]] = sql"""SELECT inhrelid::regclass AS child FROM pg_catalog.pg_inherits WHERE inhparent = $table::regclass diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImplSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImplSpec.scala index 9c2a103482..de2ae7eca2 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImplSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsImplSpec.scala @@ -20,7 +20,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture -import ch.epfl.bluebrain.nexus.testkit.IOFixedClock +import ch.epfl.bluebrain.nexus.testkit.ce.IOFixedClock import ch.epfl.bluebrain.nexus.testkit.ce.CatsIOValues import monix.bio.UIO import monix.execution.Scheduler diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsSpec.scala index 9b61cabaf6..5159a3a420 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationsSpec.scala @@ -2,14 +2,14 @@ package ch.epfl.bluebrain.nexus.delta.sdk.organizations import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.sdk.generators.OrganizationGen +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations.{evaluate, next} import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationEvent._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._ -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations.{evaluate, next} import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationState import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User -import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, EitherValuable, IOFixedClock, IOValues} -import monix.execution.Scheduler +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsIOValues, IOFixedClock} +import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, EitherValuable} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{Inspectors, OptionValues} @@ -22,12 +22,11 @@ class OrganizationsSpec with EitherValuable with OptionValues with Inspectors + with CatsIOValues with IOFixedClock - with IOValues with CirceLiteral { "The Organizations state machine" when { - implicit val sc: Scheduler = Scheduler.global val epoch: Instant = Instant.EPOCH val time2: Instant = Instant.ofEpochMilli(10L) val state: OrganizationState = OrganizationGen.state("org", 1, description = Some("desc")) From 9b96b5efa939449298f5ff59ab95c7e458cf9f7c Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 13 Oct 2023 11:59:45 +0200 Subject: [PATCH 2/2] Fix error when creating a view with invalid mappings or settings (#4365) --- .../src/main/resources/elasticsearch.conf | 4 ---- .../ElasticSearchPluginModule.scala | 6 +++--- .../config/ElasticSearchViewsConfig.scala | 17 ++--------------- .../nexus/tests/kg/ElasticSearchViewsSpec.scala | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/delta/plugins/elasticsearch/src/main/resources/elasticsearch.conf b/delta/plugins/elasticsearch/src/main/resources/elasticsearch.conf index 92e4e6bf86..b467d24983 100644 --- a/delta/plugins/elasticsearch/src/main/resources/elasticsearch.conf +++ b/delta/plugins/elasticsearch/src/main/resources/elasticsearch.conf @@ -10,8 +10,6 @@ plugins.elasticsearch { # username= "elastic_user" # password= "password" # } - # configuration of the Elasticsearch client - client = ${app.defaults.http-client-compression} # the elasticsearch event log configuration event-log = ${app.defaults.event-log} # the elasticsearch pagination config @@ -20,8 +18,6 @@ plugins.elasticsearch { prefix = ${app.defaults.indexing.prefix} # configuration of the maximum number of view references allowed on an aggregated view max-view-refs = 20 - # the maximum idle duration in between events on the indexing stream after which the stream will be stopped (min. 10 minutes). - idle-timeout = 30 minutes # In order to disable this feature, set an infinite time ('Inf') # idle-timeout = Inf #the maximum duration allowed so that synchronous indexing can complete diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchPluginModule.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchPluginModule.scala index 1387d097e9..0d3f08a5d9 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchPluginModule.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchPluginModule.scala @@ -23,7 +23,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.deletion.ProjectDeletionTask import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig -import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient +import ch.epfl.bluebrain.nexus.delta.sdk.http.{HttpClient, HttpClientConfig} import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount import ch.epfl.bluebrain.nexus.delta.sdk.model._ @@ -53,8 +53,8 @@ class ElasticSearchPluginModule(priority: Int) extends ModuleDef { make[ElasticSearchViewsConfig].from { ElasticSearchViewsConfig.load(_) } make[HttpClient].named("elasticsearch-client").from { - (cfg: ElasticSearchViewsConfig, as: ActorSystem[Nothing], sc: Scheduler) => - HttpClient()(cfg.client, as.classicSystem, sc) + val httpConfig = HttpClientConfig.noRetry(true) + (as: ActorSystem[Nothing], sc: Scheduler) => HttpClient()(httpConfig, as.classicSystem, sc) } make[ElasticSearchClient].from { diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/config/ElasticSearchViewsConfig.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/config/ElasticSearchViewsConfig.scala index c3f49e6d00..bae1d2534d 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/config/ElasticSearchViewsConfig.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/config/ElasticSearchViewsConfig.scala @@ -5,11 +5,10 @@ import akka.http.scaladsl.model.headers.BasicHttpCredentials import ch.epfl.bluebrain.nexus.delta.sdk.instances._ import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.client.ElasticSearchClient.Refresh import ch.epfl.bluebrain.nexus.delta.sdk.Defaults -import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClientConfig import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig import ch.epfl.bluebrain.nexus.delta.sourcing.config.{BatchConfig, EventLogConfig, QueryConfig} import com.typesafe.config.Config -import pureconfig.error.{CannotConvert, FailureReason} +import pureconfig.error.CannotConvert import pureconfig.generic.semiauto.deriveReader import pureconfig.{ConfigReader, ConfigSource} @@ -23,8 +22,6 @@ import scala.concurrent.duration._ * the base uri to the Elasticsearch HTTP endpoint * @param credentials * the credentials to authenticate to the Elasticsearch endpoint - * @param client - * configuration of the Elasticsearch client * @param eventLog * configuration of the event log * @param pagination @@ -35,8 +32,6 @@ import scala.concurrent.duration._ * prefix for indices * @param maxViewRefs * configuration of the maximum number of view references allowed on an aggregated view - * @param idleTimeout - * the maximum idle duration in between events on the indexing stream after which the stream will be stopped * @param syncIndexingTimeout * the maximum duration for synchronous indexing to complete * @param syncIndexingRefresh @@ -55,13 +50,11 @@ import scala.concurrent.duration._ final case class ElasticSearchViewsConfig( base: Uri, credentials: Option[BasicHttpCredentials], - client: HttpClientConfig, eventLog: EventLogConfig, pagination: PaginationConfig, batch: BatchConfig, prefix: String, maxViewRefs: Int, - idleTimeout: Duration, syncIndexingTimeout: FiniteDuration, syncIndexingRefresh: Refresh, maxIndexPathLength: Int, @@ -98,11 +91,5 @@ object ElasticSearchViewsConfig { } implicit final val elasticSearchViewsConfigReader: ConfigReader[ElasticSearchViewsConfig] = - deriveReader[ElasticSearchViewsConfig].emap { c => - Either.cond( - c.idleTimeout.gteq(10.minutes), - c, - new FailureReason { override def description: String = "'idle-timeout' must be greater than 10 minutes" } - ) - } + deriveReader[ElasticSearchViewsConfig] } diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala index 58a3fdc4b4..da55690deb 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala @@ -84,6 +84,21 @@ class ElasticSearchViewsSpec extends BaseSpec with EitherValuable with CirceEq { } } + "fail to create a view with an invalid mapping" in { + val invalidMapping = + json"""{"mapping": "fail"}""" + val payloadWithInvalidMapping = json"""{ "@type": "ElasticSearchView", "mapping": $invalidMapping }""" + deltaClient.put[Json](s"/views/$fullId/invalid", payloadWithInvalidMapping, ScoobyDoo) { expectBadRequest } + } + + "fail to create a view with invalid settings" in { + val invalidSettings = + json"""{"analysis": "fail"}""" + val payloadWithInvalidSettings = + json"""{ "@type": "ElasticSearchView", "mapping": { }, "settings": $invalidSettings }""" + deltaClient.put[Json](s"/views/$fullId/invalid", payloadWithInvalidSettings, ScoobyDoo) { expectBadRequest } + } + "create people view in project 2" in { deltaClient.put[Json]( s"/views/$fullId2/test-resource:people",