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

Add a multi-fetch operation #4132

Merged
merged 6 commits into from
Aug 7, 2023
Merged

Conversation

imsdu
Copy link
Contributor

@imsdu imsdu commented Aug 4, 2023

Fixes #4055
Fixes #4069

image
image

@@ -5,7 +5,7 @@ import akka.util.ByteString
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveReference.{FileReference, FileSelfReference, ResourceReference}
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveRejection._
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveResourceRepresentation._
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation._
Copy link
Contributor Author

@imsdu imsdu Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the same way to chose the output format as archives so I reused (moved and renamed) the class responsible for it

Comment on lines +76 to +83
case SourceJson.toString => Right(SourceJson)
case CompactedJsonLd.toString => Right(CompactedJsonLd)
case ExpandedJsonLd.toString => Right(ExpandedJsonLd)
case NTriples.toString => Right(NTriples)
case NQuads.toString => Right(NQuads)
case Dot.toString => Right(Dot)
case other => Left(s"$other is not a valid representation")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String literals are more appropriate here, as we probably don't want the API to change if we rename one of these (unless we're sure these are covered by tests)

Copy link
Contributor Author

@imsdu imsdu Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toString method is overriden.

It is not the most elegant way but I just moved this class from archives to here and it looks good enough

request.resources
.traverse { input =>
for {
authorized <- aclCheck.authorizeFor(input.project, resources.read, fetchAllCached)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources looks like a variable and it's quite confusing 😅

override def apply(request: MultiFetchRequest)(implicit
caller: Caller
): UIO[MultiFetchResponse] = {
val fetchAllCached = aclCheck.fetchAll.memoizeOnSuccess
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you group by project then you don't have to cache everything, which is nice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It avoids to go through the list several times.
Also the acl check is not only done at the project level as it tries to check at the org level and the root level so this should make it a little faster as it only queries the db once

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a cache makes sense because it does the org level check then that's fine, but avoiding going through lists multiple times isn't something we should be concerned with. It would have no performance impact and we should optimise for readability instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really depends what you do when you are doing with your lists.
And doing multiple passes can hurt readibility too, there is not a golden rule for this.
Here doing it in a single pass with the traverse also guarantees that the items are kept in order

Comment on lines 40 to 42
if (authorized) resource.fold[Result](NotFound(input.id, input.project)) { content =>
Success(input.id, input.project, content)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is personal preference, but .getOrElse is much clearer intent than folding on an Option

Comment on lines 38 to 42
resource <- if (authorized) fetchResource(input) else UIO.none
} yield {
if (authorized) resource.fold[Result](NotFound(input.id, input.project)) { content =>
Success(input.id, input.project, content)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This double if(authorized) feels like it could be simplified

val encodeItem = itemEncoder(format)
resources.traverse(encodeItem).map { r =>
Json.obj(
"format" -> format.asJson,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the caller specifies the format, I wonder if it's useful to return it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding it does not hurt and as users of the API often gets confused between the formats, it can help to answer their questions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it hurts in the sense that it's code to maintain and which we test, but if you think it's worth it that's fine

Comment on lines +59 to +84
test("Return the response matching the user acls") {

val expected = MultiFetchResponse(
ResourceRepresentation.NTriples,
NonEmptyList.of(
Success(Latest(successId), project1, successContent),
NotFound(Latest(notFoundId), project1),
AuthorizationFailed(Latest(unauthorizedId), project2)
)
)

multiFetch(request).assert(expected)
}

test("Return only unauthorized for a user with no access") {
val expected = MultiFetchResponse(
ResourceRepresentation.NTriples,
NonEmptyList.of(
AuthorizationFailed(Latest(successId), project1),
AuthorizationFailed(Latest(notFoundId), project1),
AuthorizationFailed(Latest(unauthorizedId), project2)
)
)

multiFetch(request)(Caller.Anonymous).assert(expected)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider having more, less broad tests. For example, returning not found is probably one test. There are more tests but they are more straightforward and point to the defect more directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-fetch is supposed to return the resources in the order they are requested whether it is a success or not.
That is why I took this approach to validate also this part.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm just saying that these tests are basically testing multiple things

Another way of writing these tests would be to have:

  • A resource is found
  • A resource is missing
  • A resource has no authorization
  • Resources are in order
  • Resources are returned as type X

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree if the complexity was higher but here it is simple

Comment on lines 56 to 61
def apply(identities: Identities, aclCheck: AclCheck, multiFetch: MultiFetch)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering,
s: Scheduler
): Route = new MultiFetchRoutes(identities, aclCheck, multiFetch).routes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the purpose of these apply methods to avoid the use of new in the Modules?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are only used in the tests if I remember correctly.
I was in a auto-mode and stick to what has been done before but yes, their utility is questionable


The following example shows how to perform a multi-fetch and an example of response
containing errors (missing permissions and resource not found).
As a response, a json is returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be interesting to put emphasis on the fact that this is unlike other endpoints which return json-ld

@imsdu imsdu merged commit 221b199 into BlueBrain:master Aug 7, 2023
6 checks passed
@imsdu imsdu deleted the 4055-fetch-multi-resources branch August 7, 2023 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create a multi-get operation to fetch original payloads Create a multi-get operation to fetch resources
3 participants