Skip to content

Commit

Permalink
Merge pull request #123 from onicagroup/issue/108-lambda-utils-depend…
Browse files Browse the repository at this point in the history
…encies

Issue #108 LambdaUtils dependencies/refactor
  • Loading branch information
kernwig authored Sep 15, 2021
2 parents 6da442d + ec627c9 commit e9d9ee8
Show file tree
Hide file tree
Showing 13 changed files with 5,151 additions and 4,578 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
2 changes: 1 addition & 1 deletion lambda-utils/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module.exports = {
coverageReporters: [
"json",
"text",
// "lcov",
"lcov",
"clover"
],

Expand Down
225 changes: 225 additions & 0 deletions lambda-utils/lib/handler-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
APIGatewayEventRequestContext,
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult, APIGatewayProxyResultV2, APIGatewayProxyStructuredResultV2,
Context
} from "aws-lambda";
import * as LambdaUtils from "./index";
import * as createError from "http-errors";

describe("LambdaUtils", () => {
describe("wrapApiHandler", () => {

test("wrapApiHandler apiSuccess", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// Echo the event back
return LambdaUtils.apiSuccess(event);
});

const body = { company: "Onica", tagline: "Innovation through Cloud Transformation" };
const givenEvent: APIGatewayProxyEvent = {
body: JSON.stringify(body),
headers: {
"content-length": "0",
"CONTENT-TYPE": "application/json"
},
multiValueHeaders: {},
httpMethod: "GET",
isBase64Encoded: false,
path: "/test",
pathParameters: null,
queryStringParameters: null,
multiValueQueryStringParameters: null,
stageVariables: null,
resource: "tada",
requestContext: {} as any
};

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

// THEN

// CORS header set in response
expect(response.headers?.['Access-Control-Allow-Origin']).toEqual('*');

const resultEvent: APIGatewayProxyEvent = JSON.parse(response.body);

// body was parsed from string to JSON in request event
expect(resultEvent.body).toEqual(body);

// Headers are normalized in request event
expect(resultEvent.headers['Content-Length']).toBeUndefined();
expect(resultEvent.headers['content-length']).toEqual('0');
expect(resultEvent.headers["CONTENT-TYPE"]).toBeUndefined();
expect(resultEvent.headers['content-type']).toEqual("application/json");

// pathParameters and queryStringParameters are expanded to empty objects
expect(resultEvent.pathParameters).toEqual({});
expect(resultEvent.queryStringParameters).toEqual({});
});

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

const givenEvent: APIGatewayProxyEventV2 = {
version: "2",
routeKey: "123",
body: undefined,
headers: {
Origin: "test-origin"
},
rawPath: "/test",
rawQueryString: "",
isBase64Encoded: false,
pathParameters: undefined,
queryStringParameters: undefined,
requestContext: {
accountId: "123",
apiId: "abc",
domainName: "test",
domainPrefix: "unit",
http: {
method: "get",
path: "/test",
protocol: "http",
sourceIp: "1.1.1.1",
userAgent: "unit/test"
},
requestId: "abc",
routeKey: "123",
stage: "test",
time: "2021-08-30T16:58:31Z",
timeEpoch: 1000000
}
};

// WHEN
const response = await handler(givenEvent, {} as Context, {} as any) as APIGatewayProxyResultV2<any>;

// THEN
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual("{\"message\":\"Hello\"}");
// With HTTP APIs, API Gateway handles CORS, so it is ignored here.
expect(response.headers?.["Access-Control-Allow-Origin"]).toBeUndefined();
});

test("wrapApiHandler promise empty success", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandler(async (): Promise<any> => {
return;
});

const givenEvent: APIGatewayProxyEvent = {
body: null,
headers: {},
multiValueHeaders: {},
httpMethod: "GET",
isBase64Encoded: false,
path: "/test",
pathParameters: null,
queryStringParameters: null,
multiValueQueryStringParameters: null,
stageVariables: null,
resource: "",
requestContext: {} as APIGatewayEventRequestContext
};

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

// THEN
expect(response.statusCode).toEqual(200);
expect(response.body).toBeFalsy();
expect(response.headers!["Access-Control-Allow-Origin"]).toEqual("*");
});

test("wrapApiHandler throw Error", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandler(async (): Promise<APIGatewayProxyResult> => {
throw new Error("oops");
});

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

// THEN
expect(response.statusCode).toEqual(500);
expect(response.body).toEqual("Error: oops");
});

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

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

// THEN
expect(response).toEqual({
statusCode: 404,
body: 'NotFoundError: Not Found'
});
});
});

describe('apiSuccess', () => {
test('apiSuccess without body', () => {
// WHEN
const result = LambdaUtils.apiSuccess();

// THEN
expect(result).toBeTruthy();
expect(result.statusCode).toEqual(200);
expect(result.body).toEqual('');
});

test('apiSuccess with body', () => {
// GIVEN
const resultBody = { hello: 'world' };

// WHEN
const result = LambdaUtils.apiSuccess(resultBody);

// THEN
expect(result).toBeTruthy();
expect(result.statusCode).toEqual(200);
expect(result.body).toEqual('{"hello":"world"}');
});
});

describe('apiFailure', () => {
test('apiFailure without body', () => {
// WHEN
const result = LambdaUtils.apiFailure(501);

// THEN
expect(result).toBeTruthy();
expect(result.statusCode).toEqual(501);
expect(result.body).toEqual('');
});

test('apiFailure with body', () => {
// WHEN
const result = LambdaUtils.apiFailure(418, "I'm a teapot");

// THEN
expect(result).toBeTruthy();
expect(result.statusCode).toEqual(418);
expect(result.body).toEqual("I'm a teapot");
});
});
});
Loading

0 comments on commit e9d9ee8

Please sign in to comment.