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+ } ;
0 commit comments