Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trigger handler for storage events #1027

Merged
merged 18 commits into from
Feb 14, 2024
Merged
9 changes: 9 additions & 0 deletions packages/backend-storage/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types';
import { ConstructFactory } from '@aws-amplify/plugin-types';
import { FunctionResources } from '@aws-amplify/plugin-types';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { ResourceProvider } from '@aws-amplify/plugin-types';
import { StorageOutput } from '@aws-amplify/backend-output-schemas';

Expand All @@ -18,8 +20,15 @@ export type AmplifyStorageProps = {
name: string;
versioned?: boolean;
outputStorageStrategy?: BackendOutputStorageStrategy<StorageOutput>;
triggers?: Partial<Record<AmplifyStorageTriggerEvent, ConstructFactory<ResourceProvider<FunctionResources>>>>;
};

// @public (undocumented)
export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload';

// @public (undocumented)
export type AmplifyStorageTriggerHandlers = Partial<Record<AmplifyStorageTriggerEvent, IFunction>>;

// @public
export const defineStorage: (props: AmplifyStorageProps) => ConstructFactory<ResourceProvider<StorageResources>>;

Expand Down
34 changes: 31 additions & 3 deletions packages/backend-storage/src/construct.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Construct } from 'constructs';
import { Bucket, BucketProps, IBucket } from 'aws-cdk-lib/aws-s3';
import { Bucket, BucketProps, EventType, IBucket } from 'aws-cdk-lib/aws-s3';
import {
AmplifyFunction,

Check failure on line 4 in packages/backend-storage/src/construct.ts

View workflow job for this annotation

GitHub Actions / lint

'AmplifyFunction' is defined but never used
BackendOutputStorageStrategy,
ConstructFactory,
FunctionResources,
ResourceProvider,
} from '@aws-amplify/plugin-types';
import {
Expand All @@ -14,6 +17,13 @@
StackMetadataBackendOutputStorageStrategy,
} from '@aws-amplify/backend-output-storage';
import { fileURLToPath } from 'url';
import {
FunctionInstanceProvider,
addEventSource,

Check failure on line 22 in packages/backend-storage/src/construct.ts

View workflow job for this annotation

GitHub Actions / lint

'addEventSource' is defined but never used
} from './function_instance_provider.js';
Fixed Show fixed Hide fixed
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { AmplifyStorageTriggerEvent } from './factory.js';
import { S3EventSource } from 'aws-cdk-lib/aws-lambda-event-sources';

// Be very careful editing this value. It is the string that is used to attribute stacks to Amplify Storage in BI metrics
const storageStackType = 'storage-S3';
Expand All @@ -22,6 +32,12 @@
name: string;
versioned?: boolean;
outputStorageStrategy?: BackendOutputStorageStrategy<StorageOutput>;
triggers?: Partial<
Record<
AmplifyStorageTriggerEvent,
ConstructFactory<ResourceProvider<FunctionResources>>
>
>;
};

