diff --git a/API.md b/API.md index a27b7b5f..637bde94 100644 --- a/API.md +++ b/API.md @@ -19,19 +19,20 @@ ### Entry -| Method | Route | Action | Description | -| :----: | :-------------------------------------- | :------------------------: | :-------------------------------: | -| POST | `/db/:dbname/entry` | Insert / Update an entry | Based on \_id or $id of the entry | -| GET | `/db/:dbname/entry/_all` | Get all entries | Returns an array of documents | -| GET | `/db/:dbname/entry/:uuid` | Get an entry by UUID | | -| PUT | `/db/:dbname/entry/:uuid` | Update an entry by UUID | | -| DELETE | `/db/:dbname/entry/:uuid` | Delete an an entry by UUID | | -| GET | `/db/:dbname/entry/:uuid/_owner` | Get a list of owners | | -| PUT | `/db/:dbname/entry/:uuid/_owner/:owner` | Add an owner | | -| DELETE | `/db/:dbname/entry/:uuid/_owner/:owner` | Remove an owner | | -| GET | `/db/:dbname/entry/:uuid/:attachment+` | Get an attachment | | -| PUT | `/db/:dbname/entry/:uuid/:attachment+` | Save an attachment | | -| DELETE | `/db/:dbname/entry/:uuid/:attachment+` | Delete an attachment | | +| Method | Route | Action | Description | +| :----: | :-------------------------------------- | :---------------------------------: | :----------------------------------------: | +| POST | `/db/:dbname/entry` | Insert / Update an entry | Based on \_id or $id of the entry | +| GET | `/db/:dbname/entry/_all` | Get all entries | Returns an array of documents | +| HEAD | `/db/:dbname/entry/:uuid` | Get HTTP headers about the document | Similar to HEAD /:dbname/:docid in CouchDB | +| GET | `/db/:dbname/entry/:uuid` | Get an entry by UUID | | +| PUT | `/db/:dbname/entry/:uuid` | Update an entry by UUID | | +| DELETE | `/db/:dbname/entry/:uuid` | Delete an an entry by UUID | | +| GET | `/db/:dbname/entry/:uuid/_owner` | Get a list of owners | | +| PUT | `/db/:dbname/entry/:uuid/_owner/:owner` | Add an owner | | +| DELETE | `/db/:dbname/entry/:uuid/_owner/:owner` | Remove an owner | | +| GET | `/db/:dbname/entry/:uuid/:attachment+` | Get an attachment | | +| PUT | `/db/:dbname/entry/:uuid/:attachment+` | Save an attachment | | +| DELETE | `/db/:dbname/entry/:uuid/:attachment+` | Delete an attachment | | ### User diff --git a/src/server/middleware/auth.js b/src/server/middleware/auth.js index 03b6d4bb..a2aee3a2 100644 --- a/src/server/middleware/auth.js +++ b/src/server/middleware/auth.js @@ -14,7 +14,7 @@ const nanoPromise = require('../../util/nanoPromise'); const isEmail = require('../../util/isEmail'); const respondOk = require('./respondOk'); -const decorateError = require('./decorateError'); +const { decorateError } = require('./decorateError'); passport.serializeUser(function (user, done) { done(null, user); diff --git a/src/server/middleware/couch.js b/src/server/middleware/couch.js index e5bdb236..5f58170c 100644 --- a/src/server/middleware/couch.js +++ b/src/server/middleware/couch.js @@ -67,6 +67,22 @@ exports.getAllDbs = async (ctx) => { ctx.body = result; }; +exports.headDocument = composeWithError(async (ctx) => { + const result = await ctx.state.couch.getEntry( + ctx.params.uuid, + ctx.state.userEmail, + ctx.query + ); + const ifNoneMatch = ctx.request.get('If-None-Match'); + const rev = `"${result._rev}"`; + ctx.response.set('ETag', rev); + if (ifNoneMatch === rev) { + ctx.status = 304; + } else { + ctx.status = 200; + } +}); + exports.getDocument = composeWithError(async (ctx) => { ctx.body = await ctx.state.couch.getEntry( ctx.params.uuid, diff --git a/src/server/middleware/decorateError.js b/src/server/middleware/decorateError.js index 147ffa00..b9b31145 100644 --- a/src/server/middleware/decorateError.js +++ b/src/server/middleware/decorateError.js @@ -11,10 +11,18 @@ const statusMessages = { function decorateError(ctx, status, error = true) { ctx.status = status; - ctx.body = { - error, - code: statusMessages[status] || `error ${status}` - }; + if (responseHasBody(ctx)) { + ctx.body = { + error, + code: statusMessages[status] || `error ${status}` + }; + } } -module.exports = decorateError; +function responseHasBody(ctx) { + const method = ctx.method; + if (method === 'HEAD' || method === 'OPTIONS') return false; + return true; +} + +module.exports = { decorateError, responseHasBody }; diff --git a/src/server/middleware/util.js b/src/server/middleware/util.js index b284fdc4..76865e01 100644 --- a/src/server/middleware/util.js +++ b/src/server/middleware/util.js @@ -7,7 +7,7 @@ const compose = require('koa-compose'); const config = require('../../config/config').globalConfig; const debug = require('../../util/debug')('middleware:util'); -const decorateError = require('./decorateError'); +const { decorateError, responseHasBody } = require('./decorateError'); exports.parseBody = function (options) { return bodyParser(options); @@ -68,7 +68,7 @@ function onGetError(ctx, e, secure) { } break; } - if (config.debugrest) { + if (config.debugrest && responseHasBody(ctx)) { ctx.body.stack = e.stack; } } diff --git a/src/server/middleware/zenodo.js b/src/server/middleware/zenodo.js index f1cd3b3a..d6f5ba4e 100644 --- a/src/server/middleware/zenodo.js +++ b/src/server/middleware/zenodo.js @@ -4,7 +4,7 @@ const config = require('../../config/config').globalConfig; const debug = require('../../util/debug')('zenodo'); const { RocZenodo } = require('../../roc-zenodo'); -const decorateError = require('./decorateError'); +const { decorateError } = require('./decorateError'); const { composeWithError } = require('./util'); let rocZenodoProd = new RocZenodo({ diff --git a/src/server/routes/api.js b/src/server/routes/api.js index a830a1d0..3a87dabd 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -23,6 +23,7 @@ router.use(couch.tokenLookup); // Entries router.post('/:dbname/entry', parseJson100mb, couch.newOrUpdateEntry); router.get('/:dbname/entry/_all', couch.allEntries); +router.head('/:dbname/entry/:uuid', couch.headDocument); router.get('/:dbname/entry/:uuid', couch.getDocument); router.put('/:dbname/entry/:uuid', parseJson100mb, couch.updateEntry); router.delete('/:dbname/entry/:uuid', couch.deleteEntry); diff --git a/test/rest-api/api.js b/test/rest-api/api.js index c45fcc4a..a21f95cc 100644 --- a/test/rest-api/api.js +++ b/test/rest-api/api.js @@ -150,6 +150,27 @@ describe('basic rest-api as b@b.com', () => { return data().then(() => authenticateAs(request, 'b@b.com', '123')); }); + test('head existing entry', async () => { + const entry = await couch.getEntryById('A', 'b@b.com'); + return request + .head(`/db/test/entry/${entry._id}`) + .expect(200) + .expect('ETag', `"${entry._rev}"`); + }); + + test('head non-existing entry', async () => { + return request.head('/db/test/entry/bad').expect(404); + }); + + test('head with if-none-match', async () => { + const entry = await couch.getEntryById('A', 'b@b.com'); + return request + .head(`/db/test/entry/${entry._id}`) + .set('If-None-Match', `"${entry._rev}"`) + .expect(304) + .expect('ETag', `"${entry._rev}"`); + }); + test('get an entry', () => { return couch.getEntryById('A', 'b@b.com').then((entry) => { return request.get(`/db/test/entry/${entry._id}`).expect(200);