diff --git a/README.md b/README.md index 169f1140..8b6251e8 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ The _out-binding_ parameter `"name": "$return"` is important for Serverless Expr 1. Improved API - Simpler for end-user to use and configure. 1. Promise resolution mode by default. Can specify `resolutionMode` to use `"CONTEXT"` or `"CALLBACK"` -1. Additional event sources - API Gateway V1 (REST API), API Gateway V2 (HTTP API), ALB, Lambda@Edge +1. Additional event sources - API Gateway V1 (REST API), API Gateway V2 (HTTP API), ALB, Lambda@Edge, VPC Lattice 1. Custom event source - If you have another event source you'd like to use that we don't natively support, check out the [DynamoDB Example](examples/custom-mapper-dynamodb) 1. Implementation uses mock Request/Response objects instead of running a server listening on a local socket. Thanks to @dougmoscrop from https://github.com/dougmoscrop/serverless-http 1. Automatic `isBase64Encoded` without specifying `binaryMimeTypes`. Use `binarySettings` to customize. Thanks to @dougmoscrop from https://github.com/dougmoscrop/serverless-http @@ -201,7 +201,7 @@ Set this to true to have serverless-express include the error stack trace in the ### eventSource -serverless-express natively supports API Gateway, ALB, and Lambda@Edge. If you want to use Express with other AWS Services integrated with Lambda you can provide your own custom request/response mappings via `eventSource`. See the [custom-mapper-dynamodb example](examples/custom-mapper-dynamodb). +serverless-express natively supports API Gateway, ALB, Lambda@Edge and VPC Lattice (only V2 events - event source `AWS_VPC_LATTICE_V2`). If you want to use Express with other AWS Services integrated with Lambda you can provide your own custom request/response mappings via `eventSource`. See the [custom-mapper-dynamodb example](examples/custom-mapper-dynamodb). ```js function requestMapper ({ event }) { diff --git a/__tests__/unit.lattice.js b/__tests__/unit.lattice.js new file mode 100644 index 00000000..639c4300 --- /dev/null +++ b/__tests__/unit.lattice.js @@ -0,0 +1,45 @@ +const eventSources = require('../src/event-sources') +const testUtils = require('./utils') + +const latticeEventSource = eventSources.getEventSource({ + eventSourceName: 'AWS_VPC_LATTICE_V2' +}) + +test('request is correct', () => { + const req = getReq() + expect(typeof req).toEqual('object') + expect(req.method).toEqual('GET') + expect(req.path).toEqual('/test-path?key=value') + expect(req.headers).toEqual({ + 'x-custom-header': 'test-header', + 'content-length': 15 + }) + expect(Buffer.isBuffer(req.body)).toEqual(true) +}) + +test('response is correct', () => { + const res = getRes() + expect(typeof res).toEqual('object') + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual('{"message":"Hello, world!"}') + expect(res.headers).toEqual({ 'Content-Type': 'application/json' }) + expect(res.isBase64Encoded).toEqual(false) +}) + +function getReq () { + const event = testUtils.latticeEvent + const request = latticeEventSource.getRequest({ event }) + return request +} + +function getRes () { + const event = testUtils.latticeEvent + const response = latticeEventSource.getResponse({ + event, + statusCode: 200, + body: '{"message":"Hello, world!"}', + headers: { 'Content-Type': 'application/json' }, + isBase64Encoded: false + }) + return response +} diff --git a/__tests__/utils.js b/__tests__/utils.js index c85a5e4b..4b454633 100644 --- a/__tests__/utils.js +++ b/__tests__/utils.js @@ -267,6 +267,23 @@ const selfManagedKafkaEvent = { } } +const latticeEvent = { + version: '2.0', + method: 'GET', + path: '/test-path?key=value', + queryStringParameters: { key: ['value'] }, + headers: { 'x-custom-header': ['test-header'] }, + body: JSON.stringify({ key: 'value' }), + requestContext: { + serviceNetworkArn: 'arn:aws:vpc-lattice:eu-central-1:123456789:servicenetwork/sn-123', + serviceArn: 'arn:aws:vpc-lattice:eu-central-1:014538609594:service/svc-123', + targetGroupArn: 'arn:aws:vpc-lattice:eu-central-1:014538609594:targetgroup/tg-123', + identity: { + sourceVpcArn: 'arn:aws:ec2:eu-central-1:123456789:vpc/vpc-123' + } + } +} + describe('getEventSourceNameBasedOnEvent', () => { test('throws error on empty event', () => { expect(() => getEventSourceNameBasedOnEvent({ event: {} })).toThrow( @@ -318,6 +335,11 @@ describe('getEventSourceNameBasedOnEvent', () => { const result = getEventSourceNameBasedOnEvent({ event: eventbridgeCustomerEvent }) expect(result).toEqual('AWS_EVENTBRIDGE') }) + + test('recognizes lattice event', () => { + const result = getEventSourceNameBasedOnEvent({ event: latticeEvent }) + expect(result).toEqual('AWS_VPC_LATTICE_V2') + }) }) module.exports = { @@ -329,5 +351,6 @@ module.exports = { eventbridgeScheduledEvent, eventbridgeCustomerEvent, kinesisDataStreamEvent, - selfManagedKafkaEvent + selfManagedKafkaEvent, + latticeEvent } diff --git a/src/event-sources/aws/lattice.js b/src/event-sources/aws/lattice.js new file mode 100644 index 00000000..82d15dc1 --- /dev/null +++ b/src/event-sources/aws/lattice.js @@ -0,0 +1,35 @@ +const { getRequestValuesFromEvent, getCommaDelimitedHeaders } = require('../utils') + +const getRequestValuesFromLatticeEvent = ({ event }) => { + const values = getRequestValuesFromEvent({ + event, + method: event.method, + path: event.path // query parameters are already included in the path + }) + + // Lattice always sends the headers as array that needs to be converted to a comma delimited string + values.headers = getCommaDelimitedHeaders({ headersMap: event.headers, lowerCaseKey: true }) + + return values +} + +const getResponseToLattice = ({ + statusCode, + body, + headers: responseHeaders, + isBase64Encoded +}) => { + const headers = getCommaDelimitedHeaders({ headersMap: responseHeaders }) + + return { + statusCode, + body, + headers, + isBase64Encoded + } +} + +module.exports = { + getRequest: getRequestValuesFromLatticeEvent, + getResponse: getResponseToLattice +} diff --git a/src/event-sources/index.js b/src/event-sources/index.js index a83a672a..ff4d80e6 100644 --- a/src/event-sources/index.js +++ b/src/event-sources/index.js @@ -12,6 +12,7 @@ const awsKinesisEventSource = require('./aws/kinesis') const awsS3 = require('./aws/s3') const awsStepFunctionsEventSource = require('./aws/step-functions') const awsSelfManagedKafkaEventSource = require('./aws/self-managed-kafka') +const awsLatticeEventSource = require('./aws/lattice') function getEventSource ({ eventSourceName }) { switch (eventSourceName) { @@ -43,6 +44,8 @@ function getEventSource ({ eventSourceName }) { return awsStepFunctionsEventSource case 'AWS_SELF_MANAGED_KAFKA': return awsSelfManagedKafkaEventSource + case 'AWS_VPC_LATTICE_V2': + return awsLatticeEventSource default: throw new Error('Couldn\'t detect valid event source.') } diff --git a/src/event-sources/utils.js b/src/event-sources/utils.js index da3c80f5..a9433925 100644 --- a/src/event-sources/utils.js +++ b/src/event-sources/utils.js @@ -71,6 +71,10 @@ function getEventSourceNameBasedOnEvent ({ event }) { if (event.requestContext && event.requestContext.elb) return 'AWS_ALB' + if (event.headers?.['x-amzn-lattice-network']) { + console.warn('Lattice event v1 is not supported. Please use Lattice event v2.') + } + if (event.requestContext && event.requestContext.serviceNetworkArn && event.requestContext.serviceArn) return 'AWS_VPC_LATTICE_V2' if (event.eventSource === 'SelfManagedKafka') return 'AWS_SELF_MANAGED_KAFKA' if (event.Records) { const eventSource = event.Records[0] ? event.Records[0].EventSource || event.Records[0].eventSource : undefined