Skip to content

Commit 5bd0166

Browse files
authored
feat(commons): add cleanupPowertools function (#1473)
* feat: add cleanupPowertools hook * tests: fixed tests * chore: rename cleanup function
1 parent d4ae762 commit 5bd0166

File tree

5 files changed

+173
-4
lines changed

5 files changed

+173
-4
lines changed
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
TRACER_KEY,
3+
METRICS_KEY,
4+
LOGGER_KEY,
5+
IDEMPOTENCY_KEY,
6+
} from './constants';
7+
import type { MiddyLikeRequest, CleanupFunction } from '../types/middy';
8+
9+
// Typeguard to assert that an object is of Function type
10+
const isFunction = (obj: unknown): obj is CleanupFunction => {
11+
return typeof obj === 'function';
12+
};
13+
14+
/**
15+
* Function used to cleanup Powertools for AWS resources when a Middy
16+
* middleware [returns early](https://middy.js.org/docs/intro/early-interrupt)
17+
* and terminates the middleware chain.
18+
*
19+
* When a middleware returns early, all the middleware lifecycle functions
20+
* that come after it are not executed. This means that if a middleware
21+
* was relying on certain logic to be run during the `after` or `onError`
22+
* lifecycle functions, that logic will not be executed.
23+
*
24+
* This is the case for the middlewares that are part of Powertools for AWS
25+
* which rely on these lifecycle functions to perform cleanup operations
26+
* like closing the current segment in the tracer or flushing any stored
27+
* metrics.
28+
*
29+
* When authoring a middleware that might return early, you can use this
30+
* function to cleanup Powertools resources. This function will check if
31+
* any cleanup function is present in the `request.internal` object and
32+
* execute it.
33+
*
34+
* @example
35+
* ```typescript
36+
* import middy from '@middy/core';
37+
* import { cleanupMiddlewares } from '@aws-lambda-powertools/commons/lib/middleware';
38+
*
39+
* // Example middleware that returns early
40+
* const myCustomMiddleware = (): middy.MiddlewareObj => {
41+
* const before = async (request: middy.Request): Promise<undefined | string> => {
42+
* // If the request is a GET, return early (as an example)
43+
* if (request.event.httpMethod === 'GET') {
44+
* // Cleanup Powertools resources
45+
* await cleanupMiddlewares(request);
46+
* // Then return early
47+
* return 'GET method not supported';
48+
* }
49+
* };
50+
*
51+
* return {
52+
* before,
53+
* };
54+
* };
55+
* ```
56+
*
57+
* @param request - The Middy request object
58+
* @param options - An optional object that can be used to pass options to the function
59+
*/
60+
const cleanupMiddlewares = async (request: MiddyLikeRequest): Promise<void> => {
61+
const cleanupFunctionNames = [
62+
TRACER_KEY,
63+
METRICS_KEY,
64+
LOGGER_KEY,
65+
IDEMPOTENCY_KEY,
66+
];
67+
for (const functionName of cleanupFunctionNames) {
68+
if (Object(request.internal).hasOwnProperty(functionName)) {
69+
const functionReference = request.internal[functionName];
70+
if (isFunction(functionReference)) {
71+
await functionReference(request);
72+
}
73+
}
74+
}
75+
};
76+
77+
export { cleanupMiddlewares };

Diff for: packages/commons/src/middleware/constants.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* These constants are used to store cleanup functions in Middy's `request.internal` object.
3+
* They are used by the `cleanupPowertools` function to check if any cleanup function
4+
* is present and execute it.
5+
*/
6+
const PREFIX = 'powertools-for-aws';
7+
const TRACER_KEY = `${PREFIX}.tracer`;
8+
const METRICS_KEY = `${PREFIX}.metrics`;
9+
const LOGGER_KEY = `${PREFIX}.logger`;
10+
const IDEMPOTENCY_KEY = `${PREFIX}.idempotency`;
11+
12+
export { TRACER_KEY, METRICS_KEY, LOGGER_KEY, IDEMPOTENCY_KEY };

