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
improvements to operation middleware #86
Conversation
Codecov Report
@@ Coverage Diff @@
## master #86 +/- ##
==========================================
+ Coverage 94.80% 95.25% +0.45%
==========================================
Files 11 11
Lines 250 274 +24
==========================================
+ Hits 237 261 +24
Misses 13 13
Continue to review full report at Codecov.
|
<Category> a hydra:Class ; | ||
hydra:supportedOperation | ||
<category#get>; | ||
hydra:supportedProperty [ | ||
hydra:property <post> | ||
] ; | ||
hydra:supportedProperty [ | ||
hydra:property <pinned-post> | ||
] | ||
. |
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 added Category class with those two properties to test the behavior of resources only used as objects of those properties
@@ -0,0 +1,3 @@ | |||
<http://localhost:9000/category/csvw> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://localhost:9000/api/schema/Category> . | |||
|
|||
<http://localhost:9000/category/csvw> <http://localhost:9000/api/schema/post> <http://localhost:9000/post/2> . |
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.
post/2
used only once here, will 404
@@ -0,0 +1,3 @@ | |||
<http://localhost:9000/category/rdf> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://localhost:9000/api/schema/Category> . | |||
|
|||
<http://localhost:9000/category/rdf> <http://localhost:9000/api/schema/post> <http://localhost:9000/post/3> . |
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.
post/3
used with two properties. Will give 405 when requested with a method supported by neither property
middleware.js
Outdated
router.use(waitFor(init, () => middleware.resource)) | ||
} | ||
router.use(waitFor(init, () => operation(api))) | ||
router.use(waitFor(init, () => operation(api, middleware.resource))) |
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 moved the resource middleware inside the operation because the resource
handler above may not set a single req.hydra.resource
.
lib/middleware/resource.js
Outdated
if (resources.length > 1) { | ||
return next(new Error(`no unique resource found for: <${req.hydra.term.value}>`)) | ||
debug('Multiple resource candidates found') | ||
return next() |
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.
Does not error immediately but instead set the potential "property operation" candidates to be filtered by the operation
middleware
lib/middleware/operation.js
Outdated
return clownface({ | ||
_context: [...matched._context, more._context], | ||
}) |
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 this the correct way to merge multiple clownface contexts?
I use this way to combine all potential matching operations for the property candidates
lib/middleware/resource.js
Outdated
if (resources.length > 1) { | ||
return next(new Error(`no unique resource found for: <${req.hydra.term.value}>`)) | ||
} |
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 check is moved up because only "multiple class resources" are an issue.
Multiple candidates for property operation can still be reconciled
if (middleware.resource) { | ||
router.use(middleware.resource) | ||
} | ||
router.use(invokeOperation) |
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'm a little concerned about this part. The middleware.resource
has to be aware that the req.hydra.resource
might be undefined if no resource+operation pair was selected.
It would make sense to move it inside invokeOperation
handler but because it is also an express middleware then the result might be a little awkward.
lib/middleware/operation.js
Outdated
if (operationMiddleware) { | ||
operations = operationMiddleware(operations, { req, res }) |
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.
Calling the operations middleware hook every time. This way an API might get a chance to select an operation even if none were found otherwise. Or replace those found with something else completely.
lib/middleware/operation.js
Outdated
if (operationMiddleware) { | ||
operations = operationMiddleware(operations, { req, res }) | ||
} |
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.
Calling the operations middleware hook every time. This way an API might get a chance to select an operation even if none were found otherwise. Or replace those found with something else completely.
if (middleware.resource) { | ||
router.use(middleware.resource) |
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'm a little concerned about this. The middleware.resource
must be aware that req.hydra.resource
can be undefined if no resource+operation pair was indeed found.
Maybe move it inside invokeOperation
somehow?
} | ||
|
||
return [{ resource, operation: null }] |
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.
Keeps the resources without any operations around to later determine if they are subject or object usage
a4e371b
to
4cc212b
Compare
lib/middleware/operation.js
Outdated
|
||
if (operations.length > 1) { | ||
error('Multiple operations found') | ||
return next(new Error('Ambiguous operation')) |
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'm not sure, but maybe we could use 409 Conflict?
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.
400 range is intended for client errors. Failure to find the right operations is 99% going to be a server issue.
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 most of the time it would be a missing or unclear rdf:type in a request body. But maybe it's better that the custom middleware explicitly sets that status code for such cases.
const { api } = req.hydra | ||
|
||
if (!req.hydra.operation) { | ||
const operationMap = mapOperations({ api, res }) |
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.
mapOperations
does not call operationMiddleware
, if given. For the edge case that an additional operation would be added with a different method, that method would not be included in the allowed list. I'm not sure if it needs to be covered. An additional operation would mean it was not discovered using the API description, so it's anyway somehow hidden. But it could be also the case, that we need it for inline operations, once we add that feature.
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 core benefit I'm looking for right now is ability to disambiguate operations if multiple are found.
While this also allows adding/replacing operations, I would say that one does on their own responsibility
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.
We can keep it like this now, but in a separate PR we should have a look at all the edge cases.
@@ -2,26 +2,21 @@ const { debug } = require('../log')('resource') | |||
|
|||
function factory ({ loader }) { | |||
return async (req, res, next) => { | |||
let resources = await loader.forClassOperation(req.hydra.term) | |||
const classResources = await loader.forClassOperation(req.hydra.term) | |||
const propertyObjectResources = await loader.forPropertyOperation(req.hydra.term) |
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 search for property operations can be more expensive on some stores/loaders. Maybe we can add an option to skip the property operation search if there is already a class operation. Just a remark for a possible future PR. This should not be covered in this PR.
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.
Right, that's how it was before pretty much. I would probably propose a separate PR to actually combine the resource.js and operation.js seeing how the former shrunk and only really calls the loader. This way should be easier to implement this again elegantly
lib/middleware/operation.js
Outdated
let operations = mapOperations({ api: req.hydra.api, res, method }) | ||
.filter(({ operation }) => operation) | ||
|
||
if (operationMiddleware) { |
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 operationMiddleware
is not a typical express middleware. I think that could be confusing. Last time we discussed it, I tended to use a more focused callback interface. Now that I have again a look at the code and see the existing middleware.resource
, I would prefer changing it to a normal express middlware. That means wrapping it with Router
to support all kinds of middlware arguments (plain function and arrays). operations
could be attached to req.hydra
.
2af0a95
to
81227db
Compare
81227db
to
ff22a44
Compare
const { api } = req.hydra | ||
|
||
if (!req.hydra.operation) { | ||
const operationMap = mapOperations({ api, res }) |
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.
We can keep it like this now, but in a separate PR we should have a look at all the edge cases.
lib/middleware/operation.js
Outdated
|
||
if (operations.length > 1) { | ||
error('Multiple operations found') | ||
return next(new Error('Ambiguous operation')) |
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 most of the time it would be a missing or unclear rdf:type in a request body. But maybe it's better that the custom middleware explicitly sets that status code for such cases.
No description provided.