UNPKG

serverless-offline-sns

Version:

Serverless plugin to run a local SNS server and call lambdas with events notifications.

306 lines (305 loc) 12.7 kB
import { SNSClient, ListTopicsCommand, ListSubscriptionsCommand, UnsubscribeCommand, CreateTopicCommand, SubscribeCommand, PublishCommand, ConfirmSubscriptionCommand } from "@aws-sdk/client-sns"; import _ from "lodash"; import { createMessageId, createSnsLambdaEvent } from "./helpers.js"; export class SNSAdapter { sns; pluginDebug; app; serviceName; stage; endpoint; adapterEndpoint; baseSubscribeEndpoint; accountId; sqsEndpoint; region; constructor(localPort, remotePort, region, snsEndpoint, debug, app, serviceName, stage, accountId, host, subscribeEndpoint, sqsEndpoint) { this.pluginDebug = debug; this.app = app; this.serviceName = serviceName; this.stage = stage; this.adapterEndpoint = `http://${host || "127.0.0.1"}:${localPort}`; this.baseSubscribeEndpoint = subscribeEndpoint ? `http://${subscribeEndpoint}:${remotePort}` : this.adapterEndpoint; this.endpoint = snsEndpoint || `http://127.0.0.1:${localPort}`; this.sqsEndpoint = sqsEndpoint || `http://127.0.0.1:${localPort}`; this.region = region; this.debug("using endpoint: " + this.endpoint); this.accountId = accountId; this.sns = new SNSClient({ credentials: { accessKeyId: "AKID", secretAccessKey: "SECRET", }, endpoint: this.endpoint, region, }); } async listTopics() { this.debug("listing topics"); const req = new ListTopicsCommand({}); this.debug(JSON.stringify(req.input)); return await new Promise((res, rej) => { this.sns.send(req, (err, topics) => { if (err) { this.debug(err, err.stack); rej(err instanceof Error ? err : new Error(String(err))); } else { this.debug(JSON.stringify(topics)); res(topics); } }); }); } async listSubscriptions() { this.debug("listing subs"); const req = new ListSubscriptionsCommand({}); this.debug(JSON.stringify(req.input)); return await new Promise((res, rej) => { this.sns.send(req, (err, subs) => { if (err) { this.debug(err, err.stack); rej(err instanceof Error ? err : new Error(String(err))); } else { this.debug(JSON.stringify(subs)); res(subs); } }); }); } async unsubscribe(arn) { this.debug("unsubscribing: " + arn); const unsubscribeReq = new UnsubscribeCommand({ SubscriptionArn: arn }); await new Promise((res) => { this.sns.send(unsubscribeReq, (err, data) => { if (err) { this.debug(err, err.stack); } else { this.debug("unsubscribed: " + JSON.stringify(data)); } res(true); }); }); } async createTopic(topicName) { const createTopicReq = new CreateTopicCommand({ Name: topicName }); return new Promise((res, rej) => this.sns.send(createTopicReq, (err, data) => { if (err) { this.debug(err, err.stack); rej(err instanceof Error ? err : new Error(String(err))); } else { this.debug("arn: " + JSON.stringify(data)); res(data); } })); } sent = () => { }; Deferred = new Promise((res) => (this.sent = res)); async subscribe(fn, getHandler, arn, snsConfig) { arn = this.convertPseudoParams(arn); const fnName = String(fn.name); const subscribeEndpoint = (typeof snsConfig === "object" && snsConfig.queueName) ? this.sqsEndpoint : this.baseSubscribeEndpoint + "/" + fnName; this.debug("subscribe: " + fnName + " " + arn); this.debug("subscribeEndpoint: " + subscribeEndpoint); this.app.post("/" + fnName, (req, res) => { this.debug("calling fn: " + fnName + " 1"); const oldEnv = _.extend({}, process.env); process.env = _.extend({}, process.env, fn.environment); const body = req.body; let event = req.body; if (req.is("text/plain") && req.get("x-amz-sns-rawdelivery") !== "true") { const msg = body.MessageStructure === "json" ? JSON.parse(body.Message ?? "{}").default : body.Message ?? ""; event = createSnsLambdaEvent(body.TopicArn ?? "", "EXAMPLE", body.Subject || "", msg, body.MessageId || createMessageId(), body.MessageAttributes, body.MessageGroupId); } if (body.SubscribeURL) { this.debug("Confirming subscription via SDK for topic: " + (body.TopicArn ?? "")); const confirmReq = new ConfirmSubscriptionCommand({ TopicArn: body.TopicArn, Token: body.Token, }); return this.sns.send(confirmReq).then(() => { this.debug("Subscription confirmed"); res.status(200).send(); }).catch((err) => { this.debug("Subscription confirmation failed: " + String(err)); res.status(500).send(String(err)); }); } const sendIt = (err, response) => { process.env = oldEnv; if (err) { res.status(500).send(err); this.sent(err); } else { res.send(response); this.sent(response); } }; const maybePromise = getHandler(event, this.createLambdaContext(fn, sendIt), sendIt); if (maybePromise instanceof Promise) { maybePromise .then((response) => sendIt(null, response)) .catch((error) => sendIt(error instanceof Error ? error : new Error(String(error)), null)); } }); const params = { Protocol: typeof snsConfig === "object" ? (snsConfig.protocol || "http") : "http", TopicArn: arn, Endpoint: subscribeEndpoint, Attributes: {}, }; if (typeof snsConfig === "object" && snsConfig.rawMessageDelivery === "true") { params.Attributes["RawMessageDelivery"] = "true"; } if (typeof snsConfig === "object" && snsConfig.filterPolicy) { params.Attributes["FilterPolicy"] = JSON.stringify(snsConfig.filterPolicy); } if (typeof snsConfig === "object" && snsConfig.filterPolicyScope) { params.Attributes["FilterPolicyScope"] = snsConfig.filterPolicyScope; } if (typeof snsConfig === "object" && snsConfig.queueName) { params.Attributes["QueueName"] = snsConfig.queueName; } const subscribeRequest = new SubscribeCommand(params); await new Promise((res) => { this.sns.send(subscribeRequest, (err, data) => { if (err) { this.debug(err, err.stack); } else { this.debug(`successfully subscribed fn "${fnName}" to topic: "${arn}"`); } res(true); }); }); } async subscribeQueue(queueUrl, arn, snsConfig) { arn = this.convertPseudoParams(arn); this.debug("subscribe: " + queueUrl + " " + arn); const params = { Protocol: typeof snsConfig === "object" ? (snsConfig.protocol || "sqs") : "sqs", TopicArn: arn, Endpoint: queueUrl, Attributes: {}, }; if (typeof snsConfig === "object" && snsConfig.rawMessageDelivery === "true") { params.Attributes["RawMessageDelivery"] = "true"; } if (typeof snsConfig === "object" && snsConfig.filterPolicy) { params.Attributes["FilterPolicy"] = JSON.stringify(snsConfig.filterPolicy); } if (typeof snsConfig === "object" && snsConfig.filterPolicyScope) { params.Attributes["FilterPolicyScope"] = snsConfig.filterPolicyScope; } const subscribeRequest = new SubscribeCommand(params); await new Promise((res) => { this.sns.send(subscribeRequest, (err, data) => { if (err) { this.debug(err, err.stack); } else { this.debug(`successfully subscribed queue "${queueUrl}" to topic: "${arn}"`); } res(true); }); }); } convertPseudoParams(topicArn) { const awsRegex = /#{AWS::([a-zA-Z]+)}/g; return topicArn.replace(awsRegex, this.accountId); } async publish(topicArn, message, type = "", messageAttributes = {}, subject = "", messageGroupId) { topicArn = this.convertPseudoParams(topicArn); const publishReq = new PublishCommand({ Message: message, Subject: subject, MessageStructure: type, TopicArn: topicArn, MessageAttributes: messageAttributes, ...(messageGroupId && { MessageGroupId: messageGroupId }), }); return await new Promise((resolve, reject) => this.sns.send(publishReq, (err, result) => { if (err) { this.debug(err, err.stack); reject(err instanceof Error ? err : new Error(String(err))); } else { resolve(result); } })); } async publishToTargetArn(targetArn, message, type = "", messageAttributes = {}, messageGroupId) { targetArn = this.convertPseudoParams(targetArn); const publishReq = new PublishCommand({ Message: message, MessageStructure: type, TargetArn: targetArn, MessageAttributes: messageAttributes, ...(messageGroupId && { MessageGroupId: messageGroupId }), }); return await new Promise((resolve, reject) => this.sns.send(publishReq, (err, result) => { if (err) { this.debug(err, err.stack); reject(err instanceof Error ? err : new Error(String(err))); } else { resolve(result); } })); } async publishToPhoneNumber(phoneNumber, message, type = "", messageAttributes = {}, messageGroupId) { const publishReq = new PublishCommand({ Message: message, MessageStructure: type, PhoneNumber: phoneNumber, MessageAttributes: messageAttributes, ...(messageGroupId && { MessageGroupId: messageGroupId }), }); return await new Promise((resolve, reject) => this.sns.send(publishReq, (err, result) => { if (err) { this.debug(err, err.stack); reject(err instanceof Error ? err : new Error(String(err))); } else { resolve(result); } })); } debug(msg, stack) { this.pluginDebug(msg, "adapter"); } createLambdaContext(fun, cb) { const functionName = `${this.serviceName}-${this.stage}-${fun.name}`; const endTime = new Date().getTime() + (fun.timeout ? fun.timeout * 1000 : 6000); const done = typeof cb === "function" ? cb : (x, y) => x || y; return { /* Methods */ done, succeed: (res) => done(null, res), fail: (err) => done(err, null), getRemainingTimeInMillis: () => endTime - new Date().getTime(), /* Properties */ functionName, memoryLimitInMB: fun.memorySize || 1536, functionVersion: `offline_functionVersion_for_${functionName}`, invokedFunctionArn: `offline_invokedFunctionArn_for_${functionName}`, awsRequestId: `offline_awsRequestId_${Math.random() .toString(10) .slice(2)}`, logGroupName: `offline_logGroupName_for_${functionName}`, logStreamName: `offline_logStreamName_for_${functionName}`, identity: {}, clientContext: {}, }; } }