Repository: florianholzapfel/express-restify-mongoose (js)

Issue:
Make options.outputFn() asynchronous
Right now, `options.outputFn()` is called synchronously and then `options.postProcess()` is called asynchronously [immediately afterward](https://github.com/florianholzapfel/express-restifymongoose/blob/master/src/middleware/prepareOutput.js#L55). 

If both of these hooks were to make asynchronous resource calls, the execution order would be unpredictable.

If we make `outputFn()` asynchronous, we can ensure `outputFn()` finishes before calling `postProcess()`. 

For backwards compatibility, we can perform a check on `outputFn()` to call it synchronously.

Since this is backwards compatible, I went ahead and integrated it into #314 [here](https://github.com/florianholzapfel/express-restify-mongoose/pull/314/commits/6e70102e01e149a9a0dac71c02d38f77053d2bd0)

Since `outputFn()` can now be asynchronous, it can also (safely) generate errors. Right now, they're getting passed to `onError()`. There are tests for that behavior. If anyone has different ideas about how to handle those errors, let me know.


Pull request description:
Call postProcess async when outputFn returns a Promise
Closes #327

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f70691..e2cc422 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 Changelog
 =========
 
+### 4.3.0
+
+* added support for async `outputFn` by returning a Promise
+
 ### 4.2.2
 
 * removed dependency on lodash, use specific modules and native methods when possible [#352](https://github.com/florianholzapfel/express-restify-mongoose/pull/352)
diff --git a/src/middleware/prepareOutput.js b/src/middleware/prepareOutput.js
index 947227e..9631a16 100644
--- a/src/middleware/prepareOutput.js
+++ b/src/middleware/prepareOutput.js
@@ -50,10 +50,16 @@ module.exports = function (options, excludedMap) {
         res.header(typeof options.totalCountHeader === 'string' ? options.totalCountHeader : 'X-Total-Count', req.erm.totalCount)
       }
 
-      options.outputFn(req, res)
+      const promise = options.outputFn(req, res)
 
       if (options.postProcess) {
-        options.postProcess(req, res, next)
+        if (promise && typeof promise.then === 'function') {
+          promise.then(() => {
+            options.postProcess(req, res, next)
+          }).catch(errorHandler(req, res, next))
+        } else {
+          options.postProcess(req, res, next)
+        }
       }
     })
   }


Test patch:
diff --git a/test/integration/middleware.js b/test/integration/middleware.js
index 4ac09fb..6743475 100644
--- a/test/integration/middleware.js
+++ b/test/integration/middleware.js
@@ -1183,4 +1183,58 @@ module.exports = function (createFn, setup, dismantle) {
       })
     })
   })
+
+  describe('postProcess (async outputFn)', () => {
+    let app = createFn()
+    let server
+    let options = {
+      outputFn: (req, res) => {
+        if (app.isRestify) {
+          res.send(200)
+        } else {
+          res.sendStatus(200)
+        }
+
+        return Promise.resolve()
+      },
+      postProcess: sinon.spy((req, res, next) => {
+        next()
+      }),
+      restify: app.isRestify
+    }
+
+    beforeEach((done) => {
+      setup((err) => {
+        if (err) {
+          return done(err)
+        }
+
+        erm.serve(app, db.models.Customer, options)
+
+        server = app.listen(testPort, done)
+      })
+    })
+
+    afterEach((done) => {
+      options.postProcess.reset()
+      dismantle(app, server, done)
+    })
+
+    it('GET /Customer 200', (done) => {
+      request.get({
+        url: `${testUrl}/api/v1/Customer`,
+        json: true
+      }, (err, res, body) => {
+        assert.ok(!err)
+        assert.equal(res.statusCode, 200)
+        sinon.assert.calledOnce(options.postProcess)
+        let args = options.postProcess.args[0]
+        assert.equal(args.length, 3)
+        assert.deepEqual(args[0].erm.result, [])
+        assert.equal(args[0].erm.statusCode, 200)
+        assert.equal(typeof args[2], 'function')
+        done()
+      })
+    })
+  })
 }
diff --git a/test/unit/middleware/prepareOutput.js b/test/unit/middleware/prepareOutput.js
index dfd943b..019ecd1 100644
--- a/test/unit/middleware/prepareOutput.js
+++ b/test/unit/middleware/prepareOutput.js
@@ -7,11 +7,16 @@ describe('prepareOutput', () => {
 
   let onError = sinon.spy()
   let outputFn = sinon.spy()
+  let outputFnPromise = sinon.spy(() => {
+    return Promise.resolve()
+  })
+  let postProcess = sinon.spy()
   let next = sinon.spy()
 
   afterEach(() => {
     onError.reset()
     outputFn.reset()
+    outputFnPromise.reset()
     next.reset()
   })
 
@@ -34,6 +39,69 @@ describe('prepareOutput', () => {
     sinon.assert.notCalled(next)
   })
 
+  it('calls outputFn with default options and no post* middleware (async)', () => {
+    let req = {
+      method: 'GET',
+      erm: {}
+    }
+
+    let options = {
+      onError: onError,
+      outputFn: outputFnPromise
+    }
+
+    prepareOutput(options)(req, {}, next)
+
+    sinon.assert.calledOnce(outputFnPromise)
+    sinon.assert.calledWithExactly(outputFnPromise, req, {})
+    sinon.assert.notCalled(onError)
+    sinon.assert.notCalled(next)
+  })
+
+  it('calls postProcess with default options and no post* middleware', () => {
+    let req = {
+      method: 'GET',
+      erm: {}
+    }
+
+    let options = {
+      onError: onError,
+      outputFn: outputFn,
+      postProcess: postProcess
+    }
+
+    prepareOutput(options)(req, {}, next)
+
+    sinon.assert.calledOnce(outputFn)
+    sinon.assert.calledWithExactly(outputFn, req, {})
+    sinon.assert.calledOnce(postProcess)
+    sinon.assert.calledWithExactly(postProcess, req, {}, next)
+    sinon.assert.notCalled(onError)
+    sinon.assert.notCalled(next)
+  })
+
+  it('calls postProcess with default options and no post* middleware (async outputFn)', () => {
+    let req = {
+      method: 'GET',
+      erm: {}
+    }
+
+    let options = {
+      onError: onError,
+      outputFn: outputFnPromise,
+      postProcess: postProcess
+    }
+
+    prepareOutput(options)(req, {}, next)
+
+    sinon.assert.calledOnce(outputFnPromise)
+    sinon.assert.calledWithExactly(outputFnPromise, req, {})
+    sinon.assert.calledOnce(postProcess)
+    sinon.assert.calledWithExactly(postProcess, req, {}, next)
+    sinon.assert.notCalled(onError)
+    sinon.assert.notCalled(next)
+  })
+
   it('calls outputFn with default options and postCreate middleware', () => {
     let req = {
       erm: {