Diff for: packages/commons/src/middleware/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cleanupMiddlewares';
2+
export * from './constants';

Diff for: packages/commons/src/types/middy.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context } from 'aws-lambda';
1+
import type { Context } from 'aws-lambda';
22

33
/**
44
* We need to define these types and interfaces here because we can't import them from @middy/core.
@@ -22,14 +22,14 @@ type Request<
2222
};
2323
};
2424

25-
declare type MiddlewareFn<
25+
type MiddlewareFn<
2626
TEvent = unknown,
2727
TResult = unknown,
2828
TErr = Error,
2929
TContext extends Context = Context
3030
> = (request: Request<TEvent, TResult, TErr, TContext>) => unknown;
3131

32-
export type MiddlewareLikeObj<
32+
type MiddlewareLikeObj<
3333
TEvent = unknown,
3434
TResult = unknown,
3535
TErr = Error,
@@ -40,9 +40,21 @@ export type MiddlewareLikeObj<
4040
onError?: MiddlewareFn<TEvent, TResult, TErr, TContext>;
4141
};
4242

43-
export type MiddyLikeRequest = {
43+
type MiddyLikeRequest = {
4444
event: unknown;
4545
context: Context;
4646
response: unknown | null;
4747
error: Error | null;
48+
internal: {
49+
[key: string]: unknown;
50+
};
4851
};
52+
53+
/**
54+
* Cleanup function that is used to cleanup resources when a middleware returns early.
55+
* Each Powertools for AWS middleware that needs to perform cleanup operations will
56+
* store a cleanup function with this signature in the `request.internal` object.
57+
*/
58+
type CleanupFunction = (request: MiddyLikeRequest) => Promise<void>;
59+
60+
export { MiddlewareLikeObj, MiddyLikeRequest, CleanupFunction };
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Test Middy cleanupMiddlewares function
3+
*
4+
* @group unit/commons/cleanupMiddlewares
5+
*/
6+
import {
7+
cleanupMiddlewares,
8+
TRACER_KEY,
9+
METRICS_KEY,
10+
} from '../../src/middleware';
11+
import { helloworldContext as context } from '../../src/samples/resources/contexts/hello-world';
12+
13+
describe('Function: cleanupMiddlewares', () => {
14+
it('calls the cleanup function that are present', async () => {
15+
// Prepare
16+
const mockCleanupFunction1 = jest.fn();
17+
const mockCleanupFunction2 = jest.fn();
18+
const mockRequest = {
19+
event: {},
20+
context: context,
21+
response: null,
22+
error: null,
23+
internal: {
24+
[TRACER_KEY]: mockCleanupFunction1,
25+
[METRICS_KEY]: mockCleanupFunction2,
26+
},
27+
};
28+
29+
// Act
30+
await cleanupMiddlewares(mockRequest);
31+
32+
// Assess
33+
expect(mockCleanupFunction1).toHaveBeenCalledTimes(1);
34+
expect(mockCleanupFunction1).toHaveBeenCalledWith(mockRequest);
35+
expect(mockCleanupFunction2).toHaveBeenCalledTimes(1);
36+
expect(mockCleanupFunction2).toHaveBeenCalledWith(mockRequest);
37+
});
38+
it('resolves successfully if no cleanup function is present', async () => {
39+
// Prepare
40+
const mockRequest = {
41+
event: {},
42+
context: context,
43+
response: null,
44+
error: null,
45+
internal: {},
46+
};
47+
48+
// Act & Assess
49+
await expect(cleanupMiddlewares(mockRequest)).resolves.toBeUndefined();
50+
});
51+
it('resolves successfully if cleanup function is not a function', async () => {
52+
// Prepare
53+
const mockRequest = {
54+
event: {},
55+
context: context,
56+
response: null,
57+
error: null,
58+
internal: {
59+
[TRACER_KEY]: 'not a function',
60+
},
61+
};
62+
63+
// Act & Assess
64+
await expect(cleanupMiddlewares(mockRequest)).resolves.toBeUndefined();
65+
});
66+
});

0 commit comments

Comments
 (0)