Adventures of a Hobbyist ~ Part five

Andrew Bone - Aug 22 '18 - - Dev Community

Thinking a little more about conf files

bash

What is this?

This is part five of an ongoing series where I learn about NodeJS with the end goal of making some opensource software that will make life a little easier for my work colleagues and, hopefully, other IT Support teams out there in the world.

As I mentioned this is part five, here are the first 4 parts:

Didn't we do this already?

We did already have a look at .conf files but since then I've has some advice and gone a bit further and, I think, have settled on the general route I'm going to take.

Current conf file

I'm thinking I might have a file like this to store certain information I don't want in the database. I thought having a list of plugins here might be a good idea, the program I'm making will be primarily plugin focused.

{
  "General": {
    "name": "ignis"
  },
  "Plugin_list": {}
}
Enter fullscreen mode Exit fullscreen mode

What's your solution?

I found a module called conf which lets you input a file location, it expects the format to be JSON, and then has a bunch of methods to interact with that data.

I like the idea of using an event listener to listen for key changes too but, unfortunately, conf didn't offer this functionality so I decided to write a wrapper which could be imported.

Code

Here is the code as it stands today, though I'm tweaking this a lot at the moment.

const Conf = require('conf');
const EM = require('events');

/** Class helper for reading and writing to the conf file. */
class ConfHelper {
  /**
   * Initalise file.
   * @param {string} cn - config name, without extension. Default: 'ignis'
   * @param {string} fe - file extension. Default: 'conf'
   * @param {string} loc - file location, from current location. Default '.'
   */
  constructor(cn='ignis',fe='conf',loc='.') {
    this.events = new EM.EventEmitter();
    this.config = new Conf({configName: cn, fileExtension: fe, cwd: loc});
    this.conf   = this.config.get();
  }
  /** 
   * Update `this.conf` and trigger event. 
   * @event change
   * @private
   */
  _getConf(key = '', type) {
    this.conf = this.config.get();
    this.events.emit('change', key, type);
  }
  /** Create new key. */
  createKey(key = '', val) {
    if (this.config.has(key)) throw `${key} already exists, please use updateConf`
    let keyVal = this.conf;
    let layers = key.split('.');
    let name = layers[layers.length - 1];
    for (let i = 0; i < layers.length - 1; i++) {
      if (!keyVal[layers[i]]) keyVal[layers[i]] = {};
      keyVal = keyVal[layers[i]];
    }
    keyVal[name] = val;
    this.config.set(layers[0], this.conf[layers[0]]);
    this._getConf(key, "create");
  }
  /** Delete existing key. */
  deleteKey(key = '') {
    if (!this.config.has(key)) return
    this.config.delete(key);
    this._getConf(key, "delete");
  }
  /** Update existing key. */
  updateKey(key = '', val) {
    if (!this.config.has(key)) throw `${key} does not exists please use createConf`
    if (this.config.get(key) === val) return
    this.config.set(key, val);
    this._getConf(key, "update");
  }
}

module.exports = ConfHelper;
Enter fullscreen mode Exit fullscreen mode

This is the first time I've made a module to export so I have no idea if I've followed best practices, as always, feel free to offer me some correction in the comments.

As you can see I have a method called _getConf which emits a change event with the key that was changed and the change type, again not sure if this is best practice but it seemed to work for me.

Simple test

I made a simple test to try out the event system and to make sure it was able to read and write the conf file.

const ConfHelper = require('./conf_import');
const ch = new ConfHelper()

ch.events.on('change', (key, type) => {
  let event =`
  -------
  type    ${type}               
  key     ${key}                
  newVal  ${ch.config.get(key)} 
  -------`;
  console.log(event)
});

ch.createKey('General.version', "v0.0.1");
ch.updateKey('General.version', "v0.0.2");
ch.deleteKey('General.version');
Enter fullscreen mode Exit fullscreen mode

I have an event listener that prints out a small table that shows the type of change, the key that changed and then reads the data to get the new value. I then create a new key, update that key and then delete it.

Here's the output

-------
type    create
key     General.version
newVal  v0.0.1
-------

-------
type    update
key     General.version
newVal  v0.0.2
-------

-------
type    delete
key     General.version
newVal  undefined
-------
Enter fullscreen mode Exit fullscreen mode

What now?

Well now I'd like some input, is this a decent way to do things? I want to make a method that reads the file and checked for changes since it was last read then reports them but that will be a problem for next week.

Overview

I would foresee install/update scripts for plugins looking a little something like this.

const ConfHelper = require('./conf_import');
const ch = new ConfHelper();

const key = 'SambaAD'
const ver = 'v0.4.3'

if (!ch.conf.Plugin_list[key]) {
  install();
} else if (ch.conf.Plugin_list[key] !== ver) { // this is actually a mismatch so we're not sure if it's an upgrade or a roll back.
  update();
} else {
  console.log(`${key} is already on the latest version (${ver})`);
  // uninstall included for test purposes
  uninstall()
}

function install() {
  // some install stuff
  ch.createKey(`Plugin_list.${key}`, ver);
  ch.createKey(key, {});
  ch.createKey(`${key}.name`, 'Samba AD');
  ch.createKey(`${key}.description`, 'Controller for Samba AD');
  ch.createKey(`${key}.author`, 'Andrew Bone');
  ch.createKey(`${key}.help`, 'https://lmgtfy.com');
}

function update() {
  // some update stuff
  ch.updateKey(`Plugin_list.${key}`, ver);
}

function uninstall() {
  // some delete stuff
  ch.deleteKey(key);
  ch.deleteKey(`Plugin_list.${key}`);
}
Enter fullscreen mode Exit fullscreen mode

Running this as it currently stands would result in ignis.conf being updated as follows

{
  "General": {
    "name": "ignis"
  },
  "Plugin_list": {
    "SambaAD": "v0.4.3"
  },
  "SambaAD": {
    "name": "Samba AD",
    "description": "Controller for Samba AD",
    "author": "Andrew Bone",
    "help": "https://lmgtfy.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

</post>

And there we have it another post, thank you for reading. My aim with these posts is for my journey to be interesting, engaging and beneficial for the readers as well as for me if there is anything I can do to make my posts better please let me know. The comments I've received in the past have all been great, talking to the community after a post is a definite highlight.

Thanks again 🦄

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .