diff --git a/packages/nodes-base/nodes/Aws/AwsSns.node.ts b/packages/nodes-base/nodes/Aws/AwsSns.node.ts
index 56e0248384..4ac111c8e5 100644
--- a/packages/nodes-base/nodes/Aws/AwsSns.node.ts
+++ b/packages/nodes-base/nodes/Aws/AwsSns.node.ts
@@ -3,7 +3,8 @@ import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
- INodePropertyOptions,
+ INodeListSearchItems,
+ INodeListSearchResult,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
@@ -37,6 +38,18 @@ export class AwsSns implements INodeType {
type: 'options',
noDataExpression: true,
options: [
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a topic',
+ action: 'Create a topic',
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete a topic',
+ action: 'Delete a topic',
+ },
{
name: 'Publish',
value: 'publish',
@@ -47,22 +60,106 @@ export class AwsSns implements INodeType {
default: 'publish',
},
{
- displayName: 'Topic Name or ID',
- name: 'topic',
- type: 'options',
- typeOptions: {
- loadOptionsMethod: 'getTopics',
- },
+ displayName: 'Name',
+ name: 'name',
+ type: 'string',
+ required: true,
+ default: '',
displayOptions: {
show: {
- operation: ['publish'],
+ operation: ['create'],
},
},
- options: [],
- default: '',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ displayName: 'Display Name',
+ name: 'displayName',
+ type: 'string',
+ default: '',
+ description: 'The display name to use for a topic with SMS subscriptions',
+ },
+ {
+ displayName: 'Fifo Topic',
+ name: 'fifoTopic',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether the topic you want to create is a FIFO (first-in-first-out) topic',
+ },
+ ],
+ displayOptions: {
+ show: {
+ operation: ['create'],
+ },
+ },
+ },
+ {
+ displayName: 'Topic',
+ name: 'topic',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
required: true,
- description:
- 'The topic you want to publish to. Choose from the list, or specify an ID using an expression.',
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ placeholder: 'Select a topic...',
+ typeOptions: {
+ searchListMethod: 'listTopics',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By URL',
+ name: 'url',
+ type: 'string',
+ placeholder:
+ 'https://us-east-1.console.aws.amazon.com/sns/v3/home?region=us-east-1#/topic/arn:aws:sns:us-east-1:777777777777:your_topic',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex:
+ 'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+(?:\\/.*|)',
+ errorMessage: 'Not a valid AWS SNS Topic URL',
+ },
+ },
+ ],
+ extractValue: {
+ type: 'regex',
+ regex:
+ 'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/(arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ },
+ },
+ {
+ displayName: 'ID',
+ name: 'id',
+ type: 'string',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: 'arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+',
+ errorMessage: 'Not a valid AWS SNS Topic ARN',
+ },
+ },
+ ],
+ placeholder: 'arn:aws:sns:your-aws-region:777777777777:your_topic',
+ },
+ ],
+ displayOptions: {
+ show: {
+ operation: ['publish', 'delete'],
+ },
+ },
},
{
displayName: 'Subject',
@@ -97,32 +194,52 @@ export class AwsSns implements INodeType {
};
methods = {
- loadOptions: {
- // Get all the available topics to display them to user so that he can
- // select them easily
- async getTopics(this: ILoadOptionsFunctions): Promise {
- const returnData: INodePropertyOptions[] = [];
- const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
+ listSearch: {
+ async listTopics(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+ paginationToken?: string,
+ ): Promise {
+ const returnData: INodeListSearchItems[] = [];
+ const params = paginationToken ? `NextToken=${encodeURIComponent(paginationToken)}` : '';
+
+ const data = await awsApiRequestSOAP.call(
+ this,
+ 'sns',
+ 'GET',
+ '/?Action=ListTopics&' + params,
+ );
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
+ const nextToken = data.ListTopicsResponse.ListTopicsResult.NextToken;
+
+ if (nextToken) {
+ paginationToken = nextToken as string;
+ } else {
+ paginationToken = undefined;
+ }
if (!Array.isArray(topics)) {
- // If user has only a single topic no array get returned so we make
- // one manually to be able to process everything identically
topics = [topics];
}
for (const topic of topics) {
const topicArn = topic.TopicArn as string;
- const topicName = topicArn.split(':')[5];
+ const arnParsed = topicArn.split(':');
+ const topicName = arnParsed[5];
+ const awsRegion = arnParsed[3];
+
+ if (filter && topicName.includes(filter) === false) {
+ continue;
+ }
returnData.push({
name: topicName,
value: topicArn,
+ url: `https://${awsRegion}.console.aws.amazon.com/sns/v3/home?region=${awsRegion}#/topic/${topicArn}`,
});
}
-
- return returnData;
+ return { results: returnData, paginationToken };
},
},
};
@@ -130,24 +247,78 @@ export class AwsSns implements INodeType {
async execute(this: IExecuteFunctions): Promise {
const items = this.getInputData();
const returnData: IDataObject[] = [];
+ const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
try {
- const params = [
- ('TopicArn=' + this.getNodeParameter('topic', i)) as string,
- ('Subject=' + this.getNodeParameter('subject', i)) as string,
- ('Message=' + this.getNodeParameter('message', i)) as string,
- ];
+ if (operation === 'create') {
+ let name = this.getNodeParameter('name', i) as string;
+ const fifoTopic = this.getNodeParameter('options.fifoTopic', i, false) as boolean;
+ const displayName = this.getNodeParameter('options.displayName', i, '') as string;
+ const params: string[] = [];
- const responseData = await awsApiRequestSOAP.call(
- this,
- 'sns',
- 'GET',
- '/?Action=Publish&' + params.join('&'),
- );
- returnData.push({
- MessageId: responseData.PublishResponse.PublishResult.MessageId,
- } as IDataObject);
+ if (fifoTopic && !name.endsWith('.fifo')) {
+ name = `${name}.fifo`;
+ }
+
+ params.push(`Name=${name}`);
+
+ if (fifoTopic) {
+ params.push('Attributes.entry.1.key=FifoTopic');
+ params.push('Attributes.entry.1.value=true');
+ }
+
+ if (displayName) {
+ params.push('Attributes.entry.2.key=DisplayName');
+ params.push(`Attributes.entry.2.value=${displayName}`);
+ }
+
+ const responseData = await awsApiRequestSOAP.call(
+ this,
+ 'sns',
+ 'GET',
+ '/?Action=CreateTopic&' + params.join('&'),
+ );
+ returnData.push({
+ TopicArn: responseData.CreateTopicResponse.CreateTopicResult.TopicArn,
+ } as IDataObject);
+ }
+ if (operation === 'delete') {
+ const topic = this.getNodeParameter('topic', i, undefined, {
+ extractValue: true,
+ }) as string;
+ const params = [('TopicArn=' + topic) as string];
+
+ await awsApiRequestSOAP.call(
+ this,
+ 'sns',
+ 'GET',
+ '/?Action=DeleteTopic&' + params.join('&'),
+ );
+ // response of delete is the same no matter if topic was deleted or not
+ returnData.push({ success: true } as IDataObject);
+ }
+ if (operation === 'publish') {
+ const topic = this.getNodeParameter('topic', i, undefined, {
+ extractValue: true,
+ }) as string;
+
+ const params = [
+ ('TopicArn=' + topic) as string,
+ ('Subject=' + this.getNodeParameter('subject', i)) as string,
+ ('Message=' + this.getNodeParameter('message', i)) as string,
+ ];
+
+ const responseData = await awsApiRequestSOAP.call(
+ this,
+ 'sns',
+ 'GET',
+ '/?Action=Publish&' + params.join('&'),
+ );
+ returnData.push({
+ MessageId: responseData.PublishResponse.PublishResult.MessageId,
+ } as IDataObject);
+ }
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
diff --git a/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts
index 290eb09284..7a4e192034 100644
--- a/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts
+++ b/packages/nodes-base/nodes/Aws/AwsSnsTrigger.node.ts
@@ -2,7 +2,8 @@ import { IHookFunctions, IWebhookFunctions } from 'n8n-core';
import {
ILoadOptionsFunctions,
- INodePropertyOptions,
+ INodeListSearchItems,
+ INodeListSearchResult,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
@@ -44,55 +45,123 @@ export class AwsSnsTrigger implements INodeType {
],
properties: [
{
- displayName: 'Topic Name or ID',
+ displayName: 'Topic',
name: 'topic',
- type: 'options',
- description:
- 'Choose from the list, or specify an ID using an expression',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
required: true,
- typeOptions: {
- loadOptionsMethod: 'getTopics',
- },
- default: '',
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ placeholder: 'Select a topic...',
+ typeOptions: {
+ searchListMethod: 'listTopics',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By URL',
+ name: 'url',
+ type: 'string',
+ placeholder:
+ 'https://us-east-1.console.aws.amazon.com/sns/v3/home?region=us-east-1#/topic/arn:aws:sns:us-east-1:777777777777:your_topic',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex:
+ 'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+(?:\\/.*|)',
+ errorMessage: 'Not a valid AWS SNS Topic URL',
+ },
+ },
+ ],
+ extractValue: {
+ type: 'regex',
+ regex:
+ 'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/(arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ },
+ },
+ {
+ displayName: 'ID',
+ name: 'id',
+ type: 'string',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: 'arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+',
+ errorMessage: 'Not a valid AWS SNS Topic ARN',
+ },
+ },
+ ],
+ placeholder: 'arn:aws:sns:your-aws-region:777777777777:your_topic',
+ },
+ ],
},
],
};
methods = {
- loadOptions: {
- // Get all the available topics to display them to user so that he can
- // select them easily
- async getTopics(this: ILoadOptionsFunctions): Promise {
- const returnData: INodePropertyOptions[] = [];
- const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
+ listSearch: {
+ async listTopics(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+ paginationToken?: string,
+ ): Promise {
+ const returnData: INodeListSearchItems[] = [];
+ const params = paginationToken ? `NextToken=${encodeURIComponent(paginationToken)}` : '';
+
+ const data = await awsApiRequestSOAP.call(
+ this,
+ 'sns',
+ 'GET',
+ '/?Action=ListTopics&' + params,
+ );
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
+ const nextToken = data.ListTopicsResponse.ListTopicsResult.NextToken;
+
+ if (nextToken) {
+ paginationToken = nextToken as string;
+ } else {
+ paginationToken = undefined;
+ }
if (!Array.isArray(topics)) {
- // If user has only a single topic no array get returned so we make
- // one manually to be able to process everything identically
topics = [topics];
}
for (const topic of topics) {
const topicArn = topic.TopicArn as string;
- const topicName = topicArn.split(':')[5];
+ const arnParsed = topicArn.split(':');
+ const topicName = arnParsed[5];
+ const awsRegion = arnParsed[3];
+
+ if (filter && topicName.includes(filter) === false) {
+ continue;
+ }
returnData.push({
name: topicName,
value: topicArn,
+ url: `https://${awsRegion}.console.aws.amazon.com/sns/v3/home?region=${awsRegion}#/topic/${topicArn}`,
});
}
- return returnData;
+ return { results: returnData, paginationToken };
},
},
};
- // @ts-ignore
+ //@ts-expect-error because of webhook
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
- const topic = this.getNodeParameter('topic') as string;
+ const topic = this.getNodeParameter('topic', undefined, {
+ extractValue: true,
+ }) as string;
+
if (webhookData.webhookId === undefined) {
return false;
}
@@ -127,7 +196,9 @@ export class AwsSnsTrigger implements INodeType {
async create(this: IHookFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
- const topic = this.getNodeParameter('topic') as string;
+ const topic = this.getNodeParameter('topic', undefined, {
+ extractValue: true,
+ }) as string;
if (webhookUrl.includes('%20')) {
throw new NodeOperationError(
@@ -175,7 +246,9 @@ export class AwsSnsTrigger implements INodeType {
async webhook(this: IWebhookFunctions): Promise {
const req = this.getRequestObject();
- const topic = this.getNodeParameter('topic') as string;
+ const topic = this.getNodeParameter('topic', undefined, {
+ extractValue: true,
+ }) as string;
const body = jsonParse<{ Type: string; TopicArn: string; Token: string }>(
req.rawBody.toString(),