Skip to content

Commit 02aae6d

Browse files
authored
refactor(commons): aws sdk util + identity check (#1708)
1 parent c721d35 commit 02aae6d

28 files changed

+1120
-682
lines changed

package-lock.json

Lines changed: 612 additions & 230 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/commons/package.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,5 @@
4444
"powertools",
4545
"serverless",
4646
"nodejs"
47-
],
48-
"devDependencies": {
49-
"@aws-sdk/client-appconfigdata": "^3.413.0",
50-
"@aws-sdk/client-dynamodb": "^3.413.0",
51-
"@aws-sdk/client-lambda": "^3.413.0",
52-
"@aws-sdk/client-secrets-manager": "^3.413.0",
53-
"@aws-sdk/client-ssm": "^3.413.0",
54-
"@aws-sdk/util-utf8-node": "^3.259.0"
55-
}
47+
]
5648
}

packages/commons/src/awsSdk/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { addUserAgentMiddleware } from './userAgentMiddleware';
2+
export { isSdkClient } from './utils';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { PT_VERSION } from '../version';
2+
import { isSdkClient } from './utils';
3+
import type { MiddlewareArgsLike } from '../types/awsSdk';
4+
5+
/**
6+
* @internal
7+
*/
8+
const EXEC_ENV = process.env.AWS_EXECUTION_ENV || 'NA';
9+
const middlewareOptions = {
10+
relation: 'after',
11+
toMiddleware: 'getUserAgentMiddleware',
12+
name: 'addPowertoolsToUserAgent',
13+
tags: ['POWERTOOLS', 'USER_AGENT'],
14+
};
15+
16+
/**
17+
* @internal
18+
* returns a middleware function for the MiddlewareStack, that can be used for the SDK clients
19+
* @param feature
20+
*/
21+
const customUserAgentMiddleware = (feature: string) => {
22+
return <T extends MiddlewareArgsLike>(next: (arg0: T) => Promise<T>) =>
23+
async (args: T) => {
24+
const powertoolsUserAgent = `PT/${feature}/${PT_VERSION} PTEnv/${EXEC_ENV}`;
25+
args.request.headers[
26+
'user-agent'
27+
] = `${args.request.headers['user-agent']} ${powertoolsUserAgent}`;
28+
29+
return await next(args);
30+
};
31+
};
32+
33+
const addUserAgentMiddleware = (client: unknown, feature: string): void => {
34+
try {
35+
if (isSdkClient(client)) {
36+
if (
37+
client.middlewareStack
38+
.identify()
39+
.includes('addPowertoolsToUserAgent: POWERTOOLS,USER_AGENT')
40+
) {
41+
return;
42+
}
43+
client.middlewareStack.addRelativeTo(
44+
customUserAgentMiddleware(feature),
45+
middlewareOptions
46+
);
47+
} else {
48+
throw new Error(
49+
`The client provided does not match the expected interface`
50+
);
51+
}
52+
} catch (e) {
53+
console.warn('Failed to add user agent middleware', e);
54+
}
55+
};
56+
57+
export { customUserAgentMiddleware, addUserAgentMiddleware };

packages/commons/src/awsSdk/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { SdkClient } from '../types/awsSdk';
2+
3+
/**
4+
* @internal
5+
* Type guard to check if the client provided is a valid AWS SDK v3 client
6+
*/
7+
const isSdkClient = (client: unknown): client is SdkClient =>
8+
typeof client === 'object' &&
9+
client !== null &&
10+
'send' in client &&
11+
typeof client.send === 'function' &&
12+
'config' in client &&
13+
client.config !== undefined &&
14+
typeof client.config === 'object' &&
15+
client.config !== null &&
16+
'middlewareStack' in client &&
17+
client.middlewareStack !== undefined &&
18+
typeof client.middlewareStack === 'object' &&
19+
client.middlewareStack !== null &&
20+
'identify' in client.middlewareStack &&
21+
typeof client.middlewareStack.identify === 'function' &&
22+
'addRelativeTo' in client.middlewareStack &&
23+
typeof client.middlewareStack.addRelativeTo === 'function';
24+
25+
export { isSdkClient };

packages/commons/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export * as ContextExamples from './samples/resources/contexts';
55
export * as Events from './samples/resources/events';
66
export * from './types/middy';
77
export * from './types/utils';
8-
export * from './userAgentMiddleware';
8+
export * from './awsSdk';

packages/commons/src/types/awsSdk.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @internal
3+
* Minimal interface for an AWS SDK v3 client
4+
*/
5+
interface SdkClient {
6+
send: (args: unknown) => Promise<unknown>;
7+
config: {
8+
serviceId: string;
9+
};
10+
middlewareStack: {
11+
identify: () => string[];
12+
addRelativeTo: (middleware: unknown, options: unknown) => void;
13+
};
14+
}
15+
16+
/**
17+
* @internal
18+
* Minimal type for the arguments passed to a middleware function
19+
*/
20+
type MiddlewareArgsLike = { request: { headers: { [key: string]: string } } };
21+
22+
export { SdkClient, MiddlewareArgsLike };

