diff --git a/src/adapters/aws/base/aws-simple-adapter.ts b/src/adapters/aws/base/aws-simple-adapter.ts new file mode 100644 index 00000000..095e53f4 --- /dev/null +++ b/src/adapters/aws/base/aws-simple-adapter.ts @@ -0,0 +1,179 @@ +//#region Imports + +import type { Context, SQSBatchItemFailure } from 'aws-lambda'; +import { + AdapterContract, + AdapterRequest, + GetResponseAdapterProps, + OnErrorProps, +} from '../../../contracts/index'; +import { + EmptyResponse, + IEmptyResponse, + getEventBodyAsBuffer, +} from '../../../core/index'; + +//#endregion + +/** + * The options to customize the {@link AwsSimpleAdapter} + * + * @breadcrumb Adapters / AWS / AWS Simple Adapter + * @public + */ +export interface AWSSimpleAdapterOptions { + /** + * The path that will be used to create a request to be forwarded to the framework. + */ + forwardPath: string; + + /** + * The http method that will be used to create a request to be forwarded to the framework. + */ + forwardMethod: string; + + /** + * The AWS Service host that will be injected inside headers to developer being able to validate if request originate from the library. + */ + host: string; + + /** + * Tells if this adapter should support batch item failures. + */ + batch?: true | false; +} + +/** + * The batch item failure response expected from the API server + * + * @breadcrumb Adapters / AWS / AWS Simple Adapter + * @public + */ +export type BatchItemFailureResponse = SQSBatchItemFailure; + +/** + * The possible options of response for {@link AwsSimpleAdapter} + * + * @breadcrumb Adapters / AWS / AWS Simple Adapter + * @public + */ +export type AWSSimpleAdapterResponseType = + | BatchItemFailureResponse + | IEmptyResponse; + +/** + * The abstract adapter to use to implement other simple AWS adapters + * + * @breadcrumb Adapters / AWS / AWS Simple Adapter + * @public + */ +export abstract class AwsSimpleAdapter + implements AdapterContract +{ + //#region Constructor + + /** + * Default constructor + * + * @param options - The options to customize the {@link AwsSimpleAdapter} + */ + constructor(protected readonly options: AWSSimpleAdapterOptions) {} + + //#endregion + + //#region Public Methods + + /** + * {@inheritDoc} + */ + public getAdapterName(): string { + throw new Error('not implemented.'); + } + + /** + * {@inheritDoc} + */ + public canHandle(event: unknown): event is TEvent { + throw new Error('not implemented.'); + } + + /** + * {@inheritDoc} + */ + public getRequest(event: TEvent): AdapterRequest { + const path = this.options.forwardPath; + const method = this.options.forwardMethod; + + const [body, contentLength] = getEventBodyAsBuffer( + JSON.stringify(event), + false, + ); + + const headers = { + host: this.options.host, + 'content-type': 'application/json', + 'content-length': String(contentLength), + }; + + return { + method, + headers, + body, + path, + }; + } + + /** + * {@inheritDoc} + */ + public getResponse({ + body, + headers, + isBase64Encoded, + event, + statusCode, + }: GetResponseAdapterProps): AWSSimpleAdapterResponseType { + if (this.hasInvalidStatusCode(statusCode)) { + throw new Error( + JSON.stringify({ body, headers, isBase64Encoded, event, statusCode }), + ); + } + + if (!this.options.batch) return EmptyResponse; + + if (isBase64Encoded) { + throw new Error( + 'SERVERLESS_ADAPTER: The response could not be base64 encoded when you set batch: true, the response should be a JSON.', + ); + } + + if (!body) return EmptyResponse; + + return JSON.parse(body); + } + + /** + * {@inheritDoc} + */ + public onErrorWhileForwarding({ + error, + delegatedResolver, + }: OnErrorProps): void { + delegatedResolver.fail(error); + } + + //#endregion + + //#region Protected Methods + + /** + * Check if the status code is invalid + * + * @param statusCode - The status code + */ + protected hasInvalidStatusCode(statusCode: number): boolean { + return statusCode < 200 || statusCode >= 400; + } + + //#endregion +} diff --git a/src/adapters/aws/base/index.ts b/src/adapters/aws/base/index.ts new file mode 100644 index 00000000..791a86a6 --- /dev/null +++ b/src/adapters/aws/base/index.ts @@ -0,0 +1 @@ +export * from './aws-simple-adapter'; diff --git a/src/adapters/aws/dynamodb.adapter.ts b/src/adapters/aws/dynamodb.adapter.ts index b89a9d2f..3cae84b6 100644 --- a/src/adapters/aws/dynamodb.adapter.ts +++ b/src/adapters/aws/dynamodb.adapter.ts @@ -1,13 +1,8 @@ //#region Imports -import type { Context, DynamoDBStreamEvent } from 'aws-lambda'; -import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts'; -import { - EmptyResponse, - IEmptyResponse, - getDefaultIfUndefined, - getEventBodyAsBuffer, -} from '../../core'; +import type { DynamoDBStreamEvent } from 'aws-lambda'; +import { getDefaultIfUndefined } from '../../core'; +import { AWSSimpleAdapterOptions, AwsSimpleAdapter } from './base/index'; //#endregion @@ -17,7 +12,8 @@ import { * @breadcrumb Adapters / AWS / DynamoDBAdapter * @public */ -export interface DynamoDBAdapterOptions { +export interface DynamoDBAdapterOptions + extends Pick { /** * The path that will be used to create a request to be forwarded to the framework. * @@ -50,9 +46,7 @@ export interface DynamoDBAdapterOptions { * @breadcrumb Adapters / AWS / DynamoDBAdapter * @public */ -export class DynamoDBAdapter - implements AdapterContract -{ +export class DynamoDBAdapter extends AwsSimpleAdapter { //#region Constructor /** @@ -60,7 +54,20 @@ export class DynamoDBAdapter * * @param options - The options to customize the {@link DynamoDBAdapter} */ - constructor(protected readonly options?: DynamoDBAdapterOptions) {} + constructor(options?: DynamoDBAdapterOptions) { + super({ + forwardPath: getDefaultIfUndefined( + options?.dynamoDBForwardPath, + '/dynamo', + ), + forwardMethod: getDefaultIfUndefined( + options?.dynamoDBForwardMethod, + 'POST', + ), + batch: options?.batch, + host: 'dynamodb.amazonaws.com', + }); + } //#endregion @@ -86,54 +93,5 @@ export class DynamoDBAdapter return eventSource === 'aws:dynamodb'; } - /** - * {@inheritDoc} - */ - public getRequest(event: DynamoDBStreamEvent): AdapterRequest { - const path = getDefaultIfUndefined( - this.options?.dynamoDBForwardPath, - '/dynamo', - ); - const method = getDefaultIfUndefined( - this.options?.dynamoDBForwardMethod, - 'POST', - ); - - const [body, contentLength] = getEventBodyAsBuffer( - JSON.stringify(event), - false, - ); - - const headers = { - host: 'dynamodb.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): void { - delegatedResolver.fail(error); - } - //#endregion } diff --git a/src/adapters/aws/event-bridge.adapter.ts b/src/adapters/aws/event-bridge.adapter.ts index a11320f3..b8a3d892 100644 --- a/src/adapters/aws/event-bridge.adapter.ts +++ b/src/adapters/aws/event-bridge.adapter.ts @@ -1,13 +1,8 @@ //#region Imports -import type { Context, EventBridgeEvent } from 'aws-lambda'; -import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts'; -import { - EmptyResponse, - IEmptyResponse, - getDefaultIfUndefined, - getEventBodyAsBuffer, -} from '../../core'; +import type { EventBridgeEvent } from 'aws-lambda'; +import { getDefaultIfUndefined } from '../../core'; +import { AwsSimpleAdapter } from './base/index'; //#endregion @@ -58,9 +53,7 @@ export type EventBridgeEventAll = EventBridgeEvent; * @breadcrumb Adapters / AWS / EventBridgeAdapter * @public */ -export class EventBridgeAdapter - implements AdapterContract -{ +export class EventBridgeAdapter extends AwsSimpleAdapter { //#region Constructor /** @@ -68,7 +61,20 @@ export class EventBridgeAdapter * * @param options - The options to customize the {@link EventBridgeAdapter} */ - constructor(protected readonly options?: EventBridgeOptions) {} + constructor(options?: EventBridgeOptions) { + super({ + forwardPath: getDefaultIfUndefined( + options?.eventBridgeForwardPath, + '/eventbridge', + ), + forwardMethod: getDefaultIfUndefined( + options?.eventBridgeForwardMethod, + 'POST', + ), + batch: false, + host: 'events.amazonaws.com', + }); + } //#endregion @@ -106,54 +112,5 @@ export class EventBridgeAdapter ); } - /** - * {@inheritDoc} - */ - public getRequest(event: EventBridgeEventAll): AdapterRequest { - const path = getDefaultIfUndefined( - this.options?.eventBridgeForwardPath, - '/eventbridge', - ); - const method = getDefaultIfUndefined( - this.options?.eventBridgeForwardMethod, - 'POST', - ); - - const [body, contentLength] = getEventBodyAsBuffer( - JSON.stringify(event), - false, - ); - - const headers = { - host: 'events.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): void { - delegatedResolver.fail(error); - } - //#endregion } diff --git a/src/adapters/aws/index.ts b/src/adapters/aws/index.ts index 044cb35a..a694825d 100644 --- a/src/adapters/aws/index.ts +++ b/src/adapters/aws/index.ts @@ -7,3 +7,4 @@ export * from './lambda-edge.adapter'; export * from './s3.adapter'; export * from './sns.adapter'; export * from './sqs.adapter'; +export * from './base'; diff --git a/src/adapters/aws/s3.adapter.ts b/src/adapters/aws/s3.adapter.ts index 137b852e..1ff8b8b2 100644 --- a/src/adapters/aws/s3.adapter.ts +++ b/src/adapters/aws/s3.adapter.ts @@ -1,13 +1,8 @@ //#region Imports -import type { Context, S3Event } from 'aws-lambda'; -import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts'; -import { - EmptyResponse, - IEmptyResponse, - getDefaultIfUndefined, - getEventBodyAsBuffer, -} from '../../core'; +import type { S3Event } from 'aws-lambda'; +import { getDefaultIfUndefined } from '../../core'; +import { AwsSimpleAdapter } from './base/index'; //#endregion @@ -50,17 +45,22 @@ export interface S3AdapterOptions { * @breadcrumb Adapters / AWS / S3Adapter * @public */ -export class S3Adapter - implements AdapterContract -{ +export class S3Adapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * - * @param options - The options to customize the {@link S3Adapter} + * @param options - The options to customize the {@link SNSAdapter} */ - constructor(protected readonly options?: S3AdapterOptions) {} + constructor(options?: S3AdapterOptions) { + super({ + forwardPath: getDefaultIfUndefined(options?.s3ForwardPath, '/s3'), + forwardMethod: getDefaultIfUndefined(options?.s3ForwardMethod, 'POST'), + batch: false, + host: 's3.amazonaws.com', + }); + } //#endregion @@ -86,48 +86,5 @@ export class S3Adapter 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): void { - delegatedResolver.fail(error); - } - //#endregion } diff --git a/src/adapters/aws/sns.adapter.ts b/src/adapters/aws/sns.adapter.ts index 2d44507c..48c1a5ac 100644 --- a/src/adapters/aws/sns.adapter.ts +++ b/src/adapters/aws/sns.adapter.ts @@ -1,13 +1,8 @@ //#region Imports -import type { Context, SNSEvent } from 'aws-lambda'; -import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts'; -import { - EmptyResponse, - IEmptyResponse, - getDefaultIfUndefined, - getEventBodyAsBuffer, -} from '../../core'; +import type { SNSEvent } from 'aws-lambda'; +import { getDefaultIfUndefined } from '../../core'; +import { AwsSimpleAdapter } from './base/index'; //#endregion @@ -50,9 +45,7 @@ export interface SNSAdapterOptions { * @breadcrumb Adapters / AWS / SNSAdapter * @public */ -export class SNSAdapter - implements AdapterContract -{ +export class SNSAdapter extends AwsSimpleAdapter { //#region Constructor /** @@ -60,7 +53,14 @@ export class SNSAdapter * * @param options - The options to customize the {@link SNSAdapter} */ - constructor(protected readonly options?: SNSAdapterOptions) {} + constructor(options?: SNSAdapterOptions) { + super({ + forwardPath: getDefaultIfUndefined(options?.snsForwardPath, '/sns'), + forwardMethod: getDefaultIfUndefined(options?.snsForwardMethod, 'POST'), + batch: false, + host: 'sns.amazonaws.com', + }); + } //#endregion @@ -86,51 +86,5 @@ export class SNSAdapter return eventSource === 'aws:sns'; } - /** - * {@inheritDoc} - */ - public getRequest(event: SNSEvent): AdapterRequest { - const path = getDefaultIfUndefined(this.options?.snsForwardPath, '/sns'); - const method = getDefaultIfUndefined( - this.options?.snsForwardMethod, - 'POST', - ); - - const [body, contentLength] = getEventBodyAsBuffer( - JSON.stringify(event), - false, - ); - - const headers = { - host: 'sns.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): void { - delegatedResolver.fail(error); - } - //#endregion } diff --git a/src/adapters/aws/sqs.adapter.ts b/src/adapters/aws/sqs.adapter.ts index 95960345..3fc898db 100644 --- a/src/adapters/aws/sqs.adapter.ts +++ b/src/adapters/aws/sqs.adapter.ts @@ -1,13 +1,8 @@ //#region Imports -import type { Context, SQSEvent } from 'aws-lambda'; -import { AdapterContract, AdapterRequest, OnErrorProps } from '../../contracts'; -import { - EmptyResponse, - IEmptyResponse, - getDefaultIfUndefined, - getEventBodyAsBuffer, -} from '../../core'; +import type { SQSEvent } from 'aws-lambda'; +import { getDefaultIfUndefined } from '../../core'; +import { AWSSimpleAdapterOptions, AwsSimpleAdapter } from './base/index'; //#endregion @@ -17,7 +12,8 @@ import { * @breadcrumb Adapters / AWS / SQSAdapter * @public */ -export interface SQSAdapterOptions { +export interface SQSAdapterOptions + extends Pick { /** * The path that will be used to create a request to be forwarded to the framework. * @@ -50,17 +46,22 @@ export interface SQSAdapterOptions { * @breadcrumb Adapters / AWS / SQSAdapter * @public */ -export class SQSAdapter - implements AdapterContract -{ +export class SQSAdapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * - * @param options - The options to customize the {@link SQSAdapter} + * @param options - The options to customize the {@link SNSAdapter} */ - constructor(protected readonly options?: SQSAdapterOptions) {} + constructor(options?: SQSAdapterOptions) { + super({ + forwardPath: getDefaultIfUndefined(options?.sqsForwardPath, '/sqs'), + forwardMethod: getDefaultIfUndefined(options?.sqsForwardMethod, 'POST'), + batch: options?.batch, + host: 'sqs.amazonaws.com', + }); + } //#endregion @@ -86,51 +87,5 @@ export class SQSAdapter return eventSource === 'aws:sqs'; } - /** - * {@inheritDoc} - */ - public getRequest(event: SQSEvent): AdapterRequest { - const path = getDefaultIfUndefined(this.options?.sqsForwardPath, '/sqs'); - const method = getDefaultIfUndefined( - this.options?.sqsForwardMethod, - 'POST', - ); - - const [body, contentLength] = getEventBodyAsBuffer( - JSON.stringify(event), - false, - ); - - const headers = { - host: 'sqs.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): void { - delegatedResolver.fail(error); - } - //#endregion } diff --git a/src/frameworks/body-parser/base-body-parser.framework.ts b/src/frameworks/body-parser/base-body-parser.framework.ts index 43f567d5..7d5a37e8 100644 --- a/src/frameworks/body-parser/base-body-parser.framework.ts +++ b/src/frameworks/body-parser/base-body-parser.framework.ts @@ -10,6 +10,9 @@ import { getDefaultIfUndefined } from '../../core'; /** * The options for {@link BaseBodyParserFramework} + * + * @breadcrumb Frameworks / BodyParserFramework + * @public */ export type BodyParserOptions = { /** diff --git a/test/adapters/apollo-server/utils-apollo-mutation.ts b/test/adapters/apollo-server/utils-apollo-mutation.ts index 029b8629..5df424f7 100644 --- a/test/adapters/apollo-server/utils-apollo-mutation.ts +++ b/test/adapters/apollo-server/utils-apollo-mutation.ts @@ -263,10 +263,11 @@ export function runApolloServerTests() { statusCode: serverlessResponse.statusCode, isBase64Encoded: false, }; - const response = mutationAdapter.getResponse(props); + expect(() => mutationAdapter.getResponse(props)).toThrow( + 'Cannot query field', + ); expect(sqsAdapterSpy).toHaveBeenNthCalledWith(1, props); - expect(response).toEqual(EmptyResponse); }); }); @@ -319,7 +320,7 @@ export function runApolloServerTests() { headers: {}, }; - expect(mutation.getResponse(props)).toEqual(EmptyResponse); + expect(() => mutation.getResponse(props)).toThrow('"statusCode":400'); expect(spyedOnError).toHaveBeenNthCalledWith(1, props); }); } diff --git a/test/adapters/aws/aws-simple-adapter.spec.ts b/test/adapters/aws/aws-simple-adapter.spec.ts new file mode 100644 index 00000000..d8814376 --- /dev/null +++ b/test/adapters/aws/aws-simple-adapter.spec.ts @@ -0,0 +1,304 @@ +import { AwsSimpleAdapter } from '../../../src/adapters/aws/index'; +import { + DelegatedResolver, + EmptyResponse, + ILogger, + createDefaultLogger, + getEventBodyAsBuffer, +} from '../../../src/index'; +import { createDynamoDBEvent } from './utils/dynamodb'; +import { createSNSEvent } from './utils/sns'; +import { createSQSEvent } from './utils/sqs'; + +const sampleEvents = [ + createSQSEvent(), + createSNSEvent(), + createDynamoDBEvent(), +]; + +class TestAdapter extends AwsSimpleAdapter {} + +describe(AwsSimpleAdapter.name, () => { + describe('getAdapterName', () => { + it('should throw not implemented error', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.com.com', + }); + + expect(() => adapter.getAdapterName()).toThrow('not implemented'); + }); + }); + + describe('canHandle', () => { + it('should throw not implemented error', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.com.com', + }); + + expect(() => adapter.canHandle(null)).toThrow('not implemented'); + }); + }); + + describe('getRequest', () => { + it('should return the correct mapping for the request', () => { + for (const event of sampleEvents) { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: false, + }); + + const result = adapter.getRequest(event); + + expect(result.method).toBe('POST'); + expect(result.path).toBe('/test'); + expect(result.headers).toHaveProperty('host', 'test.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 = createSQSEvent(); + + const method = 'PUT'; + const path = '/custom/test'; + + const customAdapter = new TestAdapter({ + forwardMethod: method, + forwardPath: path, + host: 'test.amazonaws.com', + }); + + const result = customAdapter.getRequest(event); + + expect(result.method).toBe(method); + expect(result.path).toBe(path); + expect(result.headers).toHaveProperty('host', 'test.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 throw for invalid status', () => { + const options: [status: number, error: boolean][] = [ + [101, true], + [200, false], + [204, false], + [301, false], + [303, false], + [400, true], + [401, true], + [404, true], + [500, true], + [503, true], + ]; + + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: false, + }); + + for (const [status, shouldThrowError] of options) { + if (shouldThrowError) { + expect(() => + adapter.getResponse({ + event: null, + body: JSON.stringify({ ok: true }), + log: createDefaultLogger(), + headers: {}, + statusCode: status, + isBase64Encoded: false, + }), + ).toThrowError(`"statusCode":${status}`); + } else { + expect(() => + adapter.getResponse({ + event: null, + body: JSON.stringify({ ok: true }), + log: createDefaultLogger(), + headers: {}, + statusCode: status, + isBase64Encoded: false, + }), + ).not.toThrowError(`"statusCode":${status}`); + } + } + }); + + describe('batch: false', () => { + it('should not throw when body is base64', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: false, + }); + + expect(() => + adapter.getResponse({ + event: null, + body: JSON.stringify({ ok: true }), + log: createDefaultLogger(), + headers: {}, + statusCode: 200, + isBase64Encoded: true, + }), + ).not.toThrowError('could not be base64 encoded'); + }); + + it('should return the correct mapping for the response', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: false, + }); + + const result = adapter.getResponse({ + event: null, + body: JSON.stringify({ ok: true }), + log: createDefaultLogger(), + headers: {}, + statusCode: 200, + isBase64Encoded: false, + }); + + expect(result).toBe(EmptyResponse); + }); + }); + }); + + describe('batch: true', () => { + it('should throw when body is base64', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: true, + }); + + expect(() => + adapter.getResponse({ + event: null, + body: JSON.stringify({ ok: true }), + log: createDefaultLogger(), + headers: {}, + statusCode: 200, + isBase64Encoded: true, + }), + ).toThrowError('could not be base64 encoded'); + }); + + it('should return the body when response is correct', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: true, + }); + + const body = { ok: true }; + + const response = adapter.getResponse({ + event: null, + body: JSON.stringify(body), + log: createDefaultLogger(), + headers: {}, + statusCode: 200, + isBase64Encoded: false, + }); + + expect(response).toStrictEqual(body); + }); + + it('should return empty when body is also empty', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: true, + }); + + const body = ''; + + const response = adapter.getResponse({ + event: null, + body, + log: createDefaultLogger(), + headers: {}, + statusCode: 200, + isBase64Encoded: false, + }); + + expect(response).toStrictEqual(EmptyResponse); + }); + }); + + describe('onErrorWhileForwarding', () => { + it('should resolver just call fail without get response', () => { + const adapter = new TestAdapter({ + forwardPath: '/test', + forwardMethod: 'POST', + host: 'test.amazonaws.com', + batch: true, + }); + + const error = new Error('fail because I need to test.'); + const resolver: DelegatedResolver = { + fail: jest.fn(), + succeed: jest.fn(), + }; + + adapter.getResponse = jest.fn(); + 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).toHaveBeenCalledWith(error); + expect(resolver.succeed).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/test/adapters/aws/dynamodb.adapter.spec.ts b/test/adapters/aws/dynamodb.adapter.spec.ts index 21695e4a..aeef2bac 100644 --- a/test/adapters/aws/dynamodb.adapter.spec.ts +++ b/test/adapters/aws/dynamodb.adapter.spec.ts @@ -1,11 +1,4 @@ -import type { DynamoDBStreamEvent } from 'aws-lambda'; -import { - DelegatedResolver, - EmptyResponse, - IEmptyResponse, - ILogger, - getEventBodyAsBuffer, -} from '../../../src'; +import { getEventBodyAsBuffer } from '../../../src'; import { DynamoDBAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createDynamoDBEvent } from './utils/dynamodb'; @@ -82,48 +75,4 @@ describe(DynamoDBAdapter.name, () => { ); }); }); - - 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 = createDynamoDBEvent(); - - const error = new Error('fail because I need to test.'); - const resolver: DelegatedResolver = { - 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); - }); - }); }); diff --git a/test/adapters/aws/event-bridge.adapter.spec.ts b/test/adapters/aws/event-bridge.adapter.spec.ts index 9bd2486c..3dc9b02b 100644 --- a/test/adapters/aws/event-bridge.adapter.spec.ts +++ b/test/adapters/aws/event-bridge.adapter.spec.ts @@ -1,19 +1,7 @@ -import { - DelegatedResolver, - EmptyResponse, - IEmptyResponse, - ILogger, - getEventBodyAsBuffer, -} from '../../../src'; -import { - EventBridgeAdapter, - EventBridgeEventAll, -} from '../../../src/adapters/aws'; +import { getEventBodyAsBuffer } from '../../../src'; +import { EventBridgeAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; -import { - createEventBridgeEvent, - createEventBridgeEventSimple, -} from './utils/event-bridge'; +import { createEventBridgeEvent } from './utils/event-bridge'; describe(EventBridgeAdapter.name, () => { let adapter!: EventBridgeAdapter; @@ -87,48 +75,4 @@ describe(EventBridgeAdapter.name, () => { ); }); }); - - 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 = createEventBridgeEventSimple(); - - const error = new Error('fail because I need to test.'); - const resolver: DelegatedResolver = { - 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); - }); - }); }); diff --git a/test/adapters/aws/s3.adapter.spec.ts b/test/adapters/aws/s3.adapter.spec.ts index 014644c4..ff7ccdbd 100644 --- a/test/adapters/aws/s3.adapter.spec.ts +++ b/test/adapters/aws/s3.adapter.spec.ts @@ -1,11 +1,4 @@ -import type { S3Event } from 'aws-lambda'; -import { - DelegatedResolver, - EmptyResponse, - IEmptyResponse, - ILogger, - getEventBodyAsBuffer, -} from '../../../src'; +import { getEventBodyAsBuffer } from '../../../src'; import { S3Adapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createS3Event } from './utils/s3'; @@ -82,48 +75,4 @@ describe(S3Adapter.name, () => { ); }); }); - - 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 = { - 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); - }); - }); }); diff --git a/test/adapters/aws/sns.adapter.spec.ts b/test/adapters/aws/sns.adapter.spec.ts index c522795e..7993a217 100644 --- a/test/adapters/aws/sns.adapter.spec.ts +++ b/test/adapters/aws/sns.adapter.spec.ts @@ -1,11 +1,4 @@ -import type { SNSEvent } from 'aws-lambda'; -import { - DelegatedResolver, - EmptyResponse, - IEmptyResponse, - ILogger, - getEventBodyAsBuffer, -} from '../../../src'; +import { getEventBodyAsBuffer } from '../../../src'; import { SNSAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createSNSEvent } from './utils/sns'; @@ -82,48 +75,4 @@ describe(SNSAdapter.name, () => { ); }); }); - - 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 = createSNSEvent(); - - const error = new Error('fail because I need to test.'); - const resolver: DelegatedResolver = { - 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); - }); - }); }); diff --git a/test/adapters/aws/sqs.adapter.spec.ts b/test/adapters/aws/sqs.adapter.spec.ts index 38fca1bb..6e0035b6 100644 --- a/test/adapters/aws/sqs.adapter.spec.ts +++ b/test/adapters/aws/sqs.adapter.spec.ts @@ -1,11 +1,4 @@ -import type { SQSEvent } from 'aws-lambda'; -import { - DelegatedResolver, - EmptyResponse, - IEmptyResponse, - ILogger, - getEventBodyAsBuffer, -} from '../../../src'; +import { getEventBodyAsBuffer } from '../../../src'; import { SQSAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createSQSEvent } from './utils/sqs'; @@ -82,48 +75,4 @@ describe(SQSAdapter.name, () => { ); }); }); - - 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 = createSQSEvent(); - - const error = new Error('fail because I need to test.'); - const resolver: DelegatedResolver = { - 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); - }); - }); }); diff --git a/www/docs/main/adapters/aws/dynamodb.mdx b/www/docs/main/adapters/aws/dynamodb.mdx index 31de4b4a..ff8ee8d0 100644 --- a/www/docs/main/adapters/aws/dynamodb.mdx +++ b/www/docs/main/adapters/aws/dynamodb.mdx @@ -102,3 +102,39 @@ You **MUST** check if the header `Host` contains the value of `dynamodb.amazonaw Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/dynamo`. + +## What happens when my response status is different from 2xx or 3xx? + +Well, this library will throw an error. +In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. + +When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, +can be sent to dead-letter queue for you to check what happens or try again. + +## Batch Item Failures + +If you enable this batch item failure option, to be able to partially return that some items failed to process, first configure your Adapter: + +```ts +const adapter = new DynamoDBAdapter({ + batch: true, +}); +``` + +And then, just return the following JSON in the route that processes the DynamoDB event. + +```json +{ + "batchItemFailures": [ + { + "itemIdentifier": "id2" + }, + { + "itemIdentifier": "id4" + } + ] +} +``` + +> [Reference](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-batchfailurereporting) + diff --git a/www/docs/main/adapters/aws/event-bridge.mdx b/www/docs/main/adapters/aws/event-bridge.mdx index 86a38579..11926333 100644 --- a/www/docs/main/adapters/aws/event-bridge.mdx +++ b/www/docs/main/adapters/aws/event-bridge.mdx @@ -187,3 +187,11 @@ You **MUST** check if the header `Host` contains the value of `events.amazonaws. Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/eventbridge`. + +## What happens when my response status is different from 2xx or 3xx? + +Well, this library will throw an error. +In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. + +When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, +can be sent to dead-letter queue for you to check what happens or try again. diff --git a/www/docs/main/adapters/aws/s3.mdx b/www/docs/main/adapters/aws/s3.mdx index bf09c7b1..71990cd5 100644 --- a/www/docs/main/adapters/aws/s3.mdx +++ b/www/docs/main/adapters/aws/s3.mdx @@ -110,3 +110,11 @@ You **MUST** check if the header `Host` contains the value of `s3.amazonaws.com` Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/s3`. + +## What happens when my response status is different from 2xx or 3xx? + +Well, this library will throw an error. +In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. + +When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, +can be sent to dead-letter queue for you to check what happens or try again. diff --git a/www/docs/main/adapters/aws/sns.mdx b/www/docs/main/adapters/aws/sns.mdx index 0d24d4be..863e71bf 100644 --- a/www/docs/main/adapters/aws/sns.mdx +++ b/www/docs/main/adapters/aws/sns.mdx @@ -103,3 +103,11 @@ You **MUST** check if the header `Host` contains the value of `sns.amazonaws.com Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/sns`. + +## What happens when my response status is different from 2xx or 3xx? + +Well, this library will throw an error. +In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. + +When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, +can be sent to dead-letter queue for you to check what happens or try again. diff --git a/www/docs/main/adapters/aws/sqs.mdx b/www/docs/main/adapters/aws/sqs.mdx index 18270718..bd1b9ac6 100644 --- a/www/docs/main/adapters/aws/sqs.mdx +++ b/www/docs/main/adapters/aws/sqs.mdx @@ -92,3 +92,39 @@ You **MUST** check if the header `Host` contains the value of `sqs.amazonaws.com Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/sqs`. + +## What happens when my response status is different from 2xx or 3xx? + +Well, this library will throw an error. +In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. + +When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, +can be sent to dead-letter queue for you to check what happens or try again. + +## Batch Item Failures + +If you enable this batch item failure option, to be able to partially return that some items failed to process, first configure your Adapter: + +```ts +const adapter = new SQSAdapter({ + batch: true, +}); +``` + +And then, just return the following JSON in the route that processes the SQS event. + +```json +{ + "batchItemFailures": [ + { + "itemIdentifier": "id2" + }, + { + "itemIdentifier": "id4" + } + ] +} +``` + +> [Reference](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting) +