UNPKG

wj-config

Version:

Javascript configuration module for NodeJS and browser frameworks such as React that works like ASP.net configuration where data sources are specified (usually JSON files) and environment variables can contribute/overwrite values by following a naming con

142 lines (139 loc) 6.31 kB
import { DictionaryDataSource } from "../dataSources/DictionaryDataSource.js"; import { EnvironmentDataSource } from "../dataSources/EnvironmentDataSource.js"; import { FetchedDataSource } from "../dataSources/FetchedDataSource.js"; import { JsonDataSource } from "../dataSources/JsonDataSource.js"; import { ObjectDataSource } from "../dataSources/ObjectDataSource.js"; import { SingleValueDataSource } from "../dataSources/SingleValueDataSource.js"; export class EnvAwareBuilder { /** * Environment source. */ _envSource; #impl; constructor(envSource, impl) { this._envSource = envSource; this.#impl = impl; } add(dataSource) { this.#impl.add(dataSource); return this; } addObject(obj) { return this.add(new ObjectDataSource(obj)); } addDictionary(dictionary, hierarchySeparator = ':', prefixOrPredicate) { // @ts-expect-error return this.add(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate)); } addEnvironment(env, prefix = 'OPT_') { /* InflateDictionary is a utility type that does generate a type that is assignable to Record<string, any>, but in a way that TypeScript does not understand. It uses a trick to merge individually-inflated keys in individual, single-property Record's into one record. So ignoring TS2344 for the time being. Maybe it is my TypeScript's lack of ability, or maybe not. Time will tell. */ // @ts-expect-error ts2344 return this.add(new EnvironmentDataSource(env, prefix)); } addFetched(input, required = true, init, procesFn) { return this.add(new FetchedDataSource(input, required, init, procesFn)); } addJson(json, jsonParser, reviver) { return this.add(new JsonDataSource(json, jsonParser, reviver)); } addSingleValue(path, valueOrHierarchySeparator, hierarchySeparator) { return this.add(new SingleValueDataSource(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator : hierarchySeparator)); } postMerge(fn) { this.#impl.postMerge(fn); return this; } name(name) { this.#impl.name(name); return this; } createUrlFunctions(wsPropertyNames, routeValuesRegExp) { this.#impl.createUrlFunctions(wsPropertyNames, routeValuesRegExp); return this; } /** * Boolean flag used to raise an error if there was no call to includeEnvironment() when it is known to be needed. */ _envIsRequired = false; /** * Dictionary of environment names that have been configured with a data source using the addPerEnvironment() * helper function. The value is the number of times the environment name has been used. */ _perEnvDsCount = null; addPerEnvironment(addDs) { if (!this._envSource) { throw new Error('Using addPerEnvironment() requires a prior call to includeEnvironment().'); } this._envSource.environment.all.forEach(n => { const result = addDs(this, n); if (result !== false) { this.forEnvironment(n, typeof result === 'string' ? result : undefined); } }); return this; } when(predicate, dataSourceName) { this.#impl.when(predicate, dataSourceName); return this; } forEnvironment(envName, dataSourceName) { this._envIsRequired = true; this._perEnvDsCount = this._perEnvDsCount ?? {}; let count = this._perEnvDsCount[envName] ?? 0; this._perEnvDsCount[envName] = ++count; dataSourceName = dataSourceName ?? (count === 1 ? `${envName} (environment-specific)` : `${envName} #${count} (environment-specific)`); return this.when(e => e?.current.name === envName, dataSourceName); } whenAllTraits(traits, dataSourceName) { this._envIsRequired = true; return this.when(env => { return env.hasTraits(traits) ?? false; }, dataSourceName); } whenAnyTrait(traits, dataSourceName) { this._envIsRequired = true; return this.when(env => { return env.hasAnyTrait(traits); }, dataSourceName); } async build(traceValueSources = false, enforcePerEnvironmentCoverage = true) { this.#impl._lastCallWasDsAdd = false; // See if environment is required. if (this._envIsRequired && !this._envSource) { throw new Error('The used build steps include at least one step that requires environment information. Ensure you are using "includeEnvironment()" as part of the build chain.'); } // See if forEnvironment was used. if (this._perEnvDsCount) { // Ensure all specified environments are part of the possible list of environments. let envCount = 0; for (const e in this._perEnvDsCount) { if (!this._envSource.environment.all.includes(e)) { throw new Error(`The environment name "${e}" was used in a call to forEnvironment(), but said name is not part of the list of possible environment names.`); } ++envCount; } if (enforcePerEnvironmentCoverage) { // Ensure all possible environment names were included. const totalEnvs = this._envSource.environment.all.length; if (envCount !== totalEnvs) { throw new Error(`Only ${envCount} environment(s) were configured using forEnvironment() out of a total of ${totalEnvs} environment(s). Either complete the list or disable this check when calling build().`); } } } const result = await this.#impl.build(traceValueSources, p => p(this._envSource?.environment)); const envPropertyName = this._envSource.name ?? 'environment'; if (result[envPropertyName] !== undefined) { throw new Error(`Cannot use property name "${envPropertyName}" for the environment object because it was defined for something else.`); } result[envPropertyName] = this._envSource.environment; return result; } }