packages/commons/src/userAgentMiddleware.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { addUserAgentMiddleware, isSdkClient } from '../../src/awsSdk';
2+
import { PT_VERSION as version } from '../../src/version';
3+
import { customUserAgentMiddleware } from '../../src/awsSdk/userAgentMiddleware';
4+
5+
describe('Helpers: awsSdk', () => {
6+
describe('Function: userAgentMiddleware', () => {
7+
beforeAll(() => {
8+
jest.spyOn(console, 'warn').mockImplementation(() => ({}));
9+
});
10+
11+
it('handles gracefully failures in adding a middleware and only log a warning', () => {
12+
// Prepare
13+
const client = {
14+
middlewareStack: {
15+
addRelativeTo: () => {
16+
throw new Error('test');
17+
},
18+
},
19+
};
20+
const warningSpy = jest
21+
.spyOn(console, 'warn')
22+
.mockImplementation(() => ({}));
23+
24+
// Act & Assess
25+
expect(() => addUserAgentMiddleware(client, 'my-feature')).not.toThrow();
26+
expect(warningSpy).toHaveBeenCalledTimes(1);
27+
});
28+
29+
it('should return and do nothing if the client already has a Powertools UA middleware', async () => {
30+
// Prepare
31+
const client = {
32+
middlewareStack: {
33+
identify: () => 'addPowertoolsToUserAgent: POWERTOOLS,USER_AGENT',
34+
addRelativeTo: jest.fn(),
35+
},
36+
send: jest.fn(),
37+
config: {
38+
defaultSigningName: 'bar',
39+
},
40+
};
41+
const feature = 'my-feature';
42+
43+
// Act
44+
addUserAgentMiddleware(client, feature);
45+
46+
// Assess
47+
expect(client.middlewareStack.addRelativeTo).toHaveBeenCalledTimes(0);
48+
});
49+
50+
it('should call the function to add the middleware with the correct arguments', async () => {
51+
// Prepare
52+
const client = {
53+
middlewareStack: {
54+
identify: () => '',
55+
addRelativeTo: jest.fn(),
56+
},
57+
send: jest.fn(),
58+
config: {
59+
defaultSigningName: 'bar',
60+
},
61+
};
62+
const feature = 'my-feature';
63+
64+
// Act
65+
addUserAgentMiddleware(client, feature);
66+
67+
// Assess
68+
expect(client.middlewareStack.addRelativeTo).toHaveBeenNthCalledWith(
69+
1,
70+
expect.any(Function),
71+
{
72+
relation: 'after',
73+
toMiddleware: 'getUserAgentMiddleware',
74+
name: 'addPowertoolsToUserAgent',
75+
tags: ['POWERTOOLS', 'USER_AGENT'],
76+
}
77+
);
78+
});
79+
});
80+
81+
describe('Function: customUserAgentMiddleware', () => {
82+
it('returns a middleware function', () => {
83+
// Prepare
84+
const feature = 'my-feature';
85+
86+
// Act
87+
const middleware = customUserAgentMiddleware(feature);
88+
89+
// Assess
90+
expect(middleware).toBeInstanceOf(Function);
91+
});
92+
93+
it('adds the Powertools UA to the request headers', async () => {
94+
// Prepare
95+
const feature = 'my-feature';
96+
const middleware = customUserAgentMiddleware(feature);
97+
const next = jest.fn();
98+
const args = {
99+
request: {
100+
headers: {
101+
'user-agent': 'foo',
102+
},
103+
},
104+
};
105+
106+
// Act
107+
await middleware(next)(args);
108+
109+
// Assess
110+
expect(args.request.headers['user-agent']).toEqual(
111+
`foo PT/my-feature/${version} PTEnv/NA`
112+
);
113+
});
114+
});
115+
116+
describe('Function: isSdkClient', () => {
117+
it('returns true if the client is a valid AWS SDK v3 client', () => {
118+
// Prepare
119+
const client = {
120+
send: jest.fn(),
121+
config: {
122+
defaultSigningName: 'bar',
123+
},
124+
middlewareStack: {
125+
identify: () => '',
126+
addRelativeTo: jest.fn(),
127+
},
128+
};
129+
130+
// Act
131+
const result = isSdkClient(client);
132+
133+
// Assess
134+
expect(result).toEqual(true);
135+
});
136+
137+
it('returns false if the client is not a valid AWS SDK v3 client', () => {
138+
// Prepare
139+
const client = {};
140+
141+
// Act
142+
const result = isSdkClient(client);
143+
144+
// Assess
145+
expect(result).toEqual(false);
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)