diff --git a/index.js b/index.js index d83ed22..803881d 100644 --- a/index.js +++ b/index.js @@ -1,41 +1,58 @@ 'use strict'; const packagePath = 'node_modules/serverless-offline-direct-lambda'; -const handlerPath = `proxy.js`; +const handlerPath = 'proxy.js'; class ServerlessPlugin { constructor(serverless, options) { this.serverless = serverless; this.options = options; + const boundStartHandler = this.startHandler.bind(this); + this.hooks = { - "before:offline:start:init": this.startHandler.bind(this), + 'before:offline:start': boundStartHandler, + 'before:offline:start:init': boundStartHandler, }; } startHandler() { + // Serverless Webpack overrides the location to its output directory. Set + // location to that directory. + let location = ''; + try { + location = this.serverless.service.custom['serverless-offline'].location || location; + this.serverless.service.custom['serverless-offline'].location = ''; + } catch (_) { } + + location = `${this.serverless.config.servicePath}/${location}`; + this.serverless.cli.log('Running Serverless Offline with direct lambda support'); - addProxies(this.serverless.service.functions); + addProxies(this.serverless.service.functions, + location, + this.serverless.service.provider.tracing === 'true'); } } -const addProxies = functionsObject => { +const addProxies = (functionsObject, location, tracing) => { Object.keys(functionsObject).forEach(fn => { // filter out functions with event config, // leaving just those intended for direct lambda-to-lambda invocation const functionObject = functionsObject[fn]; - if (!functionObject.events || functionObject.events.length == 0) { - const pf = functionProxy(functionObject); + if (!functionObject.events || + !functionObject.events.some((event) => Object.keys(event)[0] === 'http')) { + const pf = functionProxy(functionObject, location, tracing); functionsObject[pf.name] = pf; } }); }; -const functionProxy = functionBeingProxied => ({ +const functionProxy = (functionBeingProxied, location, tracing) => ({ name: `${functionBeingProxied.name}_proxy`, handler: `${packagePath}/proxy.handler`, + environment: functionBeingProxied.environment, events: [ { http: { @@ -46,8 +63,20 @@ const functionProxy = functionBeingProxied => ({ template: { 'application/json': JSON.stringify( { - targetHandler : functionBeingProxied.handler, - body: "$input.json('$')" + location, + headers: `{ + #set( $map = $input.params().header ) + #foreach($key in $map.keySet()) + "$util.escapeJavaScript($key)": "$util.escapeJavaScript($map.get($key))" + #if( $foreach.hasNext ) + , + #end + #end + }`, + body: "$input.json('$')", + targetHandler: functionBeingProxied.handler, + handlerName: functionBeingProxied.name, + tracing, } ) } diff --git a/package.json b/package.json index 318ceb7..1261f0f 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "license": "ISC", "dependencies": { "serialize-error": "^2.1.0" + }, + "devDependencies": { + "aws-xray-sdk": "^2.0.0" } } diff --git a/proxy.js b/proxy.js index 680ae31..1c5c2bc 100644 --- a/proxy.js +++ b/proxy.js @@ -1,26 +1,64 @@ +const awsXRay = require('aws-xray-sdk'); +const path = require('path'); const serializeError = require('serialize-error'); -function handler(event, context, callback) { +async function handler(event, context) { + const { ClientContext, FunctionName, InvocationType, LogType, Payload } = event.body; + // extract the path to the handler (relative to the project root) // and the function to call on the handler - const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split("."); - const target = require('../../' + targetHandlerFile); - - // call the target function - target[targetHandlerFunction](event.body, context, (error, response) => { - if (error) { - callback(null, { - StatusCode: 500, - FunctionError: 'Handled', - Payload: serializeError(error) - }) - } else { - callback(null, { - StatusCode: 200, - Payload: JSON.stringify(response) - }) + const targetParts = event.targetHandler.split('.'); + const [targetHandlerFunction, ...targetHandlerFile] = targetParts.reverse(); + const target = require(path.resolve(event.location, targetHandlerFile.reverse().join('.'))); + + const targetEvent = JSON.parse(Payload); + const targetContext = { + ...context, + }; + + if (ClientContext) { + targetContext.clientContext = JSON.parse(Buffer.from(ClientContext, 'base64')); + } + + const funcResult = new Promise((resolve, reject) => { + let targetHandler = target[targetHandlerFunction]; + + if (event.tracing) { + const ns = awsXRay.getNamespace(); + if (!ns.active) { + const amazonTracing = event.headers['X-Amzn-Trace-Id']; + if (amazonTracing) { + const [Root, Parent, Sampled] = amazonTracing.split(';'); + const segment = new awsXRay.Segment(event.handlerName, Root.split('=')[1], Parent.split('=')[1]); + targetHandler = ns.bind((event, context, callback) => { + awsXRay.setSegment(segment); + target[targetHandlerFunction](event, context, (error, result) => { + segment.close(); + callback(error, result); + }); + }); + } + } + } + + const result = targetHandler(targetEvent, targetContext, (error, response) => { + if (error) { + reject(error); + } else { + resolve(response); + } + }); + + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + result.then(resolve).catch(reject); } }); + + try { + return { StatusCode: 200, Payload: JSON.stringify(await funcResult) }; + } catch (error) { + return { StatusCode: 500, FunctionError: 'Handled (serverless-offline-direct-lambda)', Payload: serializeError(error) }; + } } module.exports.handler = handler;