Skip to content

Commit 5c4396b

Browse files
chgohlkeconnystrecker
authored andcommitted
router now supports flexible s3 event handling (spring-media#18)
* router now supports flexible s3 event handling
1 parent dd1cc5c commit 5c4396b

File tree

7 files changed

+560
-368
lines changed

7 files changed

+560
-368
lines changed

README.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
# aws-lambda-router
77

8-
A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providing routing for [API Gateway](https://aws.amazon.com/api-gateway) [Proxy Integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html) and [SNS](https://aws.amazon.com/sns).
8+
A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providing routing for [API Gateway](https://aws.amazon.com/api-gateway),
9+
[Proxy Integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html), [SNS](https://aws.amazon.com/sns)
10+
and [S3 Events](https://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html).
911

1012
## Features
1113

@@ -14,7 +16,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
1416
* Lambda Proxy Resource support for AWS API Gateway
1517
* Enable CORS for requests
1618
* No external dependencies
17-
* Currently there are two `processors` (callers for Lambda) implemented: API Gateway ANY method (called proxyIntegration), SNS and SQS.
19+
* Currently there are four `processors` (callers for Lambda) implemented: API Gateway ANY method (called proxyIntegration), SNS, SQS and S3.
1820

1921
## Installation
2022
Install via npm
@@ -122,6 +124,8 @@ and the http response then contains the configured value as response code and th
122124
123125
## SNS to Lambda Integrations
124126
127+
SNS Event Structure: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html
128+
125129
For handling calls in Lambdas initiated from AWS-SNS you can use the following code snippet:
126130
127131
```js
@@ -173,6 +177,80 @@ The `action` method gets all body elements from the router as an array.
173177
174178
If more than one route matches, only the **first** is used!
175179
180+
## S3 to Lambda Integrations
181+
182+
183+
Lambdas can be triggered by S3 events. The router now supports these events.
184+
With the router it is very easy and flexible to connect a lambda to different s3 sources (different buckets). The following possibilities are available:
185+
186+
- bucketName: By specifying a fixed _bucketName_ all s3 events with this bucket name are forwarded to a certain action. Instead of a fixed bucket a _RegExp_ is also possible.
187+
- eventName: By configuring the [S3 event name](https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types) the routing can be further restricted. A _RegExp_ is also possible here.
188+
- objectKeyPrefix: fixed string as an prefix of an object key (but not an RegExp). Is useful if you want to organize your bucket in subfolder.
189+
190+
A combination of bucketName, eventName and objectKeyPrefix is possible. If no _bucketName_, _eventName_ and _objectKeyPrefix_ is configured, all s3 events are forwarded to the action.
191+
192+
The action method will be called with the [S3Event Structure](https://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html)
193+
194+
The following examples demonstrates the most use cases:
195+
196+
```js
197+
const router = require('aws-lambda-router');
198+
199+
exports.handler = router.handler({
200+
s3: {
201+
routes: [
202+
{
203+
//match every s3 record to this action
204+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
205+
},
206+
{
207+
//match s3 events which created, bucket name is whitelisted here
208+
eventName: 'ObjectCreated:Put',
209+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
210+
},
211+
{
212+
//event name is an regex: match 'ObjectCreated:Put' or 'ObjectCreated:Copy'
213+
eventName: /ObjectCreated:*/,
214+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
215+
},
216+
{
217+
//exact name of bucket 'myBucket', event name is whitelisted and will not be checked
218+
bucketName: 'myBucket',
219+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
220+
},
221+
{
222+
//regex of bucket name (all buckets started with 'bucket-dev-' will be machted
223+
bucketName: /^bucket-dev-.*/,
224+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
225+
},
226+
{
227+
//action only will be called if bucket and event matched to the given regex
228+
bucketName: /bucket-dev-.*/,
229+
eventName: /ObjectCreated:*/,
230+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
231+
},
232+
{
233+
//action only will be called if bucket and event matched to the given fixed string
234+
bucketName: 'bucket',
235+
eventName: 'ObjectRemoved:Delete',
236+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
237+
},
238+
{
239+
//match if s3 events comes from Bucket 'bucket' with event 'ObjectRemoved:Delete'
240+
// and the object key starts with /upload
241+
objectKeyPrefix: '/upload',
242+
bucketName: 'bucket',
243+
eventName: 'ObjectRemoved:Delete',
244+
action: (event, context) => console.log(event.s3.object.key, event.eventName)
245+
}
246+
],
247+
debug: true
248+
}
249+
});
250+
```
251+
252+
Per s3 event there can be several entries per event. Than the action methods are called one after the other. The result is an array with objects insides.
253+
176254
### Custom response
177255
178256
Per default a status code 200 will be returned. This behavior can be overridden.

gulpfile.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
const gulp = require('gulp');
2-
const del = require('del');
3-
const install = require('gulp-install');
42
const jasmine = require('gulp-jasmine');
53

64
gulp.task('test', () =>

index.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,21 @@ export interface SqsConfig {
3737
debug?: boolean;
3838
}
3939

40+
export interface S3Route {
41+
bucketName?: string | RegExp;
42+
eventName?: string | RegExp;
43+
objectKeyPrefix?: string;
44+
action: (s3Event: any[], context: any) => any;
45+
}
46+
47+
export interface S3Config {
48+
routes: S3Route[];
49+
debug?: boolean;
50+
}
51+
4052
export interface RouteConfig {
4153
proxyIntegration?: ProxyIntegrationConfig;
4254
sns?: SnsConfig;
4355
sqs?: SqsConfig;
56+
s3?: S3Config;
4457
}

lib/s3.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"use strict";
2+
3+
function validateArguments(s3Config, event) {
4+
5+
if (!Array.isArray(event.Records) || event.Records.length < 1 || event.Records[0].eventSource !== 'aws:s3') {
6+
console.log('Event does not look like S3');
7+
return false;
8+
}
9+
10+
//validate config
11+
if (!Array.isArray(s3Config.routes) || s3Config.routes.length < 1) {
12+
throw new Error('s3Config.routes must not be empty');
13+
}
14+
15+
//validate config
16+
for (const route of s3Config.routes) {
17+
if (!route.action) {
18+
throw new Error('s3Config.routes.action must not be empty');
19+
}
20+
}
21+
22+
return true;
23+
24+
}
25+
26+
function matchConfigToEventValue(config, eventValue) {
27+
28+
if (!config) {
29+
return true;
30+
}
31+
32+
if (config instanceof RegExp) {
33+
if (config.test(eventValue)) {
34+
return true;
35+
}
36+
} else {
37+
if (config === eventValue) {
38+
return true;
39+
}
40+
}
41+
42+
return false;
43+
}
44+
45+
function matchObjectKeyWithPrefix(config, eventValue) {
46+
if (!config || eventValue.startsWith(config)) {
47+
return true;
48+
}
49+
50+
return false;
51+
}
52+
53+
function process(s3Config, event, context) {
54+
55+
if (s3Config.debug) {
56+
console.log('s3:Event', JSON.stringify(event));
57+
console.log('s3:context', context);
58+
}
59+
60+
if (!validateArguments(s3Config, event)) {
61+
return null;
62+
}
63+
64+
const results = [];
65+
for (const record of event.Records) {
66+
for (const routeConfig of s3Config.routes) {
67+
const bucketNameMatched = matchConfigToEventValue(routeConfig.bucketName, record.s3.bucket.name);
68+
const eventNameMatched = matchConfigToEventValue(routeConfig.eventName, record.eventName);
69+
const objectKeyPrefixMatched = matchObjectKeyWithPrefix(routeConfig.objectKeyPrefix, record.s3.object.key);
70+
71+
if (s3Config.debug) {
72+
console.log(`match record '${record.eventName}'/'${record.s3.bucket.name}'/'${record.s3.object.key}': bucketMatch '
73+
${bucketNameMatched}', eventMatch '${eventNameMatched}', key '${objectKeyPrefixMatched}' for route '${JSON.stringify(routeConfig)}'`);
74+
}
75+
76+
if (bucketNameMatched && eventNameMatched && objectKeyPrefixMatched) {
77+
const result = routeConfig.action(record, context);
78+
if (result) {
79+
results.push(result);
80+
}
81+
break;
82+
}
83+
}
84+
}
85+
86+
return results;
87+
}
88+
89+
module.exports = process;

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@
2727
},
2828
"homepage": "https://github.com/spring-media/aws-lambda-router#readme",
2929
"devDependencies": {
30-
"del": "2.2.2",
3130
"gulp": "4.0",
32-
"gulp-install": "1.1.0",
3331
"gulp-jasmine": "4.0.0",
34-
"gulp-zip": "4.2.0",
32+
"jasmine": "3.3.1",
3533
"proxyquire": "2.1.0"
3634
}
3735
}

0 commit comments

Comments
 (0)