Skip to content

Commit d717a26

Browse files
authored
feat(parameters): added BaseProvider class (#1168)
* feat: added baseprovider class * tests: added unit tests * chore: added package to unit tests run * chore: added comments * chore: break down classes in files * chore: remove redundant store init value * refactor: moved constants in separate file * chore: made store protected * fix: removed cache pruning * feat: added baseprovider class
1 parent 36caa4e commit d717a26

18 files changed

+918
-4
lines changed

Diff for: .github/workflows/reusable-run-linting-check-and-unit-tests.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ jobs:
4242
if: steps.cache-node-modules.outputs.cache-hit == 'true'
4343
run: |
4444
npm run build -w packages/commons
45-
npm run build -w packages/logger & npm run build -w packages/tracer & npm run build -w packages/metrics
45+
npm run build -w packages/logger & npm run build -w packages/tracer & npm run build -w packages/metrics -w packages/parameters
4646
- name: Run linting
47-
run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics
47+
run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters
4848
- name: Run unit tests
49-
run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics
49+
run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters
5050
check-examples:
5151
runs-on: ubuntu-latest
5252
env:

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"packages/commons",
99
"packages/logger",
1010
"packages/metrics",
11-
"packages/tracer"
11+
"packages/tracer",
12+
"packages/parameters"
1213
],
1314
"scripts": {
1415
"init-environment": "husky install",

Diff for: packages/parameters/jest.config.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module.exports = {
2+
displayName: {
3+
name: 'AWS Lambda Powertools utility: PARAMETERS',
4+
color: 'magenta',
5+
},
6+
'runner': 'groups',
7+
'preset': 'ts-jest',
8+
'transform': {
9+
'^.+\\.ts?$': 'ts-jest',
10+
},
11+
moduleFileExtensions: [ 'js', 'ts' ],
12+
'collectCoverageFrom': [
13+
'**/src/**/*.ts',
14+
'!**/node_modules/**',
15+
],
16+
'testMatch': ['**/?(*.)+(spec|test).ts'],
17+
'roots': [
18+
'<rootDir>/src',
19+
'<rootDir>/tests',
20+
],
21+
'testPathIgnorePatterns': [
22+
'/node_modules/',
23+
],
24+
'testEnvironment': 'node',
25+
'coveragePathIgnorePatterns': [
26+
'/node_modules/',
27+
'/types/',
28+
],
29+
'coverageThreshold': {
30+
'global': {
31+
'statements': 100,
32+
'branches': 100,
33+
'functions': 100,
34+
'lines': 100,
35+
},
36+
},
37+
'coverageReporters': [
38+
'json-summary',
39+
'text',
40+
'lcov'
41+
],
42+
'setupFiles': [
43+
'<rootDir>/tests/helpers/populateEnvironmentVariables.ts'
44+
]
45+
};

Diff for: packages/parameters/package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@aws-lambda-powertools/parameters",
3+
"version": "1.4.1",
4+
"description": "The parameters package for the AWS Lambda Powertools for TypeScript library",
5+
"author": {
6+
"name": "Amazon Web Services",
7+
"url": "https://aws.amazon.com"
8+
},
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"scripts": {
13+
"commit": "commit",
14+
"test": "npm run test:unit",
15+
"test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose",
16+
"test:e2e:nodejs12x": "echo \"Not implemented\"",
17+
"test:e2e:nodejs14x": "echo \"Not implemented\"",
18+
"test:e2e:nodejs16x": "echo \"Not implemented\"",
19+
"test:e2e": "echo \"Not implemented\"",
20+
"watch": "jest --watch",
21+
"build": "tsc",
22+
"lint": "eslint --ext .ts --no-error-on-unmatched-pattern src tests",
23+
"lint-fix": "eslint --fix --ext .ts --no-error-on-unmatched-pattern src tests",
24+
"package": "mkdir -p dist/ && npm pack && mv *.tgz dist/",
25+
"package-bundle": "../../package-bundler.sh parameters-bundle ./dist",
26+
"prepare": "npm run build",
27+
"postversion": "git push --tags"
28+
},
29+
"homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/parameters#readme",
30+
"license": "MIT-0",
31+
"main": "./lib/index.js",
32+
"types": "./lib/index.d.ts",
33+
"devDependencies": {},
34+
"files": [
35+
"lib"
36+
],
37+
"repository": {
38+
"type": "git",
39+
"url": "git+https://github.com/awslabs/aws-lambda-powertools-typescript.git"
40+
},
41+
"bugs": {
42+
"url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues"
43+
},
44+
"dependencies": {},
45+
"keywords": [
46+
"aws",
47+
"lambda",
48+
"powertools",
49+
"ssm",
50+
"secrets",
51+
"serverless",
52+
"nodejs"
53+
]
54+
}

