// Copyright Abridged, Inc. 2021,2024. All Rights Reserved. // Node module: @collabland/aws // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import { PublishBatchCommand, PublishBatchCommandInput, PublishBatchRequestEntry, PublishCommand, PublishCommandInput, SNSClient, SNSClientConfig, SubscribeCommand, SubscribeCommandInput, UnsubscribeCommand, UnsubscribeCommandInput, } from '@aws-sdk/client-sns'; import {generateIdSync, loggers, pMapByPage} from '@collabland/common'; import { getAWSAccountId, getAWSSNSRegion, getDefaultConfig, } from '../common/types.js'; const {trace, error} = loggers('collabland:aws:sns'); export class SNSServiceClient { readonly sns: SNSClient; constructor(options?: SNSClientConfig) { this.sns = new SNSClient({ ...getDefaultConfig(), region: getAWSSNSRegion(), ...options, }); } async publish( topic: string, message: string, options: Omit = {}, ) { const input: PublishCommandInput = { ...options, Message: message, TopicArn: topic, }; trace('SNS publish: %O', input); const cmd = new PublishCommand(input); try { const result = await this.sns.send(cmd); return result; } catch (err) { error('Fail to publish message %s: %O', message, err); throw err; } } /** * Publish messages to the given topic in batch * @param topic - Topic arn * @param messages - Messages * @returns */ async publishBatch( topic: string, messages: { message: string; options: Omit; }[], ) { const results = await pMapByPage( messages, async page => { const input: PublishBatchCommandInput = { PublishBatchRequestEntries: page.map(m => { const message: PublishBatchRequestEntry = { ...m.options, Message: m.message, Id: m.options?.Id ?? generateIdSync(), }; return message; }), TopicArn: topic, }; trace('SNS publish batch: %O', input); const cmd = new PublishBatchCommand(input); const result = await this.sns.send(cmd); return result; }, // Maximum batch size for SNS is 10 {pageSize: 10}, ); return results; } async publishBatchJson( topic: string, messages: { message: object; options: Omit; }[], ) { const data = messages.map(messageObj => { const json = this.serializeMessage(messageObj.message); return { message: json, options: { ...messageObj.options, MessageStructure: 'json', }, }; }); const result = await this.publishBatch(topic, data); return result; } async publishJson( topic: string, message: object, options: Omit = {}, ) { const json = this.serializeMessage(message); const result = await this.publish(topic, json, { ...options, MessageStructure: 'json', }); return result; } private serializeMessage(message: object) { let data: string; try { data = JSON.stringify(message); } catch (err) { error('Fail to publish message as JSON: %O. Error: %O.', message, err); throw err; } const json = JSON.stringify({ default: data, }); return json; } async subscribe( topic: string, protocol: string, endpoint: string, options: Omit< SubscribeCommandInput, 'TopicArn' | 'Endpoint' | 'Protocol' > = {}, ) { const input: SubscribeCommandInput = { Protocol: protocol, ReturnSubscriptionArn: true, ...options, TopicArn: topic, Endpoint: endpoint, }; const cmd = new SubscribeCommand(input); const result = await this.sns.send(cmd); return result; } async unsubscribe(subscription: string) { const input: UnsubscribeCommandInput = { SubscriptionArn: subscription, }; const cmd = new UnsubscribeCommand(input); const result = await this.sns.send(cmd); return result; } } /** * Get ARN of a SNS topic * @param topicName - Topic name * @param region - AWS region * @param accountId - AWS account id * @returns */ export function getSNSTopicArn( topicName: string, region = getAWSSNSRegion(), accountId = getAWSAccountId(), ) { const arn = `arn:aws:sns:${region}:${accountId}:${topicName}`; return arn; }