Skip to content

Commit

Permalink
Introduce etag header for original payloads (#5169)
Browse files Browse the repository at this point in the history
* Introduce etag header for original payloads

---------

Co-authored-by: Simon Dumas <simon.dumas@epfl.ch>
  • Loading branch information
imsdu and Simon Dumas authored Oct 3, 2024
1 parent 63e29c5 commit 0c5969a
Show file tree
Hide file tree
Showing 23 changed files with 271 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
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.marshalling.{AnnotatedSource, HttpResponseFields, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{HttpResponseFields, OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment, IdSegmentRef, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resolvers.{read => Read, write => Write}
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResolverNotFound
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, Resolver, ResolverRejection}
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{MultiResolution, Resolvers}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import io.circe.{Json, Printer}
import io.circe.Json

/**
* The resolver routes
Expand Down Expand Up @@ -74,10 +74,13 @@ final class ResolversRoutes(
private def emitMetadataOrReject(io: IO[ResolverResource]): Route =
emit(io.map(_.void).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound])

private def emitSource(io: IO[ResolverResource]): Route = {
implicit val source: Printer = sourcePrinter
emit(io.map(_.value.source).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound])
}
private def emitSource(io: IO[ResolverResource]): Route =
emit(
io
.map { resource => OriginalSource(resource, resource.value.source) }
.attemptNarrow[ResolverRejection]
.rejectOn[ResolverNotFound]
)

def routes: Route =
(baseUriPrefix(baseUri.prefix) & replaceUri("resolvers", schemas.resolvers)) {
Expand Down Expand Up @@ -179,7 +182,7 @@ final class ResolversRoutes(
case ResolvedResourceOutputType.Source =>
emit(io.map(_.value.source).attemptNarrow[ResolverRejection])
case ResolvedResourceOutputType.AnnotatedSource =>
val annotatedSourceIO = io.map { r => AnnotatedSource(r.value.resource, r.value.source) }
val annotatedSourceIO = io.map { r => OriginalSource.annotated(r.value.resource, r.value.source) }
emit(annotatedSourceIO.attemptNarrow[ResolverRejection])
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources.{read => Read, write => Write}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption
import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection._
import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceRejection}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, Resources}
import io.circe.{Json, Printer}

/**
* The resource routes
Expand Down Expand Up @@ -208,13 +207,10 @@ final class ResourcesRoutes(
resourceRef =>
authorizeFor(project, Read).apply {
annotateSource { annotate =>
implicit val source: Printer = sourcePrinter
emit(
resources
.fetch(resourceRef, project, schemaOpt)
.map { resource =>
AnnotatedSource.when(annotate)(resource, resource.value.source)
}
.map { resource => OriginalSource(resource, resource.value.source, annotate) }
.attemptNarrow[ResourceRejection]
.rejectOn[ResourceNotFound]
)
Expand Down Expand Up @@ -309,9 +305,4 @@ object ResourcesRoutes {
decodingOption: DecodingOption
): Route = new ResourcesRoutes(identities, aclCheck, resources, index).routes

def asSourceWithMetadata(
resource: ResourceF[Resource]
)(implicit baseUri: BaseUri): Json =
AnnotatedSource(resource, resource.value.source)

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ 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.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.directives.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.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.schemas.{read => Read, write => Write}
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.SchemaNotFound
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection}
import io.circe.{Json, Printer}
import io.circe.Json

/**
* The schemas routes
Expand Down Expand Up @@ -73,9 +73,8 @@ final class SchemasRoutes(
emit(io.map(_.void).attemptNarrow[SchemaRejection].rejectOn[SchemaNotFound])

private def emitSource(io: IO[SchemaResource], annotate: Boolean): Route = {
implicit val source: Printer = sourcePrinter
emit(
io.map { resource => AnnotatedSource.when(annotate)(resource, resource.value.source) }
io.map { resource => OriginalSource(resource, resource.value.source, annotate) }
.attemptNarrow[SchemaRejection]
.rejectOn[SchemaNotFound]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with ValidateResourceFixture wit
Get(s"/v1/resources/myorg/myproject/_/$id/source") ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual simplePayload(id)
response.expectConditionalCacheHeaders
}
}
}
Expand Down Expand Up @@ -601,6 +602,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with ValidateResourceFixture wit
Get(endpoint) ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual payloadWithMetadata(id)
response.expectConditionalCacheHeaders
response.headers should contain(varyHeader)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues {
"id" -> "nxv:myid2",
"self" -> ResourceUris.schema(projectRef, myId2).accessUri
)
response.expectConditionalCacheHeaders
}
}
}
Expand All @@ -336,6 +337,7 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues {
Get(endpoint) ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual payloadNoId
response.expectConditionalCacheHeaders
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import ch.epfl.bluebrain.nexus.delta.sdk.error.SDKError
import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.AuthorizationFailed
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.AnnotatedSource
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.OriginalSource
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation._
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources
import ch.epfl.bluebrain.nexus.delta.sdk.{AkkaSource, JsonLdValue, ResourceShifts}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef}
import io.circe.{Json, Printer}
import io.circe.syntax.EncoderOps

import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
Expand Down Expand Up @@ -243,8 +244,8 @@ object ArchiveDownload {
repr match {
case SourceJson => IO.pure(ByteString(prettyPrintSource(value.source)))
case AnnotatedSourceJson =>
val annotatedSource = AnnotatedSource(value.resource, value.source)
IO.pure(ByteString(prettyPrintSource(annotatedSource)))
val originalSource = OriginalSource.annotated(value.resource, value.source)
IO.pure(ByteString(prettyPrintSource(originalSource.asJson)))
case CompactedJsonLd => value.resource.toCompactedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case ExpandedJsonLd => value.resource.toExpandedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case NTriples => value.resource.toNTriples.map(v => ByteString(v.value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ 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.jsonld.JsonLdRejection.{DecodingFailed, InvalidJsonLdFormat}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import io.circe.{Json, Printer}
import io.circe.Json

/**
* The Blazegraph views routes
Expand Down Expand Up @@ -83,10 +83,12 @@ class BlazegraphViewsRoutes(
private def emitFetch(io: IO[ViewResource]): Route =
emit(io.attemptNarrow[BlazegraphViewRejection].rejectOn[ViewNotFound])

private def emitSource(io: IO[ViewResource]): Route = {
implicit val source: Printer = sourcePrinter
emit(io.map(_.value.source).attemptNarrow[BlazegraphViewRejection].rejectOn[ViewNotFound])
}
private def emitSource(io: IO[ViewResource]): Route =
emit(
io.map { resource => OriginalSource(resource, resource.value.source) }
.attemptNarrow[BlazegraphViewRejection]
.rejectOn[ViewNotFound]
)

def routes: Route =
concat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdRejection.{DecodingFailed, InvalidJsonLdFormat}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model._
import io.circe.syntax.EncoderOps
import io.circe.{Json, JsonObject, Printer}
import io.circe.{Json, JsonObject}

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration
Expand Down Expand Up @@ -82,10 +82,12 @@ final class ElasticSearchViewsRoutes(
private def emitFetch(io: IO[ViewResource]): Route =
emit(io.attemptNarrow[ElasticSearchViewRejection].rejectOn[ViewNotFound])

private def emitSource(io: IO[ViewResource]): Route = {
implicit val source: Printer = sourcePrinter
emit(io.map(_.value.source).attemptNarrow[ElasticSearchViewRejection].rejectOn[ViewNotFound])
}
private def emitSource(io: IO[ViewResource]): Route =
emit(
io.map { resource => OriginalSource(resource, resource.value.source) }
.attemptNarrow[ElasticSearchViewRejection]
.rejectOn[ViewNotFound]
)

def routes: Route =
pathPrefix("views") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import ch.epfl.bluebrain.nexus.delta.plugins.jira.{JiraClient, JiraError}
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.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.AuthorizationFailed
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileDelegationRequest.{FileDelegationCreationRequest, FileDelegationUpdateRequest}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileLinkRequest, _}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileResource, Files}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaScheme
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import io.circe.Json
import kamon.instrumentation.akka.http.TracingDirectives.operationName
Expand Down Expand Up @@ -153,13 +153,11 @@ final class StoragesRoutes(
},
// Fetch a storage original source
(pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id =>
operationName(s"$prefixSegment/storages/{org}/{project}/{id}/source") {
authorizeFor(project, Read).apply {
val sourceIO = storages
.fetch(id, project)
.map(res => res.value.source)
emit(sourceIO.attemptNarrow[StorageRejection].rejectOn[StorageNotFound])
}
authorizeFor(project, Read).apply {
val sourceIO = storages
.fetch(id, project)
.map { resource => OriginalSource(resource, resource.value.source) }
emit(sourceIO.attemptNarrow[StorageRejection].rejectOn[StorageNotFound])
}
},
(pathPrefix("statistics") & get & pathEndOrSingleSlash) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ trait DeltaDirectives extends UriDirectives {
def emit(status: StatusCode, response: ResponseToMarshaller): Route =
response(Some(status))

/**
* Completes the current Route with the provided conversion to original payloads
*/
def emit(response: ResponseToOriginalSource): Route = response()

/**
* Completes the current Route with the provided conversion to SSEs
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ trait ResponseToMarshaller {

object ResponseToMarshaller extends RdfMarshalling {

// Some resources may not have been created in the system with a strict configuration
// (and if they are, there is no need to check them again)
// To serialize errors to compacted json-ld
implicit val api: JsonLdApi = JsonLdJavaApi.lenient

private[directives] def apply[E: JsonLdEncoder, A: ToEntityMarshaller](
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ch.epfl.bluebrain.nexus.delta.sdk.directives

import akka.http.scaladsl.marshalling.ToEntityMarshaller
import akka.http.scaladsl.model.MediaTypes
import akka.http.scaladsl.server.Directives.{complete, onSuccess, reject}
import akka.http.scaladsl.server.Route
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
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.sdk.directives.DeltaDirectives.{conditionalCache, requestEncoding}
import ch.epfl.bluebrain.nexus.delta.sdk.directives.Response.{Complete, Reject}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{HttpResponseFields, OriginalSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.syntax._
import io.circe.syntax.EncoderOps

/**
* Handles serialization of [[OriginalSource]] and generates the appropriate response headers
*/
trait ResponseToOriginalSource {
def apply(): Route
}

object ResponseToOriginalSource extends RdfMarshalling {

// To serialize errors to compacted json-ld
implicit private val api: JsonLdApi = JsonLdJavaApi.lenient

implicit private def originalSourceMarshaller(implicit
ordering: JsonKeyOrdering
): ToEntityMarshaller[OriginalSource] =
jsonMarshaller(ordering, sourcePrinter).compose(_.asJson)

private[directives] def apply[E: JsonLdEncoder](
io: IO[Either[Response[E], Complete[OriginalSource]]]
)(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource =
() => {
val ioRoute = io.flatMap {
case Left(r: Reject[E]) => IO.pure(reject(r))
case Left(e: Complete[E]) => e.value.toCompactedJsonLd.map(r => complete(e.status, e.headers, r.json))
case Right(v: Complete[OriginalSource]) =>
IO.pure {
requestEncoding { encoding =>
conditionalCache(v.entityTag, v.lastModified, MediaTypes.`application/json`, encoding) {
complete(v.status, v.headers, v.value)
}
}
}
}
onSuccess(ioRoute.unsafeToFuture())(identity)
}

implicit def ioOriginalPayloadComplete[E: JsonLdEncoder: HttpResponseFields](
io: IO[Either[E, OriginalSource]]
)(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource = {
val ioComplete = io.map {
_.bimap(e => Complete(e), originalSource => Complete(originalSource))
}
ResponseToOriginalSource(ioComplete)
}

implicit def ioResponseOriginalPayloadComplete[E: JsonLdEncoder](
io: IO[Either[Response[E], OriginalSource]]
)(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource = {
val ioComplete = io.map {
_.map(originalSource => Complete(originalSource))
}
ResponseToOriginalSource(ioComplete)
}
}
Loading

0 comments on commit 0c5969a

Please sign in to comment.