Skip to content

Commit 35ee221

Browse files
committed
Add support for variable env vars
1 parent 6dcb602 commit 35ee221

4 files changed

Lines changed: 525 additions & 20 deletions

File tree

src/config/index.ts

Lines changed: 162 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,10 @@ export const buildAdapterSettings = <
397397
customSettings = {} as SettingsDefinitionMap,
398398
envVarsPrefix = '' as string,
399399
}): AdapterSettings<CustomSettings> => {
400-
const vars = {} as Record<string, SettingValueType | undefined>
400+
const vars = {} as Record<
401+
string,
402+
SettingValueType | undefined | Getter<SettingValueType | undefined>
403+
>
401404

402405
// Iterate base adapter env vars
403406
for (const [key, config] of Object.entries(BaseSettingsDefinition) as Array<
@@ -416,7 +419,7 @@ export const buildAdapterSettings = <
416419
`Custom env var "${key}" declared, but a base framework env var with that name already exists.`,
417420
)
418421
}
419-
const value = getEnv(key as string, config, envVarsPrefix) ?? config.default
422+
const value = getEnvOrEnvGetter(key as string, config, envVarsPrefix)
420423
vars[key] = value
421424
}
422425

@@ -457,13 +460,31 @@ const getEnvName = (name: string, prefix = ''): string => {
457460

458461
const isEnvNameValid = (name: string) => /^[_a-z0-9]+$/i.test(name)
459462

463+
export const getEnvOrEnvGetter = (
464+
name: string,
465+
settingsDefinition: SettingDefinition,
466+
prefix = '',
467+
): SettingValueType | undefined | Getter<SettingValueType | undefined> => {
468+
if (settingsDefinition.variablePlaceholder === undefined) {
469+
return getEnv(name, settingsDefinition, prefix) ?? settingsDefinition.default
470+
}
471+
return new EnvGetter(name, settingsDefinition, prefix)
472+
}
473+
460474
export const getEnv = (
461475
name: string,
462476
settingsDefinition: SettingDefinition,
463477
prefix = '',
464478
): SettingValueType | null => {
465479
const value = process.env[getEnvName(name, prefix)]
480+
return parseEnv(value, name, settingsDefinition)
481+
}
466482

483+
export const parseEnv = (
484+
value: string | undefined,
485+
name: string,
486+
settingsDefinition: SettingDefinition,
487+
): SettingValueType | null => {
467488
if (!value || value === '' || value === '""') {
468489
return null
469490
}
@@ -485,6 +506,92 @@ export const getEnv = (
485506
}
486507
}
487508

509+
type VariableEnvVarEntry<T extends ValidSettingValue> = {
510+
// The name used in the settings definition. E.g., 'NETWORK_RPC_URL'
511+
settingKey: string
512+
// The variable part for a specific instance. E.g., 'ETHEREUM'
513+
variable: string
514+
// The setting key with the variable part replaced. E.g., 'ETHEREUM_RPC_URL'
515+
settingName: string
516+
// The actual name in the environment. E.g., 'PREFIX_ETHEREUM_RPC_URL'
517+
envVarName: string
518+
// The parsed value. E.g., some URL.
519+
value: T
520+
}
521+
522+
export interface Getter<T extends SettingValueType | undefined> {
523+
get(variable: string): T
524+
entries(): VariableEnvVarEntry<Exclude<T, undefined>>[]
525+
}
526+
527+
export class EnvGetter<
528+
T extends SettingDefinition & IsVariable = Extract<SettingDefinition, IsVariable>,
529+
> {
530+
private name: string
531+
private settingsDefinition: T
532+
private prefix: string
533+
private variableMap: Record<string, VariableEnvVarEntry<SettingTypeWhenPresent<T>>> = {}
534+
535+
constructor(name: string, settingsDefinition: T, prefix: string) {
536+
this.name = name
537+
this.settingsDefinition = settingsDefinition
538+
this.prefix = prefix
539+
540+
// If the setting name is 'NETWORK_RPC_URL' and `variablePlaceholder` is
541+
// 'NETWORK', then `namePattern` will be /([A-Z0-9_]+)_RPC_URL/ to match
542+
// all relevant environment variables and extract the variable part.
543+
const namePattern = new RegExp(
544+
`^${getEnvName(name, prefix).replace(settingsDefinition.variablePlaceholder, '([A-Z0-9_]+)')}$`,
545+
)
546+
for (const [envVarName, value] of Object.entries(process.env)) {
547+
const match = envVarName.match(namePattern)
548+
if (!match) {
549+
continue
550+
}
551+
const variablePart = match[1]
552+
const settingName = name.replace(settingsDefinition.variablePlaceholder, variablePart)
553+
const parsed = parseEnv(value, settingName, settingsDefinition)
554+
if (parsed !== null) {
555+
this.variableMap[variablePart] = {
556+
settingKey: name,
557+
variable: variablePart,
558+
settingName,
559+
envVarName,
560+
value: parsed as SettingTypeWhenPresent<T>,
561+
}
562+
}
563+
}
564+
}
565+
566+
get(variable: string): SettingType<T> {
567+
const canonicalVariable = variable.replace(/\W/g, '_').toUpperCase()
568+
if (canonicalVariable in this.variableMap) {
569+
return this.variableMap[canonicalVariable].value as SettingType<T>
570+
}
571+
if (this.settingsDefinition.default !== undefined) {
572+
return this.settingsDefinition.default as SettingType<T>
573+
}
574+
if (!this.settingsDefinition.required) {
575+
return undefined as SettingType<T>
576+
}
577+
const envName = getEnvName(
578+
this.name.replace(this.settingsDefinition.variablePlaceholder, canonicalVariable),
579+
this.prefix,
580+
)
581+
throw new Error(`Missing required environment variable: ${envName}`)
582+
}
583+
584+
entries(): VariableEnvVarEntry<SettingTypeWhenPresent<T>>[] {
585+
return Object.values(this.variableMap)
586+
}
587+
588+
validate(validationErrors: string[]) {
589+
for (const { settingName, value } of Object.values(this.variableMap)) {
590+
validateSetting(settingName, value, this.settingsDefinition, validationErrors)
591+
}
592+
}
593+
}
594+
488595
type SettingValueType = string | number | boolean
489596
type SettingTypeWhenPresent<C extends SettingDefinition> = C['type'] extends 'string'
490597
? string
@@ -507,7 +614,7 @@ export type SettingDefinitionBase = {
507614
description: string
508615
sensitive?: boolean
509616
required?: boolean
510-
}
617+
} & ({ variablePlaceholder?: never } | { variablePlaceholder: string })
511618

