From 5c6d527b0ad983e893ba07f8a334b4085b6ae6a7 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 14 Apr 2023 17:52:08 +0200 Subject: [PATCH] feat(parameters): add adaptive types to SecretsProvider (#1411) --- .../parameters/src/secrets/SecretsProvider.ts | 29 +++++++---- packages/parameters/src/secrets/getSecret.ts | 22 ++++++-- .../parameters/src/types/SecretsProvider.ts | 51 +++++++++++++++++-- .../parameters/tests/unit/getSecret.test.ts | 48 ++++++++++++++++- 4 files changed, 128 insertions(+), 22 deletions(-) diff --git a/packages/parameters/src/secrets/SecretsProvider.ts b/packages/parameters/src/secrets/SecretsProvider.ts index c8d1e56b77..2144fe1268 100644 --- a/packages/parameters/src/secrets/SecretsProvider.ts +++ b/packages/parameters/src/secrets/SecretsProvider.ts @@ -6,7 +6,9 @@ import { import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; import type { SecretsProviderOptions, - SecretsGetOptionsInterface + SecretsGetOptions, + SecretsGetOutput, + SecretsGetOptionsUnion, } from '../types/SecretsProvider'; /** @@ -157,7 +159,7 @@ class SecretsProvider extends BaseProvider { * * @param {SecretsProviderOptions} config - The configuration object. */ - public constructor (config?: SecretsProviderOptions) { + public constructor(config?: SecretsProviderOptions) { super(); if (config?.awsSdkV3Client) { @@ -170,7 +172,6 @@ class SecretsProvider extends BaseProvider { const clientConfig = config?.clientConfig || {}; this.client = new SecretsManagerClient(clientConfig); } - } /** @@ -197,14 +198,20 @@ class SecretsProvider extends BaseProvider { * For usage examples check {@link SecretsProvider}. * * @param {string} name - The name of the secret - * @param {SecretsGetOptionsInterface} options - Options to customize the retrieval of the secret + * @param {SecretsGetOptions} options - Options to customize the retrieval of the secret * @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/ */ - public async get( + public async get< + ExplicitUserProvidedType = undefined, + InferredFromOptionsType extends SecretsGetOptionsUnion | undefined = SecretsGetOptionsUnion + >( name: string, - options?: SecretsGetOptionsInterface - ): Promise> { - return super.get(name, options); + options?: InferredFromOptionsType & SecretsGetOptions + ): Promise | undefined> { + return super.get( + name, + options + ) as Promise | undefined>; } /** @@ -221,11 +228,11 @@ class SecretsProvider extends BaseProvider { * Retrieve a configuration from AWS AppConfig. * * @param {string} name - Name of the configuration or its ID - * @param {SecretsGetOptionsInterface} options - SDK options to propagate to the AWS SDK v3 for JavaScript client + * @param {SecretsGetOptions} options - SDK options to propagate to the AWS SDK v3 for JavaScript client */ protected async _get( name: string, - options?: SecretsGetOptionsInterface + options?: SecretsGetOptions ): Promise { const sdkOptions: GetSecretValueCommandInput = { ...(options?.sdkOptions || {}), @@ -249,7 +256,7 @@ class SecretsProvider extends BaseProvider { _options?: unknown ): Promise> { throw new Error('Method not implemented.'); - } + } } export { diff --git a/packages/parameters/src/secrets/getSecret.ts b/packages/parameters/src/secrets/getSecret.ts index a6b39b3229..812e165d5b 100644 --- a/packages/parameters/src/secrets/getSecret.ts +++ b/packages/parameters/src/secrets/getSecret.ts @@ -1,6 +1,10 @@ import { DEFAULT_PROVIDERS } from '../BaseProvider'; import { SecretsProvider } from './SecretsProvider'; -import type { SecretsGetOptionsInterface } from '../types/SecretsProvider'; +import type { + SecretsGetOptions, + SecretsGetOutput, + SecretsGetOptionsUnion, +} from '../types/SecretsProvider'; /** * ## Intro @@ -100,15 +104,23 @@ import type { SecretsGetOptionsInterface } from '../types/SecretsProvider'; * * * @param {string} name - The name of the secret to retrieve - * @param {SecretsGetOptionsInterface} options - Options to configure the provider + * @param {SecretsGetOptions} options - Options to configure the provider * @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/ */ -const getSecret = async (name: string, options?: SecretsGetOptionsInterface): Promise> => { +const getSecret = async < + ExplicitUserProvidedType = undefined, + InferredFromOptionsType extends SecretsGetOptionsUnion | undefined = SecretsGetOptionsUnion +>( + name: string, + options?: InferredFromOptionsType & SecretsGetOptions +): Promise | undefined> => { if (!DEFAULT_PROVIDERS.hasOwnProperty('secrets')) { DEFAULT_PROVIDERS.secrets = new SecretsProvider(); } - - return DEFAULT_PROVIDERS.secrets.get(name, options); + + return ( + DEFAULT_PROVIDERS.secrets as SecretsProvider + ).get(name, options) as Promise | undefined>; }; export { diff --git a/packages/parameters/src/types/SecretsProvider.ts b/packages/parameters/src/types/SecretsProvider.ts index 152dff709b..07c23c82bc 100644 --- a/packages/parameters/src/types/SecretsProvider.ts +++ b/packages/parameters/src/types/SecretsProvider.ts @@ -1,5 +1,12 @@ -import type { GetOptionsInterface } from './BaseProvider'; -import type { SecretsManagerClient, SecretsManagerClientConfig, GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; +import type { + GetOptionsInterface, + TransformOptions +} from './BaseProvider'; +import type { + SecretsManagerClient, + SecretsManagerClientConfig, + GetSecretValueCommandInput +} from '@aws-sdk/client-secrets-manager'; /** * Base interface for SecretsProviderOptions. @@ -45,11 +52,47 @@ type SecretsProviderOptions = SecretsProviderOptionsWithClientConfig | SecretsPr * @property {GetSecretValueCommandInput} sdkOptions - Options to pass to the underlying SDK. * @property {TransformOptions} transform - Transform to be applied, can be 'json' or 'binary'. */ -interface SecretsGetOptionsInterface extends GetOptionsInterface { +interface SecretsGetOptions extends GetOptionsInterface { + /** + * Additional options to pass to the AWS SDK v3 client. Supports all options from `GetSecretValueCommandInput`. + */ sdkOptions?: Omit, 'SecretId'> + transform?: Exclude } +interface SecretsGetOptionsTransformJson extends SecretsGetOptions { + transform: 'json' +} + +interface SecretsGetOptionsTransformBinary extends SecretsGetOptions { + transform: 'binary' +} + +interface SecretsGetOptionsTransformNone extends SecretsGetOptions { + transform?: never +} + +type SecretsGetOptionsUnion = + SecretsGetOptionsTransformNone | + SecretsGetOptionsTransformJson | + SecretsGetOptionsTransformBinary | + undefined; + +/** + * Generic output type for the SecretsProvider get method. + */ +type SecretsGetOutput = + undefined extends ExplicitUserProvidedType ? + undefined extends InferredFromOptionsType ? string | Uint8Array : + InferredFromOptionsType extends SecretsGetOptionsTransformNone ? string | Uint8Array : + InferredFromOptionsType extends SecretsGetOptionsTransformBinary ? string : + InferredFromOptionsType extends SecretsGetOptionsTransformJson ? Record : + never + : ExplicitUserProvidedType; + export type { SecretsProviderOptions, - SecretsGetOptionsInterface, + SecretsGetOptions, + SecretsGetOutput, + SecretsGetOptionsUnion, }; \ No newline at end of file diff --git a/packages/parameters/tests/unit/getSecret.test.ts b/packages/parameters/tests/unit/getSecret.test.ts index 8fcbd634ad..ad9360daae 100644 --- a/packages/parameters/tests/unit/getSecret.test.ts +++ b/packages/parameters/tests/unit/getSecret.test.ts @@ -29,7 +29,7 @@ describe('Function: getSecret', () => { }); // Act - const result = await getSecret(secretName); + const result: string | Uint8Array | undefined = await getSecret(secretName); // Assess expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName }); @@ -50,7 +50,7 @@ describe('Function: getSecret', () => { }); // Act - const result = await getSecret(secretName); + const result: string | Uint8Array | undefined = await getSecret(secretName); // Assess expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName }); @@ -59,4 +59,48 @@ describe('Function: getSecret', () => { }); + test('when called and transform `JSON` is specified, it returns an object with correct type', async () => { + + // Prepare + const provider = new SecretsProvider(); + DEFAULT_PROVIDERS.secrets = provider; + const secretName = 'foo'; + const secretValue = JSON.stringify({ hello: 'world' }); + const client = mockClient(SecretsManagerClient).on(GetSecretValueCommand).resolves({ + SecretString: secretValue, + }); + + // Act + const value: Record | undefined = await getSecret(secretName, { transform: 'json' }); + + // Assess + expect(client).toReceiveCommandWith(GetSecretValueCommand, { + SecretId: secretName, + }); + expect(value).toStrictEqual(JSON.parse(secretValue)); + + }); + + test('when called and transform `JSON` is specified as well as an explicit `K` type, it returns a result with correct type', async () => { + + // Prepare + const provider = new SecretsProvider(); + DEFAULT_PROVIDERS.secrets = provider; + const secretName = 'foo'; + const secretValue = JSON.stringify(5); + const client = mockClient(SecretsManagerClient).on(GetSecretValueCommand).resolves({ + SecretString: secretValue, + }); + + // Act + const value: number | undefined = await getSecret(secretName, { transform: 'json' }); + + // Assess + expect(client).toReceiveCommandWith(GetSecretValueCommand, { + SecretId: secretName, + }); + expect(value).toBe(JSON.parse(secretValue)); + + }); + }); \ No newline at end of file