Skip to content

Commit

Permalink
Issue #108 LambdaUtils dependencies
Browse files Browse the repository at this point in the history
- Considerable changes made while testing in real-world application.
- Updated documentation and examples.
  • Loading branch information
Adam Fanello committed Sep 3, 2021
1 parent 5b9477a commit ec627c9
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 49 deletions.
82 changes: 64 additions & 18 deletions docs/source/lambda_utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ You can extend it with your own middleware.
Middy gives you a great start as a solid middleware framework,
but by itself you are still repeating the middleware registrations
on each handler, its exception handler only works with errors created by the http-errors package,
its Typescript declarations are overly permissive,
and you still have to format your response in the shape required by API Gateway.

``LambadUtils`` takes Middy further and is extendable so that you can add your own middleware
(authentication & authorization, maybe?) on top of it.
``LambdaUtils`` takes Middy further and is extendable so that you can add your own middleware
(ex: authentication & authorization) on top of it.

Used with API Gateway, the included middlewares:
Used with API Gateway v1 (REST API) and v2 (HTTP API), the included middlewares are:

- Set CORS headers.
- Normalize incoming headers to mixed-case
Expand All @@ -30,11 +31,9 @@ Used with API Gateway, the included middlewares:
- Catch http-errors exceptions into proper HTTP responses.
- Catch other exceptions and return as HTTP 500.
- Unique to LambdaUtils!
- Besides providing better feedback to the client, not throwing an exception out of your handler means that your
instance will not be destroyed and suffer a cold start on the next invocation.
- Leverages async syntax.
- Fully leverages Typescript and async syntax.

See `Middy middlewares <https://middy.js.org/#available-middlewares>`_ for details on those.
See `Middy middlewares <https://middy.js.org/#:~:text=available%20middlewares>`_ for details on those.
Not all Middy middlewares are in this implementation, only common ones that are generally useful in all
APIs. You may extend LambdaUtils's ``wrapApiHandler()`` function in your projects,
or use it as an example to write your own, to add more middleware!
Expand All @@ -47,26 +46,36 @@ or use it as an example to write your own, to add more middleware!
Install
^^^^^^^

**To use LambdaUtils v3.x with Middy v1.x.x (latest):**
**To use LambdaUtils v4.x with Middy v2.x.x (latest):**

.. code-block:: shell
npm install @sailplane/lambda-utils@3 @sailplane/logger @middy/core @middy/http-cors @middy/http-event-normalizer @middy/http-header-normalizer @middy/http-json-body-parser
npm install @sailplane/lambda-utils@4 @sailplane/logger @middy/core@2 @middy/http-cors@2 @middy/http-event-normalizer@2 @middy/http-header-normalizer@2 @middy/http-json-body-parser@2
The extra @middy/ middleware packages are optional if you write your own wrapper function that does not use them. See below.
The extra @middy/ middleware packages are optional if you write your own wrapper function that does not use them.
See below.

**To use LambdaUtils v3.x with Middy v1.x.x:**

.. code-block:: shell
npm install @sailplane/lambda-utils@3 @sailplane/logger @middy/core@1 @middy/http-cors@1 @middy/http-event-normalizer@1 @middy/http-header-normalizer@1 @middy/http-json-body-parser@1
The extra @middy/ middleware packages are optional if you write your own wrapper function that does not use them.
See below.

**To use LambdaUtils v2.x with Middy v0.x.x:**

.. code-block:: shell
npm install @sailplane/lambda-utils@2 @sailplane/logger middy
npm install @sailplane/lambda-utils@2 @sailplane/logger middy@0
Upgrading
^^^^^^^^^

To upgrade from lambda-utils v1.x or v2.x to the latest, remove the old with ``npm rm middy``
To upgrade from older versions of lambda-utils, remove the old lambda-utils and middy dependencies
and then follow the install instructions above to install the latest. See also the
`Middy upgrade instructions <https://middy.js.org/UPGRADE.html>`_.
`Middy upgrade instructions <https://github.com/middyjs/middy/blob/main/docs/UPGRADE.md>`_.

