Skip to content

Commit

Permalink
Allow undeprecation of Elasticsearch views (#4573)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivergrabinski authored Dec 8, 2023
1 parent 83f748c commit b4a0eb1
Show file tree
Hide file tree
Showing 20 changed files with 603 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,30 @@ final class ElasticSearchViews private (
} yield res
}.span("deprecateElasticSearchView")

/**
* Undeprecates an existing ElasticSearchView. View undeprecation implies unblocking any query capabilities and in
* case of an IndexingElasticSearchView the corresponding index is created.
*
* @param id
* the view identifier
* @param project
* the view parent project
* @param rev
* the current view revision
* @param subject
* the subject that initiated the action
*/
def undeprecate(
id: IdSegment,
project: ProjectRef,
rev: Int
)(implicit subject: Subject): IO[ViewResource] = {
for {
(iri, _) <- expandWithContext(fetchContext.onModify, project, id)
res <- eval(UndeprecateElasticSearchView(iri, project, rev, subject))
} yield res
}.span("undeprecateElasticSearchView")

/**
* Deprecates an existing ElasticSearchView without applying preliminary checks on the project status
*
Expand Down Expand Up @@ -469,11 +493,16 @@ object ElasticSearchViews {
s.copy(rev = e.rev, deprecated = true, updatedAt = e.instant, updatedBy = e.subject)
}

def undeprecated(e: ElasticSearchViewUndeprecated): Option[ElasticSearchViewState] = state.map { s =>
s.copy(rev = e.rev, deprecated = false, updatedAt = e.instant, updatedBy = e.subject)
}

event match {
case e: ElasticSearchViewCreated => created(e)
case e: ElasticSearchViewUpdated => updated(e)
case e: ElasticSearchViewTagAdded => tagAdded(e)
case e: ElasticSearchViewDeprecated => deprecated(e)
case e: ElasticSearchViewCreated => created(e)
case e: ElasticSearchViewUpdated => updated(e)
case e: ElasticSearchViewTagAdded => tagAdded(e)
case e: ElasticSearchViewDeprecated => deprecated(e)
case e: ElasticSearchViewUndeprecated => undeprecated(e)
}
}

Expand Down Expand Up @@ -528,11 +557,22 @@ object ElasticSearchViews {
)
}

def undeprecate(c: UndeprecateElasticSearchView) = state match {
case None => IO.raiseError(ViewNotFound(c.id, c.project))
case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev))
case Some(s) if !s.deprecated => IO.raiseError(ViewIsNotDeprecated(c.id))
case Some(s) =>
clock.realTimeInstant.map(
ElasticSearchViewUndeprecated(c.id, c.project, s.value.tpe, s.uuid, s.rev + 1, _, c.subject)
)
}

cmd match {
case c: CreateElasticSearchView => create(c)
case c: UpdateElasticSearchView => update(c)
case c: TagElasticSearchView => tag(c)
case c: DeprecateElasticSearchView => deprecate(c)
case c: CreateElasticSearchView => create(c)
case c: UpdateElasticSearchView => update(c)
case c: TagElasticSearchView => tag(c)
case c: DeprecateElasticSearchView => deprecate(c)
case c: UndeprecateElasticSearchView => undeprecate(c)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ object ElasticSearchViewCommand {
subject: Subject
) extends ElasticSearchViewCommand

/**
* Command for the undeprecation of an ElasticSearch view.
*
* @param id
* the view id
* @param project
* a reference to the parent project
* @param rev
* the last known revision of the view
* @param subject
* the identity associated with this command
*/
final case class UndeprecateElasticSearchView(
id: Iri,
project: ProjectRef,
rev: Int,
subject: Subject
) extends ElasticSearchViewCommand

/**
* Command for adding a tag to an ElasticSearch view.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,34 @@ object ElasticSearchViewEvent {
subject: Subject
) extends ElasticSearchViewEvent

/**
* Evidence of a view undeprecation.
*
* @param id
* the view identifier
* @param project
* the view parent project
* @param tpe
* the view type
* @param uuid
* the view unique identifier
* @param rev
* the revision that the event generates
* @param instant
* the instant when the event was emitted
* @param subject
* the subject that undeprecated the view
*/
final case class ElasticSearchViewUndeprecated(
id: Iri,
project: ProjectRef,
tpe: ElasticSearchViewType,
uuid: UUID,
rev: Int,
instant: Instant,
subject: Subject
) extends ElasticSearchViewEvent

