Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/api3/const.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"UNAUTHORIZED": 401,
"FORBIDDEN": 403,
"NOT_FOUND": 404,
"NOT_ACCEPTABLE": 406,
"GONE": 410,
"PRECONDITION_FAILED": 412,
"UNPROCESSABLE_ENTITY": 422,
Expand All @@ -40,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_406_UNSUPPORTED_FORMAT": "Unsupported output format requested",
"HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document",
"HTTP_500_INTERNAL_ERROR": "Internal Server Error",
"STORAGE_ERROR": "Database error",
Expand Down
88 changes: 88 additions & 0 deletions lib/api3/doc/formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# APIv3: Output formats

### Choosing output format
In APIv3, the standard content type is JSON for both HTTP request and HTTP response.
However, in HTTP response, the response content type can be changed to XML or CSV
for READ, SEARCH, and HISTORY operations.

The response content type can be requested in one of the following ways:
- add a file type extension to the URL, eg.
`/api/v3/entries.csv?...`
or `/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895.xml?...`
- set `Accept` HTTP request header to `text/csv` or `application/xml`

The server replies with `406 Not Acceptable` HTTP status in case of not supported content type.


### JSON

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
}
]
```

### XML

Sample output:

```
<?xml version='1.0' encoding='utf-8'?>
<items>
<item>
<type>sgv</type>
<sgv>171</sgv>
<dateString>2014-07-19T02:44:15.000-07:00</dateString>
<date>1405763055000</date>
<device>dexcom</device>
<direction>Flat</direction>
<identifier>5c5a2404e0196f4d3d9a718a</identifier>
<srvModified>1405763055000</srvModified>
<srvCreated>1405763055000</srvCreated>
</item>
<item>
<type>sgv</type>
<sgv>176</sgv>
<dateString>2014-07-19T03:09:15.000-07:00</dateString>
<date>1405764555000</date>
<device>dexcom</device>
<direction>Flat</direction>
<identifier>5c5a2404e0196f4d3d9a7187</identifier>
<srvModified>1405764555000</srvModified>
<srvCreated>1405764555000</srvCreated>
</item>
</items>
```

### CSV

Sample output:

```
type,sgv,dateString,date,device,direction,identifier,srvModified,srvCreated
sgv,171,2014-07-19T02:44:15.000-07:00,1405763055000,dexcom,Flat,5c5a2404e0196f4d3d9a718a,1405763055000,1405763055000
sgv,176,2014-07-19T03:09:15.000-07:00,1405764555000,dexcom,Flat,5c5a2404e0196f4d3d9a7187,1405764555000,1405764555000
```
3 changes: 2 additions & 1 deletion lib/api3/generic/create/insert.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
;

/**
Expand Down Expand Up @@ -33,7 +34,7 @@ async function insert (opCtx, doc) {
throw new Error('empty identifier');

res.setHeader('Last-Modified', now.toUTCString());
res.setHeader('Location', `${req.baseUrl}${req.path}/${identifier}`);
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier));
res.status(apiConst.HTTP.CREATED).send({ });

ctx.bus.emit('storage-socket-create', { colName: col.colName, doc });
Expand Down
4 changes: 3 additions & 1 deletion lib/api3/generic/history/operation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const dateTools = require('../../shared/dateTools')
, renderer = require('../../shared/renderer')
, apiConst = require('../../const.json')
, security = require('../../security')
, opTools = require('../../shared/operationTools')
Expand Down Expand Up @@ -53,7 +54,8 @@ async function history (opCtx, fieldsProjector) {

_.each(result, fieldsProjector.applyProjection);

res.status(apiConst.HTTP.OK).send(result);
res.status(apiConst.HTTP.OK);
renderer.render(res, result);
}
}

Expand Down
4 changes: 3 additions & 1 deletion lib/api3/generic/read/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
, security = require('../../security')
, opTools = require('../../shared/operationTools')
, dateTools = require('../../shared/dateTools')
, renderer = require('../../shared/renderer')
, FieldsProjector = require('../../shared/fieldsProjector')
;

Expand Down Expand Up @@ -48,7 +49,8 @@ async function read (opCtx) {

fieldsProjector.applyProjection(doc);

res.status(apiConst.HTTP.OK).send(doc);
res.status(apiConst.HTTP.OK);
renderer.render(res, doc);
}


Expand Down
4 changes: 3 additions & 1 deletion lib/api3/generic/search/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const apiConst = require('../../const.json')
, security = require('../../security')
, opTools = require('../../shared/operationTools')
, renderer = require('../../shared/renderer')
, input = require('./input')
, _each = require('lodash/each')
, FieldsProjector = require('../../shared/fieldsProjector')
Expand Down Expand Up @@ -49,7 +50,8 @@ async function search (opCtx) {

_each(result, fieldsProjector.applyProjection);

res.status(apiConst.HTTP.OK).send(result);
res.status(apiConst.HTTP.OK);
renderer.render(res, result);
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/api3/generic/update/replace.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
;

/**
Expand Down Expand Up @@ -38,7 +39,7 @@ async function replace (opCtx, doc, storageDoc, options) {
res.setHeader('Last-Modified', now.toUTCString());

if (storageDoc.identifier !== doc.identifier || isDeduplication) {
res.setHeader('Location', `${req.baseUrl}${req.path}/${doc.identifier}`);
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier));
}

res.status(apiConst.HTTP.NO_CONTENT).send({ });
Expand Down
5 changes: 4 additions & 1 deletion lib/api3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const express = require('express')
, bodyParser = require('body-parser')
, renderer = require('./shared/renderer')
, StorageSocket = require('./storageSocket')
, apiConst = require('./const.json')
, security = require('./security')
Expand Down Expand Up @@ -47,6 +48,8 @@ function configure (env, ctx) {
}
});

app.use(renderer.extension2accept);

// we don't need these here
app.set('etag', false);
app.set('x-powered-by', false); // this seems to be unreliable
Expand Down Expand Up @@ -74,7 +77,7 @@ function configure (env, ctx) {

app.get('/version', require('./specific/version')(app, ctx, env));

if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only
if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only
app.get('/test', async function test (req, res) {

try {
Expand Down
99 changes: 99 additions & 0 deletions lib/api3/shared/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use strict';

const apiConst = require('../const.json')
, mime = require('mime')
, url = require('url')
, opTools = require('./operationTools')
, EasyXml = require('easyxml')
, csvStringify = require('csv-stringify')
;


/**
* Middleware that converts url's extension to Accept HTTP request header
* @param {Object} req
* @param {Object} res
* @param {Function} next
*/
function extension2accept (req, res, next) {

const pathSplit = req.path.split('.');

if (pathSplit.length < 2)
return next();

const pathBase = pathSplit[0]
, extension = pathSplit.slice(1).join('.');

if (!extension)
return next();

const mimeType = mime.getType(extension);
if (!mimeType)
return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT);

req.extToAccept = {
url: req.url,
accept: req.headers.accept
};

req.headers.accept = mimeType;
const parsed = url.parse(req.url);
parsed.pathname = pathBase;
req.url = url.format(parsed);

next();
}


/**
* Sends data to output using the client's desired format
* @param {Object} res
* @param {any} data
*/
function render (res, data) {
res.format({
'json': () => res.send(data),
'csv': () => renderCsv(res, data),
'xml': () => renderXml(res, data),
'default': () =>
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT)
});
}


/**
* Format data to output as .csv
* @param {Object} res
* @param {any} data
*/
function renderCsv (res, data) {
const csvSource = Array.isArray(data) ? data : [data];
csvStringify(csvSource, {
header: true
},
function csvStringified (err, output) {
res.send(output);
});
}


/**
* Format data to output as .xml
* @param {Object} res
* @param {any} data
*/
function renderXml (res, data) {
const serializer = new EasyXml({
rootElement: 'item',
dateFormat: 'ISO',
manifest: true
});
res.send(serializer.render(data));
}


module.exports = {
extension2accept,
render
};
2 changes: 1 addition & 1 deletion lib/api3/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function setupSwaggerUI (app) {
const serveSwaggerDef = function serveSwaggerDef (req, res) {
res.sendFile(__dirname + '/swagger.yaml');
};
app.get('/swagger.yaml', serveSwaggerDef);
app.get('/swagger', serveSwaggerDef);
Copy link
Contributor

@jakobsandberg jakobsandberg Jan 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change which will result in a 404 for users who have bookmarked this page or scripts that programmatically request this route.

Do we need to change this? If so, what do you think about supporting both /swagger.yaml and /swagger?

Copy link
Contributor Author

@PetrOndrusek PetrOndrusek Jan 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that it looks like /swagger.yaml URL has changed, but in fact it works both /swagger.yaml, /swagger.
Actually /swagger.xxx returns JSON everytime, no matter which file extension you use.
Only READ, SEARCH, HISTORY operations (GETs for some collection) evaluate the Accept/extension - renderer.render(res, doc); does that.


const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath();
const swaggerFiles = express.static(swaggerUiAssetPath);
Expand Down
Loading