Diff for: packages/parameters/src/BaseProvider.ts

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { fromBase64 } from '@aws-sdk/util-base64-node';
2+
import { GetOptions } from './GetOptions';
3+
import { GetMultipleOptions } from './GetMultipleOptions';
4+
import { ExpirableValue } from './ExpirableValue';
5+
import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants';
6+
import { GetParameterError, TransformParameterError } from './Exceptions';
7+
import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types';
8+
9+
abstract class BaseProvider implements BaseProviderInterface {
10+
protected store: Map<string, ExpirableValue>;
11+
12+
public constructor () {
13+
this.store = new Map();
14+
}
15+
16+
public addToCache(key: string, value: string | Record<string, unknown>, maxAge: number): void {
17+
if (maxAge <= 0) return;
18+
19+
this.store.set(key, new ExpirableValue(value, maxAge));
20+
}
21+
22+
public clearCache(): void {
23+
this.store.clear();
24+
}
25+
26+
/**
27+
* Retrieve a parameter value or return the cached value
28+
*
29+
* If there are multiple calls to the same parameter but in a different transform, they will be stored multiple times.
30+
* This allows us to optimize by transforming the data only once per retrieval, thus there is no need to transform cached values multiple times.
31+
*
32+
* However, this means that we need to make multiple calls to the underlying parameter store if we need to return it in different transforms.
33+
*
34+
* Since the number of supported transform is small and the probability that a given parameter will always be used in a specific transform,
35+
* this should be an acceptable tradeoff.
36+
*
37+
* @param {string} name - Parameter name
38+
* @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
39+
*/
40+
public async get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Record<string, unknown>> {
41+
const configs = new GetOptions(options);
42+
const key = [ name, configs.transform ].toString();
43+
44+
if (!configs.forceFetch && !this.hasKeyExpiredInCache(key)) {
45+
// If the code enters in this block, then the key must exist & not have been expired
46+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
47+
return this.store.get(key)!.value;
48+
}
49+
50+
let value;
51+
try {
52+
value = await this._get(name, options?.sdkOptions);
53+
} catch (error) {
54+
throw new GetParameterError((error as Error).message);
55+
}
56+
57+
if (value && configs.transform) {
58+
value = transformValue(value, configs.transform, true);
59+
}
60+
61+
if (value) {
62+
this.addToCache(key, value, configs.maxAge);
63+
}
64+
65+
// TODO: revisit return type once providers are implemented, it might be missing binary when not transformed
66+
return value;
67+
}
68+
69+
public async getMultiple(path: string, options?: GetMultipleOptionsInterface): Promise<undefined | Record<string, unknown>> {
70+
const configs = new GetMultipleOptions(options || {});
71+
const key = [ path, configs.transform ].toString();
72+
73+
if (!configs.forceFetch && !this.hasKeyExpiredInCache(key)) {
74+
// If the code enters in this block, then the key must exist & not have been expired
75+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
76+
return this.store.get(key)!.value as Record<string, unknown>;
77+
}
78+
79+
let values: Record<string, unknown> = {};
80+
try {
81+
values = await this._getMultiple(path, options?.sdkOptions);
82+
} catch (error) {
83+
throw new GetParameterError((error as Error).message);
84+
}
85+
86+
if (Object.keys(values) && configs.transform) {
87+
values = transformValues(values, configs.transform, configs.throwOnTransformError);
88+
}
89+
90+
if (Array.from(Object.keys(values)).length !== 0) {
91+
this.addToCache(key, values, configs.maxAge);
92+
}
93+
94+
// TODO: revisit return type once providers are implemented, it might be missing something
95+
return values;
96+
}
97+
98+
/**
99+
* Retrieve parameter value from the underlying parameter store
100+
*
101+
* @param {string} name - Parameter name
102+
* @param {unknown} sdkOptions - Options to pass to the underlying AWS SDK
103+
*/
104+
protected abstract _get(name: string, sdkOptions?: unknown): Promise<string | undefined>;
105+
106+
protected abstract _getMultiple(path: string, sdkOptions?: unknown): Promise<Record<string, string|undefined>>;
107+
108+
/**
109+
* Check whether a key has expired in the cache or not
110+
*
111+
* It returns true if the key is expired or not present in the cache.
112+
*
113+
* @param {string} key - Stringified representation of the key to retrieve
114+
*/
115+
private hasKeyExpiredInCache(key: string): boolean {
116+
const value = this.store.get(key);
117+
if (value) return value.isExpired();
118+
119+
return true;
120+
}
121+
122+
}
123+
124+
// TODO: revisit `value` type once we are clearer on the types returned by the various SDKs
125+
const transformValue = (value: unknown, transform: TransformOptions, throwOnTransformError: boolean, key: string = ''): string | Record<string, unknown> | undefined => {
126+
try {
127+
const normalizedTransform = transform.toLowerCase();
128+
if (
129+
(normalizedTransform === TRANSFORM_METHOD_JSON ||
130+
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_JSON}`))) &&
131+
typeof value === 'string'
132+
) {
133+
return JSON.parse(value) as Record<string, unknown>;
134+
} else if (
135+
(normalizedTransform === TRANSFORM_METHOD_BINARY ||
136+
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`))) &&
137+
typeof value === 'string'
138+
) {
139+
return new TextDecoder('utf-8').decode(fromBase64(value));
140+
} else {
141+
// TODO: revisit this type once we are clearer on types returned by SDKs
142+
return value as string;
143+
}
144+
} catch (error) {
145+
if (throwOnTransformError)
146+
throw new TransformParameterError(transform, (error as Error).message);
147+
148+
return;
149+
}
150+
};
151+
152+
const transformValues = (value: Record<string, unknown>, transform: TransformOptions, throwOnTransformError: boolean): Record<string, unknown> => {
153+
const transformedValues: Record<string, unknown> = {};
154+
for (const [ entryKey, entryValue ] of Object.entries(value)) {
155+
try {
156+
transformedValues[entryKey] = transformValue(entryValue, transform, throwOnTransformError, entryKey);
157+
} catch (error) {
158+
if (throwOnTransformError)
159+
throw new TransformParameterError(transform, (error as Error).message);
160+
}
161+
}
162+
163+
return transformedValues;
164+
};
165+
166+
export {
167+
BaseProvider,
168+
ExpirableValue,
169+
transformValue,
170+
};

