diff --git a/package-lock.json b/package-lock.json index deacb076cc..7bcb6cbcd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16471,18 +16471,7 @@ "version": "1.4.1", "license": "MIT-0", "dependencies": { - "@aws-sdk/util-base64-node": "^3.209.0" - } - }, - "packages/parameters/node_modules/@aws-sdk/util-base64-node": { - "version": "3.209.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/util-buffer-from": "3.208.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" + "@aws-sdk/util-base64": "^3.208.0" } }, "packages/tracer": { @@ -16752,16 +16741,7 @@ "@aws-lambda-powertools/parameters": { "version": "file:packages/parameters", "requires": { - "@aws-sdk/util-base64-node": "^3.209.0" - }, - "dependencies": { - "@aws-sdk/util-base64-node": { - "version": "3.209.0", - "requires": { - "@aws-sdk/util-buffer-from": "3.208.0", - "tslib": "^2.3.1" - } - } + "@aws-sdk/util-base64": "^3.208.0" } }, "@aws-lambda-powertools/tracer": { diff --git a/packages/parameters/package.json b/packages/parameters/package.json index a204448c2e..0ade3eabe7 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -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", @@ -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", diff --git a/packages/parameters/src/BaseProvider.ts b/packages/parameters/src/BaseProvider.ts index 9a081cc7f5..d664217d06 100644 --- a/packages/parameters/src/BaseProvider.ts +++ b/packages/parameters/src/BaseProvider.ts @@ -1,4 +1,4 @@ -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'; @@ -6,14 +6,17 @@ 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 = {}; + abstract class BaseProvider implements BaseProviderInterface { protected store: Map; - public constructor () { + public constructor() { this.store = new Map(); } - public addToCache(key: string, value: string | Record, maxAge: number): void { + public addToCache(key: string, value: string | Uint8Array | Record, maxAge: number): void { if (maxAge <= 0) return; this.store.set(key, new ExpirableValue(value, maxAge)); @@ -22,7 +25,7 @@ abstract class BaseProvider implements BaseProviderInterface { public clearCache(): void { this.store.clear(); } - + /** * Retrieve a parameter value or return the cached value * @@ -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> { + public async get(name: string, options?: GetOptionsInterface): Promise> { const configs = new GetOptions(options); const key = [ name, configs.transform ].toString(); @@ -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); } @@ -76,9 +79,9 @@ abstract class BaseProvider implements BaseProviderInterface { return this.store.get(key)!.value as Record; } - let values: Record = {}; + 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); } @@ -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; + protected abstract _get(name: string, options?: unknown): Promise; - protected abstract _getMultiple(path: string, sdkOptions?: unknown): Promise>; + /** + * 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>; /** * Check whether a key has expired in the cache or not @@ -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 | undefined => { +const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string = ''): string | Record | 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; } 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, transform: TransformOptions, throwOnTransformError: boolean): Record => { - const transformedValues: Record = {}; +const transformValues = (value: Record, transform: TransformOptions, throwOnTransformError: boolean): Record | undefined> => { + const transformedValues: Record | undefined> = {}; for (const [ entryKey, entryValue ] of Object.entries(value)) { try { transformedValues[entryKey] = transformValue(entryValue, transform, throwOnTransformError, entryKey); @@ -167,4 +177,5 @@ export { BaseProvider, ExpirableValue, transformValue, + DEFAULT_PROVIDERS, }; \ No newline at end of file diff --git a/packages/parameters/src/ExpirableValue.ts b/packages/parameters/src/ExpirableValue.ts index d4b3b2bda4..7dcc3a2473 100644 --- a/packages/parameters/src/ExpirableValue.ts +++ b/packages/parameters/src/ExpirableValue.ts @@ -2,9 +2,9 @@ import type { ExpirableValueInterface } from './types'; class ExpirableValue implements ExpirableValueInterface { public ttl: number; - public value: string | Record; + public value: string | Uint8Array | Record; - public constructor(value: string | Record, maxAge: number) { + public constructor(value: string | Uint8Array | Record, maxAge: number) { this.value = value; const timeNow = new Date(); this.ttl = timeNow.setSeconds(timeNow.getSeconds() + maxAge); @@ -15,6 +15,6 @@ class ExpirableValue implements ExpirableValueInterface { } } -export { +export { ExpirableValue }; \ No newline at end of file diff --git a/packages/parameters/src/types/BaseProvider.ts b/packages/parameters/src/types/BaseProvider.ts index 8f6754d2d3..60a5a2f485 100644 --- a/packages/parameters/src/types/BaseProvider.ts +++ b/packages/parameters/src/types/BaseProvider.ts @@ -16,12 +16,12 @@ interface GetMultipleOptionsInterface { } interface ExpirableValueInterface { - value: string | Record + value: string | Uint8Array | Record ttl: number } interface BaseProviderInterface { - get(name: string, options?: GetOptionsInterface): Promise> + get(name: string, options?: GetOptionsInterface): Promise> getMultiple(path: string, options?: GetMultipleOptionsInterface): Promise> } diff --git a/packages/parameters/tests/unit/BaseProvider.test.ts b/packages/parameters/tests/unit/BaseProvider.test.ts index 8b7e0e3f78..6a2bb0593a 100644 --- a/packages/parameters/tests/unit/BaseProvider.test.ts +++ b/packages/parameters/tests/unit/BaseProvider.test.ts @@ -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(); @@ -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); }); @@ -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); }); @@ -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'); + + }); }); @@ -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); }); @@ -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); }); @@ -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); });