Get environment specific configuration from a JSON object using Lodash
Asked Answered
P

2

1

Given that I have the following JSON object,

dbConfig = {
    "db": "default",
    "default": {
        "defaultDB": "sqlite",
        "init": "init",
        "migrations": {
            "directory": "migrations",
            "tableName": "migrations"
        },
        "pool": {
            "min": "2",
            "max": "10"
        },
        "sqlite": {
            "client": "sqlite3",
            "connection": {
                "filename": "data/default/sqlitedb/test.db"
            }
        },
        "oracle": {
            "client": "oracledb",
            "config": {
                "development": {
                    "user": "test",
                    "pass": "test",
                    "db": "test"
                },
                "production": {
                    "user": "test",
                    "pass": "test",
                    "db": "test"
                },
                "test": {
                    "user": "test",
                    "pass": "test",
                    "db": "test"
                }
            }
        }
    }
};

Using Node & Lodash, is there any possibility of getting either connection or config. depending on what dbConfig.default[dbConfig.default.defaultDB] is set to.

So for instance if i set dbConfig.default.defaultDB=oracledb and process.env.NODE_ENV=development I want to be able to get dbConfig.default[dbConfig.default.defaultDB].config.development

Or if I set dbConfig.default.defaultDB=sqlite just to get dbConfig.default[dbConfig.default.defaultDB].connection

In other words, if the database has environment specific configuration then this will be in "config": {} and if not in "connection": {}

It doesn't have to be Lodash. It can also be plain javascript.

Ptolemaic answered 8/5, 2016 at 12:17 Comment(0)
T
2

Solution without lodash

var defaultDbName = dbConfig.default[dbConfig.default.defaultDB];

var db;
if (defaultDb === 'sqllite') {
  db = dbConfig.default[defaultDb].connection;
} else {
  var env = process.env.NODE_ENV;
  db = dbConfig.default[defaultDb].config[env];
}

Solution with lodash

Here I'm using lodash get function to get object field value or null if it doesn't exist. Also I'm using template string syntax: ${val} to format field path.

var defaultDbName = dbConfig.default[dbConfig.default.defaultDB];
var defaultDbConf = dbConfig.default[defaultDb];
var env = process.env.NODE_ENV;

var db = defaultDbConf.connection || _.get(defaultDbConf, `config.${env}`);

Btw, your configuration json is too complex, much better to have configuration per environment.

Trichloride answered 8/5, 2016 at 13:16 Comment(2)
Thats brilliant. Thank youPtolemaic
Many configs are this complex, it just looks worse because of the needless duplication of not-environment-specific properties.This is to say, it's no worse than the common habit of duplicating entire .properties files per environment, and then making sure all the attributes remain in sync as new features make their way forward to your final deploy environment. See below for a zero/minimal-duplication mechanism that adds readable notation to object attribute names that can be filtered down, recursively by a single function either at run/render time or as part of your pre-deploy workflow.Tartrazine
T
1

Solution without [dependencies] (originally answered here, but not AngularJS-specific)

Your JSON is complex, yes, but it could also be smaller and more readable without all the duplication, where each environment has the same set of attributes, which may or may not vary, and would be needlessly duplicated.

With a simple algorithm (jsFiddle) you can dynamically parse your JSON configuration for specific property-name suffixes (property@suffix) and have a catalogue of environment-varying properties alongside non-varying properties, without artificially structuring your configuration and without repetition, including deeply-nested configuration objects.

You can also mix-and-match suffixes and combine any number of environmental or other arbitrary factors to groom your configuration object.

Example, snippet of pre-processed JSON config:

var config = {
    'help': {
        'BLURB': 'This pre-production environment is not supported. Contact Development Team with questions.',
        'PHONE': '808-867-5309',
        'EMAIL': '[email protected]'
    },
    '[email protected]': {
        'BLURB': 'Please contact Customer Service Center',
        'BLURB@fr': 'S\'il vous plaît communiquer avec notre Centre de service à la clientèle',
        'BLURB@de': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
        'PHONE': '1-800-CUS-TOMR',
        'EMAIL': '[email protected]'
    },
}

... and post-processed (given location.hostname='www.productionwebsite.com' and navigator.language of 'de'):

prefer(config,['www.productionwebsite.com','de']); // prefer(obj,string|Array<string>)

JSON.stringify(config); // {
    'help': {
        'BLURB': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
        'PHONE': '1-800-CUS-TOMR',
        'EMAIL': '[email protected]'
    }
}

Obviously you can pull those values at render-time with location.hostname and window.navigator.language. The algorithm to process the JSON itself isn't terribly complex (but you may still feel more comfortable with an entire framework for some reason, instead of a single function):

