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