-
Notifications
You must be signed in to change notification settings - Fork 74
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
Conversation
@@ -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._ |
There was a problem hiding this comment.
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
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") | ||
} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
if (authorized) resource.fold[Result](NotFound(input.id, input.project)) { content => | ||
Success(input.id, input.project, content) | ||
} |
There was a problem hiding this comment.
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 fold
ing on an Option
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) | ||
} |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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) | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
def apply(identities: Identities, aclCheck: AclCheck, multiFetch: MultiFetch)(implicit | ||
baseUri: BaseUri, | ||
cr: RemoteContextResolution, | ||
ordering: JsonKeyOrdering, | ||
s: Scheduler | ||
): Route = new MultiFetchRoutes(identities, aclCheck, multiFetch).routes |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
Fixes #4055
Fixes #4069