Skip to content

Commit

Permalink
MAPSAPI-2426: Allow stream lambdas to have FilterCriteria defined (#…
Browse files Browse the repository at this point in the history
…147)

* v8.1.0-dev: Allow FilterCriteria to be defined for Lambdas

* v8.1.0-dev: Allow FilterCriteria to be defined for stream lambdas

* v8.1.0-dev: FilterCriteria fixture

* v8.1.0-dev: FilterCriteria fixture

* v8.1.0-dev: FilterCriteria validation + tests

* v8.1.0-dev: assert end

* v8.1.0-dev.2: dev.2 release test

* v8.1.0-dev.2: jsdoc for filtercriteria

* v8.1.0-dev.2: jsdoc tweak

* v8.1.0-dev.2: npm run shortcuts-api-doc

* v8.1.0-dev.2: remove quota check

* Revert version changes
  • Loading branch information
branyip authored Mar 27, 2024
1 parent 50b0499 commit 26c4830
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 6 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 8.1.0

- Allow `FilterCriteria` property to be defined for Stream Lambda shortcuts

## 8.0.0

- Updates `cf.shortcuts.ScheduledLambda` to use EventBridge Scheduler instead of EventBridge Rules to schedule lambda invocations. When using this version your template will have the following changes per scheduled lambda instance,
Expand Down
22 changes: 20 additions & 2 deletions lib/shortcuts/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ source mapping.
| [options.MaximumBatchingWindowInSeconds] | <code>Number</code> | | See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-maximumbatchingwindowinseconds). |
| [options.Enabled] | <code>Boolean</code> | <code>true</code> | See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-enabled). |
| [options.StartingPosition] | <code>String</code> | <code>&#x27;LATEST&#x27;</code> | See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-startingposition). |
| [options.FilterCriteria] | <code>Object</code> | | See [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html). |

