Skip to content

Commit 5c6d527

Browse files
authored
feat(parameters): add adaptive types to SecretsProvider (#1411)
1 parent 6894f9a commit 5c6d527

File tree

4 files changed

+128
-22
lines changed

4 files changed

+128
-22
lines changed

Diff for: packages/parameters/src/secrets/SecretsProvider.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';
77
import type {
88
SecretsProviderOptions,
9-
SecretsGetOptionsInterface
9+
SecretsGetOptions,
10+
SecretsGetOutput,
11+
SecretsGetOptionsUnion,
1012
} from '../types/SecretsProvider';
1113

1214
/**
@@ -157,7 +159,7 @@ class SecretsProvider extends BaseProvider {
157159
*
158160
* @param {SecretsProviderOptions} config - The configuration object.
159161
*/
160-
public constructor (config?: SecretsProviderOptions) {
162+
public constructor(config?: SecretsProviderOptions) {
161163
super();
162164

163165
if (config?.awsSdkV3Client) {
@@ -170,7 +172,6 @@ class SecretsProvider extends BaseProvider {
170172
const clientConfig = config?.clientConfig || {};
171173
this.client = new SecretsManagerClient(clientConfig);
172174
}
173-
174175
}
175176

176177
/**
@@ -197,14 +198,20 @@ class SecretsProvider extends BaseProvider {
197198
* For usage examples check {@link SecretsProvider}.
198199
*
199200
* @param {string} name - The name of the secret
200-
* @param {SecretsGetOptionsInterface} options - Options to customize the retrieval of the secret
201+
* @param {SecretsGetOptions} options - Options to customize the retrieval of the secret
201202
* @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/
202203
*/
203-
public async get(
204+
public async get<
205+
ExplicitUserProvidedType = undefined,
206+
InferredFromOptionsType extends SecretsGetOptionsUnion | undefined = SecretsGetOptionsUnion
207+
>(
204208
name: string,
205-
options?: SecretsGetOptionsInterface
206-
): Promise<undefined | string | Uint8Array | Record<string, unknown>> {
207-
return super.get(name, options);
209+
options?: InferredFromOptionsType & SecretsGetOptions
210+
): Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined> {
211+
return super.get(
212+
name,
213+
options
214+
) as Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined>;
208215
}
209216

210217
/**
@@ -221,11 +228,11 @@ class SecretsProvider extends BaseProvider {
221228
* Retrieve a configuration from AWS AppConfig.
222229
*
223230
* @param {string} name - Name of the configuration or its ID
224-
* @param {SecretsGetOptionsInterface} options - SDK options to propagate to the AWS SDK v3 for JavaScript client
231+
* @param {SecretsGetOptions} options - SDK options to propagate to the AWS SDK v3 for JavaScript client
225232
*/
226233
protected async _get(
227234
name: string,
228-
options?: SecretsGetOptionsInterface
235+
options?: SecretsGetOptions
229236
): Promise<string | Uint8Array | undefined> {
230237
const sdkOptions: GetSecretValueCommandInput = {
231238
...(options?.sdkOptions || {}),
@@ -249,7 +256,7 @@ class SecretsProvider extends BaseProvider {
249256
_options?: unknown
250257
): Promise<Record<string, string | undefined>> {
251258
throw new Error('Method not implemented.');
252-
}
259+
}
253260
}
254261

255262
export {

Diff for: packages/parameters/src/secrets/getSecret.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { DEFAULT_PROVIDERS } from '../BaseProvider';
22
import { SecretsProvider } from './SecretsProvider';
3-
import type { SecretsGetOptionsInterface } from '../types/SecretsProvider';
3+
import type {
4+
SecretsGetOptions,
5+
SecretsGetOutput,
6+
SecretsGetOptionsUnion,
7+
} from '../types/SecretsProvider';
48

59
/**
610
* ## Intro
@@ -100,15 +104,23 @@ import type { SecretsGetOptionsInterface } from '../types/SecretsProvider';
100104
*
101105
*
102106
* @param {string} name - The name of the secret to retrieve
103-
* @param {SecretsGetOptionsInterface} options - Options to configure the provider
107+
* @param {SecretsGetOptions} options - Options to configure the provider
104108
* @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/
105109
*/
106-
const getSecret = async (name: string, options?: SecretsGetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> => {
110+
const getSecret = async <
111+
ExplicitUserProvidedType = undefined,
112+
InferredFromOptionsType extends SecretsGetOptionsUnion | undefined = SecretsGetOptionsUnion
113+
>(
114+
name: string,
115+
options?: InferredFromOptionsType & SecretsGetOptions
116+
): Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined> => {
107117
if (!DEFAULT_PROVIDERS.hasOwnProperty('secrets')) {
108118
DEFAULT_PROVIDERS.secrets = new SecretsProvider();
109119
}
110-
111-
return DEFAULT_PROVIDERS.secrets.get(name, options);
120+
121+
return (
122+
DEFAULT_PROVIDERS.secrets as SecretsProvider
123+
).get(name, options) as Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined>;
112124
};
113125

114126
export {

Diff for: packages/parameters/src/types/SecretsProvider.ts

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import type { GetOptionsInterface } from './BaseProvider';
2-
import type { SecretsManagerClient, SecretsManagerClientConfig, GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';
1+
import type {
2+
GetOptionsInterface,
3+
TransformOptions
4+
} from './BaseProvider';
5+
import type {
6+
SecretsManagerClient,
7+
SecretsManagerClientConfig,
8+
GetSecretValueCommandInput
9+
} from '@aws-sdk/client-secrets-manager';
310

411
/**
512
* Base interface for SecretsProviderOptions.
@@ -45,11 +52,47 @@ type SecretsProviderOptions = SecretsProviderOptionsWithClientConfig | SecretsPr
4552
* @property {GetSecretValueCommandInput} sdkOptions - Options to pass to the underlying SDK.
4653
* @property {TransformOptions} transform - Transform to be applied, can be 'json' or 'binary'.
4754
*/
48-
interface SecretsGetOptionsInterface extends GetOptionsInterface {
55+
interface SecretsGetOptions extends GetOptionsInterface {
56+
/**
57+
* Additional options to pass to the AWS SDK v3 client. Supports all options from `GetSecretValueCommandInput`.
58+
*/
4959
sdkOptions?: Omit<Partial<GetSecretValueCommandInput>, 'SecretId'>
60+
transform?: Exclude<TransformOptions, 'auto'>
5061
}
5162

63+
interface SecretsGetOptionsTransformJson extends SecretsGetOptions {
64+
transform: 'json'
65+
}
66+
67+
interface SecretsGetOptionsTransformBinary extends SecretsGetOptions {
68+
transform: 'binary'
69+
}
70+
71+
interface SecretsGetOptionsTransformNone extends SecretsGetOptions {
72+
transform?: never
73+
}
74+
75+
type SecretsGetOptionsUnion =
76+
SecretsGetOptionsTransformNone |
77+
SecretsGetOptionsTransformJson |
78+
SecretsGetOptionsTransformBinary |
79+
undefined;
80+
81+
/**
82+
* Generic output type for the SecretsProvider get method.
83+
*/
84+
type SecretsGetOutput<ExplicitUserProvidedType = undefined, InferredFromOptionsType = undefined> =
85+
undefined extends ExplicitUserProvidedType ?
86+
undefined extends InferredFromOptionsType ? string | Uint8Array :
87+
InferredFromOptionsType extends SecretsGetOptionsTransformNone ? string | Uint8Array :
88+
InferredFromOptionsType extends SecretsGetOptionsTransformBinary ? string :
89+
InferredFromOptionsType extends SecretsGetOptionsTransformJson ? Record<string, unknown> :
90+
never
91+
: ExplicitUserProvidedType;
92+
5293
export type {
5394
SecretsProviderOptions,
54-
SecretsGetOptionsInterface,
95+
SecretsGetOptions,
96+
SecretsGetOutput,
97+
SecretsGetOptionsUnion,
5598
};

Diff for: packages/parameters/tests/unit/getSecret.test.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('Function: getSecret', () => {
2929
});
3030

3131
// Act
32-
const result = await getSecret(secretName);
32+
const result: string | Uint8Array | undefined = await getSecret(secretName);
3333

3434
// Assess
3535
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName });
@@ -50,7 +50,7 @@ describe('Function: getSecret', () => {
5050
});
5151

5252
// Act
53-
const result = await getSecret(secretName);
53+
const result: string | Uint8Array | undefined = await getSecret(secretName);
5454

5555
// Assess
5656
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName });
@@ -59,4 +59,48 @@ describe('Function: getSecret', () => {
5959

6060
});
6161

62+
test('when called and transform `JSON` is specified, it returns an object with correct type', async () => {
63+
64+
// Prepare
65+
const provider = new SecretsProvider();
66+
DEFAULT_PROVIDERS.secrets = provider;
67+
const secretName = 'foo';
68+
const secretValue = JSON.stringify({ hello: 'world' });
69+
const client = mockClient(SecretsManagerClient).on(GetSecretValueCommand).resolves({
70+
SecretString: secretValue,
71+
});
72+
73+
// Act
74+
const value: Record<string, unknown> | undefined = await getSecret(secretName, { transform: 'json' });
75+
76+
// Assess
77+
expect(client).toReceiveCommandWith(GetSecretValueCommand, {
78+
SecretId: secretName,
79+
});
80+
expect(value).toStrictEqual(JSON.parse(secretValue));
81+
82+
});
83+
84+
test('when called and transform `JSON` is specified as well as an explicit `K` type, it returns a result with correct type', async () => {
85+
86+
// Prepare
87+
const provider = new SecretsProvider();
88+
DEFAULT_PROVIDERS.secrets = provider;
89+
const secretName = 'foo';
90+
const secretValue = JSON.stringify(5);
91+
const client = mockClient(SecretsManagerClient).on(GetSecretValueCommand).resolves({
92+
SecretString: secretValue,
93+
});
94+
95+
// Act
96+
const value: number | undefined = await getSecret<number>(secretName, { transform: 'json' });
97+
98+
// Assess
99+
expect(client).toReceiveCommandWith(GetSecretValueCommand, {
100+
SecretId: secretName,
101+
});
102+
expect(value).toBe(JSON.parse(secretValue));
103+
104+
});
105+
62106
});

0 commit comments

Comments
 (0)