From d6a0d5822dd03da7c31783ecee97466b65649d60 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 24 May 2023 12:11:49 +0000 Subject: [PATCH 1/3] feat: add cleanupPowertools hook --- .../src/middleware/cleanupPowertools.ts | 76 +++++++++++++++++++ packages/commons/src/middleware/constants.ts | 12 +++ packages/commons/src/middleware/index.ts | 2 + packages/commons/src/types/middy.ts | 20 ++++- .../tests/unit/cleanupPowertools.test.ts | 66 ++++++++++++++++ 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 packages/commons/src/middleware/cleanupPowertools.ts create mode 100644 packages/commons/src/middleware/constants.ts create mode 100644 packages/commons/src/middleware/index.ts create mode 100644 packages/commons/tests/unit/cleanupPowertools.test.ts diff --git a/packages/commons/src/middleware/cleanupPowertools.ts b/packages/commons/src/middleware/cleanupPowertools.ts new file mode 100644 index 0000000000..3c14d352d9 --- /dev/null +++ b/packages/commons/src/middleware/cleanupPowertools.ts @@ -0,0 +1,76 @@ +import { + TRACER_KEY, + METRICS_KEY, + LOGGER_KEY, + IDEMPOTENCY_KEY, +} from './constants'; +import type { MiddyLikeRequest, CleanupFunction } from '../types/middy'; + +// Typeguard to assert that an object is of Function type +const isFunction = (obj: unknown): obj is CleanupFunction => { + return typeof obj === 'function'; +}; + +/** + * Function used to cleanup Powertools resources when a Middy middleware + * returns early and terminates the middleware chain. + * + * When a middleware returns early, all the middleware lifecycle functions + * that come after it are not executed. This means that if a middleware + * was relying on certain logic to be run during the `after` or `onError` + * lifecycle functions, that logic will not be executed. + * + * This is the case for the middlewares that are part of Powertools for AWS + * which rely on these lifecycle functions to perform cleanup operations + * like closing the current segment in the tracer or flushing any stored + * metrics. + * + * When authoring a middleware that might return early, you can use this + * function to cleanup Powertools resources. This function will check if + * any cleanup function is present in the `request.internal` object and + * execute it. + * + * @example + * ```typescript + * import middy from '@middy/core'; + * import { cleanupPowertools } from '@aws-lambda-powertools/commons'; + * + * // Example middleware that returns early + * const myCustomMiddleware = (): middy.MiddlewareObj => { + * const before = async (request: middy.Request): Promise => { + * // If the request is a GET, return early (as an example) + * if (request.event.httpMethod === 'GET') { + * // Cleanup Powertools resources + * await cleanupPowertools(request); + * // Then return early + * return 'GET method not supported'; + * } + * }; + * + * return { + * before, + * }; + * }; + * ``` + * + * @param request - The Middy request object + * @param options - An optional object that can be used to pass options to the function + */ +const cleanupPowertools = async (request: MiddyLikeRequest): Promise => { + const cleanupFunctionNames = [ + TRACER_KEY, + METRICS_KEY, + LOGGER_KEY, + IDEMPOTENCY_KEY, + ]; + for (const functionName of cleanupFunctionNames) { + if (Object(request.internal).hasOwnProperty(functionName)) { + const functionReference = request.internal[functionName]; + if (isFunction(functionReference)) { + await functionReference(request); + } + } + } +}; + +export { cleanupPowertools }; diff --git a/packages/commons/src/middleware/constants.ts b/packages/commons/src/middleware/constants.ts new file mode 100644 index 0000000000..9f9b30bcb6 --- /dev/null +++ b/packages/commons/src/middleware/constants.ts @@ -0,0 +1,12 @@ +/** + * These constants are used to store cleanup functions in Middy's `request.internal` object. + * They are used by the `cleanupPowertools` function to check if any cleanup function + * is present and execute it. + */ +const PREFIX = 'powertools-for-aws'; +const TRACER_KEY = `${PREFIX}.tracer`; +const METRICS_KEY = `${PREFIX}.metrics`; +const LOGGER_KEY = `${PREFIX}.logger`; +const IDEMPOTENCY_KEY = `${PREFIX}.idempotency`; + +export { TRACER_KEY, METRICS_KEY, LOGGER_KEY, IDEMPOTENCY_KEY }; diff --git a/packages/commons/src/middleware/index.ts b/packages/commons/src/middleware/index.ts new file mode 100644 index 0000000000..07bbe0764e --- /dev/null +++ b/packages/commons/src/middleware/index.ts @@ -0,0 +1,2 @@ +export * from './cleanupPowertools'; +export * from './constants'; diff --git a/packages/commons/src/types/middy.ts b/packages/commons/src/types/middy.ts index fe060ece0a..79a8cb5c3d 100644 --- a/packages/commons/src/types/middy.ts +++ b/packages/commons/src/types/middy.ts @@ -1,4 +1,4 @@ -import { Context } from 'aws-lambda'; +import type { Context } from 'aws-lambda'; /** * We need to define these types and interfaces here because we can't import them from @middy/core. @@ -22,14 +22,14 @@ type Request< }; }; -declare type MiddlewareFn< +type MiddlewareFn< TEvent = unknown, TResult = unknown, TErr = Error, TContext extends Context = Context > = (request: Request) => unknown; -export type MiddlewareLikeObj< +type MiddlewareLikeObj< TEvent = unknown, TResult = unknown, TErr = Error, @@ -40,9 +40,21 @@ export type MiddlewareLikeObj< onError?: MiddlewareFn; }; -export type MiddyLikeRequest = { +type MiddyLikeRequest = { event: unknown; context: Context; response: unknown | null; error: Error | null; + internal: { + [key: string]: unknown; + }; }; + +/** + * Cleanup function that is used to cleanup resources when a middleware returns early. + * Each Powertools for AWS middleware that needs to perform cleanup operations will + * store a cleanup function with this signature in the `request.internal` object. + */ +type CleanupFunction = (request: MiddyLikeRequest) => Promise; + +export { MiddlewareLikeObj, MiddyLikeRequest, CleanupFunction }; diff --git a/packages/commons/tests/unit/cleanupPowertools.test.ts b/packages/commons/tests/unit/cleanupPowertools.test.ts new file mode 100644 index 0000000000..e3e1655444 --- /dev/null +++ b/packages/commons/tests/unit/cleanupPowertools.test.ts @@ -0,0 +1,66 @@ +/** + * Test Middy cleanupPowertools function + * + * @group unit/commons/cleanupPowertools + */ +import { + cleanupPowertools, + TRACER_KEY, + METRICS_KEY, +} from '../../src/middleware'; +import { helloworldContext as context } from '../../src/samples/resources/contexts/hello-world'; + +describe('Function: cleanupPowertools', () => { + it('calls the cleanup function that are present', async () => { + // Prepare + const mockCleanupFunction1 = jest.fn(); + const mockCleanupFunction2 = jest.fn(); + const mockRequest = { + event: {}, + context: context, + response: null, + error: null, + internal: { + [TRACER_KEY]: mockCleanupFunction1, + [METRICS_KEY]: mockCleanupFunction2, + }, + }; + + // Act + await cleanupPowertools(mockRequest); + + // Assess + expect(mockCleanupFunction1).toHaveBeenCalledTimes(1); + expect(mockCleanupFunction1).toHaveBeenCalledWith(mockRequest, undefined); + expect(mockCleanupFunction2).toHaveBeenCalledTimes(1); + expect(mockCleanupFunction2).toHaveBeenCalledWith(mockRequest, undefined); + }); + it('resolves successfully if no cleanup function is present', async () => { + // Prepare + const mockRequest = { + event: {}, + context: context, + response: null, + error: null, + internal: {}, + }; + + // Act & Assess + await expect(cleanupPowertools(mockRequest)).resolves.toBeUndefined(); + }); + it('resolves successfully if cleanup function is not a function', async () => { + // Prepare + const mockRequest = { + event: {}, + context: context, + response: null, + error: null, + internal: { + [TRACER_KEY]: 'not a function', + }, + }; + + // Act & Assess + await expect(cleanupPowertools(mockRequest)).resolves.toBeUndefined(); + }); +}); From 992ad62c72660949d0ab43680b14559d8bf87496 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 24 May 2023 12:55:07 +0000 Subject: [PATCH 2/3] tests: fixed tests --- packages/commons/src/middleware/cleanupPowertools.ts | 7 ++++--- packages/commons/tests/unit/cleanupPowertools.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/commons/src/middleware/cleanupPowertools.ts b/packages/commons/src/middleware/cleanupPowertools.ts index 3c14d352d9..d0bbcddd37 100644 --- a/packages/commons/src/middleware/cleanupPowertools.ts +++ b/packages/commons/src/middleware/cleanupPowertools.ts @@ -12,8 +12,9 @@ const isFunction = (obj: unknown): obj is CleanupFunction => { }; /** - * Function used to cleanup Powertools resources when a Middy middleware - * returns early and terminates the middleware chain. + * Function used to cleanup Powertools for AWS resources when a Middy + * middleware [returns early](https://middy.js.org/docs/intro/early-interrupt) + * and terminates the middleware chain. * * When a middleware returns early, all the middleware lifecycle functions * that come after it are not executed. This means that if a middleware @@ -33,7 +34,7 @@ const isFunction = (obj: unknown): obj is CleanupFunction => { * @example * ```typescript * import middy from '@middy/core'; - * import { cleanupPowertools } from '@aws-lambda-powertools/commons'; + * import { cleanupPowertools } from '@aws-lambda-powertools/commons/lib/middleware'; * * // Example middleware that returns early * const myCustomMiddleware = (): middy.MiddlewareObj => { diff --git a/packages/commons/tests/unit/cleanupPowertools.test.ts b/packages/commons/tests/unit/cleanupPowertools.test.ts index e3e1655444..af5da6a804 100644 --- a/packages/commons/tests/unit/cleanupPowertools.test.ts +++ b/packages/commons/tests/unit/cleanupPowertools.test.ts @@ -31,9 +31,9 @@ describe('Function: cleanupPowertools', () => { // Assess expect(mockCleanupFunction1).toHaveBeenCalledTimes(1); - expect(mockCleanupFunction1).toHaveBeenCalledWith(mockRequest, undefined); + expect(mockCleanupFunction1).toHaveBeenCalledWith(mockRequest); expect(mockCleanupFunction2).toHaveBeenCalledTimes(1); - expect(mockCleanupFunction2).toHaveBeenCalledWith(mockRequest, undefined); + expect(mockCleanupFunction2).toHaveBeenCalledWith(mockRequest); }); it('resolves successfully if no cleanup function is present', async () => { // Prepare From 8aa403dbcc4a0c51f196ac088a71294c91b4f427 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 24 May 2023 13:28:03 +0000 Subject: [PATCH 3/3] chore: rename cleanup function --- ...{cleanupPowertools.ts => cleanupMiddlewares.ts} | 8 ++++---- packages/commons/src/middleware/index.ts | 2 +- ...wertools.test.ts => cleanupMiddlewares.test.ts} | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) rename packages/commons/src/middleware/{cleanupPowertools.ts => cleanupMiddlewares.ts} (90%) rename packages/commons/tests/unit/{cleanupPowertools.test.ts => cleanupMiddlewares.test.ts} (80%) diff --git a/packages/commons/src/middleware/cleanupPowertools.ts b/packages/commons/src/middleware/cleanupMiddlewares.ts similarity index 90% rename from packages/commons/src/middleware/cleanupPowertools.ts rename to packages/commons/src/middleware/cleanupMiddlewares.ts index d0bbcddd37..e35719b9d7 100644 --- a/packages/commons/src/middleware/cleanupPowertools.ts +++ b/packages/commons/src/middleware/cleanupMiddlewares.ts @@ -34,7 +34,7 @@ const isFunction = (obj: unknown): obj is CleanupFunction => { * @example * ```typescript * import middy from '@middy/core'; - * import { cleanupPowertools } from '@aws-lambda-powertools/commons/lib/middleware'; + * import { cleanupMiddlewares } from '@aws-lambda-powertools/commons/lib/middleware'; * * // Example middleware that returns early * const myCustomMiddleware = (): middy.MiddlewareObj => { @@ -42,7 +42,7 @@ const isFunction = (obj: unknown): obj is CleanupFunction => { * // If the request is a GET, return early (as an example) * if (request.event.httpMethod === 'GET') { * // Cleanup Powertools resources - * await cleanupPowertools(request); + * await cleanupMiddlewares(request); * // Then return early * return 'GET method not supported'; * } @@ -57,7 +57,7 @@ const isFunction = (obj: unknown): obj is CleanupFunction => { * @param request - The Middy request object * @param options - An optional object that can be used to pass options to the function */ -const cleanupPowertools = async (request: MiddyLikeRequest): Promise => { +const cleanupMiddlewares = async (request: MiddyLikeRequest): Promise => { const cleanupFunctionNames = [ TRACER_KEY, METRICS_KEY, @@ -74,4 +74,4 @@ const cleanupPowertools = async (request: MiddyLikeRequest): Promise => { } }; -export { cleanupPowertools }; +export { cleanupMiddlewares }; diff --git a/packages/commons/src/middleware/index.ts b/packages/commons/src/middleware/index.ts index 07bbe0764e..85f7388af3 100644 --- a/packages/commons/src/middleware/index.ts +++ b/packages/commons/src/middleware/index.ts @@ -1,2 +1,2 @@ -export * from './cleanupPowertools'; +export * from './cleanupMiddlewares'; export * from './constants'; diff --git a/packages/commons/tests/unit/cleanupPowertools.test.ts b/packages/commons/tests/unit/cleanupMiddlewares.test.ts similarity index 80% rename from packages/commons/tests/unit/cleanupPowertools.test.ts rename to packages/commons/tests/unit/cleanupMiddlewares.test.ts index af5da6a804..12583d9107 100644 --- a/packages/commons/tests/unit/cleanupPowertools.test.ts +++ b/packages/commons/tests/unit/cleanupMiddlewares.test.ts @@ -1,16 +1,16 @@ /** - * Test Middy cleanupPowertools function + * Test Middy cleanupMiddlewares function * - * @group unit/commons/cleanupPowertools + * @group unit/commons/cleanupMiddlewares */ import { - cleanupPowertools, + cleanupMiddlewares, TRACER_KEY, METRICS_KEY, } from '../../src/middleware'; import { helloworldContext as context } from '../../src/samples/resources/contexts/hello-world'; -describe('Function: cleanupPowertools', () => { +describe('Function: cleanupMiddlewares', () => { it('calls the cleanup function that are present', async () => { // Prepare const mockCleanupFunction1 = jest.fn(); @@ -27,7 +27,7 @@ describe('Function: cleanupPowertools', () => { }; // Act - await cleanupPowertools(mockRequest); + await cleanupMiddlewares(mockRequest); // Assess expect(mockCleanupFunction1).toHaveBeenCalledTimes(1); @@ -46,7 +46,7 @@ describe('Function: cleanupPowertools', () => { }; // Act & Assess - await expect(cleanupPowertools(mockRequest)).resolves.toBeUndefined(); + await expect(cleanupMiddlewares(mockRequest)).resolves.toBeUndefined(); }); it('resolves successfully if cleanup function is not a function', async () => { // Prepare @@ -61,6 +61,6 @@ describe('Function: cleanupPowertools', () => { }; // Act & Assess - await expect(cleanupPowertools(mockRequest)).resolves.toBeUndefined(); + await expect(cleanupMiddlewares(mockRequest)).resolves.toBeUndefined(); }); });