From 654615be809be09a43eec0eb517dc5fc2478796b Mon Sep 17 00:00:00 2001 From: "Ethan Dave B. Gomez" Date: Mon, 11 Mar 2019 17:44:14 -0400 Subject: [PATCH] fix(proxy): Support for multiValue and isBase64Encoded --- CHANGELOG.md | 6 + README.md | 33 +++-- package-lock.json | 2 +- package.json | 2 +- src/api-gateway-sim.ts | 78 ++++++++--- src/lib/aws/gateway/body-template.ts | 195 ++++++++++++++------------- 6 files changed, 193 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c469b4..ceba395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.4.0 + +- Support for proxy request multiValueHeaders +- Support for proxy request multiValueQueryStringParameters +- Support for proxy isBase64Encoded + ## 1.3.0 - Fix escape diff --git a/README.md b/README.md index a6a86dd..39dda8e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # api-gateway-sim + AWS API Gateway simulator for Node JS Lambda Install + ```bash $ npm install -g api-gateway-sim ``` @@ -13,6 +15,7 @@ Choose **"Export as Swagger + API Gateway Integrations"**. See details in [Export an API from Api Gateway](http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-export-api.html) Running the simulator using **_ags_** cli + ```bash $ cd $ ags --swagger .json @@ -20,16 +23,19 @@ $ ags --swagger .json ``` Testing your lambda + ```bash $ curl http://localhost:3000/ ``` Using different listening port + ```bash $ PORT=4000 ags --swagger .json ``` Command Line Help + ```bash Usage: ags [options] @@ -49,23 +55,24 @@ Command Line Help -g, --ags-port AGS UI port, default 4000 ``` -Features ---------- +## Features + +- Supports Body Mapping Templates +- Supports Body Mapping Template validation. -* Supports Body Mapping Templates -* Supports Body Mapping Template validation. ```bash $ ags -a # From your browser open http://localhost:4000 ``` -* Supports integration responses -* Supports event.json, context.json, and stage-variables.json -* Continues to monitoring changes in your lambda code. YES! No need to restart **_ags_** -* Support for json or yaml swagger file. -* Monitor changes in event.json, context.json, and stage-variables.json -* CORS - enabled by default -* Supports lambda timeout -* Supports base path -* Supports {proxy+} +- Supports integration responses +- Supports event.json, context.json, and stage-variables.json +- Continues to monitoring changes in your lambda code. YES! No need to restart **_ags_** +- Support for json or yaml swagger file. +- Monitor changes in event.json, context.json, and stage-variables.json +- CORS - enabled by default +- Supports lambda timeout +- Supports base path +- Supports {proxy+} +- Supports proxy integration isBase64Encoded, multiValueHeaders, and multiValueQueryStringParameters diff --git a/package-lock.json b/package-lock.json index 999de6a..68e082b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "api-gateway-sim", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dd8c8d0..e9b7e77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "api-gateway-sim", - "version": "1.3.0", + "version": "1.4.0", "description": "AWS API Gateway simulator for Node JS Lambda", "main": "./api-gateway-sim.js", "bin": { diff --git a/src/api-gateway-sim.ts b/src/api-gateway-sim.ts index e049b04..249e23e 100644 --- a/src/api-gateway-sim.ts +++ b/src/api-gateway-sim.ts @@ -172,13 +172,6 @@ class ApiGatewaySim { console.log(message); } - private getQueryParams(request: Request) { - const url = require('url'); - const url_parts = url.parse(request.url, true); - const query = url_parts.query; - return query; - } - private getPassThroughTemplateContent() { const file = __dirname + '/templates/pass-through.vtl'; return fs.readFileSync(file, 'utf8'); @@ -290,13 +283,37 @@ class ApiGatewaySim { } } + private setHeaders(bodyTemplate: BodyTemplate, request: Request) { + const keys = Object.keys(request.headers); + if (!bodyTemplate.headers) { + bodyTemplate.headers = {}; + } + for (const key of keys) { + if (Array.isArray(request.headers[key])) { + bodyTemplate.headers[key] = + request.headers[key][request.headers[key].length - 1]; + bodyTemplate.multiValueHeaders[key] = request.headers[key]; + } else { + bodyTemplate.headers[key] = request.headers[key]; + bodyTemplate.multiValueHeaders[key] = [request.headers[key]]; + } + } + } + + private setQueryParams(bodyTemplate: BodyTemplate, request: Request) { + const url = require('url'); + const url_parts = url.parse(request.url, true); + const query = url_parts.query; + bodyTemplate.queryParams = query; + } + private parseEvent(method: Method, request: Request) { const bodyTemplate = new BodyTemplate(); bodyTemplate.context = this.getContextJson(); this.setContextDefaults(bodyTemplate.context, request); - bodyTemplate.headers = request.headers; + this.setHeaders(bodyTemplate, request); bodyTemplate.pathParams = this.getBodyTemplatePathParams(request); - bodyTemplate.queryParams = this.getQueryParams(request); + this.setQueryParams(bodyTemplate, request); bodyTemplate.method = request.method; bodyTemplate.payload = this.getBodyTemplatePayload(method, request); bodyTemplate.stageVariables = this.getStageVariables(); @@ -434,11 +451,30 @@ class ApiGatewaySim { return Object.keys(object).length === 0 && object.constructor === Object; } - private getProxyQueryString(request: Request) { + private setProxyQueryString(event: any, request: Request) { if (this.isObjectEmpty(request.query)) { - return null; + event.queryStringParameters = null; + event.multiValueQueryStringParameters = null; + return; + } + if (!event.queryStringParameters) { + event.queryStringParameters = {}; + } + if (!event.multiValueQueryStringParameters) { + event.multiValueQueryStringParameters = {}; + } + const keys = Object.keys(request.query); + const queries = request.query; + for (const key of keys) { + if (Array.isArray(queries[key])) { + event.queryStringParameters[key] = + queries[key][queries[key].length - 1]; + event.multiValueQueryStringParameters[key] = queries[key]; + } else { + event.queryStringParameters[key] = queries[key]; + event.multiValueQueryStringParameters[key] = [queries[key]]; + } } - return request.query; } private processProxyData( @@ -458,9 +494,7 @@ class ApiGatewaySim { request.originalUrl ); requestObject.eventJson.headers = this.getRawHeaders(request); - requestObject.eventJson.queryStringParameters = this.getProxyQueryString( - request - ); + this.setProxyQueryString(requestObject.eventJson, request); requestObject.eventJson.httpMethod = request.method; this.removeNonProxyFields(requestObject.eventJson); this.setProxyStageVariables(path, requestObject.eventJson, request); @@ -567,6 +601,7 @@ class ApiGatewaySim { body: true, statusCode: true, headers: true, + isBase64Encoded: true, multiValueHeaders: true }; for (const property in message) { @@ -614,6 +649,16 @@ class ApiGatewaySim { } } + private setBase64Encoded(method: Method, message: any) { + if ( + method.integration && + method.integration.contentHandling === 'CONVERT_TO_BINARY' + ) { + return Buffer.from(message.body, 'base64'); + } + return message.body; + } + private sendAwsProxyResponse( httpResponse: Response, method: Method, @@ -640,6 +685,9 @@ class ApiGatewaySim { } else { this.setMessageStatusCode(httpResponse, message); this.setMessageHeaders(httpResponse, message); + if (message.isBase64Encoded) { + parseBody = this.setBase64Encoded(method, message); + } this.sendDefaultResponse(httpResponse, method, parseBody); } } catch (error) { diff --git a/src/lib/aws/gateway/body-template.ts b/src/lib/aws/gateway/body-template.ts index e16a9f6..15726fa 100644 --- a/src/lib/aws/gateway/body-template.ts +++ b/src/lib/aws/gateway/body-template.ts @@ -8,97 +8,106 @@ import Util from './util'; import Input from './input'; export default class BodyTemplate { - private _context; - - get context() { - return this._context; - } - - set context(value) { - this._context = value; - } - - private _payload; - - get payload() { - return this._payload; - } - - set payload(value) { - this._payload = value; - } - - private _headers; - - get headers() { - return this._headers; - } - - set headers(value) { - this._headers = value; - } - - private _queryParams; - - get queryParams() { - return this._queryParams; - } - - set queryParams(value) { - this._queryParams = value; - } - - private _pathParams; - - get pathParams() { - return this._pathParams; - } - - set pathParams(value) { - this._pathParams = value; - } - - private _method; - - get method() { - return this._method; - } - - set method(value) { - this._method = value; - } - - private _stageVariables; - - get stageVariables() { - return this._stageVariables; - } - - set stageVariables(value) { - this._stageVariables = value; - } - - parse(template: string): any { - const context = {context: {httpMethod: ''}, util: null, input: null, stageVariables: null}; - context.stageVariables = this.stageVariables; - context.context = Object.assign(context.context, this.context); - context.context.httpMethod = this.method; - context.util = new Util(); - context.input = this.getInput(); - const parsed = Velocity.parse(template); - const Compile = Velocity.Compile; - const compiler = new Compile(parsed); - return compiler.render(context, (error, rendered) => { - console.log(error, rendered); - }); - } - - private getInput() { - const input = new Input(); - input.paramsHeader = this.headers; - input.paramsQueryString = this.queryParams; - input.paramsPath = this.pathParams; - input.body = this.payload; - return input; - } + private _context; + + get context() { + return this._context; + } + + set context(value) { + this._context = value; + } + + private _payload; + + get payload() { + return this._payload; + } + + set payload(value) { + this._payload = value; + } + + private _headers; + + get headers() { + return this._headers; + } + + set headers(value) { + this._headers = value; + } + + private _queryParams; + + get queryParams() { + return this._queryParams; + } + + set queryParams(value) { + this._queryParams = value; + } + + private _pathParams; + + get pathParams() { + return this._pathParams; + } + + set pathParams(value) { + this._pathParams = value; + } + + private _method; + + get method() { + return this._method; + } + + set method(value) { + this._method = value; + } + + private _stageVariables; + + get stageVariables() { + return this._stageVariables; + } + + set stageVariables(value) { + this._stageVariables = value; + } + + public multiValueHeaders: any = {}; + + public multiValueQueryStringParameters: any = {}; + + parse(template: string): any { + const context = { + context: { httpMethod: '' }, + util: null, + input: null, + stageVariables: null + }; + context.stageVariables = this.stageVariables; + context.context = Object.assign(context.context, this.context); + context.context.httpMethod = this.method; + context.util = new Util(); + context.input = this.getInput(); + const parsed = Velocity.parse(template); + const Compile = Velocity.Compile; + const compiler = new Compile(parsed); + return compiler.render(context, (error, rendered) => { + console.log(error, rendered); + }); + } + + private getInput() { + const input = new Input(); + input.paramsHeader = this.headers; + input.paramsQueryString = this.queryParams; + input.paramsPath = this.pathParams; + input.body = this.payload; + return input; + } }