From 130cfb27d1c64c3d7833aca2e3cacd2acb8fd17f Mon Sep 17 00:00:00 2001 From: tpluscode Date: Tue, 27 Oct 2020 19:33:09 +0100 Subject: [PATCH 01/13] test: failing cases for property operation --- examples/blog/api.ttl | 17 +++++++++++++++++ examples/blog/category.js | 7 +++++++ examples/blog/store-default/category%2Fcsvw.nt | 3 +++ .../category%2Fknowledge-graphs.nt | 3 +++ examples/blog/store-default/category%2Frdf.nt | 3 +++ .../category/multiple-orphaned-objects.hydra | 9 +++++++++ test-e2e/blog/category/orphaned-object.hydra | 10 ++++++++++ test-e2e/blog/post/comment.hydra | 1 - 8 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 examples/blog/category.js create mode 100644 examples/blog/store-default/category%2Fcsvw.nt create mode 100644 examples/blog/store-default/category%2Fknowledge-graphs.nt create mode 100644 examples/blog/store-default/category%2Frdf.nt create mode 100644 test-e2e/blog/category/multiple-orphaned-objects.hydra create mode 100644 test-e2e/blog/category/orphaned-object.hydra diff --git a/examples/blog/api.ttl b/examples/blog/api.ttl index d7e4dbb..1877a69 100644 --- a/examples/blog/api.ttl +++ b/examples/blog/api.ttl @@ -81,3 +81,20 @@ a hydra:Link; hydra:supportedOperation . + + a hydra:Class ; + hydra:supportedOperation + ; + hydra:supportedProperty [ + hydra:property + ] +. + + a hydra:SupportedOperation; + hydra:method "GET"; + hydra:returns ; + code:implementedBy [ a code:EcmaScript; + code:link + ]. + + a hydra:Link, rdf:Property . diff --git a/examples/blog/category.js b/examples/blog/category.js new file mode 100644 index 0000000..04028e2 --- /dev/null +++ b/examples/blog/category.js @@ -0,0 +1,7 @@ +function get (req, res) { + res.dataset(req.hydra.resource.dataset) +} + +module.exports = { + get +} diff --git a/examples/blog/store-default/category%2Fcsvw.nt b/examples/blog/store-default/category%2Fcsvw.nt new file mode 100644 index 0000000..1549ae5 --- /dev/null +++ b/examples/blog/store-default/category%2Fcsvw.nt @@ -0,0 +1,3 @@ + . + + . diff --git a/examples/blog/store-default/category%2Fknowledge-graphs.nt b/examples/blog/store-default/category%2Fknowledge-graphs.nt new file mode 100644 index 0000000..8a742e5 --- /dev/null +++ b/examples/blog/store-default/category%2Fknowledge-graphs.nt @@ -0,0 +1,3 @@ + . + + . diff --git a/examples/blog/store-default/category%2Frdf.nt b/examples/blog/store-default/category%2Frdf.nt new file mode 100644 index 0000000..bf24465 --- /dev/null +++ b/examples/blog/store-default/category%2Frdf.nt @@ -0,0 +1,3 @@ + . + + . diff --git a/test-e2e/blog/category/multiple-orphaned-objects.hydra b/test-e2e/blog/category/multiple-orphaned-objects.hydra new file mode 100644 index 0000000..56c12d3 --- /dev/null +++ b/test-e2e/blog/category/multiple-orphaned-objects.hydra @@ -0,0 +1,9 @@ +PREFIX api: + +ENTRYPOINT "category/csvw" + +With Class api:Category { + Expect Link api:post { + Expect Status 404 + } +} diff --git a/test-e2e/blog/category/orphaned-object.hydra b/test-e2e/blog/category/orphaned-object.hydra new file mode 100644 index 0000000..918ef68 --- /dev/null +++ b/test-e2e/blog/category/orphaned-object.hydra @@ -0,0 +1,10 @@ +PREFIX api: +PREFIX rdfs: + +ENTRYPOINT "category/rdf" + +With Class api:Category { + Expect Link api:post { + Expect Status 404 + } +} diff --git a/test-e2e/blog/post/comment.hydra b/test-e2e/blog/post/comment.hydra index d5abb38..f508c46 100644 --- a/test-e2e/blog/post/comment.hydra +++ b/test-e2e/blog/post/comment.hydra @@ -1,6 +1,5 @@ PREFIX api: PREFIX rdfs: -PREFIX rdfs: PREFIX dc: PREFIX schema: From 6b20c4e9e3be723a2d532ab77845056c8cef5f20 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Wed, 28 Oct 2020 08:56:24 +0100 Subject: [PATCH 02/13] fix: more appropriate responses to negative result --- StoreResourceLoader.js | 2 +- examples/blog/api.ttl | 10 ++ .../category%2Fknowledge-graphs.nt | 2 +- lib/middleware/operation.js | 108 +++++++++++++----- lib/middleware/resource.js | 12 +- middleware.js | 10 +- 6 files changed, 105 insertions(+), 39 deletions(-) diff --git a/StoreResourceLoader.js b/StoreResourceLoader.js index ca17249..66f2191 100644 --- a/StoreResourceLoader.js +++ b/StoreResourceLoader.js @@ -37,7 +37,7 @@ class StoreResourceLoader { for (const quad of dataset) { result.push({ - property: quad.property, + property: quad.predicate, object: quad.object, ...await this.load(quad.subject) }) diff --git a/examples/blog/api.ttl b/examples/blog/api.ttl index 1877a69..7cd9fa9 100644 --- a/examples/blog/api.ttl +++ b/examples/blog/api.ttl @@ -87,6 +87,9 @@ ; hydra:supportedProperty [ hydra:property + ] ; + hydra:supportedProperty [ + hydra:property ] . @@ -98,3 +101,10 @@ ]. a hydra:Link, rdf:Property . + + a hydra:Link, rdf:Property ; + hydra:supportedOperation [ + hydra:method "PUT" ; + code:implementedBy "(req, res, next) => res.status(204).end()"^^code:EcmaScript + ] +. diff --git a/examples/blog/store-default/category%2Fknowledge-graphs.nt b/examples/blog/store-default/category%2Fknowledge-graphs.nt index 8a742e5..2298bb5 100644 --- a/examples/blog/store-default/category%2Fknowledge-graphs.nt +++ b/examples/blog/store-default/category%2Fknowledge-graphs.nt @@ -1,3 +1,3 @@ . - . + . diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 330313c..112e907 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -1,7 +1,9 @@ +const { Router } = require('express') const { error, warn } = require('../log')('operation') const createError = require('http-errors') const clownface = require('clownface') const ns = require('@tpluscode/rdf-ns-builders') +const TermSet = require('@rdfjs/term-set') const { code } = require('../namespaces') function findClassOperations (types, method) { @@ -14,9 +16,9 @@ function findClassOperations (types, method) { return operations } -function findPropertyOperations ({ types, method, term, resource }) { +function findCandidatePropertyOperations ({ types, method, term, resource }) { const properties = [...resource.dataset - .match(resource.term, null, term)] + .match(resource.term, resource.property, term)] .map(({ predicate }) => predicate) let operations = types @@ -32,34 +34,43 @@ function findPropertyOperations ({ types, method, term, resource }) { return operations } -function factory (api) { - return async (req, res, next) => { - if (!req.hydra.resource) { - return next() - } +function findPropertyOperations ({ term, resourceCandidates, api, method }) { + const apiGraph = clownface(api) - const method = req.method === 'HEAD' ? 'GET' : req.method - const types = clownface({ ...api, term: [...req.hydra.resource.types] }) + return resourceCandidates + .reduce((matched, resource) => { + const types = apiGraph.node([...resource.types]) - let operations - if (req.hydra.term.equals(req.hydra.resource.term)) { - // only look for direct operation when the root resource is requested - operations = findClassOperations(types, method) - } - if (!operations) { - // otherwise try finding the operation by property usage - operations = findPropertyOperations({ types, method, term: req.hydra.term, resource: req.hydra.resource }) - } + const more = findCandidatePropertyOperations({ types, method, term, resource }) + + if (!matched) { + return more + } + + if (more.terms.length === 0) { + return matched + } - const [operation, ...rest] = (operations || []).toArray() + return clownface({ + _context: [...matched._context, more._context], + }) + }, null) +} - if (!operation) { +function invokeOperation(api) { + return async function (req, res, next) { + if (!req.hydra.operation) { warn('no operations found') let allowedOperations - if (req.hydra.term.equals(req.hydra.resource.term)) { + if (req.hydra.resource) { + const types = clownface({ ...api, term: [...req.hydra.resource.types] }) allowedOperations = findClassOperations(types) } else { - allowedOperations = findPropertyOperations({ types, term: req.hydra.term, resource: req.hydra.resource }) + allowedOperations = findPropertyOperations({ term: req.hydra.term, resourceCandidates: res.locals.hydra.resourceCandidates, api }) + } + + if (allowedOperations.values.length === 0) { + return next(new createError.NotFound()) } const allowedMethods = new Set(allowedOperations.out(ns.hydra.method).values) @@ -67,20 +78,59 @@ function factory (api) { return next(new createError.MethodNotAllowed()) } - if (rest.length > 0) { - error('Multiple operations found') - return next(new Error('Ambiguous operation')) - } - - const handler = await api.loaderRegistry.load(operation.out(code.implementedBy), { basePath: api.codePath }) + const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), {basePath: api.codePath}) if (!handler) { return next(new Error('Failed to load operation')) } - req.hydra.operation = operation handler(req, res, next) } } +function setOperation(req, next, [operation, ...rest] = []) { + if (rest.length > 0) { + error('Multiple operations found') + return next(new Error('Ambiguous operation')) + } + + req.hydra.operation = operation + return next() +} + +function factory (api, resourceMiddleware) { + const router = Router() + + router.use(async (req, res, next) => { + const method = req.method === 'HEAD' ? 'GET' : req.method + + if (res.locals.hydra.resourceCandidates) { + // try finding the operation by property usage + const operations = findPropertyOperations({ term: req.hydra.term, resourceCandidates: res.locals.hydra.resourceCandidates, api, method }).toArray() + + if (operations.length === 0) { + // return next(new createError.NotFound()) + } + + return setOperation(req, next, operations) + } + + if (!req.hydra.resource) { + return next() + } + + // look for direct operation when the root resource is requested + const types = clownface({ ...api, term: [...req.hydra.resource.types] }) + + return setOperation(req, next, findClassOperations(types, method).toArray()) + }) + + if (resourceMiddleware) { + router.use(resourceMiddleware) + } + router.use(invokeOperation(api)) + + return router +} + module.exports = factory diff --git a/lib/middleware/resource.js b/lib/middleware/resource.js index 25670b0..f115df3 100644 --- a/lib/middleware/resource.js +++ b/lib/middleware/resource.js @@ -4,12 +4,16 @@ function factory ({ loader }) { return async (req, res, next) => { let resources = await loader.forClassOperation(req.hydra.term) + if (resources.length > 1) { + return next(new Error(`no unique resource found for: <${req.hydra.term.value}>`)) + } + if (resources.length === 0) { resources = await loader.forPropertyOperation(req.hydra.term) - } + res.locals.hydra.resourceCandidates = resources - if (resources.length > 1) { - return next(new Error(`no unique resource found for: <${req.hydra.term.value}>`)) + debug('Multiple resource candidates found') + return next() } req.hydra.resource = resources[0] @@ -21,7 +25,7 @@ function factory ({ loader }) { debug(`no matching resource found: ${req.hydra.term.value}`) } - next() + return next() } } diff --git a/middleware.js b/middleware.js index b523567..0d4849b 100644 --- a/middleware.js +++ b/middleware.js @@ -61,6 +61,11 @@ function middleware (api, { baseIriFromRequest, loader, store, middleware = {} } router.use(waitFor(init, () => apiHeader(api))) router.use(waitFor(init, () => iriTemplate(api))) + router.use((req, res, next) => { + res.locals.hydra = {} + next() + }) + if (loader) { router.use(resource({ loader })) } else if (store) { @@ -69,10 +74,7 @@ function middleware (api, { baseIriFromRequest, loader, store, middleware = {} } throw new Error('no loader or store provided') } - if (middleware.resource) { - router.use(waitFor(init, () => middleware.resource)) - } - router.use(waitFor(init, () => operation(api))) + router.use(waitFor(init, () => operation(api, middleware.resource))) return router } From 1e33e7c52fc18dc2b4dee54d28f252cf36cd299b Mon Sep 17 00:00:00 2001 From: tpluscode Date: Wed, 28 Oct 2020 09:05:33 +0100 Subject: [PATCH 03/13] test: update e2e scenarios --- .../blog/category/multiple-orphaned-objects.hydra | 9 --------- .../category/orphaned-object-with-operation.hydra | 11 +++++++++++ test-e2e/blog/category/orphaned-object.hydra | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) delete mode 100644 test-e2e/blog/category/multiple-orphaned-objects.hydra create mode 100644 test-e2e/blog/category/orphaned-object-with-operation.hydra diff --git a/test-e2e/blog/category/multiple-orphaned-objects.hydra b/test-e2e/blog/category/multiple-orphaned-objects.hydra deleted file mode 100644 index 56c12d3..0000000 --- a/test-e2e/blog/category/multiple-orphaned-objects.hydra +++ /dev/null @@ -1,9 +0,0 @@ -PREFIX api: - -ENTRYPOINT "category/csvw" - -With Class api:Category { - Expect Link api:post { - Expect Status 404 - } -} diff --git a/test-e2e/blog/category/orphaned-object-with-operation.hydra b/test-e2e/blog/category/orphaned-object-with-operation.hydra new file mode 100644 index 0000000..15f0d3f --- /dev/null +++ b/test-e2e/blog/category/orphaned-object-with-operation.hydra @@ -0,0 +1,11 @@ +PREFIX api: +PREFIX rdfs: + +ENTRYPOINT "category/rdf" + +With Class api:Category { + Expect Link api:post { + // will return 405 because it has an operation supported by property + Expect Status 405 + } +} diff --git a/test-e2e/blog/category/orphaned-object.hydra b/test-e2e/blog/category/orphaned-object.hydra index 918ef68..ade699a 100644 --- a/test-e2e/blog/category/orphaned-object.hydra +++ b/test-e2e/blog/category/orphaned-object.hydra @@ -1,10 +1,10 @@ PREFIX api: -PREFIX rdfs: -ENTRYPOINT "category/rdf" +ENTRYPOINT "category/csvw" With Class api:Category { Expect Link api:post { + // will return 404 because it is an triple object but none of the properties have any operations Expect Status 404 } } From 822470c5899082f8df7c4285e78db865b0dbd6a8 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Wed, 28 Oct 2020 09:17:50 +0100 Subject: [PATCH 04/13] refactor: remove commented out code --- lib/middleware/operation.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 112e907..8acedd9 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -108,10 +108,6 @@ function factory (api, resourceMiddleware) { // try finding the operation by property usage const operations = findPropertyOperations({ term: req.hydra.term, resourceCandidates: res.locals.hydra.resourceCandidates, api, method }).toArray() - if (operations.length === 0) { - // return next(new createError.NotFound()) - } - return setOperation(req, next, operations) } From 658f98008f3b1e8343ebbf0c1507a9e8005938ef Mon Sep 17 00:00:00 2001 From: tpluscode Date: Wed, 28 Oct 2020 09:25:07 +0100 Subject: [PATCH 05/13] refactor: extract and combine --- lib/middleware/operation.js | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 8acedd9..d955cc9 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -88,38 +88,46 @@ function invokeOperation(api) { } } -function setOperation(req, next, [operation, ...rest] = []) { - if (rest.length > 0) { - error('Multiple operations found') - return next(new Error('Ambiguous operation')) - } - - req.hydra.operation = operation - return next() -} - -function factory (api, resourceMiddleware) { - const router = Router() - - router.use(async (req, res, next) => { +function findOperation(api) { + return async function (req, res, next) { + let operation, rest const method = req.method === 'HEAD' ? 'GET' : req.method if (res.locals.hydra.resourceCandidates) { // try finding the operation by property usage - const operations = findPropertyOperations({ term: req.hydra.term, resourceCandidates: res.locals.hydra.resourceCandidates, api, method }).toArray() - - return setOperation(req, next, operations) + [operation, ...rest] = findPropertyOperations({ + term: req.hydra.term, + resourceCandidates: res.locals.hydra.resourceCandidates, + api, + method + }).toArray() } - if (!req.hydra.resource) { + if (!operation && !req.hydra.resource) { return next() } - // look for direct operation when the root resource is requested - const types = clownface({ ...api, term: [...req.hydra.resource.types] }) + if (!operation) { + // look for direct operation when the root resource is requested + const types = clownface({...api, term: [...req.hydra.resource.types]}) + + ;[operation, ...rest] = findClassOperations(types, method).toArray() + } + + if (rest.length > 0) { + error('Multiple operations found') + return next(new Error('Ambiguous operation')) + } + + req.hydra.operation = operation + return next() + } +} + +function factory (api, resourceMiddleware) { + const router = Router() - return setOperation(req, next, findClassOperations(types, method).toArray()) - }) + router.use(findOperation(api)) if (resourceMiddleware) { router.use(resourceMiddleware) From e496264664357d5cdd8e94189bb4f75774027974 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Thu, 26 Nov 2020 20:10:51 +0100 Subject: [PATCH 06/13] style: fix js standard --- lib/middleware/operation.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index d955cc9..4383ed7 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -3,7 +3,6 @@ const { error, warn } = require('../log')('operation') const createError = require('http-errors') const clownface = require('clownface') const ns = require('@tpluscode/rdf-ns-builders') -const TermSet = require('@rdfjs/term-set') const { code } = require('../namespaces') function findClassOperations (types, method) { @@ -52,12 +51,12 @@ function findPropertyOperations ({ term, resourceCandidates, api, method }) { } return clownface({ - _context: [...matched._context, more._context], + _context: [...matched._context, more._context] }) }, null) } -function invokeOperation(api) { +function invokeOperation (api) { return async function (req, res, next) { if (!req.hydra.operation) { warn('no operations found') @@ -78,7 +77,7 @@ function invokeOperation(api) { return next(new createError.MethodNotAllowed()) } - const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), {basePath: api.codePath}) + const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), { basePath: api.codePath }) if (!handler) { return next(new Error('Failed to load operation')) @@ -88,7 +87,7 @@ function invokeOperation(api) { } } -function findOperation(api) { +function findOperation (api) { return async function (req, res, next) { let operation, rest const method = req.method === 'HEAD' ? 'GET' : req.method @@ -109,7 +108,7 @@ function findOperation(api) { if (!operation) { // look for direct operation when the root resource is requested - const types = clownface({...api, term: [...req.hydra.resource.types]}) + const types = clownface({ ...api, term: [...req.hydra.resource.types] }) ;[operation, ...rest] = findClassOperations(types, method).toArray() } From 61d834ca64fb146b96f6517e1bcf31a6ea29873c Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 11:09:22 +0100 Subject: [PATCH 07/13] process operations paired with resources --- lib/middleware/operation.js | 80 +++++++++++++++------------- lib/middleware/resource.js | 26 ++++------ test/operation.test.js | 101 +++++++++++++++++++----------------- 3 files changed, 109 insertions(+), 98 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 4383ed7..f44c6a0 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -15,9 +15,9 @@ function findClassOperations (types, method) { return operations } -function findCandidatePropertyOperations ({ types, method, term, resource }) { +function findCandidatePropertyOperations ({ types, method, resource }) { const properties = [...resource.dataset - .match(resource.term, resource.property, term)] + .match(resource.term, resource.property, resource.object)] .map(({ predicate }) => predicate) let operations = types @@ -33,14 +33,14 @@ function findCandidatePropertyOperations ({ types, method, term, resource }) { return operations } -function findPropertyOperations ({ term, resourceCandidates, api, method }) { +function findPropertyOperations ({ resource, api, method }) { const apiGraph = clownface(api) - return resourceCandidates + return [resource] .reduce((matched, resource) => { const types = apiGraph.node([...resource.types]) - const more = findCandidatePropertyOperations({ types, method, term, resource }) + const more = findCandidatePropertyOperations({ types, method, resource }) if (!matched) { return more @@ -56,23 +56,45 @@ function findPropertyOperations ({ term, resourceCandidates, api, method }) { }, null) } +function mapOperations ({ api, req, res, method }) { + return res.locals.hydra.resources.flatMap((resource) => { + let moreOperations + if ('property' in resource) { + moreOperations = findPropertyOperations({ + term: resource.object, + resource, + method, + api + }).toArray() + } else { + const types = clownface({ ...api, term: [...resource.types] }) + moreOperations = findClassOperations(types, method).toArray() + } + + if (moreOperations.length) { + return moreOperations.map(operation => ({ resource, operation })) + } + + return [{ resource, operation: null }] + }) +} + function invokeOperation (api) { return async function (req, res, next) { if (!req.hydra.operation) { - warn('no operations found') - let allowedOperations - if (req.hydra.resource) { - const types = clownface({ ...api, term: [...req.hydra.resource.types] }) - allowedOperations = findClassOperations(types) - } else { - allowedOperations = findPropertyOperations({ term: req.hydra.term, resourceCandidates: res.locals.hydra.resourceCandidates, api }) + const operationMap = mapOperations({ api, req, res }) + const allowedMethods = new Set(operationMap + .filter(({ operation }) => operation) + .flatMap(({ operation }) => operation.out(ns.hydra.method).values)) + + if (!allowedMethods.size) { + warn('no operations found') + if (operationMap.every(({ resource }) => 'property' in resource)) { + return next(new createError.NotFound()) + } + return next(new createError.MethodNotAllowed()) } - if (allowedOperations.values.length === 0) { - return next(new createError.NotFound()) - } - - const allowedMethods = new Set(allowedOperations.out(ns.hydra.method).values) res.setHeader('Allow', [...allowedMethods]) return next(new createError.MethodNotAllowed()) } @@ -89,35 +111,21 @@ function invokeOperation (api) { function findOperation (api) { return async function (req, res, next) { - let operation, rest const method = req.method === 'HEAD' ? 'GET' : req.method - if (res.locals.hydra.resourceCandidates) { - // try finding the operation by property usage - [operation, ...rest] = findPropertyOperations({ - term: req.hydra.term, - resourceCandidates: res.locals.hydra.resourceCandidates, - api, - method - }).toArray() - } + const operations = mapOperations({ api, req, res, method }).filter(({ operation }) => operation) - if (!operation && !req.hydra.resource) { + if (!operations.length) { return next() } - if (!operation) { - // look for direct operation when the root resource is requested - const types = clownface({ ...api, term: [...req.hydra.resource.types] }) - - ;[operation, ...rest] = findClassOperations(types, method).toArray() - } - - if (rest.length > 0) { + if (operations.length > 1) { error('Multiple operations found') return next(new Error('Ambiguous operation')) } + const [{ resource, operation }] = operations + req.hydra.resource = resource req.hydra.operation = operation return next() } diff --git a/lib/middleware/resource.js b/lib/middleware/resource.js index f115df3..5df343e 100644 --- a/lib/middleware/resource.js +++ b/lib/middleware/resource.js @@ -2,27 +2,23 @@ const { debug } = require('../log')('resource') function factory ({ loader }) { return async (req, res, next) => { - let resources = await loader.forClassOperation(req.hydra.term) - - if (resources.length > 1) { - return next(new Error(`no unique resource found for: <${req.hydra.term.value}>`)) + const classResources = await loader.forClassOperation(req.hydra.term) + const propertyObjectResources = await loader.forPropertyOperation(req.hydra.term) + + res.locals.hydra.resources = [ + ...classResources, + ...propertyObjectResources + ] + if (res.locals.hydra.resources.length === 0) { + debug(`no matching resource found: ${req.hydra.term.value}`) } - - if (resources.length === 0) { - resources = await loader.forPropertyOperation(req.hydra.term) - res.locals.hydra.resourceCandidates = resources - - debug('Multiple resource candidates found') - return next() + if (res.locals.hydra.resources.length > 1) { + debug('multiple resource candidates found') } - req.hydra.resource = resources[0] - if (req.hydra.resource) { debug(`IRI: ${req.hydra.resource.term.value}`) debug(`types: ${[...req.hydra.resource.types].map(term => term.value).join(' ')}`) - } else { - debug(`no matching resource found: ${req.hydra.term.value}`) } return next() diff --git a/test/operation.test.js b/test/operation.test.js index bb65dd3..185d9e3 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -32,14 +32,32 @@ describe('middleware/operation', () => { }) }) - function hydraMock ({ types = [], term, dataset = RDF.dataset() } = {}, rootResource) { + function testResource({ types = [], term, dataset = RDF.dataset(), property, object } = {}) { + if (property && object) { + return { + term, + types, + dataset, + property, + object + } + } + + return { + term, + types, + dataset + } + } + + function hydraMock (...resources) { return function (req, res, next) { req.hydra = { term: RDF.namedNode(req.url), - resource: { - dataset, - term: rootResource ? RDF.namedNode(rootResource) : term || RDF.namedNode(req.url), - types + } + res.locals = { + hydra: { + resources } } next() @@ -51,6 +69,11 @@ describe('middleware/operation', () => { const app = express() app.use((req, res, next) => { req.hydra = {} + res.locals = { + hydra: { + resources: [] + } + } next() }) app.use(middleware(api)) @@ -65,9 +88,9 @@ describe('middleware/operation', () => { it('calls next middleware when class is not supported', async () => { // given const app = express() - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.NoSuchClass] - })) + }))) app.use(middleware(api)) // when @@ -77,12 +100,12 @@ describe('middleware/operation', () => { assert.strictEqual(response.status, 405) }) - it('returns 405 Method Not Found when no operation is found', async () => { + it('returns 405 Method Not Allowed when no operation is found', async () => { // given const app = express() - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person] - })) + }))) app.use(middleware(api)) // when @@ -96,9 +119,9 @@ describe('middleware/operation', () => { it('calls GET handler when HEAD is requested', async () => { // given const app = express() - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person] - })) + }))) app.use(middleware(api)) // when @@ -119,11 +142,13 @@ describe('middleware/operation', () => { clownface({ dataset }) .namedNode('/john-doe') .addOut(NS.friends, RDF.namedNode('/friends')) - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person], - term: '/friends', + term: RDF.namedNode('/john-doe'), + property: NS.friends, + object: RDF.namedNode('/friends'), dataset - }, '/john-doe')) + }))) app.use(middleware(api)) // when @@ -144,11 +169,13 @@ describe('middleware/operation', () => { clownface({ dataset }) .namedNode('/john-doe') .addOut(NS.friends, RDF.namedNode('/friends')) - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person], - term: '/friends', + term: RDF.namedNode('/john-doe'), + property: NS.friends, + object: RDF.namedNode('/friends'), dataset - }, '/john-doe')) + }))) app.use(middleware(api)) // when @@ -159,34 +186,12 @@ describe('middleware/operation', () => { assert.strictEqual(response.headers.allow, 'POST') }) - it('does not call supported property operation when resource does not match', async () => { - // given - const app = express() - const dataset = RDF.dataset() - clownface({ dataset }) - .namedNode('/john-doe') - .addOut(NS.friends, RDF.namedNode('/friends')) - app.use(hydraMock({ - types: [NS.Person], - term: '/friends', - dataset - }, '/john-doe')) - app.use(middleware(api)) - - // when - const response = await request(app).post('/john-doe') - - // then - assert.strictEqual(response.status, 405) - assert(api.loaderRegistry.load.notCalled) - }) - it('throws when multiple operations are found for class', async () => { // given const app = express() - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person] - })) + }))) app.use(middleware(api)) // when @@ -203,11 +208,13 @@ describe('middleware/operation', () => { clownface({ dataset }) .namedNode('/john-doe') .addOut(NS.interests, RDF.namedNode('/john-doe/interests')) - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person], - term: '/john-doe/interests', + term: RDF.namedNode('/john-doe'), + property: NS.interests, + object: RDF.namedNode('/john-doe/interests'), dataset - }, '/john-doe')) + }))) app.use(middleware(api)) // when @@ -220,9 +227,9 @@ describe('middleware/operation', () => { it('throws when operation fails to load', async () => { // given const app = express() - app.use(hydraMock({ + app.use(hydraMock(testResource({ types: [NS.Person] - })) + }))) app.use(middleware(api)) // when From 1f1e83c994286eeffdca84f106024a523e8c8cf4 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 11:19:41 +0100 Subject: [PATCH 08/13] refactor: process operations paired with resources --- lib/middleware/operation.js | 40 ++++++++----------------------------- test/middleware.test.js | 15 +++++++------- test/operation.test.js | 4 ++-- test/resource.test.js | 1 + 4 files changed, 19 insertions(+), 41 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index f44c6a0..08eddf4 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -15,7 +15,10 @@ function findClassOperations (types, method) { return operations } -function findCandidatePropertyOperations ({ types, method, resource }) { +function findPropertyOperations ({ resource, api, method }) { + const apiGraph = clownface(api) + const types = apiGraph.node([...resource.types]) + const properties = [...resource.dataset .match(resource.term, resource.property, resource.object)] .map(({ predicate }) => predicate) @@ -33,39 +36,11 @@ function findCandidatePropertyOperations ({ types, method, resource }) { return operations } -function findPropertyOperations ({ resource, api, method }) { - const apiGraph = clownface(api) - - return [resource] - .reduce((matched, resource) => { - const types = apiGraph.node([...resource.types]) - - const more = findCandidatePropertyOperations({ types, method, resource }) - - if (!matched) { - return more - } - - if (more.terms.length === 0) { - return matched - } - - return clownface({ - _context: [...matched._context, more._context] - }) - }, null) -} - -function mapOperations ({ api, req, res, method }) { +function mapOperations ({ api, res, method }) { return res.locals.hydra.resources.flatMap((resource) => { let moreOperations if ('property' in resource) { - moreOperations = findPropertyOperations({ - term: resource.object, - resource, - method, - api - }).toArray() + moreOperations = findPropertyOperations({ resource, method, api }).toArray() } else { const types = clownface({ ...api, term: [...resource.types] }) moreOperations = findClassOperations(types, method).toArray() @@ -113,7 +88,8 @@ function findOperation (api) { return async function (req, res, next) { const method = req.method === 'HEAD' ? 'GET' : req.method - const operations = mapOperations({ api, req, res, method }).filter(({ operation }) => operation) + const operations = mapOperations({ api, req, res, method }) + .filter(({ operation }) => operation) if (!operations.length) { return next() diff --git a/test/middleware.test.js b/test/middleware.test.js index 4db1c25..ee25f26 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -38,7 +38,8 @@ describe('hydra-box', () => { const middleware = sinon.spy((req, res, next) => next()) app.use(hydraBox(api, { loader: { - forClassOperation: () => [loadedResource] + forClassOperation: () => [loadedResource], + forPropertyOperation: () => [] }, middleware: { resource: middleware @@ -66,7 +67,8 @@ describe('hydra-box', () => { ] app.use(hydraBox(api, { loader: { - forClassOperation: () => [loadedResource] + forClassOperation: () => [loadedResource], + forPropertyOperation: () => [] }, middleware: { resource: middlewares @@ -84,20 +86,19 @@ describe('hydra-box', () => { it('calls resource middleware after loader', async () => { // given - let receivedResource const loadedResource = { dataset: RDF.dataset(), term: RDF.blankNode(), types: [] } const app = express() - const middleware = sinon.spy((req, res, next) => { - receivedResource = req.hydra.resource + const middleware = sinon.stub().callsFake((req, res, next) => { next() }) app.use(hydraBox(api, { loader: { - forClassOperation: () => [loadedResource] + forClassOperation: () => [loadedResource], + forPropertyOperation: () => [] }, middleware: { resource: middleware @@ -108,7 +109,7 @@ describe('hydra-box', () => { await request(app).get('/') // then - assert.strictEqual(receivedResource, loadedResource) + assert(middleware.called) }) describe('api', () => { diff --git a/test/operation.test.js b/test/operation.test.js index 185d9e3..2b3caa5 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -32,7 +32,7 @@ describe('middleware/operation', () => { }) }) - function testResource({ types = [], term, dataset = RDF.dataset(), property, object } = {}) { + function testResource ({ types = [], term, dataset = RDF.dataset(), property, object } = {}) { if (property && object) { return { term, @@ -53,7 +53,7 @@ describe('middleware/operation', () => { function hydraMock (...resources) { return function (req, res, next) { req.hydra = { - term: RDF.namedNode(req.url), + term: RDF.namedNode(req.url) } res.locals = { hydra: { diff --git a/test/resource.test.js b/test/resource.test.js index 805073b..5e4d185 100644 --- a/test/resource.test.js +++ b/test/resource.test.js @@ -23,6 +23,7 @@ describe('middleware/resource', () => { req.hydra = { term: RDF.namedNode(req.url) } + res.locals.hydra = {} next() } From 28132f8e25645d3b54fc092c62d2a9eaed37f824 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 11:44:45 +0100 Subject: [PATCH 09/13] feat: add operations hook --- lib/middleware/operation.js | 16 ++++++++++------ middleware.js | 2 +- test/operation.test.js | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 08eddf4..60a28ce 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -84,13 +84,17 @@ function invokeOperation (api) { } } -function findOperation (api) { +function findOperation (api, operationMiddleware) { return async function (req, res, next) { const method = req.method === 'HEAD' ? 'GET' : req.method - const operations = mapOperations({ api, req, res, method }) + let operations = mapOperations({ api, req, res, method }) .filter(({ operation }) => operation) + if (operationMiddleware) { + operations = operationMiddleware(operations, { req, res }) + } + if (!operations.length) { return next() } @@ -107,13 +111,13 @@ function findOperation (api) { } } -function factory (api, resourceMiddleware) { +function factory (api, middleware = {}) { const router = Router() - router.use(findOperation(api)) + router.use(findOperation(api, middleware.operations)) - if (resourceMiddleware) { - router.use(resourceMiddleware) + if (middleware.resource) { + router.use(middleware.resource) } router.use(invokeOperation(api)) diff --git a/middleware.js b/middleware.js index 0d4849b..f804268 100644 --- a/middleware.js +++ b/middleware.js @@ -74,7 +74,7 @@ function middleware (api, { baseIriFromRequest, loader, store, middleware = {} } throw new Error('no loader or store provided') } - router.use(waitFor(init, () => operation(api, middleware.resource))) + router.use(waitFor(init, () => operation(api, middleware))) return router } diff --git a/test/operation.test.js b/test/operation.test.js index 2b3caa5..7f80540 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -85,6 +85,25 @@ describe('middleware/operation', () => { assert.strictEqual(response.status, 404) }) + it('calls operations middleware hook when defined', async () => { + // given + const app = express() + app.use(hydraMock(testResource({ + types: [NS.Person] + }))) + const hooks = { + operations: sinon.stub().callsFake(operations => operations) + } + app.use(middleware(api, hooks)) + + // when + const response = await request(app).get('/') + + // then + assert(hooks.operations.called) + assert.strictEqual(response.status, 200) + }) + it('calls next middleware when class is not supported', async () => { // given const app = express() From 60b4cb357a197686165e0e0b52fca0a6df35fccf Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 11:51:34 +0100 Subject: [PATCH 10/13] refactor: remove api param from operation middleware factory --- lib/middleware/operation.js | 52 ++++++++++++++++++------------------- middleware.js | 2 +- test/operation.test.js | 19 +++++++------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 60a28ce..ddd54b3 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -54,41 +54,41 @@ function mapOperations ({ api, res, method }) { }) } -function invokeOperation (api) { - return async function (req, res, next) { - if (!req.hydra.operation) { - const operationMap = mapOperations({ api, req, res }) - const allowedMethods = new Set(operationMap - .filter(({ operation }) => operation) - .flatMap(({ operation }) => operation.out(ns.hydra.method).values)) - - if (!allowedMethods.size) { - warn('no operations found') - if (operationMap.every(({ resource }) => 'property' in resource)) { - return next(new createError.NotFound()) - } - return next(new createError.MethodNotAllowed()) - } +async function invokeOperation (req, res, next) { + const { api } = req.hydra + + if (!req.hydra.operation) { + const operationMap = mapOperations({ api, res }) + const allowedMethods = new Set(operationMap + .filter(({ operation }) => operation) + .flatMap(({ operation }) => operation.out(ns.hydra.method).values)) - res.setHeader('Allow', [...allowedMethods]) + if (!allowedMethods.size) { + warn('no operations found') + if (operationMap.every(({ resource }) => 'property' in resource)) { + return next(new createError.NotFound()) + } return next(new createError.MethodNotAllowed()) } - const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), { basePath: api.codePath }) + res.setHeader('Allow', [...allowedMethods]) + return next(new createError.MethodNotAllowed()) + } - if (!handler) { - return next(new Error('Failed to load operation')) - } + const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), { basePath: api.codePath }) - handler(req, res, next) + if (!handler) { + return next(new Error('Failed to load operation')) } + + handler(req, res, next) } -function findOperation (api, operationMiddleware) { +function findOperation (operationMiddleware) { return async function (req, res, next) { const method = req.method === 'HEAD' ? 'GET' : req.method - let operations = mapOperations({ api, req, res, method }) + let operations = mapOperations({ api: req.hydra.api, res, method }) .filter(({ operation }) => operation) if (operationMiddleware) { @@ -111,15 +111,15 @@ function findOperation (api, operationMiddleware) { } } -function factory (api, middleware = {}) { +function factory (middleware = {}) { const router = Router() - router.use(findOperation(api, middleware.operations)) + router.use(findOperation(middleware.operations)) if (middleware.resource) { router.use(middleware.resource) } - router.use(invokeOperation(api)) + router.use(invokeOperation) return router } diff --git a/middleware.js b/middleware.js index f804268..24445d9 100644 --- a/middleware.js +++ b/middleware.js @@ -74,7 +74,7 @@ function middleware (api, { baseIriFromRequest, loader, store, middleware = {} } throw new Error('no loader or store provided') } - router.use(waitFor(init, () => operation(api, middleware))) + router.use(operation(middleware)) return router } diff --git a/test/operation.test.js b/test/operation.test.js index 7f80540..c2f34b7 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -53,6 +53,7 @@ describe('middleware/operation', () => { function hydraMock (...resources) { return function (req, res, next) { req.hydra = { + api, term: RDF.namedNode(req.url) } res.locals = { @@ -94,7 +95,7 @@ describe('middleware/operation', () => { const hooks = { operations: sinon.stub().callsFake(operations => operations) } - app.use(middleware(api, hooks)) + app.use(middleware(hooks)) // when const response = await request(app).get('/') @@ -110,7 +111,7 @@ describe('middleware/operation', () => { app.use(hydraMock(testResource({ types: [NS.NoSuchClass] }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).get('/') @@ -125,7 +126,7 @@ describe('middleware/operation', () => { app.use(hydraMock(testResource({ types: [NS.Person] }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).patch('/') @@ -141,7 +142,7 @@ describe('middleware/operation', () => { app.use(hydraMock(testResource({ types: [NS.Person] }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).head('/') @@ -168,7 +169,7 @@ describe('middleware/operation', () => { object: RDF.namedNode('/friends'), dataset }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).post('/friends') @@ -195,7 +196,7 @@ describe('middleware/operation', () => { object: RDF.namedNode('/friends'), dataset }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).patch('/friends') @@ -211,7 +212,7 @@ describe('middleware/operation', () => { app.use(hydraMock(testResource({ types: [NS.Person] }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).delete('/foo') @@ -234,7 +235,7 @@ describe('middleware/operation', () => { object: RDF.namedNode('/john-doe/interests'), dataset }))) - app.use(middleware(api)) + app.use(middleware()) // when const response = await request(app).get('/john-doe/interests') @@ -249,7 +250,7 @@ describe('middleware/operation', () => { app.use(hydraMock(testResource({ types: [NS.Person] }))) - app.use(middleware(api)) + app.use(middleware()) // when api.loaderRegistry.load.returns(null) From f03041aa48f3e5cc929da881c9774640a9bfab18 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 12:05:39 +0100 Subject: [PATCH 11/13] style: reorder functions to improve diff --- lib/middleware/operation.js | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index ddd54b3..9d9773f 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -54,6 +54,33 @@ function mapOperations ({ api, res, method }) { }) } +function findOperation (operationMiddleware) { + return async function (req, res, next) { + const method = req.method === 'HEAD' ? 'GET' : req.method + + let operations = mapOperations({ api: req.hydra.api, res, method }) + .filter(({ operation }) => operation) + + if (operationMiddleware) { + operations = operationMiddleware(operations, { req, res }) + } + + if (!operations.length) { + return next() + } + + if (operations.length > 1) { + error('Multiple operations found') + return next(new Error('Ambiguous operation')) + } + + const [{ resource, operation }] = operations + req.hydra.resource = resource + req.hydra.operation = operation + return next() + } +} + async function invokeOperation (req, res, next) { const { api } = req.hydra @@ -84,33 +111,6 @@ async function invokeOperation (req, res, next) { handler(req, res, next) } -function findOperation (operationMiddleware) { - return async function (req, res, next) { - const method = req.method === 'HEAD' ? 'GET' : req.method - - let operations = mapOperations({ api: req.hydra.api, res, method }) - .filter(({ operation }) => operation) - - if (operationMiddleware) { - operations = operationMiddleware(operations, { req, res }) - } - - if (!operations.length) { - return next() - } - - if (operations.length > 1) { - error('Multiple operations found') - return next(new Error('Ambiguous operation')) - } - - const [{ resource, operation }] = operations - req.hydra.resource = resource - req.hydra.operation = operation - return next() - } -} - function factory (middleware = {}) { const router = Router() From 95f617e5bcb2bc0367a586cb10b0b4048e677df6 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Fri, 27 Nov 2020 12:12:58 +0100 Subject: [PATCH 12/13] refactor: move resource debug messages where they makes sense --- lib/middleware/operation.js | 5 ++++- lib/middleware/resource.js | 5 ----- test/operation.test.js | 9 ++++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 9d9773f..36d086f 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -1,5 +1,5 @@ const { Router } = require('express') -const { error, warn } = require('../log')('operation') +const { error, warn, debug } = require('../log')('operation') const createError = require('http-errors') const clownface = require('clownface') const ns = require('@tpluscode/rdf-ns-builders') @@ -102,6 +102,9 @@ async function invokeOperation (req, res, next) { return next(new createError.MethodNotAllowed()) } + debug(`IRI: ${req.hydra.resource.term.value}`) + debug(`types: ${[...req.hydra.resource.types].map(term => term.value).join(' ')}`) + const handler = await api.loaderRegistry.load(req.hydra.operation.out(code.implementedBy), { basePath: api.codePath }) if (!handler) { diff --git a/lib/middleware/resource.js b/lib/middleware/resource.js index 5df343e..bbf505d 100644 --- a/lib/middleware/resource.js +++ b/lib/middleware/resource.js @@ -16,11 +16,6 @@ function factory ({ loader }) { debug('multiple resource candidates found') } - if (req.hydra.resource) { - debug(`IRI: ${req.hydra.resource.term.value}`) - debug(`types: ${[...req.hydra.resource.types].map(term => term.value).join(' ')}`) - } - return next() } } diff --git a/test/operation.test.js b/test/operation.test.js index c2f34b7..c645ddc 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -90,7 +90,8 @@ describe('middleware/operation', () => { // given const app = express() app.use(hydraMock(testResource({ - types: [NS.Person] + types: [NS.Person], + term: RDF.namedNode('/') }))) const hooks = { operations: sinon.stub().callsFake(operations => operations) @@ -140,7 +141,8 @@ describe('middleware/operation', () => { // given const app = express() app.use(hydraMock(testResource({ - types: [NS.Person] + types: [NS.Person], + term: RDF.namedNode('/') }))) app.use(middleware()) @@ -248,7 +250,8 @@ describe('middleware/operation', () => { // given const app = express() app.use(hydraMock(testResource({ - types: [NS.Person] + types: [NS.Person], + term: RDF.namedNode('/john-doe') }))) app.use(middleware()) From ff22a4450f1cda25f7352e769a2b67369e07eb93 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Mon, 30 Nov 2020 11:08:01 +0100 Subject: [PATCH 13/13] fix: make operarions middleware proper express handler --- lib/middleware/operation.js | 46 ++++++++++++++++++++----------------- test/operation.test.js | 2 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/middleware/operation.js b/lib/middleware/operation.js index 36d086f..ab73aab 100644 --- a/lib/middleware/operation.js +++ b/lib/middleware/operation.js @@ -54,31 +54,29 @@ function mapOperations ({ api, res, method }) { }) } -function findOperation (operationMiddleware) { - return async function (req, res, next) { - const method = req.method === 'HEAD' ? 'GET' : req.method +function findOperations (req, res, next) { + const method = req.method === 'HEAD' ? 'GET' : req.method - let operations = mapOperations({ api: req.hydra.api, res, method }) - .filter(({ operation }) => operation) - - if (operationMiddleware) { - operations = operationMiddleware(operations, { req, res }) - } + req.hydra.operations = mapOperations({ api: req.hydra.api, res, method }) + .filter(({ operation }) => operation) - if (!operations.length) { - return next() - } - - if (operations.length > 1) { - error('Multiple operations found') - return next(new Error('Ambiguous operation')) - } + return next() +} - const [{ resource, operation }] = operations - req.hydra.resource = resource - req.hydra.operation = operation +function prepareOperation (req, res, next) { + if (!req.hydra.operations.length) { return next() } + + if (req.hydra.operations.length > 1) { + error('Multiple operations found') + return next(new Error('Ambiguous operation')) + } + + const [{ resource, operation }] = req.hydra.operations + req.hydra.resource = resource + req.hydra.operation = operation + return next() } async function invokeOperation (req, res, next) { @@ -117,7 +115,13 @@ async function invokeOperation (req, res, next) { function factory (middleware = {}) { const router = Router() - router.use(findOperation(middleware.operations)) + router.use(findOperations) + + if (middleware.operations) { + router.use(middleware.operations) + } + + router.use(prepareOperation) if (middleware.resource) { router.use(middleware.resource) diff --git a/test/operation.test.js b/test/operation.test.js index c645ddc..bba670b 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -94,7 +94,7 @@ describe('middleware/operation', () => { term: RDF.namedNode('/') }))) const hooks = { - operations: sinon.stub().callsFake(operations => operations) + operations: sinon.stub().callsFake((req, res, next) => next()) } app.use(middleware(hooks))