**Example**
```js
Expand All @@ -795,8 +796,25 @@ const lambda = new cf.shortcuts.StreamLambda({
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: cf.getAtt('MyStream', 'Arn')
EventSourceArn: cf.getAtt('MyStream', 'Arn'),
});

module.exports = cf.merge(myTemplate, lambda);
// This lambda only gets invoked for 'INSERT' events for the DynamoDb event source
const lambdaWithFilterCriteria = new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambdaWithFilterCriteria',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: cf.getAtt('MyDynamoDbStream', 'Arn'),
FilterCriteria: {
Filters: [
{
Pattern: JSON.stringify({ eventName: ['INSERT'] }),
}
]
}
});

module.exports = cf.merge(myTemplate, lambda, lambdaWithFilterCriteria);
```
44 changes: 41 additions & 3 deletions lib/shortcuts/stream-lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Lambda = require('./lambda');
* @param {Number} [options.MaximumBatchingWindowInSeconds=undefined] - See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-maximumbatchingwindowinseconds).
* @param {Boolean} [options.Enabled=true] - See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-enabled).
* @param {String} [options.StartingPosition='LATEST'] - See [AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-startingposition).
* @param {Object} [options.FilterCriteria] - See [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html).
*
* @example
* const cf = require('@mapbox/cloudfriend');
Expand All @@ -25,10 +26,27 @@ const Lambda = require('./lambda');
* S3Bucket: 'my-code-bucket',
* S3Key: 'path/to/code.zip'
* },
* EventSourceArn: cf.getAtt('MyStream', 'Arn')
* EventSourceArn: cf.getAtt('MyStream', 'Arn'),
* });
*
* module.exports = cf.merge(myTemplate, lambda);
* // This lambda only gets invoked for 'INSERT' events for the DynamoDb event source
* const lambdaWithFilterCriteria = new cf.shortcuts.StreamLambda({
* LogicalName: 'MyLambdaWithFilterCriteria',
* Code: {
* S3Bucket: 'my-code-bucket',
* S3Key: 'path/to/code.zip'
* },
* EventSourceArn: cf.getAtt('MyDynamoDbStream', 'Arn'),
* FilterCriteria: {
* Filters: [
* {
* Pattern: JSON.stringify({ eventName: ['INSERT'] }),
* }
* ]
* }
* });
*
* module.exports = cf.merge(myTemplate, lambda, lambdaWithFilterCriteria);
*/
class StreamLambda extends Lambda {
constructor(options) {
Expand All @@ -40,7 +58,8 @@ class StreamLambda extends Lambda {
BatchSize = 1,
MaximumBatchingWindowInSeconds,
Enabled = true,
StartingPosition = 'LATEST'
StartingPosition = 'LATEST',
FilterCriteria = undefined
} = options;

const required = [EventSourceArn];
Expand All @@ -59,6 +78,25 @@ class StreamLambda extends Lambda {
StartingPosition
}
};
if (FilterCriteria) {
if (Object.prototype.toString.call(FilterCriteria) !== '[object Object]'){
throw new Error('`FilterCriteria` must be a JSON-like object');
}
if (!(FilterCriteria.Filters && Array.isArray(FilterCriteria.Filters))){
throw new Error('`FilterCriteria` must contain property `Filter` of type array');
}
for (const filter of FilterCriteria.Filters){
if (!filter.Pattern){
throw new Error('An object in `FilterCriteria.Filter` was missing the required property `Pattern`');
}
try {
JSON.parse(filter.Pattern);
} catch (error) {
throw new Error('An object in `FilterCriteria.Filter` contains a `Pattern` property that is not a JSON parseable string');
}
}
this.Resources[`${this.LogicalName}EventSource`].Properties.FilterCriteria = FilterCriteria;
}

const generatedRoleRef = this.Resources[`${this.LogicalName}Role`];
const streamStatement = {
Expand Down
9 changes: 8 additions & 1 deletion test/fixtures/shortcuts/stream-lambda-no-defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@
"Properties": {
"BatchSize": 10000,
"MaximumBatchingWindowInSeconds": 300,
"FilterCriteria": {
"Filters": [
{
"Pattern": "{\"eventName\":[\"INSERT\",\"MODIFY\"]}"
}
]
},
"Enabled": false,
"EventSourceArn": "arn:aws:kinesis:us-east-1:123456789012:stream/fake",
"FunctionName": {
Expand All @@ -177,4 +184,4 @@
}
},
"Outputs": {}
}
}
91 changes: 91 additions & 0 deletions test/shortcuts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,13 @@ test('[shortcuts] stream-lambda', (assert) => {
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: {
Filters: [
{
Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] })
}
]
},
BatchSize: 10000,
MaximumBatchingWindowInSeconds: 300,
Enabled: false,
Expand All @@ -492,6 +499,90 @@ test('[shortcuts] stream-lambda', (assert) => {
assert.end();
});

test('[shortcuts] StreamLambda FilterCriteria', (assert) => {
assert.throws(
() => new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambda',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: ['test']
}),
'`FilterCriteria` must be a JSON-like object',
);
assert.throws(
() => new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambda',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: {}
}),
'`FilterCriteria` must contain property `Filter` of type array',
);
assert.throws(
() => new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambda',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: {
Filter: 613
}
}),
'`FilterCriteria` must contain property `Filter` of type array',
);
assert.throws(
() => new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambda',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: {
Filters: [
{
NotPattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] })
},
{
Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] })
}
]
}
}),
'An object in `FilterCriteria.Filter` was missing the required property `Pattern`',
);
assert.throws(
() => new cf.shortcuts.StreamLambda({
LogicalName: 'MyLambda',
Code: {
S3Bucket: 'my-code-bucket',
S3Key: 'path/to/code.zip'
},
EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake',
FilterCriteria: {
Filters: [
{
Pattern: '{"eventName":["INSERT","MODIFY"]}'
},
{
Pattern: { eventName: ['INSERT', 'MODIFY'] }
}
]
}
}),
'An object in `FilterCriteria.Filter` contains a `Pattern` property that is not a JSON parseable string',
);
assert.end();
});

test('[shortcuts] log-subscription-lambda', (assert) => {
assert.throws(
() => new cf.shortcuts.LogSubscriptionLambda(),
Expand Down

0 comments on commit 26c4830

Please sign in to comment.