@willsoto/node-konfig-core
Version:
Core configuration pacakge supporting file, static and environment variables
277 lines (276 loc) • 8.51 kB
JavaScript
import { NoValueForKeyError } from "./errors.js";
/**
* Holds the configuration object.
*
* @example
* ```
* const store = new Store();
* ```
*
* @public
*/
export class Store {
options;
// Without the type assertion, I get TS error 2322:
//// Type '{}' is not assignable to type 'TConfig'.
//// '{}' is assignable to the constraint of type 'TConfig',
//// but 'TConfig' could be instantiated with a different subtype
//// of constraint 'Record<string, unknown>'.
// I don't know how to fix it though...
config = {};
/**
* Keeps track of all the groups associated with this Store instance.
*
* @internal
*/
#groups = [];
/**
* Keeps track of all the loaders associated with this Store instance.
*
* @internal
*/
#loaders = [];
constructor(options = {}) {
const { loaders = [], loadersByEnvironment = {}, ...rest } = options;
this.options = {
name: "default",
...rest,
};
this.registerLoaders(...loaders);
// Environment loaders should always be loaded last since anything registered
// for a particular environment should always be processed last against any loader that has been registered
// without an associated environment.
this.registerLoadersByEnvironment(loadersByEnvironment);
}
/**
* @internal
*/
get name() {
return this.options.name;
}
/**
* @internal
*/
get environment() {
if (typeof process.env.NODE_KONFIG_ENV === "string") {
return process.env.NODE_KONFIG_ENV;
}
else if (typeof process.env.NODE_ENV === "string") {
return process.env.NODE_ENV;
}
return "development";
}
/**
* The primary way to retrieve values from the {@link Store | Store}.
* Can traverse through `Group` as well.
*
* @param accessor - the path to the desired value within the store.
*
* @example
* ```
* const store = new Store();
*
* const value = store.get("path.to.my.thing");
* ```
*
* @public
*/
get(accessor) {
const path = accessor.split(".");
let current = this.config;
let index = 0;
while (index < path.length && current !== undefined) {
const key = path[index++];
if (current instanceof Store) {
current = current.get(key);
}
else if (typeof current === "object" && current !== null) {
current = current[key];
}
}
if (current instanceof Store) {
return current.toJSON();
}
return current;
}
/**
* If the given accessor is not present on the store or the returned value is `null`,
* an error will be thrown.
*
* {@link Store.get}
*/
getOrThrow(accessor) {
const value = this.get(accessor);
if (value === null || value === undefined) {
throw new NoValueForKeyError(accessor);
}
return value;
}
/**
* Manually set a value to the `Store`.
* In most circumstances, you should not need to use this directly.
*
* @param accessor - the path to the desired value within the store.
* @param value - the value to set at `accessor`
*
* @public
*/
set(accessor, value) {
const path = accessor.split(".");
let current = this.config;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
const next = current[key];
if (next instanceof Store) {
return next.set(path.slice(i + 1).join("."), value);
}
if (typeof next !== "object" || next === null) {
current[key] = {};
}
current = current[key];
}
current[path[path.length - 1]] = value;
return this.config;
}
/**
* Register a `Loader` to this `Store`. Use {@link Store.init} to initialize all of the Store's
* registered loaders.
*
* @param loader - Any `Loader` subclass
*
* @public
*/
registerLoader(loader) {
this.#loaders.push(loader);
return this;
}
/**
* Registers multiple loaders at once.
*/
registerLoaders(...loaders) {
loaders.forEach((loader) => this.registerLoader(loader));
return this;
}
/**
*
* @param loadersByEnvironment - An object composed of keys representing the
* environment mapping to any loaders that should be registered. Will determine the
* environment based on the value of `NODE_KONFIG_ENV` first (if set), `NODE_ENV` second (if set)
* and default to "development"
*
* @example
* ```
* store.registerLoadersByEnvironment({
* development: [new FileLoader()],
* staging: [new VaultLoader()],
* production: [new VaultLoader()],
* });
*```
* @public
*/
registerLoadersByEnvironment(loadersByEnvironment) {
const environmentLoaders = loadersByEnvironment[this.environment] ?? [];
this.registerLoaders(...environmentLoaders);
return this;
}
/**
* Given a config, will recursively merge all of its properties onto this instance's config.
* If a Group (ie `Store`) is encountered, it will correctly merge those properties onto that Group.
*
* @param config - The config to merge with this instance's config
*
* @public
*/
assign(config) {
Object.keys(config).forEach((key) => {
const existingValue = this.config[key];
if (existingValue instanceof Store) {
existingValue.assign(config[key]);
}
else {
this.set(key, config[key]);
}
});
return this;
}
/**
* Used to initialize the `Store` and process all registered `Loaders` and groups within.
*
* @public
*/
async init() {
for (const loader of this.#loaders) {
// eslint-disable-next-line @typescript-eslint/ban-types
await loader.load(this);
}
// Process all groups after the parent store is initialized
for (const group of this.#groups) {
await group.init();
}
}
/**
* Serialize the Store's configuration object. This will traverse all groups as well.
*
* @public
*/
toJSON() {
return Object.entries(this.config).reduce((accumlator, current) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [key, value] = current;
if (value instanceof Store) {
return {
...accumlator,
[key]: value.toJSON(),
};
}
return {
...accumlator,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
[key]: value,
};
}, {});
}
/**
* Get or set a sub-`Store` (group). Once a group has been created, you can register loaders
* specfic to that group. Calling `Store#init` on the parent `Store` will also initialize all the groups
* registered to that `Store` instance.
*
* @param name - The name of the group. This name is also how you access the group after creation.
* @param options - Any options to pass to the underlying store. Note that options will be **ignored**
* on subsequent calls once the group has been initialized.
*
* @example
* ```
* const store = new Store();
*
* store.registerLoader(new Loader());
*
* const group = store.group("myGroup");
*
* group.registerLoader(new Loader());
*
* // Options can be passed to the store when creating a group
* store.group("database", {
* loaders: [new Loader()]
* })
*
* await store.init();
* ```
*
* @public
*/
group(name, options = {}) {
let group = this.#groups.find((group) => group.name === name);
if (group) {
return group;
}
group = new Store({
name,
...options,
});
this.set(name, group);
this.#groups.push(group);
return group;
}
}
//# sourceMappingURL=store.js.map