Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce etag header for original payloads #5169

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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