I have a file that has key/value pairs. This file is loaded inside the process.env
via Docker. But for the development purposes I am loading it manually, so at the end they are identical;
The configuration:
process.env['ccc.logger.winston.level']='info';
process.env['ccc.logger.winston.transports.type.file']='File';
process.env['ccc.logger.winston.transports.filename']='logs/testOne.log';
process.env['ccc.logger.winston.transports.rotate']='false';
process.env['ccc.logger.winston.transports.type.file']='File';
process.env['ccc.logger.winston.transports.filename']='logs/testTwo.log';
process.env['ccc.logger.winston.transports.rotate']='true';
My expectation is to have this object:
{
"ccc": {
"logger": {
"winston": {
"level": "info",
"transports": [
{
"type": "File",
"filename": "logs/testONE.log",
"rotate": true
},
{
"type": "File",
"filename": "logs/testTWO.log",
"rotate": false
}
]
}
}
}
}
I have come up with solution that works ok, but i am having problems if I have an array of objects, just like in the above example:
Code that loops all the keys/values and and calls function to create object:
let ENV_FROM_DOCKER = process.env;
for (let property in ENV_FROM_DOCKER) {
let checkForShallowProperties = property.split(".")[1];
if (typeof checkForShallowProperties === 'undefined') {
continue;
}
let resultObject = this.expand(property, ENV_FROM_DOCKER[property]););
emptyConfig = merge(emptyConfig, resultObject);
let stop;
}
Object creation function:
expand(str, value) {
let items = str.split(".") // split on dot notation
let output = {} // prepare an empty object, to fill later
let ref = output // keep a reference of the new object
// loop through all nodes, except the last one
for (let i = 0; i < items.length - 1; i++) {
ref[items[i]] = {}; // create a new element inside the reference
ref = ref[items[i]]; // shift the reference to the newly created object
}
ref[items[items.length - 1]] = value; // apply the final value
return output // return the full object
}
This setup works fine, But if I have an object that has array of objects (like in an example above), than it does not work properly. This is the output now:
{
"ccc": {
"logger": {
"winston": {
"level": "info",
"transports": {
"type": {
"file": "File"
},
"filename": "logs/testTwo.log",
"rotate": "true"
}
}
}
}
}
I am trying to get this code working for hours now, but just spinning in the circles. The ccc
object is one example. There are going to be other objects in the key/value list that might have also arrays as well.
For objects with thousands of named properties, our implementation indeed uses a hash table (as the question guessed), and that means complexity of Object. keys() is O(n log n). That's because a hash table (of course) stores entries in its own order.
An object can be created with figure brackets {…} with an optional list of properties. A property is a “key: value” pair, where key is a string (also called a “property name”), and value can be anything. We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key.
Objects and properties Unassigned properties of an object are undefined (and not null ). JavaScript object property names (keys) can only be strings or Symbols — all keys in the square bracket notation are converted to strings unless they are Symbols.
If you change your logic a little, and provide the value as split-able, maybe you could do something like this, where it is easier to detect what is keys and values, and if a value is split-able, you know to push it as an array.
There is yet one more decision to make, and that is how it should understand when values should add to a given object array, e.g. transports
array object, or create a new.
Maybe if the key exists in that array object, like in below snippet?
Stack snippet
var res = {};
expand(res,'ccc.logger.winston.level','info');
expand(res,'ccc.logger.winston.transports','type=File1');
expand(res,'ccc.logger.winston.transports','filename=logs/testOne.log');
expand(res,'ccc.logger.winston.transports','rotate=false');
expand(res,'ccc.logger.winston.transports','type=File2');
expand(res,'ccc.logger.winston.transports','filename=logs/testTwo.log');
expand(res,'ccc.logger.winston.transports','rotate=true');
console.log( res )
function expand(ref, str, value) {
let items = str.split(".") // split on dot notation
// loop through all key nodes
for(let i = 0; i < items.length - 1; i ++)
{
if (!ref[items[i]])
ref[items[i]] = {}; // create if not exist
ref = ref[items[i]]; // shift the object reference
}
let values = value.split("=") // split on equal sign
if (values.length > 1)
{
// apply array
var item = ref[items[items.length - 1]];
if (item) {
// add to array
if(!item[item.length - 1][values[0]]) {
item[item.length - 1][values[0]] = values[1]; // add to existing array
} else {
item.push( {[values[0]]: values[1]} ); // create new array item
}
} else {
// create array
ref[items[items.length - 1]] = [ {[values[0]]: values[1]} ];
}
} else {
// apply value
ref[items[items.length - 1]] = value;
}
//return ref; // return the full object
}
A few notes:
When assigning an object key using an array variable like this, {[values[0]]: values[1]}
, the key need to be enclosed in brackets []
.
For browsers not support ES6, use a normal variable, e.g.
var key = values[0];
{key: values[1]}
Object.assign
is usually used to merge object, though if a key exists, the value gets overwritten.
To overcome that, I passed an object reference to the expand function.
While creating your environment variables you can assign each transports.whatnot
to an index in an array transports[0].whatnot
and transports[1].whatnot
. To make this work we will have to parse it like so:
const ENV = {
'ccc.logger.winston.level': 'info',
'ccc.logger.winston.transports[0].type': 'File',
'ccc.logger.winston.transports[0].filename': 'logs/testOne.log',
'ccc.logger.winston.transports[0].rotate': 'false',
'ccc.logger.winston.transports[1].type': 'File',
'ccc.logger.winston.transports[1].filename': 'logs/testTwo.log',
'ccc.logger.winston.transports[1].rotate': 'true'
}
for (let property in ENV) {
let checkForShallowProperties = property.split('.')[1];
if (typeof checkForShallowProperties === 'undefined') {
continue;
}
let resultObject = expand(property, ENV[property])
console.log(resultObject)
}
function expand(string, value) {
const items = string.split('.').map(name => {
const match = name.match(/\[\d+?\]/)
return {
name: match ? name.slice(0, match.index) : name,
usesBrackets: !!match,
key: match && match[0].slice(1, -1)
}
})
const output = {}
let ref = output
let parent
for (const item of items) {
ref[item.name] = {}
parent = ref
ref = ref[item.name]
if (item.usesBrackets) {
ref[item.key] = {}
ref = ref[item.key]
}
}
parent[items[items.length - 1].name] = value
return output
}
As you can see, the way it works is by treating the object as its own array and placing content at the indices, or even other accessors.
However, it would most likely be in your best interest to move all of this over to a .json
file or some content management system, as this is a very volatile way of doing things. If your needs change you may need to rewrite this, with JSON you just load the JSON.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With