Skip to content

Commit

Permalink
feat(parameters): add adaptive types to SecretsProvider (#1411)
Browse files Browse the repository at this point in the history
  • Loading branch information
dreamorosi authored Apr 14, 2023
1 parent 6894f9a commit 5c6d527
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 22 deletions.
29 changes: 18 additions & 11 deletions packages/parameters/src/secrets/SecretsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';
import type {
SecretsProviderOptions,
SecretsGetOptionsInterface
SecretsGetOptions,
SecretsGetOutput,
SecretsGetOptionsUnion,
} from '../types/SecretsProvider';

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -170,7 +172,6 @@ class SecretsProvider extends BaseProvider {
const clientConfig = config?.clientConfig || {};
this.client = new SecretsManagerClient(clientConfig);
}

}

/**
Expand All @@ -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<undefined | string | Uint8Array | Record<string, unknown>> {
return super.get(name, options);
options?: InferredFromOptionsType & SecretsGetOptions
): Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined> {
return super.get(
name,
options
) as Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined>;
}

/**
Expand All @@ -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<string | Uint8Array | undefined> {
const sdkOptions: GetSecretValueCommandInput = {
...(options?.sdkOptions || {}),
Expand All @@ -249,7 +256,7 @@ class SecretsProvider extends BaseProvider {
_options?: unknown
): Promise<Record<string, string | undefined>> {
throw new Error('Method not implemented.');
}
}
}

export {
Expand Down
22 changes: 17 additions & 5 deletions packages/parameters/src/secrets/getSecret.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<undefined | string | Uint8Array | Record<string, unknown>> => {
const getSecret = async <
ExplicitUserProvidedType = undefined,
InferredFromOptionsType extends SecretsGetOptionsUnion | undefined = SecretsGetOptionsUnion
>(
name: string,
options?: InferredFromOptionsType & SecretsGetOptions
): Promise<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | 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<SecretsGetOutput<ExplicitUserProvidedType, InferredFromOptionsType> | undefined>;
};

export {
Expand Down
51 changes: 47 additions & 4 deletions packages/parameters/src/types/SecretsProvider.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Partial<GetSecretValueCommandInput>, 'SecretId'>
transform?: Exclude<TransformOptions, 'auto'>
}

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<ExplicitUserProvidedType = undefined, InferredFromOptionsType = undefined> =
undefined extends ExplicitUserProvidedType ?
undefined extends InferredFromOptionsType ? string | Uint8Array :
InferredFromOptionsType extends SecretsGetOptionsTransformNone ? string | Uint8Array :
InferredFromOptionsType extends SecretsGetOptionsTransformBinary ? string :
InferredFromOptionsType extends SecretsGetOptionsTransformJson ? Record<string, unknown> :
never
: ExplicitUserProvidedType;

export type {
SecretsProviderOptions,
SecretsGetOptionsInterface,
SecretsGetOptions,
SecretsGetOutput,
SecretsGetOptionsUnion,
};
48 changes: 46 additions & 2 deletions packages/parameters/tests/unit/getSecret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 });
Expand All @@ -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<string, unknown> | 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<number>(secretName, { transform: 'json' });

// Assess
expect(client).toReceiveCommandWith(GetSecretValueCommand, {
SecretId: secretName,
});
expect(value).toBe(JSON.parse(secretValue));

});

});

0 comments on commit 5c6d527

Please sign in to comment.