Skip to content

Commit c00c2e5

Browse files
authored
Merge pull request #43 from tidepool-org/print_reports
[BACK-2540] Print reports
2 parents 5ba8245 + 667a220 commit c00c2e5

18 files changed

+5826
-1946
lines changed

.eslintrc

Lines changed: 0 additions & 16 deletions
This file was deleted.

.eslintrc.cjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
extends: 'airbnb',
3+
parser: '@babel/eslint-parser',
4+
plugins: ['lodash'],
5+
parserOptions: {
6+
ecmaVersion: 6,
7+
requireConfigFile: false,
8+
},
9+
rules: {
10+
'no-plusplus': [
11+
'error',
12+
{
13+
allowForLoopAfterthoughts: true,
14+
},
15+
],
16+
'import/extensions': [0, { '<js>': 'always' }],
17+
},
18+
settings: {
19+
lodash: 3,
20+
},
21+
};

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16.20.1

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sudo: false
33
language: node_js
44

55
node_js:
6-
- 12.14.0
6+
- 16.20.1
77
- node
88

99
cache: yarn
@@ -28,6 +28,7 @@ services:
2828

2929
script:
3030
- yarn run lint
31+
- yarn test
3132
- ./artifact.sh
3233

3334
matrix:

Dockerfile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
### Stage 0 - Base image
2-
FROM node:12.14.0-alpine as base
2+
FROM node:16.20.1-alpine as base
33
WORKDIR /app
44
RUN apk --no-cache update && \
55
apk --no-cache upgrade && \
6-
apk add --no-cache --virtual .build-dependencies python make g++ && \
6+
apk add --no-cache --virtual .build-dependencies python3 make g++ && \
77
mkdir -p node_modules && chown -R node:node .
88

99

@@ -14,7 +14,7 @@ COPY package.json .
1414
COPY yarn.lock .
1515
RUN \
1616
# Build and separate all dependancies required for production
17-
yarn install --production && cp -R node_modules production_node_modules \
17+
yarn install --production --frozen-lockfile && cp -R node_modules production_node_modules \
1818
# Build all modules, including `devDependencies`
1919
&& yarn install \
2020
&& yarn cache clean
@@ -29,7 +29,7 @@ COPY --chown=node:node --from=dependencies /app/node_modules ./node_modules
2929
COPY --chown=node:node . .
3030
USER node
3131
EXPOSE 9300
32-
CMD node -r esm ./app.js
32+
CMD node ./app.js
3333

3434

3535
### Stage 3 - Test
@@ -46,4 +46,4 @@ COPY --from=dependencies /app/production_node_modules ./node_modules
4646
COPY --chown=node:node . .
4747
USER node
4848
EXPOSE 9300
49-
CMD node -r esm ./app.js
49+
CMD node ./app.js

app.js

Lines changed: 23 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,28 @@
11
/* eslint no-restricted-syntax: [0, "ForInStatement"] */
22

33
import _ from 'lodash';
4-
import fs from 'fs';
4+
import { existsSync, readFileSync } from 'fs';
55
import http from 'http';
66
import https from 'https';
7-
import axios from 'axios';
87
import express from 'express';
98
import bodyParser from 'body-parser';
10-
import queryString from 'query-string';
11-
import dataTools from '@tidepool/data-tools';
12-
import { Registry, Counter } from 'prom-client';
13-
import logMaker from './log';
9+
import { createTerminus } from '@godaddy/terminus';
10+
import { exportTimeout, register, logMaker } from './lib/utils.js';
11+
import { getUserData, getUserReport, postUserReport } from './lib/handlers/index.js';
1412

