Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • libs/config
1 result
Show changes
Commits on Source (1)
Showing
with 272 additions and 305 deletions
......@@ -8,8 +8,9 @@ Simple Config with ENV and Files Support.
## Usage
`const Config = require('@libs/config')`
`let config = new Config([basePath], [EnvPrefix])`
`import { createConfig } from '@libs/config';`
`const config = createConfig([basePath], [EnvPrefix]);`
Then config is your config object. (Use it like config.setting)
......@@ -21,12 +22,12 @@ It reads from the following sources, performing a deep merge:
- config.defaults.json
Enviroment Variables can target deep nested settings:
The Setting _setting.deep.key_ can be reached with *SETTING_DEEP_KEY*
The Setting _setting.deep.key_ can be reached with _SETTING_DEEP_KEY_
You may use the function __reload()_ to reload the config from all sources.
`config._reload()``
This makes *_reload* a reserved keyword
This makes __reload_ a reserved keyword
You may use the function __show()_ to display the config without:
......@@ -44,13 +45,13 @@ Values will be replaced with the value "redacted"
`config._show()``
This makes *_show* a reserved keyword
This makes __show_ a reserved keyword
If you give a basePath, the config-Files are used from there.
Else the main dir of the application will be used.
YOu can give a prefix for the enviroment variables.
e.g. if you give "PREFIX" as prefix, the setting "setting.deep.key" can be reached with *PREFIX_SETTING_DEEP_KEY*
e.g. if you give "PREFIX" as prefix, the setting "setting.deep.key" can be reached with _PREFIX_SETTING_DEEP_KEY_
### Values
......@@ -70,9 +71,10 @@ You can also use files to read the value from. This makes the config compatible
}
```
## Examples
Run `npm install` and `tsc` before running the examples.
### Only config.defaults.json
`node examples/only-defaults/index.js`
......@@ -81,7 +83,7 @@ You can also use files to read the value from. This makes the config compatible
(Kind of legacy use)
`node examples/only-local/index.js`
`node dist/examples/only-local/index.js`
### defaults and config.json
......
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "overwritten",
"another": {
"more": "settings"
}
}
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "overwritten",
"another": {
"more": "settings"
}
}
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "overwritten",
"another": {
"more": "settings"
}
}
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "overwritten",
"another": {
"more": "file:examples/files/setting_2.txt"
}
}
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("../../index");
const config = (0, index_1.createConfig)();
console.log(JSON.stringify(config, undefined, 2));
{
"setting": "value",
"another": {
"setting": "avalue"
}
}
{
"setting": "overwritten",
"another": {
"more": "settings"
}
}
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createConfig = exports.Config = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class Config {
basePath;
envPrefix;
configDefaults;
configLocal;
configData;
constructor(basePath = undefined, envPrefix = undefined) {
this.basePath = basePath;
this.envPrefix = envPrefix ? envPrefix.toUpperCase() + "_" : "";
this.configDefaults = this.basePath
? path.join(this.basePath, 'config.defaults.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.defaults.json');
this.configLocal = this.basePath
? path.join(this.basePath, 'config.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.json');
this.configData = {};
this._reload();
}
objectDeepKeys(obj) {
return Object.keys(obj)
.filter((key) => obj[key] instanceof Object)
.map((key) => this.objectDeepKeys(obj[key]).map((k) => `${key}.${k}`))
.reduce((x, y) => x.concat(y), Object.keys(obj));
}
set(obj, path, value) {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key])
schema[key] = {};
schema = schema[key];
});
schema[keys[keys.length - 1]] = value;
}
get(obj, path) {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key])
schema[key] = {};
schema = schema[key];
});
return schema[keys[keys.length - 1]];
}
merge(target, source) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key) &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
this.merge(target[key], source[key]);
}
else {
target[key] = source[key];
}
}
return target;
}
_reload() {
// Handle config.defaults.json
try {
if (fs.existsSync(this.configDefaults)) {
const defaults = JSON.parse(fs.readFileSync(this.configDefaults, 'utf8'));
this.configData = this.merge(this.configData, defaults);
}
else {
console.log(`Defaults file missing: ${this.configDefaults}`);
}
}
catch (error) {
console.error(`Error reading defaults file: ${this.configDefaults}`);
if (error instanceof Error) {
console.error(error.message);
}
else {
console.error('Unknown error occurred:', error);
}
}
// Handle config.json
try {
if (fs.existsSync(this.configLocal)) {
const local = JSON.parse(fs.readFileSync(this.configLocal, 'utf8'));
this.configData = this.merge(this.configData, local);
}
else {
console.log(`Missing file: ${this.configLocal}`);
}
}
catch (error) {
console.error(`Error reading local config file: ${this.configLocal}`);
if (error instanceof Error) {
console.error(error.message);
}
else {
console.error('Unknown error occurred:', error);
}
}
// Apply environment variables
const keys = this.objectDeepKeys(this.configData);
for (const key of keys) {
const envKey = this.envPrefix + key.toUpperCase().replace(/\./g, '_');
const envValue = process.env[envKey];
if (envValue !== undefined) {
const parsedValue = envValue === 'true' ? true : envValue === 'false' ? false : envValue;
this.set(this.configData, key, parsedValue); // Update configData
}
}
// Handle "file:" values
for (const key of keys) {
const value = this.get(this.configData, key)?.toString();
if (value?.startsWith('file:')) {
try {
const fileValue = fs.readFileSync(value.replace('file:', ''), 'utf8');
this.set(this.configData, key, fileValue);
}
catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
else {
console.error('An unknown error occurred:', error);
}
}
}
}
// Assign the configData properties to the instance
Object.assign(this, this.configData);
}
_show() {
const redactedConfig = JSON.parse(JSON.stringify(this.configData));
const keys = this.objectDeepKeys(redactedConfig);
for (const key of keys) {
if (/password|secret|token|key|apikey|apitoken|apisecret|user|username/i.test(key)) {
this.set(redactedConfig, key, 'REDACTED');
}
}
return redactedConfig;
}
toJSON() {
// Return only the properties of configData for JSON serialization
return { ...this.configData };
}
}
exports.Config = Config;
// Factory function for usage compatibility
function createConfig(basePath, envPrefix) {
return new Config(basePath, envPrefix);
}
exports.createConfig = createConfig;
exports.default = Config;
const Config = require('../../index')
let config = new Config()
console.log(JSON.stringify(config, undefined, 2))
\ No newline at end of file
import { createConfig } from '../../index';
const config = createConfig();
console.log(JSON.stringify(config, undefined, 2));
\ No newline at end of file
// Define the shape of the return type of the config function
export interface Config {
_reload(): void;
_show(): any;
[key: string]: any; // Index signature for dynamic properties
}
// Declare the function that returns a Config object
export declare function createConfig(basePath?: string, envPrefix?: string): Config;
var fs = require('fs')
var path = require('path')
var merge = require('lodash.merge')
function createConfig(basePath = undefined, envPrefix = undefined) {
let configDefaults
let configLocal
let envPrefixUpper = envPrefix ? envPrefix.toUpperCase() + "_" : ''
if (basePath) {
configDefaults = path.join(basePath, 'config.defaults.json')
configLocal = path.join(basePath, 'config.json')
} else {
configDefaults = path.join(path.dirname(require.main.filename), 'config.defaults.json')
configLocal = path.join(path.dirname(require.main.filename), 'config.json')
}
let config = {}
const objectDeepKeys = function (obj) {
return Object.keys(obj).filter(key => obj[key] instanceof Object).map(key => objectDeepKeys(obj[key]).map(k => `${key}.${k}`)).reduce((x, y) => x.concat(y), Object.keys(obj))
}
const set = function(obj, path, value) {
var schema = obj // a moving reference to internal objects within obj
var pList = path.split('.')
var len = pList.length
for(var i = 0; i < len-1; i++) {
var elem = pList[i]
if( !schema[elem] ) schema[elem] = {}
schema = schema[elem]
}
schema[pList[len-1]] = value
}
const get = function(obj, path) {
var schema = obj // a moving reference to internal objects within obj
var pList = path.split('.')
var len = pList.length
for(var i = 0; i < len-1; i++) {
var elem = pList[i]
if( !schema[elem] ) schema[elem] = {}
schema = schema[elem]
}
return schema[pList[len-1]]
}
config._reload = function () {
try {
fs.accessSync(configDefaults)
config = merge(this, JSON.parse(fs.readFileSync(configDefaults, 'utf8')))
} catch(error) {
console.log('No File ' + configDefaults)
}
try {
fs.accessSync(configLocal)
config = merge(this, JSON.parse(fs.readFileSync(configLocal, 'utf8')))
} catch (error) {
console.log('No File ' + configLocal)
}
let keys = objectDeepKeys(this)
for (let index = 0; index < keys.length; index++) {
const element = keys[index]
let env = process.env[envPrefixUpper + element.toUpperCase().replace(/\./g, '_')]
if (env) {
env = (env == 'true') ? true : env
env = (env == 'false') ? false : env
set(this, element, env)
}
}
for (let index = 0; index < keys.length; index++) {
const element = keys[index]
let value = get(config, element).toString()
if (value.startsWith('file:')) {
try {
set(config, element, fs.readFileSync(value.replace('file:', ''), 'utf8'))
} catch (error) {
console.error(error.message)
}
}
}
}
config._show = function () {
// clone the config object
let config = JSON.parse(JSON.stringify(this))
// remove _reload
delete config._reload
// remove _show
delete config._show
// remove _set
delete config._set
// remove _get
delete config._get
// redact all nested objects where the key is
// 'password' or 'secret' or 'token' or 'key' or 'apiKey' or 'apiToken' or 'apiSecret'
// or 'user' or 'username'
let keys = objectDeepKeys(config)
for (let index = 0; index < keys.length; index++) {
const element = keys[index]
if (element.toLowerCase().includes('password') || element.toLowerCase().includes('secret') || element.toLowerCase().includes('token') || element.toLowerCase().includes('key') || element.toLowerCase().includes('apikey') || element.toLowerCase().includes('apitoken') || element.toLowerCase().includes('apisecret') || element.toLowerCase().includes('user') || element.toLowerCase().includes('username')) {
set(config, element, 'REDACTED')
}
}
return config
}
config._reload()
return config
}
module.exports = { createConfig };
\ No newline at end of file
const fs = require('fs').promises
const createConfig = require('./index').createConfig
let jsonDefaults = {
setting:"defaultvalue",
another: {
setting:"avalue"
}
}
let jsonLocals = {
setting:"localvalue",
another: {
more:"stuff"
},
even: {
deeper: {
key: 'sodeep'
}
}
}
describe('config', function() {
it('should only have values from config.defaults.json', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
let config = createConfig()
await fs.unlink('config.defaults.json')
expect(config.setting).toBe('defaultvalue')
expect(config.another.setting).toBe('avalue')
})
it('should only have values from config.json', async function() {
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
let config = createConfig()
await fs.unlink('config.json')
expect(config.setting).toBe('localvalue')
expect(config.another.more).toBe('stuff')
})
it('should have both values with preference to config.json', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
let config = createConfig()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
expect(config.setting).toBe('localvalue')
expect(config.another.more).toBe('stuff')
expect(config.another.setting).toBe('avalue')
})
it('should respect a given basepath', async function() {
await fs.mkdir('tmp')
await fs.writeFile('tmp/config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('tmp/config.json', JSON.stringify(jsonLocals))
let config = createConfig('tmp')
await fs.unlink('tmp/config.json')
await fs.unlink('tmp/config.defaults.json')
await fs.rmdir('tmp')
expect(config.setting).toBe('localvalue')
expect(config.another.more).toBe('stuff')
expect(config.another.setting).toBe('avalue')
})
it('should have all values with preference to env', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
process.env['SETTING'] = 'overwritten-by-env'
process.env['ANOTHER_MORE'] = 'false'
process.env['EVEN_DEEPER_KEY'] = 'true'
let config =createConfig()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
delete process.env['SETTING']
delete process.env['ANOTHER_MORE']
delete process.env['EVEN_DEEPER_KEY']
expect(config.setting).toBe('overwritten-by-env')
expect(config.another.more).toBe(false)
expect(config.even.deeper.key).toBe(true)
expect(config.another.setting).toBe('avalue')
})
it('should have all values with preference to env and prefix', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
process.env['P_SETTING'] = 'overwritten-by-env'
process.env['P_ANOTHER_MORE'] = 'false'
process.env['P_EVEN_DEEPER_KEY'] = 'true'
let config = createConfig(undefined, 'p')
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
delete process.env['P_SETTING']
delete process.env['P_ANOTHER_MORE']
delete process.env['P_EVEN_DEEPER_KEY']
expect(config.setting).toBe('overwritten-by-env')
expect(config.another.more).toBe(false)
expect(config.even.deeper.key).toBe(true)
expect(config.another.setting).toBe('avalue')
})
it('should read in a file when given', async function() {
jsonLocals.setting = 'file:file.txt'
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
await fs.writeFile('file.txt', 'value-from-file')
let config = createConfig()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
await fs.unlink('file.txt')
jsonLocals.setting = 'localvalue'
expect(config.setting).toBe('value-from-file')
})
it('should show a message and keep the setting when file given but not found', async function() {
jsonLocals.setting = 'file:file.txt'
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
let config = createConfig()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
jsonLocals.setting = 'localvalue'
expect(config.setting).toBe('file:file.txt')
})
it('should _reload if asked', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
let config = createConfig()
expect(config.setting).toBe('localvalue')
jsonLocals.setting = 'reloaded-value'
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
config._reload()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
jsonLocals.setting = 'localvalue'
expect(config.setting).toBe('reloaded-value')
})
it('should redact passwords if using _show', async function() {
await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults))
await fs.writeFile('config.json', JSON.stringify(jsonLocals))
let config = createConfig()
await fs.unlink('config.json')
await fs.unlink('config.defaults.json')
console.dir(config._show())
expect(config._show().even.deeper.key).toBe('REDACTED')
})
})