mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
APIv3: wrapping all results in JSON (#6703)
* APIv3: isolating documents from tests (not allowing clashes of calculated identifiers) * removing unused async keyword * fixing api v3 swagger and moving it to /api3-docs * APIv3: adding cachedCollection stub of cachedCollection storage implementation * APIv3: mongo cachedCollection storage implementation * APIv3: testing and debugging cache updates * APIv3: more testing on cache updates * APIv3: fixing bad async functions * APIv3: finishing cache invalidation tests * APIv3: wrapping VERSION result * APIv3: wrapping STATUS result * APIv3: wrapping DELETE result * APIv3: wrapping READ result + partially SEARCH and HISTORY * APIv3: wrapping CREATE result * APIv3: wrapping UPDATE + PATCH result * APIv3: wrapping LAST MODIFIED result * APIv3: updating swagger doc * APIv3: updating tutorial.md * APIv3: tuning tests * APIv3: merge dev Co-authored-by: Petr Ondrusek <petr.ondrusek@seznam.cz> Co-authored-by: Petr Ondrůšek <petr.ondrusek@okin.eu> Co-authored-by: Sulka Haro <sulka@sulka.net>
This commit is contained in:
co-authored by
Petr Ondrusek
Petr Ondrůšek
Sulka Haro
parent
914ba78f36
commit
cfdbaa8bbd
+3
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+28
-25
@@ -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
|
||||
|
||||
+134
-106
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
module.exports = insert;
|
||||
|
||||
@@ -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;
|
||||
module.exports = deleteOperation;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
module.exports = patchOperation;
|
||||
|
||||
@@ -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;
|
||||
module.exports = readOperation;
|
||||
|
||||
@@ -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;
|
||||
module.exports = updateOperation;
|
||||
|
||||
@@ -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;
|
||||
module.exports = replace;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function configure (app, ctx, env) {
|
||||
}
|
||||
}
|
||||
|
||||
res.json(info);
|
||||
opTools.sendJSON({ res, result: info });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+853
-212
File diff suppressed because it is too large
Load Diff
+235
-65
@@ -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,11 +852,54 @@ components:
|
||||
######################################################################################
|
||||
responses:
|
||||
|
||||
200Ok:
|
||||
description: The request was successfully processed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
|
||||
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'
|
||||
|
||||
|
||||
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'
|
||||
|
||||
201CreatedLocation:
|
||||
description: Successfully created a new document in collection
|
||||
@@ -863,20 +908,17 @@ components:
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
'Location':
|
||||
$ref: '#/components/schemas/headerLocation'
|
||||
|
||||
204NoContent:
|
||||
description: Successfully finished operation
|
||||
headers:
|
||||
'Last-Modified':
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
|
||||
204NoContentLocation:
|
||||
description: Successfully finished operation
|
||||
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
|
||||
bearerFormat: JWT
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+50
-23
@@ -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);
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('API3 UPDATE', function() {
|
||||
.send(self.validDoc)
|
||||
.expect(404);
|
||||
|
||||
res.body.should.be.empty();
|
||||
res.body.status.should.equal(404);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
+33
-23
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+45
-27
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
+15
-13
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user