1616
1717import { JsonValue , JsonObject } from '@backstage/types' ;
1818import { AppConfig , Config } from './types' ;
19- import cloneDeep from 'lodash/cloneDeep' ;
20- import mergeWith from 'lodash/mergeWith' ;
2119
2220// Update the same pattern in config-loader package if this is changed
2321const CONFIG_KEY_PART_PATTERN = / ^ [ a - z ] [ a - z 0 - 9 ] * (?: [ - _ ] [ a - z ] [ a - z 0 - 9 ] * ) * $ / i;
@@ -26,6 +24,43 @@ function isObject(value: JsonValue | undefined): value is JsonObject {
2624 return typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
2725}
2826
27+ function cloneDeep ( value : JsonValue | null | undefined ) : JsonValue | undefined {
28+ if ( typeof value !== 'object' || value === null ) {
29+ return value ;
30+ }
31+ if ( Array . isArray ( value ) ) {
32+ return value . map ( cloneDeep ) as JsonValue ;
33+ }
34+ return Object . fromEntries (
35+ Object . entries ( value ) . map ( ( [ k , v ] ) => [ k , cloneDeep ( v ) ] ) ,
36+ ) ;
37+ }
38+
39+ function merge (
40+ into : JsonValue | undefined ,
41+ from ?: JsonValue | undefined ,
42+ ) : JsonValue | undefined {
43+ if ( into === null ) {
44+ return undefined ;
45+ }
46+ if ( into === undefined ) {
47+ return from === undefined ? undefined : merge ( from ) ;
48+ }
49+ if ( typeof into !== 'object' || Array . isArray ( into ) ) {
50+ return into ;
51+ }
52+ const fromObj = isObject ( from ) ? from : { } ;
53+
54+ const out : JsonObject = { } ;
55+ for ( const key of new Set ( [ ...Object . keys ( into ) , ...Object . keys ( fromObj ) ] ) ) {
56+ const val = merge ( into [ key ] , fromObj [ key ] ) ;
57+ if ( val !== undefined ) {
58+ out [ key ] = val ;
59+ }
60+ }
61+ return out ;
62+ }
63+
2964function typeOf ( value : JsonValue | undefined ) : string {
3065 if ( value === null ) {
3166 return 'null' ;
@@ -47,8 +82,8 @@ const errors = {
4782 type ( key : string , context : string , typeName : string , expected : string ) {
4883 return `Invalid type in config for key '${ key } ' in '${ context } ', got ${ typeName } , wanted ${ expected } ` ;
4984 } ,
50- missing ( key : string ) {
51- return `Missing required config value at '${ key } '` ;
85+ missing ( key : string , context : string ) {
86+ return `Missing required config value at '${ key } ' in ' ${ context } ' ` ;
5287 } ,
5388 convert ( key : string , context : string , expected : string ) {
5489 return `Unable to convert config value for key '${ key } ' in '${ context } ' to a ${ expected } ` ;
@@ -114,6 +149,9 @@ export class ConfigReader implements Config {
114149 /** {@inheritdoc Config.has } */
115150 has ( key : string ) : boolean {
116151 const value = this . readValue ( key ) ;
152+ if ( value === null ) {
153+ return false ;
154+ }
117155 if ( value !== undefined ) {
118156 return true ;
119157 }
@@ -124,23 +162,28 @@ export class ConfigReader implements Config {
124162 keys ( ) : string [ ] {
125163 const localKeys = this . data ? Object . keys ( this . data ) : [ ] ;
126164 const fallbackKeys = this . fallback ?. keys ( ) ?? [ ] ;
127- return [ ...new Set ( [ ...localKeys , ...fallbackKeys ] ) ] ;
165+ return [ ...new Set ( [ ...localKeys , ...fallbackKeys ] ) ] . filter (
166+ k => this . data ?. [ k ] !== null ,
167+ ) ;
128168 }
129169
130170 /** {@inheritdoc Config.get } */
131171 get < T = JsonValue > ( key ?: string ) : T {
132172 const value = this . getOptional ( key ) ;
133173 if ( value === undefined ) {
134- throw new Error ( errors . missing ( this . fullKey ( key ?? '' ) ) ) ;
174+ throw new Error ( errors . missing ( this . fullKey ( key ?? '' ) , this . context ) ) ;
135175 }
136176 return value as T ;
137177 }
138178
139179 /** {@inheritdoc Config.getOptional } */
140180 getOptional < T = JsonValue > ( key ?: string ) : T | undefined {
141181 const value = cloneDeep ( this . readValue ( key ) ) ;
142- const fallbackValue = this . fallback ?. getOptional < T > ( key ) ;
182+ const fallbackValue = this . fallback ?. getOptional ( key ) ;
143183
184+ if ( value === null ) {
185+ return undefined ;
186+ }
144187 if ( value === undefined ) {
145188 if ( process . env . NODE_ENV === 'development' ) {
146189 if ( fallbackValue === undefined && key ) {
@@ -158,23 +201,19 @@ export class ConfigReader implements Config {
158201 }
159202 }
160203 }
161- return fallbackValue ;
204+ return merge ( fallbackValue ) as T ;
162205 } else if ( fallbackValue === undefined ) {
163- return value as T ;
206+ return merge ( value ) as T ;
164207 }
165208
166- // Avoid merging arrays and primitive values, since that's how merging works for other
167- // methods for reading config.
168- return mergeWith ( { } , { value : fallbackValue } , { value } , ( into , from ) =>
169- ! isObject ( from ) || ! isObject ( into ) ? from : undefined ,
170- ) . value as T ;
209+ return merge ( value , fallbackValue ) as T ;
171210 }
172211
173212 /** {@inheritdoc Config.getConfig } */
174213 getConfig ( key : string ) : ConfigReader {
175214 const value = this . getOptionalConfig ( key ) ;
176215 if ( value === undefined ) {
177- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
216+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
178217 }
179218 return value ;
180219 }
@@ -187,6 +226,9 @@ export class ConfigReader implements Config {
187226 if ( isObject ( value ) ) {
188227 return this . copy ( value , key , fallbackConfig ) ;
189228 }
229+ if ( value === null ) {
230+ return undefined ;
231+ }
190232 if ( value !== undefined ) {
191233 throw new TypeError (
192234 errors . type ( this . fullKey ( key ) , this . context , typeOf ( value ) , 'object' ) ,
@@ -199,7 +241,7 @@ export class ConfigReader implements Config {
199241 getConfigArray ( key : string ) : ConfigReader [ ] {
200242 const value = this . getOptionalConfigArray ( key ) ;
201243 if ( value === undefined ) {
202- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
244+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
203245 }
204246 return value ;
205247 }
@@ -244,7 +286,7 @@ export class ConfigReader implements Config {
244286 getNumber ( key : string ) : number {
245287 const value = this . getOptionalNumber ( key ) ;
246288 if ( value === undefined ) {
247- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
289+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
248290 }
249291 return value ;
250292 }
@@ -273,7 +315,7 @@ export class ConfigReader implements Config {
273315 getBoolean ( key : string ) : boolean {
274316 const value = this . getOptionalBoolean ( key ) ;
275317 if ( value === undefined ) {
276- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
318+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
277319 }
278320 return value ;
279321 }
@@ -305,7 +347,7 @@ export class ConfigReader implements Config {
305347 getString ( key : string ) : string {
306348 const value = this . getOptionalString ( key ) ;
307349 if ( value === undefined ) {
308- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
350+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
309351 }
310352 return value ;
311353 }
@@ -323,7 +365,7 @@ export class ConfigReader implements Config {
323365 getStringArray ( key : string ) : string [ ] {
324366 const value = this . getOptionalStringArray ( key ) ;
325367 if ( value === undefined ) {
326- throw new Error ( errors . missing ( this . fullKey ( key ) ) ) ;
368+ throw new Error ( errors . missing ( this . fullKey ( key ) , this . context ) ) ;
327369 }
328370 return value ;
329371 }
@@ -384,6 +426,9 @@ export class ConfigReader implements Config {
384426
385427 return this . fallback ?. readConfigValue ( key , validate ) ;
386428 }
429+ if ( value === null ) {
430+ return undefined ;
431+ }
387432 const result = validate ( value ) ;
388433 if ( result !== true ) {
389434 const { key : keyName = key , value : theValue = value , expected } = result ;
@@ -416,7 +461,7 @@ export class ConfigReader implements Config {
416461 for ( const [ index , part ] of parts . entries ( ) ) {
417462 if ( isObject ( value ) ) {
418463 value = value [ part ] ;
419- } else if ( value !== undefined ) {
464+ } else if ( value !== undefined && value !== null ) {
420465 const badKey = this . fullKey ( parts . slice ( 0 , index ) . join ( '.' ) ) ;
421466 throw new TypeError (
422467 errors . type ( badKey , this . context , typeOf ( value ) , 'object' ) ,
0 commit comments