diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala index c2cd20831a..6a6f9fee11 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala @@ -19,7 +19,7 @@ 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.RdfMarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, 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} @@ -146,12 +146,13 @@ final class ResolversRoutes( resolveResource(resourceIdRef, project, resolutionType(resolver), outputType) } }, - (pathPrefix("source") & pathEndOrSingleSlash & get) { + (pathPrefix("source") & pathEndOrSingleSlash & get & annotateSource) { annotate => resolveResource( resourceIdRef, project, resolutionType(resolver), - ResolvedResourceOutputType.Source + if (annotate) ResolvedResourceOutputType.AnnotatedSource + else ResolvedResourceOutputType.Source ) } ) @@ -169,15 +170,17 @@ final class ResolversRoutes( project: ProjectRef, resolutionType: ResolutionType, output: ResolvedResourceOutputType - )(implicit - caller: Caller - ): Route = + )(implicit baseUri: BaseUri, caller: Caller): Route = authorizeFor(project, Permissions.resources.read).apply { def emitResult[R: JsonLdEncoder](io: IO[MultiResolutionResult[R]]) = { output match { - case ResolvedResourceOutputType.Report => emit(io.map(_.report).attemptNarrow[ResolverRejection]) - case ResolvedResourceOutputType.JsonLd => emit(io.map(_.value.jsonLdValue).attemptNarrow[ResolverRejection]) - case ResolvedResourceOutputType.Source => emit(io.map(_.value.source).attemptNarrow[ResolverRejection]) + case ResolvedResourceOutputType.Report => emit(io.map(_.report).attemptNarrow[ResolverRejection]) + case ResolvedResourceOutputType.JsonLd => emit(io.map(_.value.jsonLdValue).attemptNarrow[ResolverRejection]) + 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) } + emit(annotatedSourceIO.attemptNarrow[ResolverRejection]) } } @@ -203,9 +206,10 @@ object ResolutionType { sealed trait ResolvedResourceOutputType object ResolvedResourceOutputType { - case object Report extends ResolvedResourceOutputType - case object JsonLd extends ResolvedResourceOutputType - case object Source extends ResolvedResourceOutputType + case object Report extends ResolvedResourceOutputType + case object JsonLd extends ResolvedResourceOutputType + case object Source extends ResolvedResourceOutputType + case object AnnotatedSource extends ResolvedResourceOutputType } object ResolversRoutes { diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index df72a43ede..bdc045e89e 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -203,7 +203,7 @@ final class ResourcesRoutes( (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(resource) & varyAcceptHeaders) { resourceRef => authorizeFor(project, Read).apply { - parameter("annotate".as[Boolean].withDefault(false)) { annotate => + annotateSource { annotate => implicit val source: Printer = sourcePrinter if (annotate) { emit( diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala index 6b692216c2..fc2a31de00 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala @@ -597,6 +597,26 @@ class ResolversRoutesSpec extends BaseRouteSpec { } } + "succeed as a resource and return the original payload" in { + // First we resolve with a in-project resolver, the second one with a cross-project resolver + forAll(List(project, project2)) { p => + Get(s"/v1/resolvers/${p.ref}/_/$idResourceEncoded/source") ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + response.asJson shouldEqual resourceFR.value.source + } + } + } + + "succeed as a resource and return the annotated original payload" in { + // First we resolve with a in-project resolver, the second one with a cross-project resolver + forAll(List(project, project2)) { p => + Get(s"/v1/resolvers/${p.ref}/_/$idResourceEncoded/source?annotate=true") ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + response.asJson shouldEqual resourceResolved + } + } + } + "succeed as a resource and return the resolution report" in { Get(s"/v1/resolvers/${project.ref}/_/$idResourceEncoded?showReport=true") ~> asAlice ~> routes ~> check { response.status shouldEqual StatusCodes.OK diff --git a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/syntax/JsonSyntax.scala b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/syntax/JsonSyntax.scala index 7062daade1..f5cae65dac 100644 --- a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/syntax/JsonSyntax.scala +++ b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/syntax/JsonSyntax.scala @@ -76,6 +76,11 @@ final class JsonObjectOps(private val obj: JsonObject) extends AnyVal { */ def removeAllKeys(keys: String*): JsonObject = JsonUtils.removeAllKeys(obj.asJson, keys: _*).asObject.get + /** + * Removes the metadata keys from the current json. + */ + def removeMetadataKeys(): JsonObject = JsonUtils.removeMetadataKeys(obj.asJson).asObject.get + /** * Removes the provided key value pairs from everywhere on the json object. */ diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala index 6b2b400062..f3fad41bcc 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala @@ -106,6 +106,11 @@ trait UriDirectives extends QueryParamsUnmarshalling { ProjectRef(org, proj) } + /** + * Extracts the annotate param as a boolean with a default false value + */ + def annotateSource: Directive1[Boolean] = parameter("annotate".as[Boolean].withDefault(false)) + /** * This directive passes when the query parameter specified is not present * diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala index 66383a672c..99ab0fb110 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala @@ -13,7 +13,7 @@ object AnnotatedSource { * Merges the source with the metadata of [[ResourceF]] */ def apply(resourceF: ResourceF[_], source: Json)(implicit baseUri: BaseUri): Json = { - val sourceWithoutMetadata = source.removeMetadataKeys + val sourceWithoutMetadata = source.removeMetadataKeys() val metadataJson = resourceF.void.asJson metadataJson.deepMerge(sourceWithoutMetadata).addContext(contexts.metadata) } diff --git a/docs/src/main/paradox/docs/delta/api/resolvers-api.md b/docs/src/main/paradox/docs/delta/api/resolvers-api.md index 000260d6cc..53a5985dfd 100644 --- a/docs/src/main/paradox/docs/delta/api/resolvers-api.md +++ b/docs/src/main/paradox/docs/delta/api/resolvers-api.md @@ -370,15 +370,19 @@ If the resolver segment (`{resolver_id}`) is `_` the resource is fetched from th project (`{org_label}/{project_label}`). The resolvers are ordered by its priority field. ``` -GET /v1/resolvers/{org_label}/{project_label}/{resolver_id}/{resource_id}/source?rev={rev}&tag={tag} +GET /v1/resolvers/{org_label}/{project_label}/{resolver_id}/{resource_id}/source?rev={rev}&tag={tag}&annotate={annotate} ``` where ... - `{resource_id}`: Iri - the @id value of the resource to be retrieved. - `{rev}`: Number - the targeted revision to be fetched. This field is optional and defaults to the latest revision. - `{tag}`: String - the targeted tag to be fetched. This field is optional. +- `{annotate}`: Boolean - annotate the response with the resource metadata. This field only applies to standard resources. This field is optional. `{rev}` and `{tag}` fields cannot be simultaneously present. +If `{annotate}` is set, the metadata is injected alongside with the original payload where the ones from the original payload take precedence. +The context in the original payload is also amended with the metadata context. + **Example** Request diff --git a/docs/src/main/paradox/docs/delta/api/resources-api.md b/docs/src/main/paradox/docs/delta/api/resources-api.md index 676c240b49..2481195d38 100644 --- a/docs/src/main/paradox/docs/delta/api/resources-api.md +++ b/docs/src/main/paradox/docs/delta/api/resources-api.md @@ -342,7 +342,8 @@ where ... `{rev}` and `{tag}` fields cannot be simultaneously present. -If `{annotate}` is set, fields present in the metadata will override fields with the same name from the payload. The `@id` field is an exception to this rule +If `{annotate}` is set, the metadata is injected alongside with the original payload where the ones from the original payload take precedence. +The context in the original payload is also amended with the metadata context. **Example** diff --git a/docs/src/main/paradox/docs/releases/v1.10-release-notes.md b/docs/src/main/paradox/docs/releases/v1.10-release-notes.md index c8b6f85e15..9aee98db14 100644 --- a/docs/src/main/paradox/docs/releases/v1.10-release-notes.md +++ b/docs/src/main/paradox/docs/releases/v1.10-release-notes.md @@ -81,6 +81,12 @@ update operations. ### Resolvers +#### Fetching the annotated original payload of a resolved resource. + +The annotate parameter has been introduced to the endpoint to get the original payload of a resolved resource. + +@ref:[More information](../delta/api/resolvers-api.md#fetch-original-resource-payload-using-resolvers) + #### Deprecations * The ability to tag a resolver has been removed. It is also no longer possible to fetch a resolver by tag. diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala index f5312060f9..84576e7260 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala @@ -220,6 +220,14 @@ class ResourcesSpec extends BaseIntegrationSpec { } } + "fetch the original payload with metadata through a resolver" in { + val expected = resource1AnnotatedSource(1, 5).accepted + deltaClient.get[Json](s"/resolvers/$project1/_/test-resource:1/source?annotate=true", Morty) { (json, response) => + response.status shouldEqual StatusCodes.OK + filterMetadataKeys(json) should equalIgnoreArrayOrder(expected) + } + } + "fetch the original payload with unexpanded id with metadata" in { val payload = SimpleResource.sourcePayload("42", 5).accepted