Skip to content

refactor(parameters): BaseProvider support for Uint8Array #1205

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
Dec 29, 2022
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
24 changes: 2 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/parameters/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aws-lambda-powertools/parameters",
"version": "1.4.1",
"version": "1.5.0",
"description": "The parameters package for the AWS Lambda Powertools for TypeScript library",
"author": {
"name": "Amazon Web Services",
Expand Down Expand Up @@ -42,7 +42,7 @@
"url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues"
},
"dependencies": {
"@aws-sdk/util-base64-node": "^3.209.0"
"@aws-sdk/util-base64": "^3.208.0"
},
"keywords": [
"aws",
Expand Down
55 changes: 33 additions & 22 deletions packages/parameters/src/BaseProvider.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { fromBase64 } from '@aws-sdk/util-base64-node';
import { fromBase64 } from '@aws-sdk/util-base64';
import { GetOptions } from './GetOptions';
import { GetMultipleOptions } from './GetMultipleOptions';
import { ExpirableValue } from './ExpirableValue';
import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants';
import { GetParameterError, TransformParameterError } from './Exceptions';
import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types';

// These providers are dinamycally intialized on first use of the helper functions
const DEFAULT_PROVIDERS: Record<string, BaseProvider> = {};

abstract class BaseProvider implements BaseProviderInterface {
protected store: Map<string, ExpirableValue>;

public constructor () {
public constructor() {
this.store = new Map();
}

public addToCache(key: string, value: string | Record<string, unknown>, maxAge: number): void {
public addToCache(key: string, value: string | Uint8Array | Record<string, unknown>, maxAge: number): void {
if (maxAge <= 0) return;

this.store.set(key, new ExpirableValue(value, maxAge));
Expand All @@ -22,7 +25,7 @@ abstract class BaseProvider implements BaseProviderInterface {
public clearCache(): void {
this.store.clear();
}

/**
* Retrieve a parameter value or return the cached value
*
Expand All @@ -37,7 +40,7 @@ abstract class BaseProvider implements BaseProviderInterface {
* @param {string} name - Parameter name
* @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
*/
public async get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Record<string, unknown>> {
public async get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> {
const configs = new GetOptions(options);
const key = [ name, configs.transform ].toString();

Expand All @@ -49,7 +52,7 @@ abstract class BaseProvider implements BaseProviderInterface {

let value;
try {
value = await this._get(name, options?.sdkOptions);
value = await this._get(name, options);
} catch (error) {
throw new GetParameterError((error as Error).message);
}
Expand All @@ -76,9 +79,9 @@ abstract class BaseProvider implements BaseProviderInterface {
return this.store.get(key)!.value as Record<string, unknown>;
}

let values: Record<string, unknown> = {};
let values = {};
try {
values = await this._getMultiple(path, options?.sdkOptions);
values = await this._getMultiple(path, options);
} catch (error) {
throw new GetParameterError((error as Error).message);
}
Expand All @@ -99,11 +102,17 @@ abstract class BaseProvider implements BaseProviderInterface {
* Retrieve parameter value from the underlying parameter store
*
* @param {string} name - Parameter name
* @param {unknown} sdkOptions - Options to pass to the underlying AWS SDK
* @param {unknown} options - Options to pass to the underlying implemented method
*/
protected abstract _get(name: string, sdkOptions?: unknown): Promise<string | undefined>;
protected abstract _get(name: string, options?: unknown): Promise<string | Uint8Array | undefined>;

protected abstract _getMultiple(path: string, sdkOptions?: unknown): Promise<Record<string, string|undefined>>;
/**
* Retrieve multiple parameter values from the underlying parameter store
*
* @param {string} path - Parameter name
* @param {unknown} options - Options to pass to the underlying implementated method
*/
protected abstract _getMultiple(path: string, options?: unknown): Promise<Record<string, string | undefined>>;

/**
* Check whether a key has expired in the cache or not
Expand All @@ -115,42 +124,43 @@ abstract class BaseProvider implements BaseProviderInterface {
private hasKeyExpiredInCache(key: string): boolean {
const value = this.store.get(key);
if (value) return value.isExpired();

return true;
}

}

// TODO: revisit `value` type once we are clearer on the types returned by the various SDKs
const transformValue = (value: unknown, transform: TransformOptions, throwOnTransformError: boolean, key: string = ''): string | Record<string, unknown> | undefined => {
const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string = ''): string | Record<string, unknown> | undefined => {
try {
const normalizedTransform = transform.toLowerCase();
if (
(normalizedTransform === TRANSFORM_METHOD_JSON ||
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_JSON}`))) &&
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_JSON}`))) &&
typeof value === 'string'
) {
return JSON.parse(value) as Record<string, unknown>;
} else if (
(normalizedTransform === TRANSFORM_METHOD_BINARY ||
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`))) &&
typeof value === 'string'
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`)))
) {
return new TextDecoder('utf-8').decode(fromBase64(value));
if (typeof value === 'string') {
return new TextDecoder('utf-8').decode(fromBase64(value));
} else {
return new TextDecoder('utf-8').decode(value);
}
} else {
// TODO: revisit this type once we are clearer on types returned by SDKs
return value as string;
}
} catch (error) {
if (throwOnTransformError)
throw new TransformParameterError(transform, (error as Error).message);

return;
}
};

const transformValues = (value: Record<string, unknown>, transform: TransformOptions, throwOnTransformError: boolean): Record<string, unknown> => {
const transformedValues: Record<string, unknown> = {};
const transformValues = (value: Record<string, string | undefined>, transform: TransformOptions, throwOnTransformError: boolean): Record<string, string | Record<string, unknown> | undefined> => {
const transformedValues: Record<string, string | Record<string, unknown> | undefined> = {};
for (const [ entryKey, entryValue ] of Object.entries(value)) {
try {
transformedValues[entryKey] = transformValue(entryValue, transform, throwOnTransformError, entryKey);
Expand All @@ -167,4 +177,5 @@ export {
BaseProvider,
ExpirableValue,
transformValue,
DEFAULT_PROVIDERS,
};
6 changes: 3 additions & 3 deletions packages/parameters/src/ExpirableValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { ExpirableValueInterface } from './types';

class ExpirableValue implements ExpirableValueInterface {
public ttl: number;
public value: string | Record<string, unknown>;
public value: string | Uint8Array | Record<string, unknown>;

public constructor(value: string | Record<string, unknown>, maxAge: number) {
public constructor(value: string | Uint8Array | Record<string, unknown>, maxAge: number) {
this.value = value;
const timeNow = new Date();
this.ttl = timeNow.setSeconds(timeNow.getSeconds() + maxAge);
Expand All @@ -15,6 +15,6 @@ class ExpirableValue implements ExpirableValueInterface {
}
}

export {
export {
ExpirableValue
};
4 changes: 2 additions & 2 deletions packages/parameters/src/types/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ interface GetMultipleOptionsInterface {
}

interface ExpirableValueInterface {
value: string | Record<string, unknown>
value: string | Uint8Array | Record<string, unknown>
ttl: number
}

interface BaseProviderInterface {
get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Record<string, unknown>>
get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>>
getMultiple(path: string, options?: GetMultipleOptionsInterface): Promise<void | Record<string, unknown>>
}

Expand Down
46 changes: 39 additions & 7 deletions packages/parameters/tests/unit/BaseProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { BaseProvider, ExpirableValue, GetParameterError, TransformParameterError } from '../../src';
import { toBase64 } from '@aws-sdk/util-base64-node';
import { toBase64 } from '@aws-sdk/util-base64';

const encoder = new TextEncoder();

Expand Down Expand Up @@ -76,7 +76,7 @@ describe('Class: BaseProvider', () => {
// Prepare
const provider = new TestProvider();

// Act / Assess
// Act & Assess
await expect(provider.get('my-parameter')).rejects.toThrowError(GetParameterError);

});
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('Class: BaseProvider', () => {
const provider = new TestProvider();
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData)));

// Act / Assess
// Act & Assess
await expect(provider.get('my-parameter', { transform: 'json' })).rejects.toThrowError(TransformParameterError);

});
Expand All @@ -181,10 +181,42 @@ describe('Class: BaseProvider', () => {
const provider = new TestProvider();
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData)));

// Act / Assess
// Act & Assess
await expect(provider.get('my-parameter', { transform: 'binary' })).rejects.toThrowError(TransformParameterError);

});

test('when called with no transform, and the value is a valid binary, it returns the binary as-is', async () => {

// Prepare
const mockData = encoder.encode('my-value');
const provider = new TestProvider();
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData as unknown as string)));

// Act
const value = await provider.get('my-parameter');

// Assess
expect(value).toBeInstanceOf(Uint8Array);
expect(value).toEqual(mockData);

});

test('when called with a binary transform, and the value is a valid binary, it returns the decoded value', async () => {

// Prepare
const mockData = encoder.encode('my-value');
const provider = new TestProvider();
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData as unknown as string)));

// Act
const value = await provider.get('my-parameter', { transform: 'binary' });

// Assess
expect(typeof value).toBe('string');
expect(value).toEqual('my-value');

});

});

Expand All @@ -195,7 +227,7 @@ describe('Class: BaseProvider', () => {
const provider = new TestProvider();
jest.spyOn(provider, '_getMultiple').mockImplementation(() => new Promise((_resolve, reject) => reject(new Error('Some error.'))));

// Act / Assess
// Act & Assess
await expect(provider.getMultiple('my-parameter')).rejects.toThrowError(GetParameterError);

});
Expand Down Expand Up @@ -267,7 +299,7 @@ describe('Class: BaseProvider', () => {
const provider = new TestProvider();
jest.spyOn(provider, '_getMultiple').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData)));

// Act / Assess
// Act & Assess
await expect(provider.getMultiple('my-path', { transform: 'json', throwOnTransformError: true })).rejects.toThrowError(TransformParameterError);

});
Expand Down Expand Up @@ -316,7 +348,7 @@ describe('Class: BaseProvider', () => {
const provider = new TestProvider();
jest.spyOn(provider, '_getMultiple').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData)));

// Act / Assess
// Act & Assess
await expect(provider.getMultiple('my-path', { transform: 'binary', throwOnTransformError: true })).rejects.toThrowError(TransformParameterError);

});
Expand Down