diff --git a/CHANGELOG.md b/CHANGELOG.md index af3055f..fbe0905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2.1.7] - 2022-12-14 + +##### Changed + +- Replace request with undici (Node's official fetch package), update dependencies, patch vulnerabilities (#155) + ## [2.1.6] - 2022-10-27 ##### Changed diff --git a/README.md b/README.md index 6429d5e..247d725 100644 --- a/README.md +++ b/README.md @@ -119,17 +119,17 @@ DEBUG=api,transformer,markdown,language node app.js Releases are done each time a dependent tool needs an `enketo-transformer` change. 1. Create release PR +1. Update `CHANGELOG.md` +1. Update version in `package.json` + - Bump to major version if downstream has to make changes. 1. Check [Dependabot](https://github.com/enketo/enketo-transformer/security/dependabot) for alerts 1. Run `npm update` - Check if `node-libxslt` has been updated because it has caused problems in the past 1. Run `npm audit` - Run `npm audit fix --production` to apply most important fixes -1. Run `npm ci` +1. Run `npm i` 1. Run `npm test` 1. Run `npm run build-docs` -1. Update `CHANGELOG.md` -1. Update version in `package.json` - - Bump to major version if downstream has to make changes. 1. Merge PR with all changes 1. Create GitHub release 1. Tag and publish the release diff --git a/docs/api.js.html b/docs/api.js.html index 1ffc334..ce2e083 100644 --- a/docs/api.js.html +++ b/docs/api.js.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -51,140 +51,143 @@

api.js