function prefer(obj,suf) {
    function pr(o,s) {
        for (var p in o) {
            if (!o.hasOwnProperty(p) || !p.split('@')[1] || p.split('@@')[1] ) continue; // ignore: proto-prop OR not-suffixed OR temp prop score
            var b = p.split('@')[0]; // base prop name
            if(!!!o['@@'+b]) o['@@'+b] = 0; // +score placeholder
            var ps = p.split('@')[1].split('&'); // array of property suffixes
            var sc = 0; var v = 0; // reset (running)score and value
            while(ps.length) {
                // suffix value: index(of found suffix in prefs)^10
                v = Math.floor(Math.pow(10,s.indexOf(ps.pop())));
                if(!v) { sc = 0; break; } // found suf NOT in prefs, zero score (delete later)
                sc += v;
            }
            if(sc > o['@@'+b]) { o['@@'+b] = sc; o[b] = o[p]; } // hi-score! promote to base prop
            delete o[p];
        }
        for (var p in o) if(p.split('@@')[1]) delete o[p]; // remove scores
        for (var p in o) if(typeof o[p] === 'object') pr(o[p],s); // recurse surviving objs
    }
    if( typeof obj !== 'object' ) return; // validate
    suf = ( (suf || suf === 0 ) && ( suf.length || suf === parseFloat(suf) ) ? suf.toString().split(',') : []); // array|string|number|comma-separated-string -> array-of-strings
    pr(obj,suf.reverse());
}

The property name suffix can have any number of suffixes after the '@', delimited by '&' (ampersand) and, where there are two properties with different but preferred suffixes, will be preferred in the order in which they are passed to the function. Suffixes that contain BOTH preferred strings will be preferred above all others. Suffixes found in the JSON that are not specified as preferred will be discarded.

Preference/discrimination will be applied top-down on your object tree, and if higher-level objects survive, they will be subsequently inspected for preferred suffixes.

With this approach, your JSON (I'm making some assumptions about which attributes vary between your environments and which do not) might be simplified as follows:

dbConfig = {
    "pool": {
        "min": "2",
        "max": "10"
    },
    "init": "init",
    "migrations": {
        "directory": "migrations",
        "tableName": "migrations"
    },
    "db":
        "client": "sqlite",
        "filename": "data/default/sqlitedb/development.db"
        "filename@tst": "data/default/sqlitedb/test.db"
        "filename@prd": "data/default/sqlitedb/production.db"
    },
    "db@oracle": {
        "client": "oracle",
        "user": "devuser",
        "user@tst": "testdbuser",
        "user@prd": "testdbuser",
        "pass": "devpass",
        "pass@tst": "testdbpass",
        "pass@prd": "testdbpass",
        "db": "devdb",
        "db@tst": "testdbschema",
        "db@prd": "testdbschema"
    }
};

So that you could feed this into the prefer() function with these args+results:

for sqlite, test env:

prefer(dbConfig,'tst');
JSON.stringify(dbConfig); // dbConfig: {
    "pool": {
        "min": "2",
        "max": "10"
    },
    "init": "init",
    "migrations": {
        "directory": "migrations",
        "tableName": "migrations"
    },
    "db": {
        "client": "sqlite",
        "filename": "data/default/sqlitedb/test.db"
    }
};

for oracle, default/development environment:

prefer(dbConfig,'oracle'); // oracle, dev(default) env
JSON.stringify(dbConfig); // dbConfig: {
    "pool": {
        "min": "2",
        "max": "10"
    },
    "init": "init",
    "migrations": {
        "directory": "migrations",
        "tableName": "migrations"
    },
    "db": {
        "client": "oracle",
        "user": "devdbuser",
        "pass": "devdbpass",
        "db": "devdbschema"
    }
};

prefer(dbConfig,'oracle,prd'); // oracle, production env
JSON.stringify(dbConfig); // dbConfig: {
    "pool": {
        "min": "2",
        "max": "10"
    },
    "init": "init",
    "migrations": {
        "directory": "migrations",
        "tableName": "migrations"
    },
    "db": {
        "client": "oracle",
        "user": "prddbuser",
        "pass": "prddbpass",
        "db": "prddbschema"
    }
};

Abstract usage and examples:

var o = { 'a':'apple', 'a@dev':'apple-dev', 'a@fr':'pomme',
          'b':'banana', 'b@fr':'banane', 'b@dev&fr':'banane-dev',
          'c':{ 'o':'c-dot-oh', 'o@fr':'c-point-oh' }, 'c@dev': { 'o':'c-dot-oh-dev', 'o@fr':'c-point-oh-dev' } };

/*1*/ prefer(o,'dev');        // { a:'apple-dev', b:'banana',     c:{o:'c-dot-oh-dev'}   }
/*2*/ prefer(o,'fr');         // { a:'pomme',     b:'banane',     c:{o:'c-point-oh'}     }
/*3*/ prefer(o,'dev,fr');     // { a:'apple-dev', b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*4*/ prefer(o,['fr','dev']); // { a:'pomme',     b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*5*/ prefer(o);              // { a:'apple',     b:'banana',     c:{o:'c-dot-oh'}       }

Caveats Usage of the @ in property name is NOT standard and is invalid in dot-notation, but so far has not broken any browsers we've tested this in. The UPSIDE of this is that it prevents developers from expecting they can refer to your pre-processed, suffixed attributes. A developer would have to be aware of, and a bit unconventional and refer to your attribute as a string (obj['key@suf']) to do that, which, by the way, is the reason this function is possible.

If future JavaScript engines reject it, substitute for any other tolerable convention, just be consistent. This algorithm has not been profiled for performance, or rigorously tested for other potential problems. In its current form, used one-time on startup/load, we have yet to run into problems with. As always, YMMV.

Tartrazine answered 16/12, 2016 at 21:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.