Skip to content

feat(parameters): add adaptive types to SecretsProvider #1411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));

});

});