15-
const log = logMaker('app.js', { level: process.env.DEBUG_LEVEL || 'info' });
16-
17-
const { createTerminus } = require('@godaddy/terminus');
18-
19-
const client = require('prom-client');
20-
21-
const { collectDefaultMetrics } = client;
22-
const register = new Registry();
23-
24-
collectDefaultMetrics({ register });
25-
26-
const createCounter = (name, help, labelNames) => new Counter({
27-
name, help, labelNames, registers: [register],
13+
export const log = logMaker('app.js', {
14+
level: process.env.DEBUG_LEVEL || 'info',
2815
});
2916

30-
const statusCount = createCounter('tidepool_export_status_count', 'The number of errors for each status code.', ['status_code', 'export_format']);
31-
3217
function maybeReplaceWithContentsOfFile(obj, field) {
3318
const potentialFile = obj[field];
34-
if (potentialFile != null && fs.existsSync(potentialFile)) {
19+
if (potentialFile != null && existsSync(potentialFile)) {
3520
// eslint-disable-next-line no-param-reassign
36-
obj[field] = fs.readFileSync(potentialFile).toString();
21+
obj[field] = readFileSync(potentialFile).toString();
3722
}
3823
}
3924

40-
const config = {};
25+
export const config = {};
4126
config.httpPort = process.env.HTTP_PORT;
4227
config.httpsPort = process.env.HTTPS_PORT;
4328
if (process.env.HTTPS_CONFIG) {
@@ -50,134 +35,27 @@ if (process.env.HTTPS_CONFIG) {
5035
if (!config.httpPort) {
5136
config.httpPort = 9300;
5237
}
53-
config.exportTimeout = _.defaultTo(parseInt(process.env.EXPORT_TIMEOUT, 10), 120000);
38+
config.exportTimeout = exportTimeout;
5439
log.info(`Export download timeout set to ${config.exportTimeout} ms`);
5540

5641
const app = express();
5742

5843
app.get('/metrics', async (req, res) => {
5944
res.set('Content-Type', register.contentType);
60-
res.end(register.metrics());
45+
res.end(await register.metrics());
6146
});
6247

63-
function buildHeaders(request) {
64-
if (request.headers['x-tidepool-session-token']) {
65-
return {
66-
headers: {
67-
'x-tidepool-session-token': request.headers['x-tidepool-session-token'],
68-
},
69-
};
70-
}
71-
return {};
72-
}
73-
74-
app.use(bodyParser.urlencoded({
75-
extended: false,
76-
}));
77-
78-
app.get('/export/:userid', async (req, res) => {
79-
// Set the timeout for the request. Make it 10 seconds longer than
80-
// our configured timeout to give the service time to cancel the API data
81-
// request, and close the outgoing data stream cleanly.
82-
req.setTimeout(config.exportTimeout + 10000);
83-
84-
const queryData = [];
85-
86-
let logString = `Requesting download for User ${req.params.userid}`;
87-
if (req.query.bgUnits) {
88-
logString += ` in ${req.query.bgUnits}`;
89-
}
90-
if (req.query.startDate) {
91-
queryData.startDate = req.query.startDate;
92-
logString += ` from ${req.query.startDate}`;
93-
}
94-
if (req.query.endDate) {
95-
queryData.endDate = req.query.endDate;
96-
logString += ` until ${req.query.endDate}`;
97-
}
98-
if (req.query.restricted_token) {
99-
queryData.restricted_token = req.query.restricted_token;
100-
logString += ' with restricted_token';
101-
}
102-
log.info(logString);
103-
104-
const exportFormat = req.query.format;
105-
106-
try {
107-
const cancelRequest = axios.CancelToken.source();
108-
109-
const requestConfig = buildHeaders(req);
110-
requestConfig.responseType = 'stream';
111-
requestConfig.cancelToken = cancelRequest.token;
112-
const dataResponse = await axios.get(`${process.env.API_HOST}/data/${req.params.userid}?${queryString.stringify(queryData)}`, requestConfig);
113-
log.debug(`Downloading data for User ${req.params.userid}...`);
114-
115-
const processorConfig = { bgUnits: req.query.bgUnits || 'mmol/L' };
116-
117-
let writeStream = null;
48+
app.use(
49+
bodyParser.urlencoded({
50+
extended: false,
51+
}),
52+
);
11853

119-
if (exportFormat === 'json') {
120-
res.attachment('TidepoolExport.json');
121-
writeStream = dataTools.jsonStreamWriter();
122-
123-
dataResponse.data
124-
.pipe(dataTools.jsonParser())
125-
.pipe(dataTools.splitPumpSettingsData())
126-
.pipe(dataTools.tidepoolProcessor(processorConfig))
127-
.pipe(writeStream)
128-
.pipe(res);
129-
} else {
130-
res.attachment('TidepoolExport.xlsx');
131-
writeStream = dataTools.xlsxStreamWriter(res, processorConfig);
132-
133-
dataResponse.data
134-
.pipe(dataTools.jsonParser())
135-
.pipe(dataTools.splitPumpSettingsData())
136-
.pipe(dataTools.tidepoolProcessor(processorConfig))
137-
.pipe(writeStream);
138-
}
139-
140-
// Create a timeout timer that will let us cancel the incoming request gracefully if
141-
// it's taking too long to fulfil.
142-
const timer = setTimeout(() => {
143-
res.emit('timeout', config.exportTimeout);
144-
}, config.exportTimeout);
145-
146-
// Wait for the stream to complete, by wrapping the stream completion events in a Promise.
147-
try {
148-
await new Promise((resolve, reject) => {
149-
dataResponse.data.on('end', resolve);
150-
dataResponse.data.on('error', (err) => reject(err));
151-
res.on('error', (err) => reject(err));
152-
res.on('timeout', async () => {
153-
statusCount.inc({ status_code: 408, export_format: exportFormat });
154-
reject(new Error('Data export request took too long to complete. Cancelling the request.'));
155-
});
156-
});
157-
statusCount.inc({ status_code: 200, export_format: exportFormat });
158-
log.debug(`Finished downloading data for User ${req.params.userid}`);
159-
} catch (e) {
160-
log.error(`Error while downloading: ${e}`);
161-
// Cancel the writeStream, rather than let it close normally.
162-
// We do this to show error messages in the downloaded files.
163-
writeStream.cancel();
164-
cancelRequest.cancel('Data export timed out.');
165-
}
166-
167-
clearTimeout(timer);
168-
} catch (error) {
169-
if (error.response && error.response.status === 403) {
170-
statusCount.inc({ status_code: 403, export_format: exportFormat });
171-
res.status(error.response.status).send('Not authorized to export data for this user.');
172-
log.error(`${error.response.status}: ${error}`);
173-
} else {
174-
statusCount.inc({ status_code: 500, export_format: exportFormat });
175-
res.status(500).send('Server error while processing data. Please contact Tidepool Support.');
176-
log.error(`500: ${error}`);
177-
}
178-
}
179-
});
54+
app.use(bodyParser.json());
18055

56+
app.get('/export/:userid', getUserData());
57+
app.get('/export/report/:userid', getUserReport());
58+
app.post('/export/report/:userid', postUserReport());
18159

18260
function beforeShutdown() {
18361
return new Promise((resolve) => {
@@ -208,7 +86,9 @@ if (config.httpPort) {
20886

20987
if (config.httpsPort) {
21088
if (_.isEmpty(config.httpsConfig)) {
211-
log.error('SSL endpoint is enabled, but no valid config was found. Exiting.');
89+
log.error(
90+
'SSL endpoint is enabled, but no valid config was found. Exiting.',
91+
);
21292
process.exit(1);
21393
} else {
21494
const server = https.createServer(config.httpsConfig, app);

0 commit comments

Comments
 (0)