import * as cdk from "@aws-cdk/core"; import * as s3 from "@aws-cdk/aws-s3"; import * as s3Notifications from "@aws-cdk/aws-s3-notifications"; import { Function as Fn, FunctionProps, FunctionDefinition } from "./Function"; import { Queue } from "./Queue"; import { Topic } from "./Topic"; import { Permissions } from "./util/permission"; ///////////////////// // Interfaces ///////////////////// export interface BucketProps { readonly s3Bucket?: s3.Bucket | s3.BucketProps; readonly notifications?: ( | FunctionDefinition | BucketFunctionNotificationProps | Queue | BucketQueueNotificationProps | Topic | BucketTopicNotificationProps )[]; readonly defaultFunctionProps?: FunctionProps; } export interface BucketNotificationProps { readonly events?: s3.EventType[]; readonly filters?: s3.NotificationKeyFilter[]; } export interface BucketFunctionNotificationProps { readonly function: FunctionDefinition; readonly notificationProps?: BucketNotificationProps; } export interface BucketQueueNotificationProps { readonly queue: Queue; readonly notificationProps?: BucketNotificationProps; } export interface BucketTopicNotificationProps { readonly topic: Topic; readonly notificationProps?: BucketNotificationProps; } ///////////////////// // Construct ///////////////////// export class Bucket extends cdk.Construct { public readonly s3Bucket: s3.Bucket; private readonly notifications: (Fn | Queue | Topic)[]; private readonly permissionsAttachedForAllNotifications: Permissions[]; private readonly defaultFunctionProps?: FunctionProps; constructor(scope: cdk.Construct, id: string, props?: BucketProps) { super(scope, id); const { s3Bucket, notifications, defaultFunctionProps } = props || {}; this.notifications = []; this.permissionsAttachedForAllNotifications = []; this.defaultFunctionProps = defaultFunctionProps; //////////////////// // Create Bucket //////////////////// if (cdk.Construct.isConstruct(s3Bucket)) { this.s3Bucket = s3Bucket as s3.Bucket; } else { const s3BucketProps = (s3Bucket || {}) as s3.BucketProps; this.s3Bucket = new s3.Bucket(this, "Bucket", { ...s3BucketProps, }); } /////////////////////////// // Create Notifications /////////////////////////// this.addNotifications(this, notifications || []); } public get bucketArn(): string { return this.s3Bucket.bucketArn; } public get bucketName(): string { return this.s3Bucket.bucketName; } public get notificationFunctions(): Fn[] { return this.notifications.filter( (notification) => notification instanceof Fn ) as Fn[]; } public addNotifications( scope: cdk.Construct, notifications: ( | FunctionDefinition | BucketFunctionNotificationProps | Queue | BucketQueueNotificationProps | Topic | BucketTopicNotificationProps )[] ): void { notifications.forEach((notification) => this.addNotification(scope, notification) ); } public attachPermissions(permissions: Permissions): void { this.notifications .filter((notification) => notification instanceof Fn) .forEach((notification) => notification.attachPermissions(permissions)); this.permissionsAttachedForAllNotifications.push(permissions); } public attachPermissionsToNotification( index: number, permissions: Permissions ): void { const notification = this.notifications[index]; if (!(notification instanceof Fn)) { throw new Error( `Cannot attach permissions to the "${this.node.id}" Bucket notification because it's not a Lambda function` ); } notification.attachPermissions(permissions); } private addNotification( scope: cdk.Construct, notification: | FunctionDefinition | BucketFunctionNotificationProps | Queue | BucketQueueNotificationProps | Topic | BucketTopicNotificationProps ): void { if ( notification instanceof Queue || (notification as BucketQueueNotificationProps).queue ) { notification = notification as Queue | BucketQueueNotificationProps; this.addQueueNotification(scope, notification); } else if ( notification instanceof Topic || (notification as BucketTopicNotificationProps).topic ) { notification = notification as Topic | BucketTopicNotificationProps; this.addTopicNotification(scope, notification); } else { notification = notification as | FunctionDefinition | BucketFunctionNotificationProps; this.addFunctionNotification(scope, notification); } } private addQueueNotification( scope: cdk.Construct, notification: Queue | BucketQueueNotificationProps ): void { // Parse notification props let notificationProps; let queue: Queue; if (notification instanceof Queue) { notification = notification as Queue; queue = notification; } else { notification = notification as BucketQueueNotificationProps; notificationProps = notification.notificationProps; queue = notification.queue; } this.notifications.push(queue); // Create Notifications const events = notificationProps?.events || [ s3.EventType.OBJECT_CREATED, s3.EventType.OBJECT_REMOVED ]; const filters = notificationProps?.filters || []; events.forEach(event => this.s3Bucket.addEventNotification(event, new s3Notifications.SqsDestination(queue.sqsQueue), ...filters) ); } private addTopicNotification( scope: cdk.Construct, notification: Topic | BucketTopicNotificationProps ): void { // Parse notification props let notificationProps; let topic: Topic; if (notification instanceof Topic) { notification = notification as Topic; topic = notification; } else { notification = notification as BucketTopicNotificationProps; notificationProps = notification.notificationProps; topic = notification.topic; } this.notifications.push(topic); // Create Notifications const events = notificationProps?.events || [ s3.EventType.OBJECT_CREATED, s3.EventType.OBJECT_REMOVED ]; const filters = notificationProps?.filters || []; events.forEach(event => this.s3Bucket.addEventNotification(event, new s3Notifications.SnsDestination(topic.snsTopic), ...filters) ); } private addFunctionNotification( scope: cdk.Construct, notification: FunctionDefinition | BucketFunctionNotificationProps ): void { // parse notification let notificationFunction, notificationProps; if ((notification as BucketFunctionNotificationProps).function) { notification = notification as BucketFunctionNotificationProps; notificationFunction = notification.function; notificationProps = notification.notificationProps; } else { notificationFunction = notification as FunctionDefinition; } // create function const i = this.notifications.length; const fn = Fn.fromDefinition( scope, `Notification_${i}`, notificationFunction, this.defaultFunctionProps, `The "defaultFunctionProps" cannot be applied if an instance of a Function construct is passed in. Make sure to define all the consumers using FunctionProps, so the Table construct can apply the "defaultFunctionProps" to them.` ); this.notifications.push(fn); // create Notifications const events = notificationProps?.events || [ s3.EventType.OBJECT_CREATED, s3.EventType.OBJECT_REMOVED ]; const filters = notificationProps?.filters || []; events.forEach(event => this.s3Bucket.addEventNotification(event, new s3Notifications.LambdaDestination(fn), ...filters) ); // attached permissions this.permissionsAttachedForAllNotifications.forEach((permissions) => fn.attachPermissions(permissions) ); } }