512619
export type NonEnumSettingDefinition<TypeString, Type> = SettingDefinitionBase & {
513620
type: TypeString
@@ -541,6 +648,21 @@ type IsOptional = {
541648
description: string
542649
}
543650

651+
type IsVariable = { variablePlaceholder: string }
652+
type IsFixed = {
653+
variablePlaceholder?: never
654+
// Add description for the same issue with weak types as in IsOptional.
655+
description: string
656+
}
657+
658+
type VariableSettingKeys<T extends SettingsDefinitionMap> = {
659+
[K in keyof T]: T[K] extends IsVariable ? K : never
660+
}[keyof T]
661+
662+
type FixedSettingKeys<T extends SettingsDefinitionMap> = {
663+
[K in keyof T]: T[K] extends IsFixed ? K : never
664+
}[keyof T]
665+
544666
type NonOptionalSettingKeys<T extends SettingsDefinitionMap> = {
545667
[K in keyof T]: T[K] extends HasDefault | IsRequired ? K : never
546668
}[keyof T]
@@ -550,9 +672,15 @@ type OptionalSettingKeys<T extends SettingsDefinitionMap> = {
550672
}[keyof T]
551673

552674
export type Settings<T extends SettingsDefinitionMap> = {
553-
-readonly [K in OptionalSettingKeys<T>]?: SettingType<T[K]>
675+
-readonly [K in Extract<FixedSettingKeys<T>, OptionalSettingKeys<T>>]?:
676+
| SettingTypeWhenPresent<T[K]>
677+
| undefined
678+
} & {
679+
-readonly [K in Extract<FixedSettingKeys<T>, NonOptionalSettingKeys<T>>]: SettingTypeWhenPresent<
680+
T[K]
681+
>
554682
} & {
555-
-readonly [K in NonOptionalSettingKeys<T>]: SettingType<T[K]>
683+
-readonly [K in VariableSettingKeys<T>]: Getter<SettingType<T[K]>>
556684
}
557685

558686
export type BaseAdapterSettings = Settings<BaseSettingsDefinitionType>
@@ -617,12 +745,16 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
617745
Object.entries(BaseSettingsDefinition as SettingsDefinitionMap)
618746
.concat(Object.entries(this.settingsDefinition || {}))
619747
.forEach(([name, setting]) => {
620-
validateSetting(
621-
name,
622-
(this.settings as Record<string, ValidSettingValue>)[name],
623-
setting,
624-
validationErrors,
625-
)
748+
if (setting.variablePlaceholder !== undefined) {
749+
;(this.settings as unknown as Record<string, EnvGetter>)[name].validate(validationErrors)
750+
} else {
751+
validateSetting(
752+
name,
753+
(this.settings as Record<string, ValidSettingValue>)[name],
754+
setting,
755+
validationErrors,
756+
)
757+
}
626758
})
627759

628760
if (validationErrors.length > 0) {
@@ -651,10 +783,26 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
651783
alwaysCensored.some((pattern) => name.includes(pattern))) &&
652784
(this.settings as Record<string, ValidSettingValue>)[name],
653785
)
654-
.map(([name]) => ({
786+
.flatMap(([name]) => {
787+
const settings = this.settings as Record<
788+
string,
789+
ValidSettingValue | Getter<ValidSettingValue>
790+
>
791+
const settingValue = settings[name]
792+
if (settingValue instanceof EnvGetter) {
793+
return settingValue
794+
.entries()
795+
.map(
796+
({ settingName, value }) =>
797+
[settingName, value] satisfies [string, ValidSettingValue],
798+
)
799+
}
800+
return [[name as string, settingValue]] as [string, ValidSettingValue][]
801+
})
802+
.map(([name, value]: [string, ValidSettingValue]) => ({
655803
key: name,
656804
value: new RegExp(
657-
((this.settings as Record<string, ValidSettingValue>)[name]! as string)
805+
(value! as string)
658806
// Escaping potential special characters in values before creating regex
659807
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
660808
// Escaping special case for new line characters. This is needed to properly match and censor private keys,
@@ -689,5 +837,5 @@ export class AdapterConfig<T extends SettingsDefinitionMap = SettingsDefinitionM
689837
type SettingsObjectSpecifier = {
690838
__reserved_settings: never
691839
}
692-
type ValidSettingValue = string | number | boolean
840+
export type ValidSettingValue = string | number | boolean
693841
export type GenericConfigStructure = BaseAdapterSettings & SettingsObjectSpecifier

src/util/settings.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Adapter } from '../adapter'
2-
import { SettingDefinitionDetails } from '../config'
2+
import {
3+
SettingsDefinitionMap,
4+
SettingDefinitionDetails,
5+
EnvGetter,
6+
ValidSettingValue,
7+
} from '../config'
38
import { censor } from './index'
49
import CensorList from './censor/censor-list'
510

@@ -9,16 +14,31 @@ export type DebugPageSetting = SettingDefinitionDetails & { name: string; value:
914
* Builds a list of adapter settings with sensitive values censored
1015
* Used by both debug settings page and status endpoint
1116
*/
12-
export const buildSettingsList = (adapter: Adapter): DebugPageSetting[] => {
17+
export const buildSettingsList = <T extends SettingsDefinitionMap>(
18+
adapter: Adapter<T>,
19+
): DebugPageSetting[] => {
1320
// Censor EA settings
1421
const settings = adapter.config.settings
1522
const censoredValues = CensorList.getAll()
1623
const censoredSettings: Array<SettingDefinitionDetails & { name: string; value: unknown }> = []
1724

18-
for (const [key, value] of Object.entries(settings)) {
19-
const definitionDetails = adapter.config.getSettingDebugDetails(key)
25+
const settingsEntries = Object.entries(settings).flatMap(
26+
([settingName, settingValue]): {
27+
settingName: string
28+
envVarName: string
29+
value: ValidSettingValue
30+
}[] => {
31+
const getter = settingValue as unknown as EnvGetter
32+
if (getter instanceof EnvGetter) {
33+
return getter.entries().map(({ envVarName, value }) => ({ settingName, envVarName, value }))
34+
}
35+
return [{ settingName, envVarName: settingName, value: settingValue as ValidSettingValue }]
36+
},
37+
)
38+
for (const { settingName, envVarName, value } of settingsEntries) {
39+
const definitionDetails = adapter.config.getSettingDebugDetails(settingName)
2040
censoredSettings.push({
21-
name: key,
41+
name: envVarName,
2242
...definitionDetails,
2343
value: censor(value, censoredValues),
2444
})

0 commit comments

Comments
 (0)