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(),