Skip to content

Commit

Permalink
Merge pull request #49 from H4ad/feature/s3
Browse files Browse the repository at this point in the history
feat(s3): added s3 adapter to aws
  • Loading branch information
H4ad authored Sep 7, 2022
2 parents bdb2755 + 1c0ceb3 commit 856de8e
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/adapters/aws/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from './api-gateway-v2.adapter';
export * from './dynamodb.adapter';
export * from './event-bridge.adapter';
export * from './lambda-edge.adapter';
export * from './s3.adapter';
export * from './sns.adapter';
export * from './sqs.adapter';
133 changes: 133 additions & 0 deletions src/adapters/aws/s3.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//#region Imports

import type { Context, S3Event } from 'aws-lambda';
import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts';
import {
EmptyResponse,
IEmptyResponse,
getDefaultIfUndefined,
getEventBodyAsBuffer,
} from '../../core';

//#endregion

/**
* The options to customize the {@link S3Adapter}
*
* @breadcrumb Adapters / AWS / S3Adapter
* @public
*/
export interface S3AdapterOptions {
/**
* The path that will be used to create a request to be forwarded to the framework.
*
* @defaultValue /s3
*/
s3ForwardPath?: string;

/**
* The http method that will be used to create a request to be forwarded to the framework.
*
* @defaultValue POST
*/
s3ForwardMethod?: string;
}

/**
* The adapter to handle requests from AWS S3.
*
* The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error.
*
* {@link https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html | Event Reference}
*
* @example
* ```typescript
* const s3ForwardPath = '/your/route/s3'; // default /s3
* const s3ForwardMethod = 'POST'; // default POST
* const adapter = new S3Adapter({ s3ForwardPath, s3ForwardMethod });
* ```
*
* @breadcrumb Adapters / AWS / S3Adapter
* @public
*/
export class S3Adapter
implements AdapterContract<S3Event, Context, IEmptyResponse>
{
//#region Constructor

/**
* Default constructor
*
* @param options - The options to customize the {@link S3Adapter}
*/
constructor(protected readonly options?: S3AdapterOptions) {}

//#endregion

//#region Public Methods

/**
* {@inheritDoc}
*/
public getAdapterName(): string {
return S3Adapter.name;
}

/**
* {@inheritDoc}
*/
public canHandle(event: unknown): event is S3Event {
const s3Event = event as Partial<S3Event>;

if (!Array.isArray(s3Event?.Records)) return false;

const eventSource = s3Event.Records[0]?.eventSource;

return eventSource === 'aws:s3';
}

/**
* {@inheritDoc}
*/
public getRequest(event: S3Event): AdapterRequest {
const path = getDefaultIfUndefined(this.options?.s3ForwardPath, '/s3');
const method = getDefaultIfUndefined(this.options?.s3ForwardMethod, 'POST');

const [body, contentLength] = getEventBodyAsBuffer(
JSON.stringify(event),
false,
);

const headers = {
host: 's3.amazonaws.com',
'content-type': 'application/json',
'content-length': String(contentLength),
};

return {
method,
headers,
body,
path,
};
}

/**
* {@inheritDoc}
*/
public getResponse(): IEmptyResponse {
return EmptyResponse;
}

/**
* {@inheritDoc}
*/
public onErrorWhileForwarding({
error,
delegatedResolver,
}: OnErrorProps<S3Event, IEmptyResponse>): void {
delegatedResolver.fail(error);
}

//#endregion
}
129 changes: 129 additions & 0 deletions test/adapters/aws/s3.adapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { S3Event } from 'aws-lambda';
import {
DelegatedResolver,
EmptyResponse,
IEmptyResponse,
ILogger,
getEventBodyAsBuffer,
} from '../../../src';
import { S3Adapter } from '../../../src/adapters/aws';
import { createCanHandleTestsForAdapter } from '../utils/can-handle';
import { createS3Event } from './utils/s3';

