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
JavaScript
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: {},
};
}
}