@nivinjoseph/n-config
Version:
Configuration management library
308 lines (254 loc) • 9.92 kB
text/typescript
import { given } from "@nivinjoseph/n-defensive";
import { ApplicationException } from "@nivinjoseph/n-exception";
import "@nivinjoseph/n-ext";
import { TypeHelper } from "@nivinjoseph/n-util";
declare const APP_CONFIG: any;
let config: Object = {};
const parseProcessDotEnv = (): object =>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const obj = process.env || {};
// we need to remove useless values from obj
const uselessValue = "[object Object]";
Object.keys(obj).forEach(t =>
{
if (obj[t] === uselessValue)
delete obj[t];
});
// console.log("parseProcessDotEnv", JSON.stringify(obj));
return obj;
};
const parseCommandLineArgs = (): object =>
{
const obj: Record<string, any> = {};
const args = process.argv;
if (args.length <= 2)
return obj;
for (let i = 2; i < args.length; i++)
{
const arg = args[i].trim();
if (!arg.contains("="))
continue;
const parts = arg.split("=");
if (parts.length !== 2)
continue;
const key = parts[0].trim();
const value = parts[1].trim();
if (key.isEmptyOrWhiteSpace() || value.isEmptyOrWhiteSpace())
continue;
const boolVal = value.toLowerCase();
if (boolVal === "true" || boolVal === "false")
{
obj[key] = boolVal === "true";
continue;
}
try
{
// const numVal = value.contains(".") ? Number.parseFloat(value) : Number.parseInt(value);
// if (!Number.isNaN(numVal))
// {
// obj[key] = numVal;
// continue;
// }
const parsed = +value;
if (!isNaN(parsed) && isFinite(parsed))
{
obj[key] = parsed;
continue;
}
}
catch (error)
{
// suppress parse error?
}
const strVal = value;
obj[key] = strVal;
}
// console.log("parseCommandLineArgs", JSON.stringify(obj));
return obj;
};
if (typeof window !== "undefined" && typeof document !== "undefined")
{
const conf = APP_CONFIG;
if (conf && typeof conf === "object")
config = Object.assign(config, conf);
if ((<any>window).config != null && typeof (<any>window).config === "string")
config = Object.assign(config, JSON.parse((<string>(<any>window).config).hexDecode()));
}
else
{
const fs = await import("node:fs");
const path = await import("node:path");
const parsePackageDotJson = (): object =>
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const packageDotJsonPath = path.resolve(process.cwd(), "package.json");
const obj: Record<string, any> = {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (!fs.existsSync(packageDotJsonPath))
return obj;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const json: string = fs.readFileSync(packageDotJsonPath, "utf8");
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (json != null && !json.toString().isEmptyOrWhiteSpace())
{
const parsed = JSON.parse(json.toString()) as Object;
obj.package = {
name: parsed.getValue("name"),
description: parsed.getValue("description"),
version: parsed.getValue("version")
};
}
// console.log("parsePackageDotJson", JSON.stringify(obj));
return obj;
};
const parseConfigDotJson = (): object =>
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const configDotJsonPath = path.resolve(process.cwd(), "config.json");
let obj: Record<string, any> = {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (!fs.existsSync(configDotJsonPath))
return obj;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const json: string = fs.readFileSync(configDotJsonPath, "utf8");
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (json != null && !json.toString().isEmptyOrWhiteSpace())
obj = JSON.parse(json.toString());
// console.log("parseConfigDotJson", JSON.stringify(obj));
return obj;
};
/* BORROWED FROM https://github.com/motdotla/dotenv/blob/master/lib/main.js
* Parses a string or buffer into an object
* @param {(string|Buffer)} src - source to be parsed
* @returns {Object} keys and values from src
*/
const parseDotEnv = (): object =>
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const dotEnvPath: string = path.resolve(process.cwd(), ".env");
const obj: Record<string, any> = {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (!fs.existsSync(dotEnvPath))
return obj;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const src: string = fs.readFileSync(dotEnvPath, "utf8");
src.toString().split("\n").forEach((line) =>
{
// matching "KEY' and 'VAL' in 'KEY=VAL'
// eslint-disable-next-line no-useless-escape
const keyValueArr = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/);
// matched?
if (keyValueArr != null)
{
const key = keyValueArr[1];
// default undefined or missing values to empty string
let value = keyValueArr[2] || "";
// expand newlines in quoted values
const len = value ? value.length : 0;
if (len > 0 && value.startsWith(`"`) && value.charAt(len - 1) === `"`)
{
value = value.replace(/\\n/gm, "\n");
}
// remove any surrounding quotes and extra spaces
value = value.replace(/(^['"]|['"]$)/g, "").trim();
obj[key] = value;
}
});
// console.log("parseDotEnv", JSON.stringify(obj));
return obj;
};
const mergedConfig = Object.assign(config, parsePackageDotJson(), parseConfigDotJson()) as Object;
[
...Object.entries(parseDotEnv()),
...Object.entries(parseProcessDotEnv()),
...Object.entries(parseCommandLineArgs())
].forEach((entry) => mergedConfig.setValue(entry[0], entry[1]));
config = mergedConfig;
}
export abstract class ConfigurationManager
{
private constructor() { }
public static async initializeProviders(providers: ReadonlyArray<ConfigurationProvider>): Promise<void>
{
given(providers, "providers").ensureHasValue().ensureIsArray().ensure(t => t.isNotEmpty);
const providedConfig = (await Promise.all(providers.map(t => t.provide())))
.reduce((acc, t) => Object.assign(acc, t), {});
[
...Object.entries(providedConfig),
...Object.entries(parseProcessDotEnv()),
...Object.entries(parseCommandLineArgs())
].forEach((entry) => config.setValue(entry[0], entry[1]));
}
public static getConfig<T>(key: string): T
{
given(key, "key").ensureHasValue().ensureIsString().ensure(t => !t.isEmptyOrWhiteSpace());
key = key.trim();
if (key === "*")
{
return JSON.parse(JSON.stringify(config)) as T;
}
else if (key.startsWith("*") && key.endsWith("*"))
{
key = key.substring(1, key.length - 1);
return Object.entries(config).reduce<Record<string, unknown>>((acc, entry) =>
{
if (entry[0].contains(key))
acc[entry[0]] = entry[1];
return acc;
}, {}) as T;
}
else if (key.startsWith("*"))
{
key = key.substring(1);
return Object.entries(config).reduce<Record<string, unknown>>((acc, entry) =>
{
if (entry[0].endsWith(key))
acc[entry[0]] = entry[1];
return acc;
}, {}) as T;
}
else if (key.endsWith("*"))
{
key = key.substring(0, key.length - 1);
return Object.entries(config).reduce<Record<string, unknown>>((acc, entry) =>
{
if (entry[0].startsWith(key))
acc[entry[0]] = entry[1];
return acc;
}, {}) as T;
}
else
return config.getValue(key) as T;
}
public static requireConfig(key: string): unknown
{
const value = ConfigurationManager.getConfig(key);
if (value == null || (typeof value === "string" && value.isEmptyOrWhiteSpace()))
throw new ApplicationException(`Required config '${key}' not found`);
return value;
}
public static requireStringConfig(key: string): string
{
const value = ConfigurationManager.requireConfig(key) as string;
return value.toString();
}
public static requireNumberConfig(key: string): number
{
const value = TypeHelper.parseNumber(ConfigurationManager.requireConfig(key));
if (value == null)
throw new ApplicationException(`Required number config '${key}' not found`);
return value;
}
public static requireBooleanConfig(key: string): boolean
{
const value = TypeHelper.parseBoolean(ConfigurationManager.requireConfig(key));
if (value == null)
throw new ApplicationException(`Required boolean config '${key}' not found`);
return value;
}
}
export interface ConfigurationProvider
{
provide(): Promise<Object>;
}