import { intersection, isEqual } from 'lodash'; import 'reflect-metadata'; import { CONFIG_METADATA_KEY } from './metadata'; import type { ReloadEvents } from './reload'; import type { PropertyMetadata, SectionMetadata } from './types'; import type { Vars } from './vars.class'; /** * It is unconventional to use raw values from process.env or somewhere * else because it will not be autodocumented. Instead developer is strongly * encouraged to extends this abstract class and annotate all fields with * @Property() decorator providing nesessary metadata. */ export class Config { /** * Keys are prefixed and sources are added. */ private readonly propertiesMetadata: Record; // TODO: why not to pass this.vars and event emitter? constructor(private readonly events: ReloadEvents) { const metadata = Reflect.getMetadata( CONFIG_METADATA_KEY, this.constructor, ) as SectionMetadata; this.propertiesMetadata = metadata.properties; } // eslint-disable-next-line @typescript-eslint/ban-types static isConfigSection(value: Function): value is typeof Config { return ( Reflect.getMetadata(CONFIG_METADATA_KEY, value) && value.prototype instanceof Config ); } onUpdate(fn: (keys: string[]) => void): void { this.events.on('update', (_: Config, updatedKeys: string[]) => { const prefixedKeys = Object.keys(this.propertiesMetadata).map( (x) => `${this.constructor.name}/${x}`, ); if (intersection(prefixedKeys, updatedKeys).length > 0) { fn(updatedKeys); } }); } read(vars: Vars): string[] { const updated: string[] = []; for (const [prop, metadata] of Object.entries(this.propertiesMetadata)) { const value = (this as any)[prop]; const newValue = vars.extract(metadata); // FIXME: Doesn't work with object types. if (!isEqual(newValue, value)) { (this as any)[prop] = newValue; updated.push(`${this.constructor.name}/${prop}`); } } return updated; } tryUpdate(vars: Vars): void { try { const updated = this.read(vars); this.events.emit('update', this, updated); } catch (error: any) { this.events.emit('update_failure', this, error); } } }