* @module api * * @description This is not a robust, secure web API. It is just a quick starting point. - * This is repo is not used in production as a web API (only as a library). + * This repo is not used in production as a web API (only as a library). * * See inventory of work to be done here: https://github.com/enketo/enketo-transformer/labels/web-api-only. * * PRs are very welcome! */ -const request = require('request'); const express = require('express'); +const { request } = require('undici'); +const transformer = require('./transformer'); const router = express.Router(); -const transformer = require('./transformer'); + +class FetchError extends Error { + /** + * @param {number} status + * @param {string} message + */ + constructor(status, message) { + super(message); + this.status = status; + } +} + +/** + * @param {express.Request} req + * @returns {Promise<string>} + */ +const getXForm = async (req) => { + const url = req.query.xform; + + try { + const response = await request(url, { + headers: { + 'X-OpenRosa-Version': '1.0', + }, + }); + const { statusCode } = response; + + if (statusCode === 401) { + throw new FetchError( + statusCode, + 'Forbidden. Authorization Required.' + ); + } + + if (statusCode < 200 || statusCode >= 300) { + throw new FetchError(statusCode, `Request to ${url} failed.`); + } + + return response.body.text(); + } catch (error) { + console.error(`Error occurred when requesting ${url}`, error); + + if (error.status == null) { + throw new FetchError(500, error.message ?? 'Unknown error.'); + } + + throw error; + } +}; + +/** + * @param {import('express').Request} req + * @returns {Promise<import('./transformer').TransformedSurvey>} + */ +const getTransformedSurvey = async (req) => { + const isPost = req.method === 'POST'; + const payload = isPost ? req.body : req.query; + const { markdown, openclinica, theme } = payload; + const media = isPost ? payload.media : {}; + const xform = req.method === 'POST' ? payload.xform : await getXForm(req); + + return transformer.transform({ + xform, + markdown: markdown !== 'false', + openclinica: openclinica === 'true', + media, + theme, + }); +}; router .all('/', (req, res, next) => { - if (req.app.get('secure')) { - res.status(405).send('Not Allowed'); - } else if (!req.query.xform) { - res.status(400).send('Bad Request.'); - } else { - // allow requests from anywhere + try { + if (req.app.get('secure')) { + throw new FetchError(405, 'Not Allowed.'); + } + + const payload = req.method === 'POST' ? req.body : req.query; + + if (payload.xform == null) { + throw new FetchError(400, 'Bad Request.'); + } + + // Allow requests from anywhere res.set('Access-Control-Allow-Origin', '*'); next(); + } catch (error) { + next(error); } }) - .get('/', (req, res) => { - _request({ - method: 'get', - url: req.query.xform, - }) - .then((xform) => - transformer.transform({ - xform, - theme: req.query.theme, - openclinica: req.query.openclinica === 'true', - markdown: req.query.markdown !== 'false', - }) - ) - .then((result) => { - res.json(result); - }) - .catch((error) => { - error.status = error.status || 500; - error.message = error.message || 'Unknown error.'; - res.status(error.status).send( - `${error.message} (stack: ${error.stack})` - ); - }); + .get('/', async (req, res) => { + try { + const survey = await getTransformedSurvey(req); + + res.json(survey); + } catch (error) { + res.status(error.status).send( + `${error.message} (stack: ${error.stack})` + ); + } }) - .post('/', (req, res) => { - transformer - .transform({ - xform: req.body.xform, - theme: req.body.theme, - media: req.body.media, - openclinica: req.body.openclinica === 'true', - markdown: req.body.markdown !== 'false', - }) - .then((result) => { - res.json(result); - }) - .catch((error) => { - error.status = error.status || 500; - error.message = error.message || 'Unknown error.'; - res.status(error.status).send( - `${error.message} (stack: ${error.stack})` - ); - }); + .post('/', async (req, res) => { + try { + const survey = await getTransformedSurvey(req); + + res.json(survey); + } catch (error) { + res.status(error.status).send( + `${error.message} (stack: ${error.stack})` + ); + } }) // for development purposes, to return HTML that can be easily inspected in the developer console - .get('/htmlform', (req, res) => { - _request({ - method: 'get', - url: req.query.xform, - }) - .then((xform) => - transformer.transform({ - xform, - theme: req.query.theme, - openclinica: req.query.openclinica === 'true', - markdown: req.query.markdown !== 'false', - }) - ) - .then((result) => { - res.send(result.form); - }) - .catch((error) => { - error.status = error.status || 500; - error.message = error.message || 'Unknown error.'; - res.status(error.status).send( - `${error.message} (stack: ${error.stack})` - ); - }); - }); - -/** - * Sends a request to an OpenRosa server. Only for basic retrieval of - * public forms that do not require authentication. - * - * @param { {url: string, method: string} } options - Request options object. - * @return { Promise<Error|object> } a promise that resolves in request body. - */ -function _request(options) { - options.headers = options.headers || {}; - options.headers['X-OpenRosa-Version'] = '1.0'; - - const method = options.method || 'get'; - - return new Promise((resolve, reject) => { - request[method](options, (error, response, body) => { - if (error) { - console.error( - `Error occurred when requesting ${options.url}`, - error - ); - reject(error); - } else if (response.statusCode === 401) { - const error = new Error('Forbidden. Authorization Required.'); - error.status = response.statusCode; - reject(error); - } else if ( - response.statusCode < 200 || - response.statusCode >= 300 - ) { - const error = new Error(`Request to ${options.url} failed.`); - error.status = response.statusCode; - reject(error); - } else { - // console.log( `response of request to ${options.url} has status code: `, response.statusCode ); - resolve(body); - } - }); + .get('/htmlform', async (req, res) => { + try { + const { form } = await getTransformedSurvey(req); + + res.set('Content-Type', 'text/html'); + res.end(form); + } catch (error) { + res.status(error.status).send.send( + `${error.message} (stack: ${error.stack})` + ); + } }); -} module.exports = (app) => { app.use('/transform', router); @@ -203,7 +206,7 @@

api.js


diff --git a/docs/global.html b/docs/global.html index 716917d..aa44f16 100644 --- a/docs/global.html +++ b/docs/global.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -449,7 +449,7 @@
Parameters:

diff --git a/docs/index.html b/docs/index.html index cf0ec76..7f41126 100644 --- a/docs/index.html +++ b/docs/index.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -213,7 +213,7 @@

Change Log


diff --git a/docs/language.js.html b/docs/language.js.html index bbaf98a..1695e7a 100644 --- a/docs/language.js.html +++ b/docs/language.js.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -168,7 +168,7 @@

language.js


diff --git a/docs/markdown.js.html b/docs/markdown.js.html index 9c28ae5..6853c4e 100644 --- a/docs/markdown.js.html +++ b/docs/markdown.js.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -250,7 +250,7 @@

