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
JavaScript
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;
}
}