export type StorageResources = {
Expand All @@ -38,6 +54,7 @@
implements ResourceProvider<StorageResources>
{
readonly resources: StorageResources;
private readonly functionInstanceProvider: FunctionInstanceProvider;
/**
* Create a new AmplifyStorage instance
*/
Expand All @@ -47,11 +64,11 @@
const bucketProps: BucketProps = {
versioned: props.versioned || false,
};
const bucket = new Bucket(this, 'Bucket', bucketProps);

this.resources = {
bucket: new Bucket(this, 'Bucket', bucketProps),
bucket,
};

this.storeOutput(props.outputStorageStrategy);

new AttributionMetadataStorage().storeAttributionMetadata(
Expand All @@ -61,6 +78,17 @@
);
}

/**
* Attach a Lambda function trigger handler to the S3 events
* @param events - list of S3 events that will trigger the handler
* @param handler - The function that will handle the event
*/
addTrigger = (events: EventType[], handler: IFunction): void => {
handler.addEventSource(
new S3EventSource(this.resources.bucket as Bucket, { events })
bombguy marked this conversation as resolved.
Show resolved Hide resolved
);
};

/**
* Store storage outputs using provided strategy
*/
Expand Down
43 changes: 37 additions & 6 deletions packages/backend-storage/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,24 @@ import {
StorageResources,
} from './construct.js';
import { AmplifyUserError } from '@aws-amplify/platform-core';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import {
FunctionInstanceProvider,
buildConstructFactoryFunctionInstanceProvider,
} from './function_instance_provider.js';
import { EventType } from 'aws-cdk-lib/aws-s3';

export type AmplifyStorageFactoryProps = Omit<
AmplifyStorageProps,
'outputStorageStrategy'
>;

export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload';

export type AmplifyStorageTriggerHandlers = Partial<
Record<AmplifyStorageTriggerEvent, IFunction>
>;

/**
* Singleton factory for a Storage bucket that can be used in `resource.ts` files
*/
Expand All @@ -39,11 +51,9 @@ class AmplifyStorageFactory
/**
* Get a singleton instance of the Bucket
*/
getInstance = ({
constructContainer,
outputStorageStrategy,
importPathVerifier,
}: ConstructFactoryGetInstanceProps): AmplifyStorage => {
getInstance = (props: ConstructFactoryGetInstanceProps): AmplifyStorage => {
const { constructContainer, outputStorageStrategy, importPathVerifier } =
props;
importPathVerifier?.verify(
this.importStack,
path.join('amplify', 'storage', 'resource'),
Expand All @@ -53,6 +63,7 @@ class AmplifyStorageFactory
if (!this.generator) {
this.generator = new AmplifyStorageGenerator(
this.props,
buildConstructFactoryFunctionInstanceProvider(props),
outputStorageStrategy
);
}
Expand All @@ -76,14 +87,34 @@ class AmplifyStorageGenerator implements ConstructContainerEntryGenerator {

constructor(
private readonly props: AmplifyStorageProps,
private readonly functionInstanceProvider: FunctionInstanceProvider,
private readonly outputStorageStrategy: BackendOutputStorageStrategy<BackendOutputEntry>
) {}

generateContainerEntry = (scope: Construct) => {
return new AmplifyStorage(scope, `${this.props.name}`, {
const storageConstruct = new AmplifyStorage(scope, `${this.props.name}`, {
...this.props,
outputStorageStrategy: this.outputStorageStrategy,
});

Object.entries(this.props.triggers || {}).forEach(
([triggerEvent, handlerFactory]) => {
const events = [];
const handler = this.functionInstanceProvider.provide(handlerFactory);
// triggerEvent is converted string from Object.entries
switch (triggerEvent as AmplifyStorageTriggerEvent) {
case 'onDelete':
events.push(EventType.OBJECT_REMOVED);
break;
case 'onUpload':
events.push(EventType.OBJECT_CREATED);
break;
}
storageConstruct.addTrigger(events, handler);
}
);

return storageConstruct;
};
}

Expand Down
36 changes: 36 additions & 0 deletions packages/backend-storage/src/function_instance_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
AmplifyFunction,
ConstructFactory,
ConstructFactoryGetInstanceProps,
} from '@aws-amplify/plugin-types';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import {
S3EventSource,
S3EventSourceProps,
} from 'aws-cdk-lib/aws-lambda-event-sources';
import { Bucket } from 'aws-cdk-lib/aws-s3';

export type FunctionInstanceProvider = {
provide: (func: ConstructFactory<AmplifyFunction>) => IFunction;
};

/**
* Build a function instance provider using the construct factory.
*/
export const buildConstructFactoryFunctionInstanceProvider = (
props: ConstructFactoryGetInstanceProps
) => ({
provide: (func: ConstructFactory<AmplifyFunction>): IFunction =>
bombguy marked this conversation as resolved.
Show resolved Hide resolved
func.getInstance(props).resources.lambda,
});

/**
* Add s3 event source for lambda function.
*/
export const addEventSource = (
bucket: Bucket,
lambda: IFunction,
eventSourceProp: S3EventSourceProps
) => {
lambda.addEventSource(new S3EventSource(bucket, eventSourceProp));
};
12 changes: 12 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { defineFunction } from '@aws-amplify/backend-function';
import { defineStorage } from '@aws-amplify/backend-storage';

export { defineBackend } from './backend_factory.js';
export * from './backend.js';
export * from './secret.js';
Expand All @@ -16,3 +19,12 @@ export { defineStorage } from '@aws-amplify/backend-storage';

// function
export { defineFunction } from '@aws-amplify/backend-function';

const onDeleteHandler = defineFunction();

defineStorage({
name: 'myStorage',
triggers: {
onDelete: onDeleteHandler,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import { it } from 'node:test';
import { dataWithoutAuth } from '../test-projects/standalone-data-auth-modes/amplify/test_factories.js';
import { dataWithoutAuthNoAuthMode } from '../test-projects/standalone-data-sandbox-mode/amplify/test_factories.js';
import assert from 'node:assert';

Check failure on line 9 in packages/integration-tests/src/test-in-memory/integration_tests.test.ts

View workflow job for this annotation

GitHub Actions / lint

'assert' is defined but never used
Fixed Show fixed Hide fixed
import { Match } from 'aws-cdk-lib/assertions';

Check failure on line 10 in packages/integration-tests/src/test-in-memory/integration_tests.test.ts

View workflow job for this annotation

GitHub Actions / lint

'Match' is defined but never used
Fixed Show fixed Hide fixed

/**
* This test suite is meant to provide a fast feedback loop to sanity check that different feature verticals are working properly together.
Expand Down Expand Up @@ -41,6 +43,23 @@
// eslint-disable-next-line spellcheck/spell-checker
'testNameBucketB4152AD5',
]);

/* eslint-disable spellcheck/spell-checker */
templates.storage.hasResource('Custom::S3BucketNotifications', {
DependsOn: [
'testNameBucketAllowBucketNotificationsToamplifytestAppIdtestBranchNamebranch7d6f6c854afunctiononDeletelambda572CB9D7EA473960',
'testNameBucketAllowBucketNotificationsToamplifytestAppIdtestBranchNamebranch7d6f6c854afunctiononUploadlambda74F01BD6AFF08959',
],
});

templates.myFunc.hasResource('AWS::Lambda::Function', {
DependsOn: ['onDeletelambdaServiceRole3B882F08'],
bombguy marked this conversation as resolved.
Show resolved Hide resolved
});

templates.myFunc.hasResource('AWS::Lambda::Function', {
DependsOn: ['onUploadlambdaServiceRoleDBC7D933'],
});
/* eslint-enable spellcheck/spell-checker */
});

void it('data without auth with lambda auth mode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export const node16Func = defineFunction({
},
runtime: 16
})


export const onDelete = defineFunction({ 'name': 'onDelete', entry: './func-src/handler.ts' });
export const onUpload = defineFunction({ 'name': 'onUpload', entry: './func-src/handler.ts' });
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { defineStorage } from '@aws-amplify/backend';
import { onDelete, onUpload } from '../function.js';

export const storage = defineStorage({name: 'testName'});
export const storage = defineStorage({
name: 'testName',
triggers: {
onDelete,
onUpload,
}
});
Loading