1
+ import { fromBase64 } from '@aws-sdk/util-base64-node' ;
2
+ import { GetParameterError , TransformParameterError } from 'Exceptions' ;
3
+ import type { BaseProviderInterface , ExpirableValueInterface , GetMultipleOptionsInterface , GetOptionsInterface , Key , TransformOptions } from './types' ;
4
+
5
+ const DEFAULT_MAX_AGE_SECS = 5 ;
6
+ // These providers will be dynamically initialized on first use of the helper functions
7
+ const DEFAULT_PROVIDERS = new Map ( ) ;
8
+ const TRANSFORM_METHOD_JSON = 'json' ;
9
+ const TRANSFORM_METHOD_BINARY = 'binary' ;
10
+
11
+ class GetOptions implements GetOptionsInterface {
12
+ public forceFetch : boolean = false ;
13
+ public maxAge : number = DEFAULT_MAX_AGE_SECS ;
14
+ public sdkOptions ?: unknown ;
15
+ public transform ?: TransformOptions ;
16
+
17
+ public constructor ( options : GetOptionsInterface = { } ) {
18
+ Object . assign ( this , options ) ;
19
+ }
20
+ }
21
+
22
+ class GetMultipleOptions implements GetMultipleOptionsInterface {
23
+ public forceFetch : boolean = false ;
24
+ public maxAge : number = DEFAULT_MAX_AGE_SECS ;
25
+ public sdkOptions ?: unknown ;
26
+ public throwOnTransformError ?: boolean = false ;
27
+ public transform ?: TransformOptions ;
28
+
29
+ public constructor ( options : GetMultipleOptionsInterface ) {
30
+ Object . assign ( this , options ) ;
31
+ }
32
+ }
33
+
34
+ class ExpirableValue implements ExpirableValueInterface {
35
+ public ttl : number ;
36
+ public value : string | Record < string , unknown > ;
37
+
38
+ public constructor ( value : string | Record < string , unknown > , maxAge : number ) {
39
+ this . value = value ;
40
+ const timeNow = new Date ( ) ;
41
+ this . ttl = timeNow . setSeconds ( timeNow . getSeconds ( ) + maxAge ) ;
42
+ }
43
+
44
+ public isExpired ( ) : boolean {
45
+ return this . ttl < Date . now ( ) ;
46
+ }
47
+ }
48
+
49
+ abstract class BaseProvider implements BaseProviderInterface {
50
+ public store : Map < Key , ExpirableValue > = new Map ;
51
+
52
+ private constructor ( ) {
53
+ this . store = new Map ( ) ;
54
+ }
55
+
56
+ public addToCache ( key : Key , value : string | Record < string , unknown > , maxAge : number ) : void {
57
+ if ( maxAge <= 0 ) return ;
58
+
59
+ this . store . set ( key , new ExpirableValue ( value , maxAge ) ) ;
60
+ }
61
+
62
+ public clearCache ( ) : void {
63
+ this . store . clear ( ) ;
64
+ }
65
+
66
+ /**
67
+ * Retrieve a parameter value or return the cached value
68
+ *
69
+ * If there are multiple calls to the same parameter but in a different transform, they will be stored multiple times.
70
+ * This allows us to optimize by transforming the data only once per retrieval, thus there is no need to transform cached values multiple times.
71
+ *
72
+ * However, this means that we need to make multiple calls to the underlying parameter store if we need to return it in different transforms.
73
+ *
74
+ * Since the number of supported transform is small and the probability that a given parameter will always be used in a specific transform,
75
+ * this should be an acceptable tradeoff.
76
+ *
77
+ * @param {string } name - Parameter name
78
+ * @param {GetOptionsInterface } options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
79
+ */
80
+ public async get ( name : string , options ?: GetOptionsInterface ) : Promise < undefined | string | Record < string , unknown > > {
81
+ const configs = new GetOptions ( options ) ;
82
+ const key = { [ name ] : configs . transform } ;
83
+
84
+ if ( ! configs . forceFetch && this . hasNotExpiredInCache ( key ) ) {
85
+ return this . store . get ( key ) ?. value ;
86
+ }
87
+
88
+ let value ;
89
+ try {
90
+ value = await this . _get ( name , options ?. sdkOptions ) ;
91
+ } catch ( error ) {
92
+ throw new GetParameterError ( ( error as Error ) . message ) ;
93
+ }
94
+
95
+ if ( value && configs . transform ) {
96
+ value = transformValue ( value , configs . transform ) ;
97
+ }
98
+
99
+ if ( value ) {
100
+ this . addToCache ( key , value , configs . maxAge ) ;
101
+ }
102
+
103
+ // TODO: revisit return type once providers are implemented, it might be missing binary when not transformed
104
+ return value ;
105
+ }
106
+
107
+ public async getMultiple ( path : string , options ?: GetMultipleOptionsInterface ) : Promise < undefined | Record < string , unknown > > {
108
+ const configs = new GetMultipleOptions ( options || { } ) ;
109
+ const key = { [ path ] : configs . transform } ;
110
+
111
+ if ( ! configs . forceFetch && this . hasNotExpiredInCache ( key ) ) {
112
+ return this . store . get ( key ) ?. value as Record < string , unknown > ; // In this case we know that if it exists, this key corresponds to a Record
113
+ }
114
+
115
+ let values : Record < string , unknown > = { } ;
116
+ try {
117
+ values = await this . _getMultiple ( path , options ?. sdkOptions ) ;
118
+ } catch ( error ) {
119
+ throw new GetParameterError ( ( error as Error ) . message ) ;
120
+ }
121
+
122
+ if ( configs . transform ) {
123
+ values = transformValues ( values , configs . transform ) ;
124
+ }
125
+
126
+ if ( Array . from ( Object . keys ( values ) ) . length !== 0 ) {
127
+ this . addToCache ( key , values , configs . maxAge ) ;
128
+ }
129
+
130
+ // TODO: revisit return type once providers are implemented, it might be missing something
131
+ return values ;
132
+ }
133
+
134
+ /**
135
+ * Retrieve parameter value from the underlying parameter store
136
+ *
137
+ * @param {string } name - Parameter name
138
+ * @param {unknown } sdkOptions - Options to pass to the underlying AWS SDK
139
+ */
140
+ protected abstract _get ( name : string , sdkOptions ?: unknown ) : Promise < string | undefined > ;
141
+
142
+ protected abstract _getMultiple ( path : string , sdkOptions ?: unknown ) : Promise < Record < string , string | undefined > > ;
143
+
144
+ /**
145
+ * Check whether a key has not expired in the cache
146
+ *
147
+ * It returns false if the key is expired or not present in the cache.
148
+ *
149
+ * @param {Key } key - Key to retrieve
150
+ */
151
+ private hasNotExpiredInCache ( key : Key ) : boolean {
152
+ const value = this . store . get ( key ) ;
153
+ if ( value ) value . isExpired ( ) ;
154
+
155
+ return false ;
156
+ }
157
+
158
+ }
159
+
160
+ const transformValue = ( value : string , transform : TransformOptions , throwOnTransformError : boolean = true , key : string = '' ) : string | Record < string , unknown > | undefined => {
161
+ try {
162
+ const normalizedTransform = transform . toLowerCase ( ) ;
163
+ if (
164
+ normalizedTransform === TRANSFORM_METHOD_JSON ||
165
+ ( normalizedTransform === 'auto' && key . toLowerCase ( ) . endsWith ( `.${ TRANSFORM_METHOD_JSON } ` ) )
166
+ ) {
167
+ return JSON . parse ( value ) as Record < string , unknown > ;
168
+ } else if (
169
+ normalizedTransform === TRANSFORM_METHOD_BINARY ||
170
+ ( normalizedTransform === 'auto' && key . toLowerCase ( ) . endsWith ( `.${ TRANSFORM_METHOD_BINARY } ` ) )
171
+ ) {
172
+ return new TextDecoder ( 'utf-8' ) . decode ( fromBase64 ( value ) ) ;
173
+ } else {
174
+ throw Error ( `Invalid transform type ${ normalizedTransform } .` ) ;
175
+ }
176
+ } catch ( error ) {
177
+ if ( throwOnTransformError )
178
+ throw new TransformParameterError ( transform , ( error as Error ) . message ) ;
179
+
180
+ return ;
181
+ }
182
+ } ;
183
+
184
+ const transformValues = ( value : string | Uint8Array | Record < string , unknown > , transform : TransformOptions , throwOnTransformError : boolean = true ) : Record < string , unknown > => {
185
+ const transformedValues : Record < string , unknown > = { } ;
186
+ for ( const entry in Object . entries ( value ) ) {
187
+ const [ entryKey , entryValue ] = entry ;
188
+ try {
189
+ transformedValues [ entryKey ] = transformValue ( entryValue , transform , true , entryKey ) ;
190
+ } catch ( error ) {
191
+ if ( throwOnTransformError )
192
+ throw new TransformParameterError ( transform , ( error as Error ) . message ) ;
193
+ transformedValues [ entryKey ] = undefined ;
194
+ }
195
+ }
196
+
197
+ return transformedValues ;
198
+ } ;
199
+
200
+ const clearCaches = ( ) : void => DEFAULT_PROVIDERS . clear ( ) ;
201
+
202
+ export {
203
+ clearCaches ,
204
+ BaseProvider ,
205
+ transformValue ,
206
+ DEFAULT_PROVIDERS
207
+ } ;
0 commit comments