11/* eslint no-restricted-syntax: [0, "ForInStatement"] */
22
33import _ from 'lodash' ;
4- import fs from 'fs' ;
4+ import { existsSync , readFileSync } from 'fs' ;
55import http from 'http' ;
66import https from 'https' ;
7- import axios from 'axios' ;
87import express from 'express' ;
98import 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-
3217function 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 = { } ;
4126config . httpPort = process . env . HTTP_PORT ;
4227config . httpsPort = process . env . HTTPS_PORT ;
4328if ( process . env . HTTPS_CONFIG ) {
@@ -50,134 +35,27 @@ if (process.env.HTTPS_CONFIG) {
5035if ( ! config . httpPort ) {
5136 config . httpPort = 9300 ;
5237}
53- config . exportTimeout = _ . defaultTo ( parseInt ( process . env . EXPORT_TIMEOUT , 10 ) , 120000 ) ;
38+ config . exportTimeout = exportTimeout ;
5439log . info ( `Export download timeout set to ${ config . exportTimeout } ms` ) ;
5540
5641const app = express ( ) ;
5742
5843app . 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
18260function beforeShutdown ( ) {
18361 return new Promise ( ( resolve ) => {
@@ -208,7 +86,9 @@ if (config.httpPort) {
20886
20987if ( 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