@nowarn("cat=unused")
val serializer: Serializer[Iri, ElasticSearchViewEvent] = {
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Database._
Expand All @@ -204,10 +232,11 @@ object ElasticSearchViewEvent {
ProjectScopedMetric.from(
event,
event match {
case _: ElasticSearchViewCreated => Created
case _: ElasticSearchViewUpdated => Updated
case _: ElasticSearchViewTagAdded => Tagged
case _: ElasticSearchViewDeprecated => Deprecated
case _: ElasticSearchViewCreated => Created
case _: ElasticSearchViewUpdated => Updated
case _: ElasticSearchViewTagAdded => Tagged
case _: ElasticSearchViewDeprecated => Deprecated
case _: ElasticSearchViewUndeprecated => Undeprecated
},
event.id,
event.tpe.types,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ object ElasticSearchViewRejection {
final case class ViewIsDeprecated(id: Iri)
extends ElasticSearchViewRejection(s"ElasticSearch view '$id' is deprecated.")

/**
* Rejection returned when attempting to undeprecate a view that is not deprecated.
*
* @param id
* the view id
*/
final case class ViewIsNotDeprecated(id: Iri)
extends ElasticSearchViewRejection(s"ElasticSearch view '$id' is not deprecated.")

/**
* Rejection returned when a subject intends to perform an operation on the current view, but either provided an
* incorrect revision or a concurrent update won over this attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ final class ElasticSearchViewsRoutes(
}
)
},
// Undeprecate an elasticsearch view
(pathPrefix("undeprecate") & put & pathEndOrSingleSlash & parameter("rev".as[Int])) { rev =>
authorizeFor(ref, Write).apply {
emit(
views
.undeprecate(id, ref, rev)
.flatTap(index(ref, _, mode))
.mapValue(_.metadata)
.attemptNarrow[ElasticSearchViewRejection]
.rejectWhen(decodingFailedOrViewNotFound)
)
}
},
// Query an elasticsearch view
(pathPrefix("_search") & post & pathEndOrSingleSlash) {
(extractQueryParams & entity(as[JsonObject])) { (qp, query) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id" : "https://bluebrain.github.io/nexus/vocabulary/indexing-view",
"project" : "myorg/myproj",
"uuid" : "f8468909-a797-4b10-8b5f-000cba337bfa",
"rev" : 5,
"instant" : "1970-01-01T00:00:00Z",
"subject" : {
"subject" : "username",
"realm" : "myrealm",
"@type" : "User"
},
"@type" : "ElasticSearchViewUndeprecated",
"tpe": "ElasticSearchView"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"@context": [
"https://bluebrain.github.io/nexus/contexts/metadata.json",
"https://bluebrain.github.io/nexus/contexts/elasticsearch.json"
],
"@type": "ElasticSearchViewUndeprecated",
"_constrainedBy": "https://bluebrain.github.io/nexus/schemas/views.json",
"_instant": "1970-01-01T00:00:00Z",
"_project": "http://localhost/v1/projects/myorg/myproj",
"_resourceId": "https://bluebrain.github.io/nexus/vocabulary/indexing-view",
"_rev": 5,
"_subject": "http://localhost/v1/realms/myrealm/users/username",
"_types": [
"https://bluebrain.github.io/nexus/vocabulary/ElasticSearchView",
"https://bluebrain.github.io/nexus/vocabulary/View"
],
"_uuid": "f8468909-a797-4b10-8b5f-000cba337bfa",
"_viewId": "https://bluebrain.github.io/nexus/vocabulary/indexing-view"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "ViewIsNotDeprecated",
"reason": "ElasticSearch view '{{id}}' is not deprecated."
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,29 @@ class ElasticSearchViewSTMSpec extends CatsEffectSpec {
}
}

"evaluating the UndeprecateElasticSearchView command" should {
"emit an ElasticSearchViewUndeprecated" in {
val deprecatedState = Some(current(deprecated = true))
val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject)
val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject)
eval(deprecatedState, undeprecateCmd).accepted shouldEqual undeprecatedEvent
}
"raise a ViewNotFound rejection" in {
val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject)
eval(None, undeprecateCmd).rejectedWith[ViewNotFound]
}
"raise a IncorrectRev rejection" in {
val deprecatedState = Some(current(deprecated = true))
val undeprecateCmd = UndeprecateElasticSearchView(id, project, 2, subject)
eval(deprecatedState, undeprecateCmd).rejectedWith[IncorrectRev]
}
"raise a ViewIsNotDeprecated rejection" in {
val activeView = Some(current())
val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject)
eval(activeView, undeprecateCmd).rejectedWith[ViewIsNotDeprecated]
}
}

"applying an ElasticSearchViewCreated event" should {
"discard the event for a Current state" in {
next(
Expand Down Expand Up @@ -285,6 +308,19 @@ class ElasticSearchViewSTMSpec extends CatsEffectSpec {
}
}

"applying an ElasticSearchViewUndeprecated event" should {
"discard the event for an Initial state" in {
val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject)
next(None, undeprecatedEvent) shouldEqual None
}
"change the state" in {
val deprecatedState = Some(current(deprecated = true))
val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject)
next(deprecatedState, undeprecatedEvent).value shouldEqual
current(deprecated = false, rev = 2, updatedBy = subject)
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch
import cats.data.NonEmptySet
import cats.effect.IO
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewRejection.{DifferentElasticSearchViewType, IncorrectRev, InvalidPipeline, InvalidViewReferences, PermissionIsNotDefined, ProjectContextRejection, ResourceAlreadyExists, RevisionNotFound, TagNotFound, TooManyViewReferences, ViewIsDeprecated, ViewNotFound}
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewRejection.{DifferentElasticSearchViewType, IncorrectRev, InvalidPipeline, InvalidViewReferences, PermissionIsNotDefined, ProjectContextRejection, ResourceAlreadyExists, RevisionNotFound, TagNotFound, TooManyViewReferences, ViewIsDeprecated, ViewIsNotDeprecated, ViewNotFound}
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewValue.{AggregateElasticSearchViewValue, IndexingElasticSearchViewValue}
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model._
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.permissions.{query => queryPermissions}
Expand All @@ -29,6 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.stream.pipes.{FilterBySchema, Filt
import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec
import io.circe.Json
import io.circe.literal._
import org.scalatest.Assertion

import java.time.Instant
import java.util.UUID
Expand Down Expand Up @@ -405,6 +406,55 @@ class ElasticSearchViewsSpec extends CatsEffectSpec with DoobieScalaTestFixture
}
}

"undeprecate a view" when {
"using the correct revision" in {
givenADeprecatedView { view =>
views.undeprecate(view, projectRef, 2).accepted shouldEqual
resourceFor(
id = nxv + view,
deprecated = false,
rev = 3,
value = IndexingElasticSearchViewValue(
resourceTag = None,
IndexingElasticSearchViewValue.defaultPipeline,
mapping = Some(mapping),
settings = None,
context = None,
permission = queryPermissions
),
source = json"""{"@type": "ElasticSearchView", "mapping": $mapping}"""
)
views.fetch(view, projectRef).accepted.deprecated shouldEqual false
}
}
}

"fail to undeprecate a view" when {
"the view is not deprecated" in {
givenAView { view =>
views.undeprecate(view, projectRef, 1).assertRejectedWith[ViewIsNotDeprecated]
}
}
"providing an incorrect revision for an IndexingElasticSearchViewValue" in {
givenADeprecatedView { view =>
views.undeprecate(view, projectRef, 100).assertRejectedWith[IncorrectRev]
}
}
"the target view is not found" in {
val nonExistentView = iri"http://localhost/${genString()}"
views.undeprecate(nonExistentView, projectRef, 2).rejectedWith[ViewNotFound]
}
"the project of the target view is not found" in {
givenAView { view =>
views.undeprecate(view, unknownProjectRef, 2).assertRejectedWith[ProjectContextRejection]
}
}
"the referenced project is deprecated" in {
val id = iri"http://localhost/${genString()}"
views.undeprecate(id, deprecatedProjectRef, 2).rejectedWith[ProjectContextRejection]
}
}

"fetch a view by id" when {
"no rev nor tag is provided" in {
val id = iri"http://localhost/${genString()}"
Expand Down Expand Up @@ -484,5 +534,20 @@ class ElasticSearchViewsSpec extends CatsEffectSpec with DoobieScalaTestFixture
views.fetch(IdSegmentRef(id, tag), projectRef).rejectedWith[TagNotFound]
}
}

def givenAView(test: String => Assertion): Assertion = {
val id = genString()
val source = json"""{"@type": "ElasticSearchView", "mapping": $mapping}"""
views.create(id, projectRef, source).accepted
test(id)
}

def givenADeprecatedView(test: String => Assertion): Assertion = {
givenAView { view =>
views.deprecate(view, projectRef, 1).accepted
test(view)
}
}

}
}
Loading

0 comments on commit b4a0eb1

Please sign in to comment.