markdown.js


diff --git a/docs/module-api-FetchError.html b/docs/module-api-FetchError.html new file mode 100644 index 0000000..2b1f43d --- /dev/null +++ b/docs/module-api-FetchError.html @@ -0,0 +1,279 @@ + + + + + + FetchError - Enketo Transformer + + + + + + + + + + + + + + + + + + + + + + +
+ +

FetchError

+ + + + + + + +
+ +
+ +

+ api~ + + FetchError +

+ + +
+ +
+ +
+ + + + + +

new FetchError(status, message)

+ + + + + + +
+ + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
status + + +number + + + +
message + + +string + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + +

Extends

+ + + + +
    +
  • Error
  • +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/module-api.html b/docs/module-api.html index 068be08..5e20e88 100644 --- a/docs/module-api.html +++ b/docs/module-api.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -61,7 +61,7 @@

api

This is not a robust, secure web API. It is just a quick starting point. -This is repo is not used in production as a web API (only as a library).

+This repo is not used in production as a web API (only as a library).

See inventory of work to be done here: https://github.com/enketo/enketo-transformer/labels/web-api-only.

PRs are very welcome!

@@ -153,6 +153,13 @@

api

+

Classes

+ +
+
FetchError
+
+
+ @@ -170,7 +177,7 @@

Methods

-

(inner) _request(options) → {Promise.<(Error|object)>}

+

(async, inner) getTransformedSurvey(req) → {Promise.<module:transformer~TransformedSurvey>}

@@ -182,7 +189,7 @@

(inner) _req
Source:
@@ -221,11 +228,6 @@

(inner) _req -
-

Sends a request to an OpenRosa server. Only for basic retrieval of -public forms that do not require authentication.

-
- @@ -261,13 +263,13 @@

Parameters:
- options + req -Object +module:express~Request @@ -277,7 +279,7 @@
Parameters:
-

Request options object.

+ @@ -302,11 +304,157 @@
Parameters:
Returns:
-
-

a promise that resolves in request body.

-
+
+
+ Type +
+
+ +Promise.<module:transformer~TransformedSurvey> + + +
+
+ + + + + + + + + + +

(async, inner) getXForm(req) → {Promise.<string>}

+ + + + + + +
+ + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
req + + +express.Request + + + +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
@@ -314,7 +462,7 @@
Returns:
-Promise.<(Error|object)> +Promise.<string>
@@ -344,7 +492,7 @@
Returns:

diff --git a/docs/module-language.html b/docs/module-language.html index 3b80d83..876ce49 100644 --- a/docs/module-language.html +++ b/docs/module-language.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -1105,7 +1105,7 @@
Properties:

diff --git a/docs/module-markdown.html b/docs/module-markdown.html index ff461ba..735e205 100644 --- a/docs/module-markdown.html +++ b/docs/module-markdown.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -1906,7 +1906,7 @@
Returns:

diff --git a/docs/module-transformer.html b/docs/module-transformer.html index 72daa22..ddd90b7 100644 --- a/docs/module-transformer.html +++ b/docs/module-transformer.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -3379,7 +3379,7 @@
Type:

diff --git a/docs/transformer.js.html b/docs/transformer.js.html index 308a705..04648a3 100644 --- a/docs/transformer.js.html +++ b/docs/transformer.js.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -639,7 +639,7 @@

transformer.js


diff --git a/docs/url.js.html b/docs/url.js.html index b6cb1dd..14f5e6c 100644 --- a/docs/url.js.html +++ b/docs/url.js.html @@ -32,7 +32,7 @@ -

Home

Github repo

Change log

Modules

Global

+

Home

Github repo

Change log

Classes

Modules

Global

@@ -103,7 +103,7 @@

url.js


diff --git a/package-lock.json b/package-lock.json index 7725981..ff2556d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "enketo-transformer", - "version": "2.1.6", + "version": "2.1.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2ae0497..d5f60ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enketo-transformer", - "version": "2.1.6", + "version": "2.1.7", "description": "Library/app that transforms ODK-compliant XForms into a format that Enketo can consume", "license": "Apache-2.0", "main": "src/transformer.js",