Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iot): allow setting Actions of TopicRule #17110

Merged
merged 11 commits into from
Oct 28, 2021
36 changes: 32 additions & 4 deletions packages/@aws-cdk/aws-iot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,44 @@ import * as iot from '@aws-cdk/aws-iot';

## `TopicRule`

The `TopicRule` construct defined Rules that give your devices the ability to
interact with AWS services.

For example, to define a rule:
Create a rule that give your devices the ability to interact with AWS services.
You can create a rule with an action that invoke the Lambda action as following:

```ts
import * as iot from '@aws-cdk/aws-iot';

new iot.TopicRule(stack, 'MyTopicRule', {
topicRuleName: 'MyRuleName', // optional property
sql: iot.IotSql.fromStringAsVer20160323(
"SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
),
actions: [
{
bind: () => ({
configuration: {
lambda: { functionArn: 'test-functionArn' },
},
}),
},
],
});
```

Or, you can add an action after constructing the `TopicRule` instance as following:

```ts
import * as iot from '@aws-cdk/aws-iot';

const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323(
"SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
),
});
topicRule.addAction({
bind: () => ({
configuration: {
lambda: { functionArn: 'test-functionArn' },
},
}),
})
```
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-iot/lib/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CfnTopicRule } from './iot.generated';
import { ITopicRule } from './topic-rule';

/**
* An abstract action for TopicRule.
*/
export interface IAction {
/**
* Returns the topic rule action specification.
*
* @param rule The TopicRule that would trigger this action.
*/
bind(rule: ITopicRule): ActionConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bind(rule: ITopicRule): ActionConfig;
bind(topicRule: ITopicRule): ActionConfig;

}

/**
* Properties for an topic rule action
*/
export interface ActionConfig {
/**
* The configuration for this action.
*/
readonly configuration: CfnTopicRule.ActionProperty;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iot/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './action';
export * from './iot-sql';
export * from './topic-rule';

Expand Down
37 changes: 35 additions & 2 deletions packages/@aws-cdk/aws-iot/lib/topic-rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArnFormat, Resource, Stack, IResource } from '@aws-cdk/core';
import { ArnFormat, Resource, Stack, IResource, Lazy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { IAction } from './action';
import { IotSql } from './iot-sql';
import { CfnTopicRule } from './iot.generated';

Expand Down Expand Up @@ -33,6 +34,13 @@ export interface TopicRuleProps {
*/
readonly topicRuleName?: string;

/**
* The actions associated with the rule.
*
* @default No actions will be perform
*/
readonly actions?: Array<IAction>;

/**
* A simplified SQL syntax to filter messages received on an MQTT topic and push the data elsewhere.
*
Expand Down Expand Up @@ -80,6 +88,8 @@ export class TopicRule extends Resource implements ITopicRule {
*/
public readonly topicRuleName: string;

private readonly actions = new Array<CfnTopicRule.ActionProperty>();

constructor(scope: Construct, id: string, props: TopicRuleProps) {
super(scope, id, {
physicalName: props.topicRuleName,
Expand All @@ -90,7 +100,7 @@ export class TopicRule extends Resource implements ITopicRule {
const resource = new CfnTopicRule(this, 'Resource', {
ruleName: this.physicalName,
topicRulePayload: {
actions: [],
actions: Lazy.any({ produce: () => this.actions }),
awsIotSqlVersion: sqlConfig.awsIotSqlVersion,
sql: sqlConfig.sql,
},
Expand All @@ -102,5 +112,28 @@ export class TopicRule extends Resource implements ITopicRule {
resourceName: this.physicalName,
});
this.topicRuleName = this.getResourceNameAttribute(resource.ref);

props.actions?.forEach(action => {
this.addAction(action);
});
}

/**
* Add a action to the rule.
*
* @param action the action to associate with the rule.
*/
public addAction(action: IAction): void {
const { configuration } = action.bind(this);

const keys = Object.keys(configuration);
if (keys.length === 0) {
throw new Error('An action property cannot be an empty object.');
}
if (keys.length >= 2) {
throw new Error(`An action property cannot have multiple keys, received: ${keys}`);
}

this.actions.push(configuration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
"Type": "AWS::IoT::TopicRule",
"Properties": {
"TopicRulePayload": {
"Actions": [],
"Actions": [
{
"Http": {
"Url": "https://example.com"
}
}
],
"AwsIotSqlVersion": "2015-10-08",
"Sql": "SELECT topic(2) as device_id FROM 'device/+/data'"
}
Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ class TestStack extends cdk.Stack {

new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id FROM 'device/+/data'"),
actions: [
{
bind: () => ({
configuration: {
http: { url: 'https://example.com' },
},
}),
},
],
});
}
}
Expand Down
113 changes: 113 additions & 0 deletions packages/@aws-cdk/aws-iot/test/topic-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,119 @@ test.each([
}).toThrow('IoT SQL string cannot be empty');
});

test('can set actions', () => {
const stack = new cdk.Stack();

const action1: iot.IAction = {
bind: () => ({
configuration: {
http: { url: 'http://example.com' },
},
}),
};
const action2: iot.IAction = {
bind: () => ({
configuration: {
lambda: { functionArn: 'test-functionArn' },
},
}),
};

new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"),
actions: [action1, action2],
});

Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
Http: { Url: 'http://example.com' },
},
{
Lambda: { FunctionArn: 'test-functionArn' },
},
],
Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
},
});
});

test('can add an action', () => {
const stack = new cdk.Stack();

const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"),
});
topicRule.addAction({
bind: () => ({
configuration: {
http: { url: 'http://example.com' },
},
}),
});
topicRule.addAction({
bind: () => ({
configuration: {
lambda: { functionArn: 'test-functionArn' },
},
}),
});

Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
Http: { Url: 'http://example.com' },
},
{
Lambda: { FunctionArn: 'test-functionArn' },
},
],
Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
},
});
});

test('cannot add an action as empty object', () => {
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"),
});

const emptyKeysAction: iot.IAction = {
bind: () => ({
configuration: {},
}),
};

expect(() => {
topicRule.addAction(emptyKeysAction);
}).toThrow('An action property cannot be an empty object.');
});

test('cannot add an action that have multiple keys', () => {
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"),
});

const multipleKeysAction: iot.IAction = {
bind: () => ({
configuration: {
http: { url: 'http://example.com' },
lambda: { functionArn: 'test-functionArn' },
},
}),
};

expect(() => {
topicRule.addAction(multipleKeysAction);
}).toThrow(
'An action property cannot have multiple keys, received: http,lambda',
);
});

test('can import a TopicRule by ARN', () => {
const stack = new cdk.Stack();

Expand Down