describe(S3Adapter.name, () => {
let adapter!: S3Adapter;

beforeEach(() => {
adapter = new S3Adapter();
});

describe('getAdapterName', () => {
it('should be the same name of the class', () => {
expect(adapter.getAdapterName()).toBe(S3Adapter.name);
});
});

createCanHandleTestsForAdapter(() => new S3Adapter(), undefined);

describe('getRequest', () => {
it('should return the correct mapping for the request', () => {
const event = createS3Event();

const result = adapter.getRequest(event);

expect(result.method).toBe('POST');
expect(result.path).toBe('/s3');
expect(result.headers).toHaveProperty('host', 's3.amazonaws.com');
expect(result.headers).toHaveProperty('content-type', 'application/json');

const [bodyBuffer, contentLength] = getEventBodyAsBuffer(
JSON.stringify(event),
false,
);

expect(result.body).toBeInstanceOf(Buffer);
expect(result.body).toStrictEqual(bodyBuffer);

expect(result.headers).toHaveProperty(
'content-length',
String(contentLength),
);
});

it('should return the correct mapping for the request with custom path and method', () => {
const event = createS3Event();

const method = 'PUT';
const path = '/custom/s3';

const customAdapter = new S3Adapter({
s3ForwardMethod: method,
s3ForwardPath: path,
});

const result = customAdapter.getRequest(event);

expect(result.method).toBe(method);
expect(result.path).toBe(path);
expect(result.headers).toHaveProperty('host', 's3.amazonaws.com');
expect(result.headers).toHaveProperty('content-type', 'application/json');

const [bodyBuffer, contentLength] = getEventBodyAsBuffer(
JSON.stringify(event),
false,
);

expect(result.body).toBeInstanceOf(Buffer);
expect(result.body).toStrictEqual(bodyBuffer);

expect(result.headers).toHaveProperty(
'content-length',
String(contentLength),
);
});
});

describe('getResponse', () => {
it('should return the correct mapping for the response', () => {
const result = adapter.getResponse();

expect(result).toBe(EmptyResponse);
});
});

describe('onErrorWhileForwarding', () => {
it('should resolver just call fail without get response', () => {
const event = createS3Event();

const error = new Error('fail because I need to test.');
const resolver: DelegatedResolver<S3Event> = {
fail: jest.fn(),
succeed: jest.fn(),
};

const oldGetResponse = adapter.getResponse.bind(adapter);

let getResponseResult: IEmptyResponse;

adapter.getResponse = jest.fn(() => {
getResponseResult = oldGetResponse();

return getResponseResult;
});

adapter.onErrorWhileForwarding({
event,
error,
delegatedResolver: resolver,
log: {} as ILogger,
respondWithErrors: false,
});

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(adapter.getResponse).toHaveBeenCalledTimes(0);

expect(resolver.fail).toHaveBeenCalledTimes(1);
expect(resolver.succeed).toHaveBeenCalledTimes(0);
});
});
});
3 changes: 3 additions & 0 deletions test/adapters/aws/utils/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DynamoDBAdapter,
EventBridgeAdapter,
LambdaEdgeAdapter,
S3Adapter,
SNSAdapter,
SQSAdapter,
} from '../../../../src/adapters/aws';
Expand All @@ -23,6 +24,7 @@ import {
createLambdaEdgeOriginEvent,
createLambdaEdgeViewerEvent,
} from './lambda-edge';
import { createS3Event } from './s3';
import { createSNSEvent } from './sns';
import { createSQSEvent } from './sqs';

Expand Down Expand Up @@ -84,4 +86,5 @@ export const allAWSEvents: Array<[string, any]> = [
base64: Buffer.from('batata', 'utf-8').toString('base64'),
}),
],
[S3Adapter.name, createS3Event()],
];
46 changes: 46 additions & 0 deletions test/adapters/aws/utils/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { S3Event } from 'aws-lambda';

/**
* Sample event from {@link https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html}
*/
export function createS3Event(): S3Event {
return {
Records: [
{
eventVersion: '2.1',
eventSource: 'aws:s3',
awsRegion: 'us-east-2',
eventTime: '2019-09-03T19:37:27.192Z',
eventName: 'ObjectCreated:Put',
userIdentity: {
principalId: 'AWS:AIDAINPONIXQXHT3IKHL2',
},
requestParameters: {
sourceIPAddress: '205.255.255.255',
},
responseElements: {
'x-amz-request-id': 'D82B88E5F771F645',
'x-amz-id-2':
'vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=',
},
s3: {
s3SchemaVersion: '1.0',
configurationId: '828aa6fc-f7b5-4305-8584-487c791949c1',
bucket: {
name: 'DOC-EXAMPLE-BUCKET',
ownerIdentity: {
principalId: 'A3I5XTEXAMAI3E',
},
arn: 'arn:aws:s3:::lambda-artifacts-deafc19498e3f2df',
},
object: {
key: 'b21b84d653bb07b05b1e6b33684dc11b',
size: 1305107,
eTag: 'b21b84d653bb07b05b1e6b33684dc11b',
sequencer: '0C0F6F405D6ED209E1',
},
},
},
],
};
}
Loading

0 comments on commit 856de8e

Please sign in to comment.