diff --git a/lib/api3/const.json b/lib/api3/const.json index fd874a1175a..e82104ddb53 100644 --- a/lib/api3/const.json +++ b/lib/api3/const.json @@ -1,5 +1,5 @@ { - "API3_VERSION": "3.0.0-alpha", + "API3_VERSION": "3.0.2-alpha", "API3_SECURITY_ENABLE": true, "API3_TIME_SKEW_TOLERANCE": 5, "API3_DEDUP_FALLBACK_ENABLED": true, @@ -41,6 +41,7 @@ "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", + "HTTP_404_BAD_OPERATION": "Bad operation or collection", "HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested", "HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document", "HTTP_500_INTERNAL_ERROR": "Internal Server Error", @@ -52,4 +53,4 @@ "MIN_TIMESTAMP": 946684800000, "MIN_UTC_OFFSET": -1440, "MAX_UTC_OFFSET": 1440 -} \ No newline at end of file +} diff --git a/lib/api3/doc/formats.md b/lib/api3/doc/formats.md index 5a01a802f3e..a0c9f73b3fa 100644 --- a/lib/api3/doc/formats.md +++ b/lib/api3/doc/formats.md @@ -18,31 +18,34 @@ The server replies with `406 Not Acceptable` HTTP status in case of not supporte Default content type is JSON, output can look like this: -``` -[ - { - "type":"sgv", - "sgv":"171", - "dateString":"2014-07-19T02:44:15.000-07:00", - "date":1405763055000, - "device":"dexcom", - "direction":"Flat", - "identifier":"5c5a2404e0196f4d3d9a718a", - "srvModified":1405763055000, - "srvCreated":1405763055000 - }, - { - "type":"sgv", - "sgv":"176", - "dateString":"2014-07-19T03:09:15.000-07:00", - "date":1405764555000, - "device":"dexcom", - "direction":"Flat", - "identifier":"5c5a2404e0196f4d3d9a7187", - "srvModified":1405764555000, - "srvCreated":1405764555000 - } -] +```json +{ + "status": 200, + "result": [ + { + "type": "sgv", + "sgv": "171", + "dateString": "2014-07-19T02:44:15.000-07:00", + "date": 1405763055000, + "device": "dexcom", + "direction": "Flat", + "identifier": "5c5a2404e0196f4d3d9a718a", + "srvModified": 1405763055000, + "srvCreated": 1405763055000 + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:09:15.000-07:00", + "date": 1405764555000, + "device": "dexcom", + "direction": "Flat", + "identifier": "5c5a2404e0196f4d3d9a7187", + "srvModified": 1405764555000, + "srvCreated": 1405764555000 + } + ] +} ``` ### XML diff --git a/lib/api3/doc/tutorial.md b/lib/api3/doc/tutorial.md index d69e95ef4a4..73bc6c99f8d 100644 --- a/lib/api3/doc/tutorial.md +++ b/lib/api3/doc/tutorial.md @@ -22,15 +22,18 @@ request('https://nsapiv3.herokuapp.com/api/v3/version', (error, response, body) => console.log(body)); ``` Sample result: -```javascript -{ - "version":"0.12.2", - "apiVersion":"3.0.0-alpha", - "srvDate":1564386001772, - "storage":{ - "storage":"mongodb", - "version":"3.6.12" - } +```json +{ + "status": 200, + "result": { + "version": "14.1.0", + "apiVersion": "3.0.2-alpha", + "srvDate": 1609402081548, + "storage": { + "storage": "mongodb", + "version": "4.2.11" + } + } } ``` @@ -50,22 +53,25 @@ request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`, (error, response, body) => console.log(body)); ``` Sample result: -```javascript -{ - "version":"0.12.2", - "apiVersion":"3.0.0-alpha", - "srvDate":1564391740738, - "storage":{ - "storage":"mongodb", - "version":"3.6.12" - }, - "apiPermissions":{ - "devicestatus":"crud", - "entries":"crud", - "food":"crud", - "profile":"crud", - "settings":"crud", - "treatments":"crud" +```json +{ + "status": 200, + "result": { + "version": "14.1.0", + "apiVersion": "3.0.2-alpha", + "srvDate": 1609427571833, + "storage": { + "storage": "mongodb", + "version": "4.2.11" + }, + "apiPermissions": { + "devicestatus": "crud", + "entries": "crud", + "food": "crud", + "profile": "crud", + "settings": "crud", + "treatments": "crud" + } } } ``` @@ -86,24 +92,27 @@ request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&lim (error, response, body) => console.log(body)); ``` Sample result: -``` -[ - { - "dateString":"2019-07-30T02:24:50.434+0200", - "sgv":115, - "direction":"FortyFiveDown" - }, - { - "dateString":"2019-07-30T02:19:50.374+0200", - "sgv":121, - "direction":"FortyFiveDown" - }, - { - "dateString":"2019-07-30T02:14:50.450+0200", - "sgv":129, - "direction":"FortyFiveDown" - } -] +```json +{ + "status": 200, + "result": [ + { + "dateString": "2019-07-30T02:24:50.434+0200", + "sgv": 115, + "direction": "FortyFiveDown" + }, + { + "dateString": "2019-07-30T02:19:50.374+0200", + "sgv": 121, + "direction": "FortyFiveDown" + }, + { + "dateString": "2019-07-30T02:14:50.450+0200", + "sgv": 129, + "direction": "FortyFiveDown" + } + ] +} ``` @@ -129,11 +138,15 @@ request({ json: true, url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}` }, - (error, response, body) => console.log(response.headers.location)); + (error, response, body) => console.log(body)); ``` Sample result: -``` -/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895 +```json +{ + "status": 201, + "identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "lastModified": 1564591511711 +} ``` @@ -152,19 +165,22 @@ request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`, (error, response, body) => console.log(body)); ``` Sample result: -``` -{ - "date":1564591511232, - "app":"AndroidAPS", - "device":"Samsung XCover 4-861536030196001", - "eventType":"Correction Bolus", - "insulin":0.3, - "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", - "utcOffset":0, - "created_at":"2019-07-31T16:45:11.232Z", - "srvModified":1564591627732, - "srvCreated":1564591511711, - "subject":"test-admin" +```json +{ + "status": 200, + "result": { + "date": 1564591511232, + "app": "AndroidAPS", + "device": "Samsung XCover 4-861536030196001", + "eventType": "Correction Bolus", + "insulin": 0.3, + "identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "utcOffset": 0, + "created_at": "2019-07-31T16:45:11.232Z", + "srvModified": 1564591627732, + "srvCreated": 1564591511711, + "subject": "test-admin" + } } ``` @@ -183,14 +199,17 @@ request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`, (error, response, body) => console.log(body)); ``` Sample result: -```javascript +```json { - "srvDate":1564591783202, - "collections":{ - "devicestatus":1564591490074, - "entries":1564591486801, - "profile":1548524042744, - "treatments":1564591627732 + "status": 200, + "result": { + "srvDate": 1564591783202, + "collections": { + "devicestatus": 1564591490074, + "entries": 1564591486801, + "profile": 1548524042744, + "treatments": 1564591627732 + } } } ``` @@ -220,11 +239,13 @@ request({ json: true, url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` }, - (error, response, body) => console.log(response.statusCode)); + (error, response, body) => console.log(body)); ``` Sample result: -``` -204 +```json +{ + "status": 200 +} ``` @@ -248,11 +269,13 @@ request({ json: true, url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` }, - (error, response, body) => console.log(response.statusCode)); + (error, response, body) => console.log(body)); ``` Sample result: -``` -204 +```json +{ + "status": 200 +} ``` @@ -271,11 +294,13 @@ request({ method: 'delete', url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` }, - (error, response, body) => console.log(response.statusCode)); + (error, response, body) => console.log(body)); ``` Sample result: -``` -204 +```json +{ + "status": 200 +} ``` @@ -294,36 +319,39 @@ request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified} (error, response, body) => console.log(response.body)); ``` Sample result: -``` -[ - { - "date":1564521267421, - "app":"AndroidAPS", - "device":"Samsung XCover 4-861536030196001", - "eventType":"Correction Bolus", - "insulin":0.5, - "utcOffset":0, - "created_at":"2019-07-30T21:14:27.421Z", - "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", - "srvModified":1564592440416, - "srvCreated":1564592334853, - "subject":"test-admin", - "modifiedBy":"test-admin", - "isValid":false - }, - { - "date":1564592545299, - "app":"AndroidAPS", - "device":"Samsung XCover 4-861536030196001", - "eventType":"Snack Bolus", - "carbs":10, - "identifier":"267c43c2-f629-5191-a542-4f410c69e486", - "utcOffset":0, - "created_at":"2019-07-31T17:02:25.299Z", - "srvModified":1564592545781, - "srvCreated":1564592545781, - "subject":"test-admin" - } -] +```json +{ + "status": 200, + "result": [ + { + "date": 1564521267421, + "app": "AndroidAPS", + "device": "Samsung XCover 4-861536030196001", + "eventType": "Correction Bolus", + "insulin": 0.5, + "utcOffset": 0, + "created_at": "2019-07-30T21:14:27.421Z", + "identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "srvModified": 1564592440416, + "srvCreated": 1564592334853, + "subject": "test-admin", + "modifiedBy": "test-admin", + "isValid": false + }, + { + "date": 1564592545299, + "app": "AndroidAPS", + "device": "Samsung XCover 4-861536030196001", + "eventType": "Snack Bolus", + "carbs": 10, + "identifier": "267c43c2-f629-5191-a542-4f410c69e486", + "utcOffset": 0, + "created_at": "2019-07-31T17:02:25.299Z", + "srvModified": 1564592545781, + "srvCreated": 1564592545781, + "subject": "test-admin" + } + ] +} ``` Notice the `"isValid":false` field marking the deletion of the document. diff --git a/lib/api3/generic/create/insert.js b/lib/api3/generic/create/insert.js index b643818569a..56cb608cdf2 100644 --- a/lib/api3/generic/create/insert.js +++ b/lib/api3/generic/create/insert.js @@ -4,6 +4,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , validate = require('./validate.js') , path = require('path') + , opTools = require('../../shared/operationTools') ; /** @@ -35,7 +36,13 @@ async function insert (opCtx, doc) { res.setHeader('Last-Modified', now.toUTCString()); res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier)); - res.status(apiConst.HTTP.CREATED).send({ }); + + + const fields = { + identifier: identifier, + lastModified: now.getTime() + }; + opTools.sendJSON({ res, status: apiConst.HTTP.CREATED, fields: fields }); ctx.bus.emit('storage-socket-create', { colName: col.colName, doc }); col.autoPrune(); @@ -43,4 +50,4 @@ async function insert (opCtx, doc) { } -module.exports = insert; \ No newline at end of file +module.exports = insert; diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js index 8f9463f3ef9..f5e8c3b5b27 100644 --- a/lib/api3/generic/delete/operation.js +++ b/lib/api3/generic/delete/operation.js @@ -36,7 +36,7 @@ async function validateDelete (opCtx) { throw new Error('empty result'); if (result.length === 0) { - return res.status(apiConst.HTTP.NOT_FOUND).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); } else { const storageDoc = result[0]; @@ -62,13 +62,13 @@ async function deletePermanently (opCtx) { throw new Error('empty result'); if (!result.deleted) { - return res.status(apiConst.HTTP.NOT_FOUND).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); } col.autoPrune(); ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); ctx.bus.emit('data-received'); - return res.status(apiConst.HTTP.NO_CONTENT).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.OK }); } @@ -89,13 +89,13 @@ async function markAsDeleted (opCtx) { throw new Error('empty result'); if (!result.updated) { - return res.status(apiConst.HTTP.NOT_FOUND).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); } ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); col.autoPrune(); ctx.bus.emit('data-received'); - return res.status(apiConst.HTTP.NO_CONTENT).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.OK }); } @@ -119,4 +119,4 @@ function deleteOperation (ctx, env, app, col) { }; } -module.exports = deleteOperation; \ No newline at end of file +module.exports = deleteOperation; diff --git a/lib/api3/generic/history/operation.js b/lib/api3/generic/history/operation.js index fb3ed0184ec..47bd41b2056 100644 --- a/lib/api3/generic/history/operation.js +++ b/lib/api3/generic/history/operation.js @@ -39,7 +39,8 @@ async function history (opCtx, fieldsProjector) { throw new Error('empty result'); if (result.length === 0) { - return res.status(apiConst.HTTP.NO_CONTENT).end(); + res.status(apiConst.HTTP.OK); + return renderer.render(res, result); } _.each(result, col.resolveDates); diff --git a/lib/api3/generic/patch/operation.js b/lib/api3/generic/patch/operation.js index d7bb5fc2b4d..1e5cd0f129b 100644 --- a/lib/api3/generic/patch/operation.js +++ b/lib/api3/generic/patch/operation.js @@ -36,7 +36,7 @@ async function patch (opCtx) { const storageDoc = result[0]; if (storageDoc.isValid === false) { - return res.status(apiConst.HTTP.GONE).end(); + return opTools.sendJSONStatus(res, apiConst.HTTP.GONE); } const modifiedDate = col.resolveDates(storageDoc) @@ -44,13 +44,13 @@ async function patch (opCtx) { if (ifUnmodifiedSince && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { - return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED); } await applyPatch(opCtx, identifier, doc, storageDoc); } else { - return res.status(apiConst.HTTP.NOT_FOUND).end(); + return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND); } } @@ -82,7 +82,7 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) { throw new Error('matchedCount empty'); res.setHeader('Last-Modified', now.toUTCString()); - res.status(apiConst.HTTP.NO_CONTENT).send({ }); + opTools.sendJSONStatus(res, apiConst.HTTP.OK); const fieldsProjector = new FieldsProjector('_all'); const patchedDocs = await col.storage.findOne(identifier, fieldsProjector); @@ -115,4 +115,4 @@ function patchOperation (ctx, env, app, col) { }; } -module.exports = patchOperation; \ No newline at end of file +module.exports = patchOperation; diff --git a/lib/api3/generic/read/operation.js b/lib/api3/generic/read/operation.js index c2e65a4afcc..3f4efb75dd9 100644 --- a/lib/api3/generic/read/operation.js +++ b/lib/api3/generic/read/operation.js @@ -26,12 +26,12 @@ async function read (opCtx) { throw new Error('empty result'); if (result.length === 0) { - return res.status(apiConst.HTTP.NOT_FOUND).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); } const doc = result[0]; if (doc.isValid === false) { - return res.status(apiConst.HTTP.GONE).end(); + return opTools.sendJSON({ res, status: apiConst.HTTP.GONE }); } @@ -74,4 +74,4 @@ function readOperation (ctx, env, app, col) { }; } -module.exports = readOperation; \ No newline at end of file +module.exports = readOperation; diff --git a/lib/api3/generic/update/operation.js b/lib/api3/generic/update/operation.js index 3e517a32d11..5098ffa1773 100644 --- a/lib/api3/generic/update/operation.js +++ b/lib/api3/generic/update/operation.js @@ -48,7 +48,7 @@ async function updateConditional (opCtx, doc, storageDoc) { const { col, req, res } = opCtx; if (storageDoc.isValid === false) { - return res.status(apiConst.HTTP.GONE).end(); + return opTools.sendJSONStatus(res, apiConst.HTTP.GONE); } const modifiedDate = col.resolveDates(storageDoc) @@ -56,7 +56,7 @@ async function updateConditional (opCtx, doc, storageDoc) { if (ifUnmodifiedSince && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { - return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED); } await replace(opCtx, doc, storageDoc); @@ -83,4 +83,4 @@ function updateOperation (ctx, env, app, col) { }; } -module.exports = updateOperation; \ No newline at end of file +module.exports = updateOperation; diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js index fdf803ed16f..c0c76c4b6eb 100644 --- a/lib/api3/generic/update/replace.js +++ b/lib/api3/generic/update/replace.js @@ -4,6 +4,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , validate = require('./validate.js') , path = require('path') + , opTools = require('../../shared/operationTools') ; /** @@ -37,12 +38,20 @@ async function replace (opCtx, doc, storageDoc, options) { throw new Error('empty matchedCount'); res.setHeader('Last-Modified', now.toUTCString()); + const fields = { + lastModified: now.getTime() + } if (storageDoc.identifier !== doc.identifier || isDeduplication) { res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier)); + fields.identifier = doc.identifier; + fields.isDeduplication = true; + if (storageDoc.identifier !== doc.identifier) { + fields.deduplicatedIdentifier = storageDoc.identifier; + } } - res.status(apiConst.HTTP.NO_CONTENT).send({ }); + opTools.sendJSON({ res, status: apiConst.HTTP.OK, fields }); ctx.bus.emit('storage-socket-update', { colName: col.colName, doc }); col.autoPrune(); @@ -50,4 +59,4 @@ async function replace (opCtx, doc, storageDoc, options) { } -module.exports = replace; \ No newline at end of file +module.exports = replace; diff --git a/lib/api3/index.js b/lib/api3/index.js index bff0899536f..e1cde9f2ae8 100644 --- a/lib/api3/index.js +++ b/lib/api3/index.js @@ -7,6 +7,7 @@ const express = require('express') , apiConst = require('./const.json') , security = require('./security') , genericSetup = require('./generic/setup') + , opTools = require('./shared/operationTools') ; function configure (env, ctx) { @@ -104,6 +105,10 @@ function configure (env, ctx) { res.redirect(307, '../../../api3-docs'); }); + app.use((req, res) => { + opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION); + }) + ctx.storageSocket = new StorageSocket(app, env, ctx); return app; diff --git a/lib/api3/shared/operationTools.js b/lib/api3/shared/operationTools.js index 1955b9c2068..6b2e62e12e7 100644 --- a/lib/api3/shared/operationTools.js +++ b/lib/api3/shared/operationTools.js @@ -6,18 +6,41 @@ const apiConst = require('../const.json') , uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-) ; + +function sendJSON ({ res, result, status, fields }) { + + const json = { + status: status || apiConst.HTTP.OK, + result: result + }; + + if (result) { + json.result = result + } + + if (fields) { + Object.assign(json, fields); + } + + res.status(json.status).json(json); +} + + function sendJSONStatus (res, status, title, description, warning) { const json = { - status: status, - message: title, - description: description + status: status }; + if (title) { json.message = title } + + if (description) { json.description = description } + // Add optional warning message. if (warning) { json.warning = warning; } - res.status(status).json(json); + res.status(status) + .json(json); return title; } @@ -104,8 +127,9 @@ function resolveIdentifier (doc) { module.exports = { + sendJSON, sendJSONStatus, validateCommon, calculateIdentifier, resolveIdentifier -}; \ No newline at end of file +}; diff --git a/lib/api3/shared/renderer.js b/lib/api3/shared/renderer.js index a3588819a72..842785709b6 100644 --- a/lib/api3/shared/renderer.js +++ b/lib/api3/shared/renderer.js @@ -53,7 +53,7 @@ function extension2accept (req, res, next) { */ function render (res, data) { res.format({ - 'json': () => res.send(data), + 'json': () => renderJson(res, data), 'csv': () => renderCsv(res, data), 'xml': () => renderXml(res, data), 'default': () => @@ -62,6 +62,19 @@ function render (res, data) { } +/** + * Format data to output as JSON + * @param {Object} res + * @param {any} data + */ +function renderJson (res, data) { + res.send({ + status: apiConst.HTTP.OK, + result: data + }); +} + + /** * Format data to output as .csv * @param {Object} res @@ -96,4 +109,4 @@ function renderXml (res, data) { module.exports = { extension2accept, render -}; \ No newline at end of file +}; diff --git a/lib/api3/specific/lastModified.js b/lib/api3/specific/lastModified.js index b27ecaca852..f550d56f89c 100644 --- a/lib/api3/specific/lastModified.js +++ b/lib/api3/specific/lastModified.js @@ -38,7 +38,7 @@ function configure (app, ctx, env) { return { colName: col.colName, lastModified: result }; } - + async function collectionsAsync (auth) { const cols = app.get('collections') @@ -77,7 +77,7 @@ function configure (app, ctx, env) { info.collections = await collectionsAsync(auth); - res.json(info); + opTools.sendJSON({ res, result: info }); } diff --git a/lib/api3/specific/status.js b/lib/api3/specific/status.js index 7b70b24ab71..5ec28929fbe 100644 --- a/lib/api3/specific/status.js +++ b/lib/api3/specific/status.js @@ -47,7 +47,7 @@ function configure (app, ctx, env) { } } - res.json(info); + opTools.sendJSON({ res, result: info }); } diff --git a/lib/api3/specific/version.js b/lib/api3/specific/version.js index 25392fe99d7..ba7685fe486 100644 --- a/lib/api3/specific/version.js +++ b/lib/api3/specific/version.js @@ -13,7 +13,7 @@ function configure (app) { try { const versionInfo = await storageTools.getVersionInfo(app); - res.json(versionInfo); + opTools.sendJSON({ res, result: versionInfo }); } catch(error) { console.error(error); diff --git a/lib/api3/swagger.json b/lib/api3/swagger.json index b20bb0e6761..a380be3e0f5 100644 --- a/lib/api3/swagger.json +++ b/lib/api3/swagger.json @@ -11,7 +11,7 @@ "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" }, - "version": "3.0.1" + "version": "3.0.2" }, "servers": [ { @@ -35,7 +35,7 @@ "generic" ], "summary": "SEARCH: Search documents from the collection", - "description": "General search operation through documents of one collection, matching the specified filtering criteria. You can apply:\n\n1) filtering - combining any number of filtering parameters\n\n2) ordering - using `sort` or `sort$desc` parameter\n\n3) paging - using `limit` and `skip` parameters\n\nWhen there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content.\n\nThis operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", + "description": "General search operation through documents of one collection, matching the specified filtering criteria. You can apply:\n\n1) filtering - combining any number of filtering parameters\n\n2) ordering - using `sort` or `sort$desc` parameter\n\n3) paging - using `limit` and `skip` parameters\n\nIf successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty).\n\nThis operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", "operationId": "SEARCH", "parameters": [ { @@ -172,7 +172,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DocumentArray" + "$ref": "#/components/schemas/inline_response_200" } }, "text/csv": { @@ -187,28 +187,63 @@ } } }, - "204": { - "description": "Successful operation - no documents matching the filtering criteria" - }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "406": { - "description": "The requested content type (in `Accept` header) is not supported." + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] }, @@ -217,7 +252,7 @@ "generic" ], "summary": "CREATE: Inserts a new document into the collection", - "description": "Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header.\n\nWhen the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`)", + "description": "Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified. `identifier` is included in response body or it can be parsed from the `Location` response header.\n\nWhen the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 200 HTTP status code along with `Last-Modified` and correct `Location` headers. The response body then includes `isDeduplication`=`true` and `deduplicatedIdentifier` fields.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`)", "parameters": [ { "name": "collection", @@ -277,8 +312,8 @@ "required": true }, "responses": { - "201": { - "description": "Successfully created a new document in collection", + "200": { + "description": "Successfully updated a duplicate document in the collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" @@ -286,10 +321,17 @@ "Location": { "$ref": "#/components/schemas/headerLocation" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_1" + } + } } }, - "204": { - "description": "Successfully finished operation", + "201": { + "description": "Successfully created a new document in collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" @@ -297,27 +339,72 @@ "Location": { "$ref": "#/components/schemas/headerLocation" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } } }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "422": { - "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`)." + "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -328,7 +415,7 @@ "generic" ], "summary": "READ: Retrieves a single document from the collection", - "description": "Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.\n\nWhen `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side. With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned.\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)", + "description": "Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side. With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned.\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)", "parameters": [ { "name": "collection", @@ -431,7 +518,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Document" + "$ref": "#/components/schemas/inline_response_200_2" } }, "text/csv": { @@ -455,24 +542,62 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "406": { - "description": "The requested content type (in `Accept` header) is not supported." + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } }, "410": { - "description": "The requested document has already been deleted." + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] }, @@ -481,7 +606,7 @@ "generic" ], "summary": "UPDATE: Updates a document in the collection", - "description": "Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.\n\nWhen no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter).\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`)", + "description": "Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 200 HTTP status code will be returned.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter).\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`)", "parameters": [ { "name": "collection", @@ -563,50 +688,108 @@ "required": true }, "responses": { - "201": { - "description": "Successfully created a new document in collection", - "headers": { - "Last-Modified": { - "$ref": "#/components/schemas/headerLastModified" + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } } } }, - "204": { - "description": "Successfully finished operation", + "201": { + "description": "Successfully created a new document in collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" - }, - "Location": { - "$ref": "#/components/schemas/headerLocation" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } } } }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "410": { - "description": "The requested document has already been deleted." + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } }, "412": { - "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header)." + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } }, "422": { - "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`)." + "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] }, @@ -686,25 +869,63 @@ } ], "responses": { - "204": { - "description": "Successful operation - empty response" + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "422": { - "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`)." + "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] }, @@ -713,7 +934,7 @@ "generic" ], "summary": "PATCH: Partially updates document in the collection", - "description": "Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.\n\nWhen no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\n`PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence.\n\nWhile patching the document, the field `modifiedBy` is automatically set to the authorized subject's name.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`)", + "description": "Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 200 HTTP status code will be returned.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\n`PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence.\n\nWhile patching the document, the field `modifiedBy` is automatically set to the authorized subject's name.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`)", "parameters": [ { "name": "collection", @@ -795,42 +1016,93 @@ "required": true }, "responses": { - "204": { - "description": "Successfully finished operation", - "headers": { - "Last-Modified": { - "$ref": "#/components/schemas/headerLastModified" - }, - "Location": { - "$ref": "#/components/schemas/headerLocation" + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } } } }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "410": { - "description": "The requested document has already been deleted." + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } }, "412": { - "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header)." + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } }, "422": { - "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`)." + "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -841,7 +1113,7 @@ "generic" ], "summary": "HISTORY: Retrieves incremental changes since timestamp", - "description": "HISTORY operation is intended for continuous data synchronization with other systems.\nEvery insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter.\n\nDeleted documents will appear with `isValid` = `false` field.\n\nWhen there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content.\n\nHISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections).\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", + "description": "HISTORY operation is intended for continuous data synchronization with other systems.\nEvery insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter.\n\nDeleted documents will appear with `isValid` = `false` field.\n\nHISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections).\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", "operationId": "HISTORY", "parameters": [ { @@ -950,7 +1222,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DocumentArray" + "$ref": "#/components/schemas/inline_response_200" } }, "text/csv": { @@ -965,28 +1237,63 @@ } } }, - "204": { - "description": "No changes detected" - }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "406": { - "description": "The requested content type (in `Accept` header) is not supported." + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -1107,7 +1414,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DocumentArray" + "$ref": "#/components/schemas/inline_response_200" } }, "text/csv": { @@ -1122,28 +1429,63 @@ } } }, - "204": { - "description": "No changes detected" - }, "400": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "406": { - "description": "The requested content type (in `Accept` header) is not supported." + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -1188,15 +1530,32 @@ } }, "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -1251,21 +1610,38 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LastModifiedResult" + "$ref": "#/components/schemas/inline_response_200_4" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" } } } }, - "401": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." - }, "403": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } } }, "security": [ { - "apiKeyAuth": [] + "accessToken": [] + }, + { + "jwtoken": [] } ] } @@ -1305,6 +1681,32 @@ "type": "string", "example": "53409478-105f-11e9-ab14-d663bd873d93" }, + "identifierField": { + "type": "string", + "description": "Identifier of created or modified document", + "example": "53409478-105f-11e9-ab14-d663bd873d93" + }, + "lastModifiedField": { + "type": "integer", + "description": "Timestamp of the last document modification on the server, formatted as\nUnix epoch in milliseconds (1525383610088)", + "format": "int64", + "example": 1525383610088 + }, + "statusField": { + "type": "integer", + "description": "HTTP response status code. The status appears also in response body's field for those clients that are unable to process standard HTTP status code.", + "example": 200 + }, + "isDeduplicationField": { + "type": "boolean", + "description": "Flag whether the operation found a duplicate document (to update)", + "example": true + }, + "deduplicatedIdentifierField": { + "type": "string", + "description": "The original document that has been marked as a duplicate document and which has been updated", + "example": "abc09478-105f-11e9-ab14-d663bd873d93" + }, "DocumentBase": { "required": [ "app", @@ -1795,6 +2197,18 @@ ] }, "Version": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/statusField" + }, + "result": { + "$ref": "#/components/schemas/VersionResult" + } + }, + "description": "Information about versions" + }, + "VersionResult": { "type": "object", "properties": { "version": { @@ -1813,56 +2227,31 @@ "example": 1532936118000 }, "storage": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Type of storage engine used", - "example": "mongodb" - }, - "version": { - "type": "string", - "description": "Version of the storage engine", - "example": "4.0.6" - } - } + "$ref": "#/components/schemas/VersionResult_storage" } - }, - "description": "Information about versions" + } }, "Status": { - "description": "Information about versions and API permissions", + "properties": { + "status": { + "$ref": "#/components/schemas/statusField" + }, + "result": { + "$ref": "#/components/schemas/StatusResult" + } + }, + "description": "Information about versions and API permissions" + }, + "StatusResult": { "allOf": [ { - "$ref": "#/components/schemas/Version" + "$ref": "#/components/schemas/VersionResult" }, { "type": "object", "properties": { "apiPermissions": { - "type": "object", - "properties": { - "devicestatus": { - "type": "string", - "example": "crud" - }, - "entries": { - "type": "string", - "example": "r" - }, - "food": { - "type": "string", - "example": "crud" - }, - "profile": { - "type": "string", - "example": "r" - }, - "treatments": { - "type": "string", - "example": "crud" - } - } + "$ref": "#/components/schemas/StatusResult_apiPermissions" } } } @@ -1877,50 +2266,231 @@ "example": 1556260878776 }, "collections": { - "type": "object", - "properties": { - "devicestatus": { - "type": "integer", - "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", - "format": "int64", - "example": 1556260760974 - }, - "treatments": { - "type": "integer", - "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", - "format": "int64", - "example": 1553374184169 - }, - "entries": { - "type": "integer", - "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", - "format": "int64", - "example": 1556260758768 - }, - "profile": { - "type": "integer", - "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", - "format": "int64", - "example": 1548524042744 - } - }, - "description": "Collections which the user have read access to." + "$ref": "#/components/schemas/LastModifiedResult_collections" } }, "description": "Result of LAST MODIFIED operation" + }, + "inline_response_200": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/DocumentArray" + } + } + }, + "inline_response_400": { + "properties": { + "status": { + "type": "integer", + "example": 400 + } + } + }, + "inline_response_401": { + "properties": { + "status": { + "type": "integer", + "example": 401 + } + } + }, + "inline_response_403": { + "properties": { + "status": { + "type": "integer", + "example": 403 + } + } + }, + "inline_response_404": { + "properties": { + "status": { + "type": "integer", + "example": 404 + } + } + }, + "inline_response_406": { + "properties": { + "status": { + "type": "integer", + "example": 406 + } + } + }, + "inline_response_200_1": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "identifier": { + "$ref": "#/components/schemas/identifierField" + }, + "isDeduplication": { + "$ref": "#/components/schemas/isDeduplicationField" + }, + "deduplicatedIdentifier": { + "$ref": "#/components/schemas/deduplicatedIdentifierField" + } + } + }, + "inline_response_201": { + "properties": { + "status": { + "type": "integer", + "example": 201 + }, + "identifier": { + "$ref": "#/components/schemas/identifierField" + }, + "lastModified": { + "$ref": "#/components/schemas/lastModifiedField" + } + } + }, + "inline_response_422": { + "properties": { + "status": { + "type": "integer", + "example": 422 + } + } + }, + "inline_response_200_2": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/Document" + } + } + }, + "inline_response_410": { + "properties": { + "status": { + "type": "integer", + "example": 410 + } + } + }, + "inline_response_200_3": { + "properties": { + "status": { + "type": "integer", + "example": 200 + } + } + }, + "inline_response_412": { + "properties": { + "status": { + "type": "integer", + "example": 412 + } + } + }, + "inline_response_200_4": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/LastModifiedResult" + } + } + }, + "VersionResult_storage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of storage engine used", + "example": "mongodb" + }, + "version": { + "type": "string", + "description": "Version of the storage engine", + "example": "4.0.6" + } + } + }, + "StatusResult_apiPermissions": { + "type": "object", + "properties": { + "devicestatus": { + "type": "string", + "example": "crud" + }, + "entries": { + "type": "string", + "example": "r" + }, + "food": { + "type": "string", + "example": "crud" + }, + "profile": { + "type": "string", + "example": "r" + }, + "treatments": { + "type": "string", + "example": "crud" + } + } + }, + "LastModifiedResult_collections": { + "type": "object", + "properties": { + "devicestatus": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1556260760974 + }, + "treatments": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1553374184169 + }, + "entries": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1556260758768 + }, + "profile": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1548524042744 + } + }, + "description": "Collections which the user have read access to." } }, "responses": { - "201Created": { - "description": "Successfully created a new document in collection", - "headers": { - "Last-Modified": { - "$ref": "#/components/schemas/headerLastModified" + "200Ok": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } } } }, - "201CreatedLocation": { - "description": "Successfully created a new document in collection", + "200Deduplication": { + "description": "Successfully updated a duplicate document in the collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" @@ -1928,18 +2498,32 @@ "Location": { "$ref": "#/components/schemas/headerLocation" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_1" + } + } } }, - "204NoContent": { - "description": "Successfully finished operation", + "201Created": { + "description": "Successfully created a new document in collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } } }, - "204NoContentLocation": { - "description": "Successfully finished operation", + "201CreatedLocation": { + "description": "Successfully created a new document in collection", "headers": { "Last-Modified": { "$ref": "#/components/schemas/headerLastModified" @@ -1947,6 +2531,13 @@ "Location": { "$ref": "#/components/schemas/headerLocation" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } } }, "304NotModified": { @@ -1958,35 +2549,91 @@ } }, "400BadRequest": { - "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present." + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } }, "401Unauthorized": { - "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy." + "description": "The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } }, "403Forbidden": { - "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation." + "description": "Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } }, "404NotFound": { - "description": "The collection or document specified was not found." + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } }, "406NotAcceptable": { - "description": "The requested content type (in `Accept` header) is not supported." + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } }, "412PreconditionFailed": { - "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header)." + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } }, "410Gone": { - "description": "The requested document has already been deleted." + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } }, "422UnprocessableEntity": { - "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`)." + "description": "The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } }, "search200": { "description": "Successful operation returning array of documents matching the filtering criteria", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DocumentArray" + "$ref": "#/components/schemas/inline_response_200" } }, "text/csv": { @@ -2001,9 +2648,6 @@ } } }, - "search204": { - "description": "Successful operation - no documents matching the filtering criteria" - }, "read200": { "description": "The document has been succesfully found and its JSON form returned in the response content.", "headers": { @@ -2014,7 +2658,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Document" + "$ref": "#/components/schemas/inline_response_200_2" } }, "text/csv": { @@ -2042,7 +2686,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DocumentArray" + "$ref": "#/components/schemas/inline_response_200" } }, "text/csv": { @@ -2057,15 +2701,12 @@ } } }, - "history204": { - "description": "No changes detected" - }, "lastModified200": { "description": "Successful operation returning the timestamps", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LastModifiedResult" + "$ref": "#/components/schemas/inline_response_200_4" } } } diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml index 5cbb2a05544..56175263924 100644 --- a/lib/api3/swagger.yaml +++ b/lib/api3/swagger.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 servers: - url: '/api/v3' info: - version: "3.0.1" + version: "3.0.2" title: Nightscout API contact: name: NS development discussion channel @@ -102,7 +102,7 @@ paths: 3) paging - using `limit` and `skip` parameters - When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content. + If successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty). This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`). @@ -120,13 +120,12 @@ paths: - $ref: '#/components/parameters/fieldsParam' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: $ref: '#/components/responses/search200' - 204: - $ref: '#/components/responses/search204' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -145,10 +144,11 @@ paths: - generic summary: 'CREATE: Inserts a new document into the collection' description: - Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header. + Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified. + `identifier` is included in response body or it can be parsed from the `Location` response header. - When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers. + When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 200 HTTP status code along with `Last-Modified` and correct `Location` headers. The response body then includes `isDeduplication`=`true` and `deduplicatedIdentifier` fields. This operation provides autopruning of the collection (if autopruning is enabled). @@ -165,13 +165,14 @@ paths: $ref: '#/components/schemas/DocumentToPost' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: + 200: + $ref: '#/components/responses/200Deduplication' 201: $ref: '#/components/responses/201CreatedLocation' - 204: - $ref: '#/components/responses/204NoContentLocation' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -215,7 +216,7 @@ paths: Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code. - If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned. When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side. @@ -229,7 +230,8 @@ paths: - $ref: '#/components/parameters/fieldsParam' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: @@ -254,10 +256,10 @@ paths: - generic summary: 'UPDATE: Updates a document in the collection' description: - Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body. + Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 200 HTTP status code will be returned. - If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned. When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter). @@ -283,13 +285,14 @@ paths: $ref: '#/components/schemas/DocumentToPost' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: + 200: + $ref: '#/components/responses/200Ok' 201: $ref: '#/components/responses/201Created' - 204: - $ref: '#/components/responses/204NoContentLocation' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -312,10 +315,10 @@ paths: - generic summary: 'PATCH: Partially updates document in the collection' description: - Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body. + Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 200 HTTP status code will be returned. - If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned. When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code. @@ -347,11 +350,12 @@ paths: $ref: '#/components/schemas/DocumentToPost' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: - 204: - $ref: '#/components/responses/204NoContentLocation' + 200: + $ref: '#/components/responses/200Ok' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -360,10 +364,10 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' - 412: - $ref: '#/components/responses/412PreconditionFailed' 410: $ref: '#/components/responses/410Gone' + 412: + $ref: '#/components/responses/412PreconditionFailed' 422: $ref: '#/components/responses/422UnprocessableEntity' @@ -387,11 +391,12 @@ paths: - $ref: '#/components/parameters/permanentParam' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: - 204: - description: Successful operation - empty response + 200: + $ref: '#/components/responses/200Ok' 401: $ref: '#/components/responses/401Unauthorized' 403: @@ -430,9 +435,6 @@ paths: Deleted documents will appear with `isValid` = `false` field. - When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content. - - HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections). @@ -448,13 +450,12 @@ paths: - $ref: '#/components/parameters/fieldsParam' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: $ref: '#/components/responses/history200' - 204: - $ref: '#/components/responses/history204' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -509,13 +510,12 @@ paths: - $ref: '#/components/parameters/fieldsParam' security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: $ref: '#/components/responses/history200' - 204: - $ref: '#/components/responses/history204' 400: $ref: '#/components/responses/400BadRequest' 401: @@ -556,7 +556,8 @@ paths: This operation requires authorization in contrast with VERSION operation. security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: @@ -591,7 +592,8 @@ paths: This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to. security: - - apiKeyAuth: [] + - accessToken: [] + - jwtoken: [] responses: 200: @@ -850,33 +852,73 @@ components: ###################################################################################### responses: - 201Created: - description: Successfully created a new document in collection - headers: - 'Last-Modified': - $ref: '#/components/schemas/headerLastModified' + 200Ok: + description: The request was successfully processed + content: + application/json: + schema: + properties: + status: + type: integer + example: 200 - 201CreatedLocation: - description: Successfully created a new document in collection + 200Deduplication: + description: Successfully updated a duplicate document in the collection headers: 'Last-Modified': $ref: '#/components/schemas/headerLastModified' 'Location': $ref: '#/components/schemas/headerLocation' + content: + application/json: + schema: + properties: + status: + type: integer + example: 200 + identifier: + $ref: '#/components/schemas/identifierField' + isDeduplication: + $ref: '#/components/schemas/isDeduplicationField' + deduplicatedIdentifier: + $ref: '#/components/schemas/deduplicatedIdentifierField' + - 204NoContent: - description: Successfully finished operation + 201Created: + description: Successfully created a new document in collection headers: 'Last-Modified': $ref: '#/components/schemas/headerLastModified' + content: + application/json: + schema: + properties: + status: + type: integer + example: 201 + identifier: + $ref: '#/components/schemas/identifierField' + lastModified: + $ref: '#/components/schemas/lastModifiedField' - 204NoContentLocation: - description: Successfully finished operation + 201CreatedLocation: + description: Successfully created a new document in collection headers: 'Last-Modified': $ref: '#/components/schemas/headerLastModified' 'Location': $ref: '#/components/schemas/headerLocation' + content: + application/json: + schema: + properties: + status: + type: integer + example: 201 + identifier: + $ref: '#/components/schemas/identifierField' + lastModified: + $ref: '#/components/schemas/lastModifiedField' 304NotModified: description: The document has not been modified on the server since timestamp specified in If-Modified-Since header @@ -886,34 +928,95 @@ components: 400BadRequest: description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present. + content: + application/json: + schema: + properties: + status: + type: integer + example: 400 401Unauthorized: description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy. + content: + application/json: + schema: + properties: + status: + type: integer + example: 401 403Forbidden: description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation. + content: + application/json: + schema: + properties: + status: + type: integer + example: 403 404NotFound: description: The collection or document specified was not found. + content: + application/json: + schema: + properties: + status: + type: integer + example: 404 406NotAcceptable: description: The requested content type (in `Accept` header) is not supported. + content: + application/json: + schema: + properties: + status: + type: integer + example: 406 412PreconditionFailed: description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header). + content: + application/json: + schema: + properties: + status: + type: integer + example: 412 410Gone: description: The requested document has already been deleted. + content: + application/json: + schema: + properties: + status: + type: integer + example: 410 422UnprocessableEntity: description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`). + content: + application/json: + schema: + properties: + status: + type: integer + example: 422 search200: description: Successful operation returning array of documents matching the filtering criteria content: application/json: schema: - $ref: '#/components/schemas/DocumentArray' + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/DocumentArray' text/csv: schema: $ref: '#/components/schemas/DocumentArray' @@ -921,15 +1024,17 @@ components: schema: $ref: '#/components/schemas/DocumentArray' - search204: - description: Successful operation - no documents matching the filtering criteria - read200: description: The document has been succesfully found and its JSON form returned in the response content. content: application/json: schema: - $ref: '#/components/schemas/Document' + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/Document' text/csv: schema: $ref: '#/components/schemas/Document' @@ -946,7 +1051,12 @@ components: content: application/json: schema: - $ref: '#/components/schemas/DocumentArray' + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/DocumentArray' text/csv: schema: $ref: '#/components/schemas/DocumentArray' @@ -959,15 +1069,18 @@ components: 'ETag': $ref: '#/components/schemas/headerEtagLastModifiedMaximum' - history204: - description: No changes detected - lastModified200: description: Successful operation returning the timestamps content: application/json: schema: - $ref: '#/components/schemas/LastModifiedResult' + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/LastModifiedResult' + ###################################################################################### schemas: @@ -1025,6 +1138,44 @@ components: example: '53409478-105f-11e9-ab14-d663bd873d93' + identifierField: + description: + Identifier of created or modified document + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + + lastModifiedField: + type: integer + format: int64 + description: + Timestamp of the last document modification on the server, formatted as + + Unix epoch in milliseconds (1525383610088) + example: 1525383610088 + + statusField: + type: integer + description: + HTTP response status code. The status appears also in response body's field for those clients + that are unable to process standard HTTP status code. + example: 200 + + + isDeduplicationField: + type: boolean + description: + Flag whether the operation found a duplicate document (to update) + example: true + + + deduplicatedIdentifierField: + type: string + description: + The original document that has been marked as a duplicate document and which has been updated + example: 'abc09478-105f-11e9-ab14-d663bd873d93' + + DocumentBase: description: Shared base for all documents properties: @@ -1527,6 +1678,17 @@ components: Version: description: Information about versions + type: object + properties: + + status: + $ref: '#/components/schemas/statusField' + + result: + $ref: '#/components/schemas/VersionResult' + + + VersionResult: type: object properties: @@ -1562,8 +1724,17 @@ components: Status: description: Information about versions and API permissions + properties: + status: + $ref: '#/components/schemas/statusField' + + result: + $ref: '#/components/schemas/StatusResult' + + + StatusResult: allOf: - - $ref: '#/components/schemas/Version' + - $ref: '#/components/schemas/VersionResult' - type: object properties: @@ -1586,7 +1757,6 @@ components: type: string example: 'crud' - LastModifiedResult: description: Result of LAST MODIFIED operation properties: @@ -1644,4 +1814,4 @@ components: type: http scheme: bearer description: Use this if you know the temporary json webtoken. - bearerFormat: JWT \ No newline at end of file + bearerFormat: JWT diff --git a/tests/api3.basic.test.js b/tests/api3.basic.test.js index d13b8628562..28f4c667ee1 100644 --- a/tests/api3.basic.test.js +++ b/tests/api3.basic.test.js @@ -29,11 +29,13 @@ describe('Basic REST API3', function() { .expect(200); const apiConst = require('../lib/api3/const.json') - , software = require('../package.json'); + , software = require('../package.json') + , result = res.body.result; - res.body.version.should.equal(software.version); - res.body.apiVersion.should.equal(apiConst.API3_VERSION); - res.body.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050); + res.body.status.should.equal(200); + result.version.should.equal(software.version); + result.apiVersion.should.equal(apiConst.API3_VERSION); + result.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050); }); }); diff --git a/tests/api3.create.test.js b/tests/api3.create.test.js index 0ab4f3d5b42..5d155f4ecc1 100644 --- a/tests/api3.create.test.js +++ b/tests/api3.create.test.js @@ -29,8 +29,10 @@ describe('API3 CREATE', function() { * Cleanup after successful creation */ self.delete = async function deletePermanent (identifier) { - await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) - .expect(204); + let res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) + .expect(200); + + res.body.status.should.equal(200); }; @@ -41,7 +43,8 @@ describe('API3 CREATE', function() { let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) .expect(200); - return res.body; + res.body.status.should.equal(200); + return res.body.result; }; @@ -52,7 +55,8 @@ describe('API3 CREATE', function() { let res = await self.instance.get(`${self.url}?date$eq=${date}&token=${self.token.read}`) .expect(200); - return res.body; + res.body.status.should.equal(200); + return res.body.result; }; @@ -103,7 +107,8 @@ describe('API3 CREATE', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); + should.not.exist(res.body.result); }); @@ -118,9 +123,11 @@ describe('API3 CREATE', function() { it('should reject empty body', async () => { - await self.instance.post(self.urlToken) + let res = await self.instance.post(self.urlToken) .send({ }) .expect(400); + + res.body.status.should.equal(400); }); @@ -129,12 +136,15 @@ describe('API3 CREATE', function() { .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); + res.body.identifier.should.equal(self.validDoc.identifier); res.headers.location.should.equal(`${self.url}/${self.validDoc.identifier}`); + const lastModifiedBody = res.body.lastModified; const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds let body = await self.get(self.validDoc.identifier); body.should.containEql(self.validDoc); + body.srvModified.should.equal(lastModifiedBody); self.cache.nextShouldEql(self.col, self.validDoc) const ms = body.srvModified % 1000; @@ -331,7 +341,7 @@ describe('API3 CREATE', function() { }); - it('should deduplicate document by identifier', async () => { + it('should upsert document (matched by identifier)', async () => { self.validDoc.date = (new Date()).getTime(); self.validDoc.identifier = utils.randomString('32', 'aA#'); @@ -349,9 +359,11 @@ describe('API3 CREATE', function() { insulin: 0.5 }); - await self.instance.post(`${self.url}?token=${self.token.all}`) + let resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`) .send(doc2) - .expect(204); + .expect(200); + + resPost2.body.status.should.equal(200); let updatedBody = await self.get(doc2.identifier); updatedBody.should.containEql(doc2); @@ -387,9 +399,15 @@ describe('API3 CREATE', function() { }); delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round - await self.instance.post(`${self.url}?token=${self.token.all}`) + const resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`) .send(doc2) - .expect(204); + .expect(200); + + resPost2.body.status.should.equal(200); + resPost2.body.identifier.should.equal(doc2.identifier); + resPost2.body.isDeduplication.should.equal(true); + // doc (first document) was created "the old way" without identifier, so _id becomes the identifier for doc + resPost2.body.deduplicatedIdentifier.should.equal(doc._id); let updatedBody = await self.get(doc2.identifier); updatedBody.should.containEql(doc2); @@ -436,6 +454,8 @@ describe('API3 CREATE', function() { let updatedBody = await self.get(doc2.identifier); updatedBody.should.containEql(doc2); updatedBody.identifier.should.not.equal(oldBody.identifier); + should.not.exist(updatedBody.isDeduplication); + should.not.exist(updatedBody.deduplicatedIdentifier); self.cache.nextShouldEql(self.col, doc2) await self.delete(doc2.identifier); @@ -456,25 +476,27 @@ describe('API3 CREATE', function() { .expect(201); self.cache.nextShouldEql(self.col, Object.assign({}, doc, { date: date1.getTime() })); - await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) - .expect(204); + let res = await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + .expect(200); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) const date2 = new Date(); - let res = await self.instance.post(self.urlToken) + res = await self.instance.post(self.urlToken) .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) .expect(403); - res.body.status.should.be.equal(403); - res.body.message.should.be.equal('Missing permission api:treatments:update'); + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:update'); self.cache.shouldBeEmpty() const doc2 = Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() }); res = await self.instance.post(`${self.url}?token=${self.token.all}`) .send(doc2) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); + res.body.identifier.should.equal(identifier); self.cache.nextShouldEql(self.col, Object.assign({}, doc2, { date: date2.getTime() })); let body = await self.get(identifier); @@ -495,7 +517,8 @@ describe('API3 CREATE', function() { .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); + res.body.identifier.should.equal(validIdentifier); res.headers.location.should.equal(`${self.url}/${validIdentifier}`); self.validDoc.identifier = validIdentifier; @@ -517,7 +540,8 @@ describe('API3 CREATE', function() { .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); + res.body.identifier.should.equal(validIdentifier); res.headers.location.should.equal(`${self.url}/${validIdentifier}`); self.validDoc.identifier = validIdentifier; @@ -528,9 +552,12 @@ describe('API3 CREATE', function() { delete self.validDoc.identifier; res = await self.instance.post(`${self.url}?token=${self.token.update}`) .send(self.validDoc) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); + res.body.identifier.should.equal(validIdentifier); + res.body.isDeduplication.should.equal(true); + should.not.exist(res.body.deduplicatedIdentifier); // no identifier change occured res.headers.location.should.equal(`${self.url}/${validIdentifier}`); self.validDoc.identifier = validIdentifier; self.cache.nextShouldEql(self.col, self.validDoc); diff --git a/tests/api3.delete.test.js b/tests/api3.delete.test.js index 5ca87a0d57b..fb15754ed9d 100644 --- a/tests/api3.delete.test.js +++ b/tests/api3.delete.test.js @@ -57,7 +57,7 @@ describe('API3 UPDATE', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); }); }); diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js index 49bd7664d30..a4888505fe8 100644 --- a/tests/api3.generic.workflow.test.js +++ b/tests/api3.generic.workflow.test.js @@ -65,8 +65,9 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`) .expect(200); - res.body.length.should.be.above(0); - res.body.should.matchAny(value => { + res.body.status.should.equal(200); + res.body.result.length.should.be.above(0); + res.body.result.should.matchAny(value => { value.identifier.should.be.eql(self.identifier); value.srvModified.should.be.above(self.historyTimestamp); @@ -83,9 +84,10 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlLastModified}?token=${self.token.read}`) .expect(200); - self.historyTimestamp = res.body.collections.treatments; + res.body.status.should.equal(200); + self.historyTimestamp = res.body.result.collections.treatments; if (!self.historyTimestamp) { - self.historyTimestamp = res.body.srvDate - (10 * 60 * 1000); + self.historyTimestamp = res.body.result.srvDate - (10 * 60 * 1000); } self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); }); @@ -95,7 +97,8 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`/api/v3/status?token=${self.token.read}`) .expect(200); - self.historyTimestamp = res.body.srvDate; + res.body.status.should.equal(200); + self.historyTimestamp = res.body.result.srvDate; self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); }); @@ -111,7 +114,8 @@ describe('Generic REST API3', function() { .query({ 'identifier_eq': self.identifier }) .expect(200); - res.body.should.have.length(0); + res.body.status.should.equal(200); + res.body.result.should.have.length(0); }); @@ -134,8 +138,9 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) .expect(200); - res.body.should.containEql(self.docOriginal); - self.docActual = res.body; + res.body.status.should.equal(200); + res.body.result.should.containEql(self.docOriginal); + self.docActual = res.body.result; if (self.historyTimestamp >= self.docActual.srvModified) { self.historyTimestamp = self.docActual.srvModified - 1; @@ -148,8 +153,9 @@ describe('Generic REST API3', function() { .query({ 'identifier$eq': self.identifier }) .expect(200); - res.body.length.should.be.above(0); - res.body.should.matchAny(value => { + res.body.status.should.equal(200); + res.body.result.length.should.be.above(0); + res.body.result.should.matchAny(value => { value.identifier.should.be.eql(self.identifier); }); }); @@ -163,10 +169,11 @@ describe('Generic REST API3', function() { it('UPDATE document', async () => { self.docActual.insulin = 0.5; - await self.instance.put(`${self.urlResource}?token=${self.token.update}`) + let res = await self.instance.put(`${self.urlResource}?token=${self.token.update}`) .send(self.docActual) - .expect(204); + .expect(200); + res.body.status.should.equal(200); self.docActual.subject = self.subject.apiUpdate.name; delete self.docActual.srvModified; @@ -183,9 +190,10 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) .expect(200); + res.body.status.should.equal(200); delete self.docActual.srvModified; - res.body.should.containEql(self.docActual); - self.docActual = res.body; + res.body.result.should.containEql(self.docActual); + self.docActual = res.body.result; }); @@ -193,10 +201,11 @@ describe('Generic REST API3', function() { self.docActual.carbs = 5; self.docActual.insulin = 0.4; - await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + let res = await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) - .expect(204); + .expect(200); + res.body.status.should.equal(200); delete self.docActual.srvModified; self.cache.nextShouldEql(self.col, self.docActual) @@ -212,16 +221,18 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) .expect(200); + res.body.status.should.equal(200); delete self.docActual.srvModified; - res.body.should.containEql(self.docActual); - self.docActual = res.body; + res.body.result.should.containEql(self.docActual); + self.docActual = res.body.result; }); it('soft DELETE', async () => { - await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) - .expect(204); + let res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(200); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) }); @@ -238,7 +249,8 @@ describe('Generic REST API3', function() { .query({ 'identifier_eq': self.identifier }) .expect(200); - res.body.should.have.length(0); + res.body.status.should.equal(200); + res.body.result.should.have.length(0); }); @@ -250,10 +262,11 @@ describe('Generic REST API3', function() { it('permanent DELETE', async () => { - await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + let res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) .query({ 'permanent': 'true' }) - .expect(204); + .expect(200); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) }); @@ -267,13 +280,10 @@ describe('Generic REST API3', function() { it('document permanently deleted not in HISTORY', async () => { let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`); - if (res.status === 200) { - res.body.should.matchEach(value => { - value.identifier.should.not.be.eql(self.identifier); - }); - } else { - res.status.should.equal(204); - } + res.body.status.should.equal(200); + res.body.result.should.matchEach(value => { + value.identifier.should.not.be.eql(self.identifier); + }); }); @@ -285,7 +295,8 @@ describe('Generic REST API3', function() { let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) .expect(200); - self.docActual = res.body; + res.body.status.should.equal(200); + self.docActual = res.body.result; delete self.docActual.srvModified; const readOnlyMessage = 'Trying to modify read-only document'; @@ -314,7 +325,8 @@ describe('Generic REST API3', function() { res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) .expect(200); - res.body.should.containEql(self.docOriginal); + res.body.status.should.equal(200); + res.body.result.should.containEql(self.docOriginal); }); }); diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js index 750bb37b745..8bf13a68f4f 100644 --- a/tests/api3.patch.test.js +++ b/tests/api3.patch.test.js @@ -31,7 +31,8 @@ describe('API3 PATCH', function() { let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) .expect(200); - return res.body; + res.body.status.should.equal(200); + return res.body.result; }; @@ -81,7 +82,7 @@ describe('API3 PATCH', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); }); @@ -90,14 +91,14 @@ describe('API3 PATCH', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); // now let's insert the document for further patching res = await self.instance.post(`${self.url}?token=${self.token.create}`) .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); self.cache.nextShouldEql(self.col, self.validDoc) }); @@ -217,9 +218,9 @@ describe('API3 PATCH', function() { let res = await self.instance.patch(self.urlToken) .send(self.validDoc) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); let body = await self.get(self.validDoc.identifier); body.carbs.should.equal(10); diff --git a/tests/api3.read.test.js b/tests/api3.read.test.js index 3c7f0eaf964..7a330769f7c 100644 --- a/tests/api3.read.test.js +++ b/tests/api3.read.test.js @@ -68,16 +68,18 @@ describe('API3 READ', function () { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); - + res.body.status.should.equal(404); + should.not.exist(res.body.result); self.cache.shouldBeEmpty() }); it('should not found not existing document', async () => { - await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) .expect(404); + res.body.status.should.equal(404); + should.not.exist(res.body.result); self.cache.shouldBeEmpty() }); @@ -87,16 +89,18 @@ describe('API3 READ', function () { .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) .expect(200); - res.body.should.containEql(self.validDoc); - res.body.should.have.property('srvCreated').which.is.a.Number(); - res.body.should.have.property('srvModified').which.is.a.Number(); - res.body.should.have.property('subject'); - self.validDoc.subject = res.body.subject; // let's store subject for later tests + res.body.status.should.equal(200); + const result = res.body.result; + result.should.containEql(self.validDoc); + result.should.have.property('srvCreated').which.is.a.Number(); + result.should.have.property('srvModified').which.is.a.Number(); + result.should.have.property('subject'); + self.validDoc.subject = result.subject; // let's store subject for later tests self.cache.nextShouldEql(self.col, self.validDoc) }); @@ -106,12 +110,13 @@ describe('API3 READ', function () { let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=date,device,subject&token=${self.token.read}`) .expect(200); + res.body.status.should.equal(200); const correct = { date: self.validDoc.date, device: self.validDoc.device, subject: self.validDoc.subject }; - res.body.should.eql(correct); + res.body.result.should.eql(correct); }); @@ -119,8 +124,9 @@ describe('API3 READ', function () { let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=_all&token=${self.token.read}`) .expect(200); + res.body.status.should.equal(200); for (let fieldName of ['app', 'date', 'device', 'identifier', 'srvModified', 'uploaderBattery', 'subject']) { - res.body.should.have.property(fieldName); + res.body.result.should.have.property(fieldName); } }); @@ -139,39 +145,42 @@ describe('API3 READ', function () { .set('If-Modified-Since', new Date(new Date(self.validDoc.date).getTime() - 1000).toUTCString()) .expect(200); - res.body.should.containEql(self.validDoc); + res.body.status.should.equal(200); + res.body.result.should.containEql(self.validDoc); }); it('should recognize softly deleted document', async () => { let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) .expect(410); - res.body.should.be.empty(); + res.body.status.should.equal(410); + should.not.exist(res.body.result); }); - it('should not found permanently deleted document', async () => { + it('should not find permanently deleted document', async () => { let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?permanent=true&token=${self.token.delete}`) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); + should.not.exist(res.body.result); }); - it('should found document created by APIv1', async () => { + it('should find document created by APIv1', async () => { const doc = Object.assign({}, self.validDoc, { created_at: new Date(self.validDoc.date).toISOString() @@ -195,12 +204,13 @@ describe('API3 READ', function () { let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) .expect(200); - res.body.should.containEql(doc); + res.body.status.should.equal(200); + res.body.result.should.containEql(doc); res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) }); diff --git a/tests/api3.renderer.test.js b/tests/api3.renderer.test.js index f92af5b7d72..d897cf96306 100644 --- a/tests/api3.renderer.test.js +++ b/tests/api3.renderer.test.js @@ -143,7 +143,7 @@ describe('API3 output renderers', function() { .send(doc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); res = await self.instance.get(`${self.url}/${doc.identifier}?token=${self.token.read}`) .expect(200); @@ -163,7 +163,9 @@ describe('API3 output renderers', function() { async function check406 (request) { const res = await request .expect(406); + res.status.should.equal(406); res.body.message.should.eql('Unsupported output format requested'); + should.not.exist(res.body.result); } await check406(self.instance.get(`${self.url}/${self.doc1.identifier}.ttf?fields=_all&token=${self.token.read}`)); @@ -271,16 +273,16 @@ describe('API3 output renderers', function() { it('should remove mock documents', async () => { async function deleteDoc (identifier) { - await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + let res = await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) .query({ 'permanent': 'true' }) - .expect(204); + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col); } await deleteDoc(self.doc1.identifier); - self.cache.nextShouldDeleteLast(self.col) - await deleteDoc(self.doc2.identifier); - self.cache.nextShouldDeleteLast(self.col) }); }); diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js index af109a18451..d504bdb7937 100644 --- a/tests/api3.search.test.js +++ b/tests/api3.search.test.js @@ -75,6 +75,7 @@ describe('API3 SEARCH', function() { res.body.status.should.equal(401); res.body.message.should.equal('Missing or bad access token or JWT'); + should.not.exist(res.body.result); }); @@ -83,7 +84,8 @@ describe('API3 SEARCH', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); + should.not.exist(res.body.result); }); @@ -91,7 +93,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(self.urlToken) .expect(200); - res.body.length.should.be.aboveOrEqual(self.docs.length); + res.body.status.should.equal(200); + res.body.result.length.should.be.aboveOrEqual(self.docs.length); }); @@ -99,7 +102,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(self.urlTest) .expect(200); - res.body.length.should.be.aboveOrEqual(self.docs.length); + res.body.status.should.equal(200); + res.body.result.length.should.be.aboveOrEqual(self.docs.length); }); @@ -107,8 +111,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&limit=INVALID`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter limit out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); }); @@ -116,8 +121,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&limit=-1`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter limit out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); }); @@ -125,8 +131,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&limit=0`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter limit out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); }); @@ -134,7 +141,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&limit=3`) .expect(200); - res.body.length.should.be.equal(3); + res.body.status.should.equal(200); + res.body.result.length.should.equal(3); }); @@ -142,8 +150,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&skip=INVALID`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter skip out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter skip out of tolerance'); + should.not.exist(res.body.result); }); @@ -151,8 +160,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&skip=-5`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter skip out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter skip out of tolerance'); + should.not.exist(res.body.result); }); @@ -160,8 +170,9 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&sort=date&sort$desc=created_at`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameters sort and sort_desc cannot be combined'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameters sort and sort_desc cannot be combined'); + should.not.exist(res.body.result); }); @@ -169,14 +180,16 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlTest}&sort=date`) .expect(200); - const ascending = res.body; + res.body.status.should.equal(200); + const ascending = res.body.result; const length = ascending.length; length.should.be.aboveOrEqual(self.docs.length); res = await self.instance.get(`${self.urlTest}&sort$desc=date`) .expect(200); - const descending = res.body; + res.body.status.should.equal(200); + const descending = res.body.result; descending.length.should.equal(length); for (let i in ascending) { @@ -193,14 +206,16 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&sort=date&limit=8`) .expect(200); - const fullDocs = res.body; - fullDocs.length.should.be.equal(8); + res.body.status.should.equal(200); + const fullDocs = res.body.result; + fullDocs.length.should.equal(8); res = await self.instance.get(`${self.urlToken}&sort=date&skip=3&limit=5`) .expect(200); - const skipDocs = res.body; - skipDocs.length.should.be.equal(5); + res.body.status.should.equal(200); + const skipDocs = res.body.result; + skipDocs.length.should.equal(5); for (let i = 0; i < 3; i++) { skipDocs[i].should.be.eql(fullDocs[i + 3]); @@ -212,7 +227,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&fields=date,app,subject`) .expect(200); - res.body.forEach(doc => { + res.body.status.should.equal(200); + res.body.result.forEach(doc => { const docFields = Object.getOwnPropertyNames(doc); docFields.sort().should.be.eql(['app', 'date', 'subject']); }); @@ -223,7 +239,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&fields=_all`) .expect(200); - res.body.forEach(doc => { + res.body.status.should.equal(200); + res.body.result.forEach(doc => { Object.getOwnPropertyNames(doc).length.should.be.aboveOrEqual(10); Object.prototype.hasOwnProperty.call(doc, '_id').should.not.be.true(); Object.prototype.hasOwnProperty.call(doc, 'identifier').should.be.true(); @@ -240,8 +257,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}&limit=10`) .expect(400); - res.body.status.should.be.equal(400); - res.body.message.should.be.equal('Parameter limit out of tolerance'); + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); apiApp.set('API3_MAX_LIMIT', limitBackup); }); @@ -253,7 +270,8 @@ describe('API3 SEARCH', function() { let res = await self.instance.get(`${self.urlToken}`) .expect(200); - res.body.length.should.be.equal(5); + res.body.status.should.equal(200); + res.body.result.length.should.equal(5); apiApp.set('API3_MAX_LIMIT', limitBackup); }); diff --git a/tests/api3.socket.test.js b/tests/api3.socket.test.js index 9560c200c65..8c63486bba1 100644 --- a/tests/api3.socket.test.js +++ b/tests/api3.socket.test.js @@ -128,7 +128,7 @@ describe('Socket.IO in REST API3', function() { self.instance.put(`${self.urlResource}?token=${self.token.update}`) .send(self.docActual) - .expect(204) + .expect(200) .end((err) => { should.not.exist(err); self.docActual.subject = self.subject.apiUpdate.name; @@ -152,7 +152,7 @@ describe('Socket.IO in REST API3', function() { self.instance.patch(`${self.urlResource}?token=${self.token.update}`) .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) - .expect(204) + .expect(200) .end((err) => { should.not.exist(err); }); @@ -168,7 +168,7 @@ describe('Socket.IO in REST API3', function() { }); self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) - .expect(204) + .expect(200) .end((err) => { should.not.exist(err); }); diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js index 71f1c021192..7c28dbb2205 100644 --- a/tests/api3.update.test.js +++ b/tests/api3.update.test.js @@ -32,7 +32,8 @@ describe('API3 UPDATE', function() { let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) .expect(200); - return res.body; + res.body.status.should.equal(200); + return res.body.result; }; @@ -82,7 +83,7 @@ describe('API3 UPDATE', function() { .send(self.validDoc) .expect(404); - res.body.should.be.empty(); + res.body.status.should.equal(404); }); @@ -101,7 +102,8 @@ describe('API3 UPDATE', function() { .send(self.validDoc) .expect(201); - res.body.should.be.empty(); + res.body.status.should.equal(201); + res.body.identifier.should.equal(self.validDoc.identifier); self.cache.nextShouldEql(self.col, self.validDoc) const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds @@ -123,9 +125,9 @@ describe('API3 UPDATE', function() { let res = await self.instance.put(self.urlToken) .send(self.validDoc) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldEql(self.col, self.validDoc) const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds @@ -148,9 +150,9 @@ describe('API3 UPDATE', function() { let res = await self.instance.put(self.urlToken) .set('If-Unmodified-Since', new Date(new Date().getTime() + 1000).toUTCString()) .send(doc) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldEql(self.col, doc) let body = await self.get(self.validDoc.identifier); @@ -170,7 +172,7 @@ describe('API3 UPDATE', function() { .send(doc) .expect(412); - res.body.should.be.empty(); + res.body.status.should.equal(412); body = await self.get(doc.identifier); body.should.eql(self.validDoc); @@ -282,9 +284,9 @@ describe('API3 UPDATE', function() { let res = await self.instance.put(self.urlToken) .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED' })) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); delete self.validDoc.srvModified; self.cache.nextShouldEql(self.col, self.validDoc) }); @@ -292,16 +294,16 @@ describe('API3 UPDATE', function() { it('should not update deleted document', async () => { let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) - .expect(204); + .expect(200); - res.body.should.be.empty(); + res.body.status.should.equal(200); self.cache.nextShouldDeleteLast(self.col) res = await self.instance.put(self.urlToken) .send(self.validDoc) .expect(410); - res.body.should.be.empty(); + res.body.status.should.equal(410); }); });