Skip to content

Commit

Permalink
Add annotate param to fetching original payload via resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Dumas committed Feb 21, 2024
1 parent da8b86a commit df29a79
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
)
}
)
Expand All @@ -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])
}
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 5 additions & 1 deletion docs/src/main/paradox/docs/delta/api/resolvers-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/src/main/paradox/docs/delta/api/resources-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
6 changes: 6 additions & 0 deletions docs/src/main/paradox/docs/releases/v1.10-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit df29a79

Please sign in to comment.