Diff for: packages/parameters/src/Exceptions.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class GetParameterError extends Error {}
2+
3+
class TransformParameterError extends Error {
4+
public constructor(transform: string, message: string) {
5+
super(message);
6+
7+
this.message = `Unable to transform value using '${transform}' transform: ${message}`;
8+
}
9+
}
10+
11+
export {
12+
GetParameterError,
13+
TransformParameterError,
14+
};

Diff for: packages/parameters/src/ExpirableValue.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ExpirableValueInterface } from './types';
2+
3+
class ExpirableValue implements ExpirableValueInterface {
4+
public ttl: number;
5+
public value: string | Record<string, unknown>;
6+
7+
public constructor(value: string | Record<string, unknown>, maxAge: number) {
8+
this.value = value;
9+
const timeNow = new Date();
10+
this.ttl = timeNow.setSeconds(timeNow.getSeconds() + maxAge);
11+
}
12+
13+
public isExpired(): boolean {
14+
return this.ttl < Date.now();
15+
}
16+
}
17+
18+
export {
19+
ExpirableValue
20+
};

Diff for: packages/parameters/src/GetMultipleOptions.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DEFAULT_MAX_AGE_SECS } from './constants';
2+
import type { GetMultipleOptionsInterface, TransformOptions } from './types';
3+
4+
class GetMultipleOptions implements GetMultipleOptionsInterface {
5+
public forceFetch: boolean = false;
6+
public maxAge: number = DEFAULT_MAX_AGE_SECS;
7+
public sdkOptions?: unknown;
8+
public throwOnTransformError: boolean = false;
9+
public transform?: TransformOptions;
10+
11+
public constructor(options: GetMultipleOptionsInterface) {
12+
Object.assign(this, options);
13+
}
14+
}
15+
16+
export {
17+
GetMultipleOptions
18+
};

Diff for: packages/parameters/src/GetOptions.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { DEFAULT_MAX_AGE_SECS } from './constants';
2+
import type { GetOptionsInterface, TransformOptions } from './types';
3+
4+
class GetOptions implements GetOptionsInterface {
5+
public forceFetch: boolean = false;
6+
public maxAge: number = DEFAULT_MAX_AGE_SECS;
7+
public sdkOptions?: unknown;
8+
public transform?: TransformOptions;
9+
10+
public constructor(options: GetOptionsInterface = {}) {
11+
Object.assign(this, options);
12+
}
13+
}
14+
15+
export {
16+
GetOptions
17+
};

Diff for: packages/parameters/src/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const DEFAULT_MAX_AGE_SECS = 5;
2+
export const TRANSFORM_METHOD_JSON = 'json';
3+
export const TRANSFORM_METHOD_BINARY = 'binary';

Diff for: packages/parameters/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './BaseProvider';
2+
export * from './Exceptions';

0 commit comments

Comments
 (0)