serverless-offline-sns
Version:
Serverless plugin to run a local SNS server and call lambdas with events notifications.
266 lines (265 loc) • 10.5 kB
JavaScript
import { SNSClient, ListTopicsCommand, ListSubscriptionsCommand, UnsubscribeCommand, CreateTopicCommand, SubscribeCommand, PublishCommand } from "@aws-sdk/client-sns";
import _ from "lodash";
import fetch from "node-fetch";
import { createMessageId, createSnsLambdaEvent } from "./helpers.js";
export class SNSAdapter {
sns;
pluginDebug;
port;
server;
app;
serviceName;
stage;
endpoint;
adapterEndpoint;
baseSubscribeEndpoint;
accountId;
constructor(localPort, remotePort, region, snsEndpoint, debug, app, serviceName, stage, accountId, host, subscribeEndpoint) {
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.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) => {
this.sns.send(req, (err, topics) => {
if (err) {
this.debug(err, err.stack);
}
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) => {
this.sns.send(req, (err, subs) => {
if (err) {
this.debug(err, err.stack);
}
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) => this.sns.send(createTopicReq, (err, data) => {
if (err) {
this.debug(err, err.stack);
}
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 subscribeEndpoint = this.baseSubscribeEndpoint + "/" + fn.name;
this.debug("subscribe: " + fn.name + " " + arn);
this.debug("subscribeEndpoint: " + subscribeEndpoint);
this.app.post("/" + fn.name, (req, res) => {
this.debug("calling fn: " + fn.name + " 1");
const oldEnv = _.extend({}, process.env);
process.env = _.extend({}, process.env, fn.environment);
let event = req.body;
if (req.is("text/plain") && req.get("x-amz-sns-rawdelivery") !== "true") {
const msg = event.MessageStructure === "json"
? JSON.parse(event.Message).default
: event.Message;
event = createSnsLambdaEvent(event.TopicArn, "EXAMPLE", event.Subject || "", msg, event.MessageId || createMessageId(), event.MessageAttributes || {}, event.MessageGroupId);
}
if (req.body.SubscribeURL) {
this.debug("Visiting subscribe url: " + req.body.SubscribeURL);
return fetch(req.body.SubscribeURL, {
method: "GET"
}).then((fetchResponse) => {
this.debug("Subscribed: " + fetchResponse);
res.status(200).send();
});
}
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 && maybePromise.then) {
maybePromise
.then((response) => sendIt(null, response))
.catch((error) => sendIt(error, null));
}
});
const params = {
Protocol: snsConfig.protocol || "http",
TopicArn: arn,
Endpoint: subscribeEndpoint,
Attributes: {},
};
if (snsConfig.rawMessageDelivery === "true") {
params.Attributes["RawMessageDelivery"] = "true";
}
if (snsConfig.filterPolicy) {
params.Attributes["FilterPolicy"] = JSON.stringify(snsConfig.filterPolicy);
}
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 "${fn.name}" to topic: "${arn}"`);
}
res(true);
});
});
}
async subscribeQueue(queueUrl, arn, snsConfig) {
arn = this.convertPseudoParams(arn);
this.debug("subscribe: " + queueUrl + " " + arn);
const params = {
Protocol: snsConfig.protocol || "sqs",
TopicArn: arn,
Endpoint: queueUrl,
Attributes: {},
};
if (snsConfig.rawMessageDelivery === "true") {
params.Attributes["RawMessageDelivery"] = "true";
}
if (snsConfig.filterPolicy) {
params.Attributes["FilterPolicy"] = JSON.stringify(snsConfig.filterPolicy);
}
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) => {
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) => {
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) => {
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; // eslint-disable-line no-extra-parens
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: {},
};
}
}