Skip to content

Commit

Permalink
feat(adapters): added support to api gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
H4ad committed Feb 13, 2022
1 parent 744d73f commit 6726c0e
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 0 deletions.
183 changes: 183 additions & 0 deletions src/v2/adapters/aws/api-gateway-v1.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//#region Imports

import { APIGatewayProxyResult, Context } from 'aws-lambda';
import { APIGatewayProxyEvent } from 'aws-lambda/trigger/api-gateway-proxy';
import {
AdapterContract,
AdapterRequest,
GetResponseAdapterProps,
OnErrorProps,
} from '../../contracts';
import {
getEventBodyAsBuffer,
getFlattenedHeadersMap,
getMultiValueHeadersMap,
getPathWithQueryStringParams,
} from '../../core';

//#endregion

/**
* The adapter to handle requests from AWS Api Gateway V1
*
* As per {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html know issues}, we throw an exception when you send the `transfer-encoding=chunked`, currently, API Gateway doesn't support chunked transfer.
*
* @note This adapter is not fully compatible with @vendia/serverless-express, on @vendia they filter `transfer-encoding=chunked` but we throw an exception.
*
* @example```typescript
* const stripBasePath = '/any/custom/base/path'; // default ''
* const adapter = new ApiGatewayV1Adapter(stripBasePath);
* ```
*
* {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html Event Reference}
*/
export class ApiGatewayV1Adapter
implements
AdapterContract<APIGatewayProxyEvent, Context, APIGatewayProxyResult>
{
//#region Constructor

/**
* Default constructor
*
* @param stripBasePath Strip base path for custom domains
*/
constructor(protected readonly stripBasePath: string = '') {}

//#endregion

//#region Public Methods

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

/**
* @inheritDoc
*/
public canHandle(event: unknown): event is APIGatewayProxyEvent {
const apiGatewayEvent = event as Partial<APIGatewayProxyEvent> & {
version?: string;
};

return !!(
apiGatewayEvent?.requestContext && apiGatewayEvent.version === '1.0'
);
}

/**
* @inheritDoc
*/
public getRequest(event: APIGatewayProxyEvent): AdapterRequest {
const method = event.httpMethod;
const path = this.getPathFromEvent(event);

const headers = event.multiValueHeaders
? getFlattenedHeadersMap(event.multiValueHeaders, ',', true)
: event.headers || {};

let body: Buffer | undefined;

if (event.body) {
const [bufferBody, contentLength] = getEventBodyAsBuffer(
event.body,
event.isBase64Encoded
);

body = bufferBody;
headers['content-length'] = String(contentLength);
}

const remoteAddress =
(event &&
event.requestContext &&
event.requestContext.identity &&
event.requestContext.identity.sourceIp) ||
'';

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

/**
* @inheritDoc
*/
public getResponse({
headers: responseHeaders,
body,
isBase64Encoded,
statusCode,
response,
}: GetResponseAdapterProps<APIGatewayProxyEvent>): APIGatewayProxyResult {
const multiValueHeaders = getMultiValueHeadersMap(responseHeaders);

const transferEncodingHeader: string[] =
multiValueHeaders['transfer-encoding'];

if (
(transferEncodingHeader && transferEncodingHeader.includes('chunked')) ||
response?.chunkedEncoding
) {
throw new Error('chunked encoding is not supported by API Gateway');
}

return {
statusCode,
body,
multiValueHeaders,
isBase64Encoded,
};
}

/**
* @inheritDoc
*/
public onErrorWhileForwarding({
error,
resolver,
respondWithErrors,
event,
log,
}: OnErrorProps<APIGatewayProxyEvent, APIGatewayProxyResult>): void {
const body = respondWithErrors ? error.stack : '';
const errorResponse = this.getResponse({
event,
statusCode: 500,
body: body || '',
headers: {},
isBase64Encoded: false,
log,
});

resolver.succeed(errorResponse);
}

//#endregion

//#region Protected Methods

/**
* Get path from event with query strings
*
* @param event The event sent by serverless
*/
protected getPathFromEvent(event: APIGatewayProxyEvent): string {
// NOTE: Strip base path for custom domains
const replaceRegex = new RegExp(`^${this.stripBasePath}`);
const path = event.path.replace(replaceRegex, '');

const queryParams = event.multiValueQueryStringParameters;

return getPathWithQueryStringParams(path, queryParams || {});
}

//#endregion
}
189 changes: 189 additions & 0 deletions src/v2/adapters/aws/api-gateway-v2.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//#region Imports

import { APIGatewayProxyEventV2, Context } from 'aws-lambda';
import { APIGatewayProxyStructuredResultV2 } from 'aws-lambda/trigger/api-gateway-proxy';
import {
AdapterContract,
AdapterRequest,
GetResponseAdapterProps,
OnErrorProps,
} from '../../contracts';
import {
getEventBodyAsBuffer,
getFlattenedHeadersMap,
getPathWithQueryStringParams,
} from '../../core';

//#endregion

/**
* The adapter to handle requests from AWS Api Gateway V2
*
* As per {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html know issues}, we throw an exception when you send the `transfer-encoding=chunked`, currently, API Gateway doesn't support chunked transfer.
*
* @example```typescript
* const stripBasePath = '/any/custom/base/path'; // default ''
* const adapter = new ApiGatewayV2Adapter(stripBasePath);
* ```
*
* {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html Event Reference}
*/
export class ApiGatewayV2Adapter
implements
AdapterContract<
APIGatewayProxyEventV2,
Context,
APIGatewayProxyStructuredResultV2
>
{
//#region Constructor

/**
* Default constructor
*
* @param stripBasePath Strip base path for custom domains
*/
constructor(protected readonly stripBasePath: string = '') {}

//#endregion

//#region Public Methods

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

/**
* @inheritDoc
*/
public canHandle(event: unknown): event is APIGatewayProxyEventV2 {
const apiGatewayEvent = event as Partial<APIGatewayProxyEventV2> & {
version?: string;
};

return !!(
apiGatewayEvent?.requestContext && apiGatewayEvent.version === '2.0'
);
}

/**
* @inheritDoc
*/
public getRequest(event: APIGatewayProxyEventV2): AdapterRequest {
const method = event.requestContext.http.method;
const path = this.getPathFromEvent(event);

const headers = getFlattenedHeadersMap(event.headers, ',', true);

if (event.cookies) {
headers.cookie = event.cookies.join('; ');
}

let body: Buffer | undefined;

if (event.body) {
const [bufferBody, contentLength] = getEventBodyAsBuffer(
event.body,
event.isBase64Encoded
);

body = bufferBody;
headers['content-length'] = String(contentLength);
}

const remoteAddress = event.requestContext.http.sourceIp;

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

/**
* @inheritDoc
*/
public getResponse({
headers: responseHeaders,
body,
isBase64Encoded,
statusCode,
response,
}: GetResponseAdapterProps<APIGatewayProxyEventV2>): APIGatewayProxyStructuredResultV2 {
const headers = getFlattenedHeadersMap(responseHeaders);

if (
headers['transfer-encoding'] === 'chunked' ||
response?.chunkedEncoding
) {
throw new Error('chunked encoding is not supported by API Gateway');
}

const cookies = headers['set-cookie'];

if (cookies) {
headers.cookies = cookies;

delete headers['set-cookie'];
}

return {
statusCode,
body,
headers,
isBase64Encoded,
};
}

/**
* @inheritDoc
*/
public onErrorWhileForwarding({
error,
resolver,
respondWithErrors,
event,
log,
}: OnErrorProps<
APIGatewayProxyEventV2,
APIGatewayProxyStructuredResultV2
>): void {
const body = respondWithErrors ? error.stack : '';
const errorResponse = this.getResponse({
event,
statusCode: 500,
body: body || '',
headers: {},
isBase64Encoded: false,
log,
});

resolver.succeed(errorResponse);
}

//#endregion

//#region Protected Methods

/**
* Get path from event with query strings
*
* @param event The event sent by serverless
*/
protected getPathFromEvent(event: APIGatewayProxyEventV2): string {
// NOTE: Strip base path for custom domains
const replaceRegex = new RegExp(`^${this.stripBasePath}`);
const path = event.rawPath.replace(replaceRegex, '');

const queryParams = event.rawQueryString;

return getPathWithQueryStringParams(path, queryParams || {});
}

//#endregion
}
2 changes: 2 additions & 0 deletions src/v2/adapters/aws/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './alb.adapter';
export * from './api-gateway-v1.adapter';
export * from './api-gateway-v2.adapter';
export * from './dynamodb.adapter';
export * from './event-bridge.adapter';
export * from './lambda-edge.adapter';
Expand Down

0 comments on commit 6726c0e

Please sign in to comment.