import * as cdk from "@aws-cdk/core"; import * as sns from "@aws-cdk/aws-sns"; import * as snsSubscriptions from "@aws-cdk/aws-sns-subscriptions"; import { App } from "./App"; import { Function as Fn, FunctionProps, FunctionDefinition } from "./Function"; import { Queue } from "./Queue"; import { Permissions } from "./util/permission"; import { isConstructOf } from "./util/construct"; ///////////////////// // Interfaces ///////////////////// export interface TopicProps { readonly snsTopic?: sns.ITopic | sns.TopicProps; readonly subscribers?: ( | FunctionDefinition | TopicFunctionSubscriberProps | Queue | TopicQueueSubscriberProps )[]; readonly defaultFunctionProps?: FunctionProps; } export interface TopicFunctionSubscriberProps { readonly function: FunctionDefinition; readonly subscriberProps?: snsSubscriptions.LambdaSubscriptionProps; } export interface TopicQueueSubscriberProps { readonly queue: Queue; readonly subscriberProps?: snsSubscriptions.SqsSubscriptionProps; } ///////////////////// // Construct ///////////////////// export class Topic extends cdk.Construct { public readonly snsTopic: sns.Topic; private readonly subscribers: (Fn | Queue)[]; private readonly permissionsAttachedForAllSubscribers: Permissions[]; private readonly defaultFunctionProps?: FunctionProps; constructor(scope: cdk.Construct, id: string, props?: TopicProps) { super(scope, id); const root = scope.node.root as App; const { snsTopic, subscribers, defaultFunctionProps } = props || {}; this.subscribers = []; this.permissionsAttachedForAllSubscribers = []; this.defaultFunctionProps = defaultFunctionProps; //////////////////// // Create Topic //////////////////// if (cdk.Construct.isConstruct(snsTopic)) { this.snsTopic = snsTopic as sns.Topic; } else { const snsTopicProps = (snsTopic || {}) as sns.TopicProps; this.snsTopic = new sns.Topic(this, "Topic", { topicName: root.logicalPrefixedName(id), ...snsTopicProps, }); } /////////////////////////// // Create Subscribers /////////////////////////// this.addSubscribers(this, subscribers || []); } public get topicArn(): string { return this.snsTopic.topicArn; } public get topicName(): string { return this.snsTopic.topicName; } public get subscriberFunctions(): Fn[] { return this.subscribers.filter( (subscriber) => subscriber instanceof Fn ) as Fn[]; } public get snsSubscriptions(): sns.Subscription[] { return this.subscribers.map((sub) => { let children; // look for sns.Subscription inside Queue.sqsQueue if (sub instanceof Queue) { children = sub.sqsQueue.node.children; } // look for sns.Subscription inside Function else { children = sub.node.children; } const child = children.find((child) => { return isConstructOf(child as cdk.Construct, "aws-sns.Subscription"); }); return child as sns.Subscription; }); } public addSubscribers( scope: cdk.Construct, subscribers: ( | FunctionDefinition | TopicFunctionSubscriberProps | Queue | TopicQueueSubscriberProps )[] ): void { subscribers.forEach((subscriber) => this.addSubscriber(scope, subscriber)); } public attachPermissions(permissions: Permissions): void { this.subscribers .filter((subscriber) => subscriber instanceof Fn) .forEach((subscriber) => subscriber.attachPermissions(permissions)); this.permissionsAttachedForAllSubscribers.push(permissions); } public attachPermissionsToSubscriber( index: number, permissions: Permissions ): void { const subscriber = this.subscribers[index]; if (!(subscriber instanceof Fn)) { throw new Error( `Cannot attach permissions to the "${this.node.id}" Topic subscriber because it's not a Lambda function` ); } subscriber.attachPermissions(permissions); } private addSubscriber( scope: cdk.Construct, subscriber: | FunctionDefinition | TopicFunctionSubscriberProps | Queue | TopicQueueSubscriberProps ): void { if ( subscriber instanceof Queue || (subscriber as TopicQueueSubscriberProps).queue ) { subscriber = subscriber as Queue | TopicQueueSubscriberProps; this.addQueueSubscriber(scope, subscriber); } else { subscriber = subscriber as | FunctionDefinition | TopicFunctionSubscriberProps; this.addFunctionSubscriber(scope, subscriber); } } private addQueueSubscriber( scope: cdk.Construct, subscriber: Queue | TopicQueueSubscriberProps ): void { // Parse subscriber props let subscriberProps; let queue; if (subscriber instanceof Queue) { subscriber = subscriber as Queue; queue = subscriber; } else { subscriber = subscriber as TopicQueueSubscriberProps; subscriberProps = subscriber.subscriberProps; queue = subscriber.queue; } this.subscribers.push(queue); // Create Subscription this.snsTopic.addSubscription( new snsSubscriptions.SqsSubscription(queue.sqsQueue, subscriberProps) ); } private addFunctionSubscriber( scope: cdk.Construct, subscriber: FunctionDefinition | TopicFunctionSubscriberProps ): void { // Parse subscriber props let subscriberProps; let functionDefinition; if ((subscriber as TopicFunctionSubscriberProps).function) { subscriber = subscriber as TopicFunctionSubscriberProps; subscriberProps = subscriber.subscriberProps; functionDefinition = subscriber.function; } else { subscriber = subscriber as FunctionDefinition; functionDefinition = subscriber; } // Create function const i = this.subscribers.length; const fn = Fn.fromDefinition( scope, `Subscriber_${i}`, functionDefinition, this.defaultFunctionProps, `The "defaultFunctionProps" cannot be applied if an instance of a Function construct is passed in. Make sure to define all the subscribers using FunctionProps, so the Topic construct can apply the "defaultFunctionProps" to them.` ); this.subscribers.push(fn); // Create Subscription this.snsTopic.addSubscription( new snsSubscriptions.LambdaSubscription(fn, subscriberProps) ); // Attach existing permissions this.permissionsAttachedForAllSubscribers.forEach((permissions) => fn.attachPermissions(permissions) ); } }