From 582b88d87e4014b038df038ffc74c0fd71e7b2aa Mon Sep 17 00:00:00 2001 From: Brett Andrews Date: Thu, 16 Aug 2018 10:31:08 -0700 Subject: [PATCH] feat: add option of specifying resolveMode (#173) * feat: add option of specifying resolveMode Allows resolving using Promise, callback, or the default context.succeed. API decisions were made to support backwards compatibility. We will clean up the interface in a future major version bump --- __tests__/integration.js | 54 ++++++++++++++++- __tests__/unit.js | 123 +++++++++++++++++++++++++++++++++------ src/index.js | 78 +++++++++++++++++++------ 3 files changed, 218 insertions(+), 37 deletions(-) diff --git a/__tests__/integration.js b/__tests__/integration.js index 21390b2e..e7052b91 100644 --- a/__tests__/integration.js +++ b/__tests__/integration.js @@ -6,7 +6,7 @@ const app = require('../examples/basic-starter/app') const server = awsServerlessExpress.createServer(app) const lambdaFunction = { - handler: (event, context) => awsServerlessExpress.proxy(server, event, context) + handler: (event, context, resolutionMode, callback) => awsServerlessExpress.proxy(server, event, context, resolutionMode, callback) } function clone (json) { @@ -50,6 +50,20 @@ function makeResponse (response) { } describe('integration tests', () => { + test('proxy returns server', (done) => { + const succeed = () => { + done() + } + + const server = lambdaFunction.handler(makeEvent({ + path: '/', + httpMethod: 'GET' + }), { + succeed + }) + expect(server._socketPathSuffix).toBeTruthy() + }) + test('GET HTML (initial request)', (done) => { const succeed = response => { delete response.headers.date @@ -82,7 +96,6 @@ describe('integration tests', () => { succeed }) }) - test('GET JSON collection', (done) => { const succeed = response => { delete response.headers.date @@ -149,6 +162,43 @@ describe('integration tests', () => { }) }) + test('GET JSON single (resolutionMode = CALLBACK)', (done) => { + const callback = (e, response) => { + delete response.headers.date + expect(response).toEqual(makeResponse({ + 'body': '{"id":1,"name":"Joe"}', + 'headers': { + 'content-length': '21', + 'etag': 'W/"15-rRboW+j/yFKqYqV6yklp53+fANQ"' + } + })) + done() + } + lambdaFunction.handler(makeEvent({ + path: '/users/1', + httpMethod: 'GET' + }), {}, 'CALLBACK', callback) + }) + + test('GET JSON single (resolutionMode = PROMISE)', (done) => { + const succeed = response => { + delete response.headers.date + expect(response).toEqual(makeResponse({ + 'body': '{"id":1,"name":"Joe"}', + 'headers': { + 'content-length': '21', + 'etag': 'W/"15-rRboW+j/yFKqYqV6yklp53+fANQ"' + } + })) + done() + } + lambdaFunction.handler(makeEvent({ + path: '/users/1', + httpMethod: 'GET' + }), {}, 'PROMISE') + .promise.then(succeed) + }) + test('GET JSON single 404', (done) => { const succeed = response => { delete response.headers.date diff --git a/__tests__/unit.js b/__tests__/unit.js index 14d909c9..05f32507 100644 --- a/__tests__/unit.js +++ b/__tests__/unit.js @@ -127,6 +127,50 @@ class MockContext { } } +describe('forwardConnectionErrorResponseToApiGateway', () => { + test('responds with 502 status', () => { + return new Promise( + (resolve, reject) => { + const context = new MockContext(resolve) + const contextResolver = { + succeed: (p) => context.succeed(p.response) + } + awsServerlessExpress.forwardConnectionErrorResponseToApiGateway('ERROR', contextResolver) + } + ).then(successResponse => expect(successResponse).toEqual({ + statusCode: 502, + body: '', + headers: {} + })) + }) +}) + +describe('forwardLibraryErrorResponseToApiGateway', () => { + test('responds with 500 status', () => { + return new Promise( + (resolve, reject) => { + const context = new MockContext(resolve) + const contextResolver = { + succeed: (p) => context.succeed(p.response) + } + awsServerlessExpress.forwardLibraryErrorResponseToApiGateway('ERROR', contextResolver) + } + ).then(successResponse => expect(successResponse).toEqual({ + statusCode: 500, + body: '', + headers: {} + })) + }) +}) + +function getContextResolver (resolve) { + const context = new MockContext(resolve) + const contextResolver = { + succeed: (p) => context.succeed(p.response) + } + + return contextResolver +} describe('forwardResponseToApiGateway: header handling', () => { test('multiple headers with the same name get transformed', () => { const server = new MockServer() @@ -135,9 +179,8 @@ describe('forwardResponseToApiGateway: header handling', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -156,9 +199,8 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -175,9 +217,8 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -194,9 +235,8 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -213,9 +253,8 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -232,9 +271,8 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { const response = new MockResponse(200, headers, body) return new Promise( (resolve, reject) => { - const context = new MockContext(resolve) - awsServerlessExpress.forwardResponseToApiGateway( - server, response, context) + const contextResolver = getContextResolver(resolve) + awsServerlessExpress.forwardResponseToApiGateway(server, response, contextResolver) } ).then(successResponse => expect(successResponse).toEqual({ statusCode: 200, @@ -244,3 +282,52 @@ describe('forwardResponseToApiGateway: content-type encoding', () => { })) }) }) + +describe('makeResolver', () => { + test('CONTEXT_SUCCEED (specified)', () => { + return new Promise( + (resolve, reject) => { + const context = new MockContext(resolve) + const contextResolver = awsServerlessExpress.makeResolver({ + context, + resolutionMode: 'CONTEXT_SUCCEED' + }) + + return contextResolver.succeed({ + response: 'success' + }) + }).then(successResponse => expect(successResponse).toEqual('success')) + }) + + test('CALLBACK', () => { + const callback = (e, response) => response + const callbackResolver = awsServerlessExpress.makeResolver({ + callback, + resolutionMode: 'CALLBACK' + }) + const successResponse = callbackResolver.succeed({ + response: 'success' + }) + + expect(successResponse).toEqual('success') + }) + + test('PROMISE', () => { + return new Promise((resolve, reject) => { + const promise = { + resolve, + reject + } + const promiseResolver = awsServerlessExpress.makeResolver({ + promise, + resolutionMode: 'PROMISE' + }) + + return promiseResolver.succeed({ + response: 'success' + }) + }).then(successResponse => { + expect(successResponse).toEqual('success') + }) + }) +}) diff --git a/src/index.js b/src/index.js index 8463cc43..98fc2169 100644 --- a/src/index.js +++ b/src/index.js @@ -51,7 +51,7 @@ function mapApiGatewayEventToHttpRequest (event, context, socketPath) { } } -function forwardResponseToApiGateway (server, response, context) { +function forwardResponseToApiGateway (server, response, resolver) { let buf = [] response @@ -88,11 +88,11 @@ function forwardResponseToApiGateway (server, response, context) { const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8') const successResponse = {statusCode, body, headers, isBase64Encoded} - context.succeed(successResponse) + resolver.succeed({ response: successResponse }) }) } -function forwardConnectionErrorResponseToApiGateway (server, error, context) { +function forwardConnectionErrorResponseToApiGateway (error, resolver) { console.log('ERROR: aws-serverless-express connection error') console.error(error) const errorResponse = { @@ -101,10 +101,10 @@ function forwardConnectionErrorResponseToApiGateway (server, error, context) { headers: {} } - context.succeed(errorResponse) + resolver.succeed({ response: errorResponse }) } -function forwardLibraryErrorResponseToApiGateway (server, error, context) { +function forwardLibraryErrorResponseToApiGateway (error, resolver) { console.log('ERROR: aws-serverless-express error') console.error(error) const errorResponse = { @@ -113,13 +113,13 @@ function forwardLibraryErrorResponseToApiGateway (server, error, context) { headers: {} } - context.succeed(errorResponse) + resolver.succeed({ response: errorResponse }) } -function forwardRequestToNodeServer (server, event, context) { +function forwardRequestToNodeServer (server, event, context, resolver) { try { const requestOptions = mapApiGatewayEventToHttpRequest(event, context, getSocketPath(server._socketPathSuffix)) - const req = http.request(requestOptions, (response, body) => forwardResponseToApiGateway(server, response, context)) + const req = http.request(requestOptions, (response) => forwardResponseToApiGateway(server, response, resolver)) if (event.body) { if (event.isBase64Encoded) { event.body = Buffer.from(event.body, 'base64') @@ -128,10 +128,10 @@ function forwardRequestToNodeServer (server, event, context) { req.write(event.body) } - req.on('error', (error) => forwardConnectionErrorResponseToApiGateway(server, error, context)) + req.on('error', (error) => forwardConnectionErrorResponseToApiGateway(error, resolver)) .end() } catch (error) { - forwardLibraryErrorResponseToApiGateway(server, error, context) + forwardLibraryErrorResponseToApiGateway(error, resolver) return server } } @@ -182,13 +182,56 @@ function createServer (requestListener, serverListenCallback, binaryTypes) { return server } -function proxy (server, event, context) { - if (server._isListening) { - forwardRequestToNodeServer(server, event, context) - return server - } else { - return startServer(server) - .on('listening', () => proxy(server, event, context)) +function proxy (server, event, context, resolutionMode, callback) { + // DEPRECATED: Legacy support + if (!resolutionMode) { + const resolver = makeResolver({ context, resolutionMode: 'CONTEXT_SUCCEED' }) + if (server._isListening) { + forwardRequestToNodeServer(server, event, context, resolver) + return server + } else { + return startServer(server) + .on('listening', () => proxy(server, event, context)) + } + } + + return { + promise: new Promise((resolve, reject) => { + const promise = { + resolve, + reject + } + const resolver = makeResolver({ + context, + callback, + promise, + resolutionMode + }) + + if (server._isListening) { + forwardRequestToNodeServer(server, event, context, resolver) + } else { + startServer(server) + .on('listening', () => forwardRequestToNodeServer(server, event, context, resolver)) + } + }) + } +} + +function makeResolver (params/* { + context, + callback, + promise, + resolutionMode +} */) { + return { + succeed: (params2/* { + response + } */) => { + if (params.resolutionMode === 'CONTEXT_SUCCEED') return params.context.succeed(params2.response) + if (params.resolutionMode === 'CALLBACK') return params.callback(null, params2.response) + if (params.resolutionMode === 'PROMISE') return params.promise.resolve(params2.response) + } } } @@ -205,4 +248,5 @@ if (process.env.NODE_ENV === 'test') { exports.forwardRequestToNodeServer = forwardRequestToNodeServer exports.startServer = startServer exports.getSocketPath = getSocketPath + exports.makeResolver = makeResolver }