Examples
^^^^^^^^
Expand Down Expand Up @@ -106,17 +115,54 @@ Extending LambdaUtils for your own app
.. code-block:: ts
import {ProxyHandler} from "aws-lambda";
import middy from "@middy/core";
import * as createError from "http-errors";
import * as LambdaUtils from "@sailplane/lambda-utils";
import {userAuthMiddleware} from "./user-auth"; //you write this
/** ID user user authenticated in running Lambda */
let authenticatedUserId: string|undefined;
export getAuthenticatedUserId(): string|undefined {
return authenticatedUserId;
}
/**
* Middleware for LambdaUtils to automatically manage AuthService context.
*/
const authMiddleware = (requiredRole?: string): Required<middy.MiddlewareObj> => {
return {
before: async (request) => {
const claims = request.event.requestContext.authorizer?.claims;
const role = claims['custom:role'];
if (requiredRole && role !== requiredRole) {
throw new createError.Forbidden();
}
authenticatedUserId = claims?.sub;
if (!authenticatedUserId) {
throw new createError.Unauthorized("No user authorized");
}
},
after: async (_) => {
authenticatedUserId = undefined;
},
onError: async (_) => {
authenticatedUserId = undefined;
}
};
}
export interface WrapApiHandlerOptions {
noUserAuth?: boolean;
requiredRole?: string;
}
export function wrapApiHandlerWithAuth(options: WrapApiHandlerOptions,
handler: LambdaUtils.AsyncProxyHandler): ProxyHandler {
let wrap = LambdaUtils.wrapApiHandler(handler);
export function wrapApiHandlerWithAuth(
options: WrapApiHandlerOptions,
handler: LambdaUtils.AsyncProxyHandlerV2
): LambdaUtils.AsyncMiddyifedHandlerV2 {
const wrap = LambdaUtils.wrapApiHandlerV2(handler);
if (!options.noUserAuth) {
wrap.use(userAuthMiddleware(options.requiredRole));
Expand All @@ -128,5 +174,5 @@ Extending LambdaUtils for your own app
Type Declarations
^^^^^^^^^^^^^^^^^

.. literalinclude:: ../../lambda-utils/dist/lambda-utils.d.ts
.. literalinclude:: ../../lambda-utils/dist/handler-utils.d.ts
:language: typescript
10 changes: 5 additions & 5 deletions lambda-utils/lib/handler-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
APIGatewayEventRequestContext,
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult, APIGatewayProxyResultV2,
APIGatewayProxyResult, APIGatewayProxyResultV2, APIGatewayProxyStructuredResultV2,
Context
} from "aws-lambda";
import * as LambdaUtils from "./index";
Expand Down Expand Up @@ -63,7 +63,7 @@ describe("LambdaUtils", () => {

test("wrapApiHandler v2 promise object success", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandler(async (): Promise<any> => {
const handler = LambdaUtils.wrapApiHandlerV2(async (): Promise<any> => {
return {message: 'Hello'};
});

Expand Down Expand Up @@ -157,15 +157,15 @@ describe("LambdaUtils", () => {
expect(response.body).toEqual("Error: oops");
});

test("wrapApiHandler throw http-error", async () => {
test("wrapApiHandlerV2 throw http-error", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandler(async (): Promise<APIGatewayProxyResult> => {
const handler = LambdaUtils.wrapApiHandlerV2(async (): Promise<APIGatewayProxyStructuredResultV2> => {
throw new createError.NotFound();
});

// WHEN
const response = await handler(
{} as unknown as APIGatewayProxyEvent, {} as Context, {} as any
{} as unknown as APIGatewayProxyEventV2, {} as Context, {} as any
) as APIGatewayProxyResult;

// THEN
Expand Down
46 changes: 38 additions & 8 deletions lambda-utils/lib/handler-utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {APIGatewayProxyResult, Callback, Context} from "aws-lambda";
import {APIGatewayProxyResult} from "aws-lambda";
import middy from '@middy/core';
import cors from '@middy/http-cors';
import httpEventNormalizer from '@middy/http-event-normalizer';
import httpHeaderNormalizer from '@middy/http-header-normalizer';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import {Logger} from "@sailplane/logger";
import {APIGatewayProxyEventAnyVersion, APIGatewayProxyResultAnyVersion} from "./types";
import {
AsyncMiddyifedHandlerV1, AsyncMiddyifedHandlerV2,
AsyncProxyHandlerV1, AsyncProxyHandlerV2
} from "./types";
import {resolvedPromiseIsSuccessMiddleware} from "./resolved-promise-is-success";
import {unhandledExceptionMiddleware} from "./unhandled-exception";

const logger = new Logger('lambda-utils');

/**
* Wrap an API Gateway proxy lambda function handler to add features:
* Wrap an API Gateway V1 format proxy lambda function handler to add features:
* - Set CORS headers.
* - Normalize incoming headers to lowercase
* - If incoming content is JSON text, replace event.body with parsed object.
Expand All @@ -31,14 +34,41 @@ const logger = new Logger('lambda-utils');
* @see https://middy.js.org/#:~:text=available%20middlewares
* @see https://www.npmjs.com/package/http-errors
*/
export function wrapApiHandler<TEvent extends APIGatewayProxyEventAnyVersion, TResult extends APIGatewayProxyResultAnyVersion>(
handler: (event: TEvent, context: Context, callback?: Callback<TResult>) => Promise<TResult>
) : middy.MiddyfiedHandler<TEvent, TResult> {
export function wrapApiHandler(handler: AsyncProxyHandlerV1): AsyncMiddyifedHandlerV1 {
return middy(handler)
.use(httpEventNormalizer()).use(httpHeaderNormalizer()).use(httpJsonBodyParser())
.use(cors())
.use(resolvedPromiseIsSuccessMiddleware<TEvent, TResult>())
.use(unhandledExceptionMiddleware<TEvent, TResult>());
.use(resolvedPromiseIsSuccessMiddleware())
.use(unhandledExceptionMiddleware());
}
export const wrapApiHandlerV1 = wrapApiHandler;

/**
* Wrap an API Gateway V2 format proxy lambda function handler to add features:
* - Set CORS headers.
* - Normalize incoming headers to lowercase
* - If incoming content is JSON text, replace event.body with parsed object.
* - Ensures that event.queryStringParameters and event.pathParameters are defined,
* to avoid TypeErrors.
* - Ensures that handler response is formatted properly as a successful
* API Gateway result.
* - Catch http-errors exceptions into proper HTTP responses.
* - Catch other exceptions and return as HTTP 500
*
* This wrapper includes commonly useful middleware. You may further wrap it
* with your own function that adds additional middleware, or just use it as
* an example.
*
* @param handler async function to wrap
* @see https://middy.js.org/#:~:text=available%20middlewares
* @see https://www.npmjs.com/package/http-errors
*/
export function wrapApiHandlerV2(handler: AsyncProxyHandlerV2): AsyncMiddyifedHandlerV2 {
return middy(handler)
.use(httpEventNormalizer()).use(httpHeaderNormalizer()).use(httpJsonBodyParser())
.use(cors())
.use(resolvedPromiseIsSuccessMiddleware())
.use(unhandledExceptionMiddleware());
}

/**
Expand Down
9 changes: 4 additions & 5 deletions lambda-utils/lib/resolved-promise-is-success.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import {APIGatewayProxyEventAnyVersion, APIGatewayProxyResultAnyVersion} from ".
* This middleware will wrap it up as an APIGatewayProxyResult.
* Must be registered as the last (thus first to run) "after" middleware.
*/
export const resolvedPromiseIsSuccessMiddleware = <TEvent extends APIGatewayProxyEventAnyVersion, TResult extends APIGatewayProxyResultAnyVersion>(): middy.MiddlewareObj<TEvent, TResult> => ({
after: async (request): Promise<void> => {
export const resolvedPromiseIsSuccessMiddleware = (): middy.MiddlewareObj<APIGatewayProxyEventAnyVersion, APIGatewayProxyResultAnyVersion> => ({
after: async (request) => {
// If response isn't a proper API result object, convert it into one.
let response = request.response as TResult;
let response = request.response;
if (!response || typeof response !== 'object' || (!response.statusCode && !response.body)) {
request.response = {
statusCode: 200,
body: response ? JSON.stringify(response) : ''
} as TResult;
};
}
}
});

24 changes: 17 additions & 7 deletions lambda-utils/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {
APIGatewayProxyEvent as AWS_APIGatewayProxyEvent,
APIGatewayProxyEventV2 as AWS_APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyStructuredResultV2, Context
APIGatewayProxyStructuredResultV2, Callback, Context
} from "aws-lambda";
import middy from "@middy/core";

/**
* Casted interface for APIGatewayProxyEvents as converted through the middleware
Expand Down Expand Up @@ -47,10 +48,19 @@ export interface APIGatewayProxyEventV2 extends AWS_APIGatewayProxyEventV2 {
queryStringParameters: { [name: string]: string };
}

export type APIGatewayProxyEventAnyVersion = AWS_APIGatewayProxyEvent | AWS_APIGatewayProxyEventV2;
export type APIGatewayProxyResultAnyVersion = APIGatewayProxyResult | APIGatewayProxyStructuredResultV2;
export type APIGatewayProxyEventAnyVersion =
AWS_APIGatewayProxyEvent | APIGatewayProxyEvent |
AWS_APIGatewayProxyEventV2 | APIGatewayProxyEventV2;

/**
* Define the async version of ProxyHandler for either V1 or V2 payload format.
*/
export type AsyncProxyHandlerAnyVersion = (event: AWS_APIGatewayProxyEvent | AWS_APIGatewayProxyEventV2, context: Context) => Promise<any>;
export type APIGatewayProxyResultAnyVersion =
APIGatewayProxyResult | APIGatewayProxyStructuredResultV2;

/** LambdaUtils version of ProxyHandler for API Gateway v1 payload format */
export type AsyncProxyHandlerV1 = (event: APIGatewayProxyEvent, context: Context, callback?: Callback<APIGatewayProxyResult>) => Promise<APIGatewayProxyResult|object|void>;
/** LambdaUtils version of an API Gateway v1 payload handler wrapped with middy */
export type AsyncMiddyifedHandlerV1 = middy.MiddyfiedHandler<AWS_APIGatewayProxyEvent, APIGatewayProxyResult|object|void>;

/** LambdaUtils version of ProxyHandler for API Gateway v2 payload format */
export type AsyncProxyHandlerV2 = (event: APIGatewayProxyEventV2, context: Context, callback?: Callback<APIGatewayProxyStructuredResultV2>) => Promise<APIGatewayProxyStructuredResultV2|object|void>;
/** LambdaUtils version of an API Gateway v12payload handler wrapped with middy */
export type AsyncMiddyifedHandlerV2 = middy.MiddyfiedHandler<AWS_APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2|object|void>;
6 changes: 3 additions & 3 deletions lambda-utils/lib/unhandled-exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ const logger = new Logger('lambda-utils');
*
* Fine tuned to work better than the Middy version, and uses @sailplane/logger.
*/
export const unhandledExceptionMiddleware = <TEvent extends APIGatewayProxyEventAnyVersion, TResult extends APIGatewayProxyResultAnyVersion>(): middy.MiddlewareObj<TEvent, TResult> => ({
onError: async (request): Promise<void> => {
export const unhandledExceptionMiddleware = (): middy.MiddlewareObj<APIGatewayProxyEventAnyVersion, APIGatewayProxyResultAnyVersion> => ({
onError: async (request) => {
logger.error('Unhandled exception:', request.error);

request.response = request.response || {} as TResult;
request.response = request.response || {};
/* istanbul ignore else - nominal path is for response to be brand new*/
if ((request.response.statusCode || 0) < 400) {
request.response.statusCode = (request.error as any)?.statusCode ?? 500;
Expand Down
4 changes: 2 additions & 2 deletions lambda-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sailplane/lambda-utils",
"version": "4.0.0",
"version": "4.0.0-beta.4",
"description": "Use middleware to remove redundancy in AWS Lambda handlers.",
"keywords": [
"aws",
Expand Down Expand Up @@ -48,7 +48,7 @@
"@middy/http-event-normalizer": "2.x.x",
"@middy/http-header-normalizer": "2.x.x",
"@middy/http-json-body-parser": "2.x.x",
"@sailplane/logger": "3.x.x"
"@sailplane/logger": ">=3.x.x"
},
"main": "dist/index.js",
"files": [
Expand Down
3 changes: 2 additions & 1 deletion lambda-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"es2017"
]
},
"exclude" : ["lib/*.test.ts"]
"include": ["lib/*.ts"],
"exclude": ["lib/*.test.ts"]
}

0 comments on commit ec627c9

Please sign in to comment.