diff --git a/README.md b/README.md index c47f259..1bc0b82 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,25 @@ autoenvconfig Stop worrying about the hassle of **loading environment config files everywhere**, **config files with missing entries** or **schemas that gets old when you forget to add a new config key to it**. -**AutoEnvConfig** is a fully-tested no-nonsense no-dependencies package to help you keep your ever expanding project under control. +**AutoEnvConfig** is a fully-tested* no-nonsense no-dependencies package to help you keep your ever expanding project under control. _*(bear with me on the beta features)_ **AutoEnvConfig** was designed with simple but very powerful goals: * **Blazingly fast**: it caches everything it can, loads your files only once and check your schema on load, not on usage; * **No extra configuration needed**: this package follows the idea of _convention over configuration_, so it keeps the required environment files to a minimum and there is nothing to thinker with before start using it; * **Never out of sync**: when loading the environment configuration file, it checks the schema for *missing AND for extra keys*, alerting you when you are missing some key in your config or in your schema file; -* **Auto load the right config file**: you can set the root path of the project in the environment config file and it will load automatically. +* **Auto load of the right config file**: you can set the root path of the project in the environment config file and it will load automatically; +* **Possible local [eventual persistence](#eventualpersistence)** (beta): you can overwrite data by code and have it persisted across restarts (or even deploys!) whenever needed. Things like general settings' dashboard for a small project gets really fast and easy (look, ma!, no databases!). You can backup your persistence file whenever you need or simply merge it with your env config periodically to make changes definitive. -Simply create your [schema file](#sampleschema) specifying optional and required keys (plus: it can contain default values, too!), setup an [environment file](#sampleenv) with optional specific overrides and you're good to go! +To use it right away, [npm install it](#installation) and follow the super simple [Quick Start Guide](#quickstart). -Check out the [Installation Instructions](#installation), [Quick Start Guide](#quickstart), [Conventions](#conventions) and the [Public Methods API](#methods). +For more details, check out the [Conventions](#conventions) and [Public Methods API](#methods). ## Installation The simplest way to install this package is using [npm](http://www.npmjs.com/): ```bash -$ npm i -S AutoEnvConfig +$ npm i AutoEnvConfig ``` You can also manually download the [latest release](https://github.com/dnunes/autoenvconfig/zipball/master) from [our project page](http://dnunes.com/autoenvconfig/) or any release from [our GitHub repository](https://github.com/dnunes/autoenvconfig/) on the [releases page](https://github.com/dnunes/autoenvconfig/releases/). @@ -33,8 +34,8 @@ There are just four steps needed to start using this package: 1. Create a folder named `envs` on your project's root; 2. Create a [`config.schema`](#sampleschema) file with your schema; -3. Create a [`ENVNAME.json`](#sampleenv) file for each environment with its specific configuration (where `ENVNAME` is whatever name you wish to use); -4. Load the package. It is ready to read your config. +3. Create a [`ENVNAME.json`](#sampleenv) file for your environments with their specific configuration (where `ENVNAME` is whatever name you wish to use); +4. Load the package. Done. In your code: @@ -46,6 +47,9 @@ if (isValuePresent) { let valueFromConfig = AutoEnvConfig.get('deep.key.supported')); console.log(valueFromConfig); //"myValue" } + +let itDoesDefaults = AutoEnvConfig.get('invalid.key', 'default'); +console.log(itDoesDefaults); //"default" ``` ## Conventions @@ -54,7 +58,7 @@ if (isValuePresent) { One of the nicest features of the package is that you don't need to specify the environment, as it will magicly detect and load the correct file based on some values. This auto generated instance is called [_magic instance_](#magicload). -For the magic load to happen, your [`config.schema`](#sampleschema) and [`ENVNAME.json`](#sampleenv) files must have a "path" key with the path of your project's root. It will find the correct environment checking this value by default. You can, however, safely ignore this convention and manually specify the file name. +For the magic load to happen, your [`config.schema`](#sampleschema) and [`ENVNAME.json`](#sampleenv) files must have a `_path` key with the path of your project's root. It will find the correct environment checking this value by default. You can, however, safely ignore this convention and manually specify the file name if needed. ### Schema and Environment File formats The schema and environment config files are simple JSON files. The only limit for the _keys_ is the dot character ("`.`") which is forbidden (because it is used as a separator when loading), but I suggest you to limit your keys to alphanumeric chars for simplicity. @@ -67,9 +71,10 @@ In the schema files, every key _MUST_ be prefixed with either `#` or `?`, indica ### Sample config.schema ```json { + "# _path": "", + "# id": "", "# envtype": "", - "# path": "", "# requiredKey": "", "? deep": { @@ -86,47 +91,79 @@ You can have a required key inside an optional object (in this sample, the `supp ### Sample ENVNAME.json ```json { + "_path": "/home/dnunes/code/node/autoenvconfig", + "id": "dev", "envtype": "local", - "path": "/home/dnunes/code/node/autoenvconfig", "requiredKey": "value" } ``` +## Eventual Persistence + +If you need to overwrite some settings through code you can use [`AutoEnvConfig.set(, ))`](#mautoset) method (or its [instance counterpart](#minsset)). This change will not survive restarts or new deploys, though. For any time you need to have something persisted across local restarts or even deploys, you can now use [`AutoEnvConfig.persist(, ))`](#mautoper) and it will be loaded automatically on the next run/instance creation. + +The persistence file defaults to the path `envs/ENVNAME.persist.json` but you can freely configure it using the `_persistFile` key in your [`ENVNAME.json`](#sampleenv). The path resolution defaults to the root of your project, so a value of `custom.persist.json` for the `_persistFile` key would create the persistence file in your project's root folder. + +To avoid filling your I/O with multiple writes to disk, it will only persist the data periodically and efficiently using a _minimum interval_ setting and _dirty detection_ logic, so it's safe to call persist methods multiples times a second with no impact on performance whatsoever. An even improved logic suitable for eventual bursts in settings will be implemented in the future. + + ## Methods -All the methods can be called in a specific instance (from a `AutoEnvConfig.load` call) or in the [_magic instance_](#magicload). You can save a reference for the [_magic instance_](#magicload) using a `AutoEnvConfig.load()` call and call methods on this instance as well and it will work exactly the same as calling the methods directly on the package. +All the methods can be called in a specific instance (from a `AutoEnvConfig.load` call) or in the [_magic instance_](#magicload). You can save a reference for the [_magic instance_](#magicload) using a `AutoEnvConfig.load()` call and call methods on this instance as well and it will work exactly the same as calling the methods directly on the package. The only exception is the "AutoEnvConfig.enablePersistence()" and "AutoEnvConfig.disablePersistence()" ### Magic Methods -- `AutoEnvConfig.load([])` -This method will return a new instance of `AutoEnvConfig` class (actually, prototype). If you ommit the `` parameter, it will try to [_magic load_](#magicload) it. If you pass the `` parameter, it will just return the config for the specified env. It returns false if it cannot find an environment config. +- `AutoEnvConfig.load([envName [, forceNew = false]])` +This method will return a new instance of `AutoEnvConfig`. If you ommit the `[envName]` parameter, it will try to [_magic load_](#magicload) the right env file. If you pass the `[envName]` parameter, it will just return the config for the specified env. It returns false if it cannot find a suitable environment config. +If `[forceNew]` is `false`, it will reuse the first instance for the same `[envName]` created before. -- `AutoEnvConfig.get([, ])` -This method will return the value of `` in the [_magic instance_](#magicload). If `key` is not present in the [_magic instance_](#magicload), it will either return `` or throw an error if there the default value parameter was committed. +- `AutoEnvConfig.get([, defaultValueIfNotPresent])` +This method runs [`.get([, defaultValueIfNotPresent])`](#minsget) on the [_magic instance_](#magicload). - `AutoEnvConfig.has()` -This method will return boolean `true` if the `` is present in the [_magic instance_](#magicload) or boolean `false` if not. +This method runs [`.has()`](#minshas) on the [_magic instance_](#magicload). - `AutoEnvConfig.set(, )` -This method will replace the contents of `` for the [_magic instance_](#magicload) with ``; +This method runs [`.set(, ))`](#minsset) on the [_magic instance_](#magicload). + +- `AutoEnvConfig.enablePersistence([minInterval = 120[, affectMagic = true]])` +This method set persistence global setting to _on_. It will affect all new instances created after setting it. +You can set the minimum interval between disk writes using the `[minInterval]` parameter. +If `[affectMagic]` is `true`, it will also enable persistence on the current [_magic instance_](#magicload), if any. +- `AutoEnvConfig.disablePersistence([affectMagic = true])` +This method set persistence global setting to _off_. It will affect all new instances created after setting it. If `[affectMagic]` is `true`, it will also disable persistence on the current [_magic instance_](#magicload), if any. + +- `AutoEnvConfig.persist(, )` +This method runs [`.persist(, ))`](#minsper) on a [_magic instance_](#magicload). ### Instance Methods -- `.load([])` -This method will return a new instance of `AutoEnvConfig` class (actually, prototype). If you ommit the `` parameter, it will try to [_magic load_](#magicload) it. If you pass the `` parameter, it will just return the config for the specified env. It returns false if it cannot find an environment config. +- `.load([envName [, forceNew = false]])` +The same as [`AutoEnvConfig.load([envName [, forceNew = false]])`](#mautoload). -- `.get([, ])` -This method will return the value of `` in the `` object. If `` is not present in the `` object, it will either return `` or throw an error if there the default value parameter was committed. +- `.get([, defaultValueIfNotPresent])` +This method will return the value of `` in the `` object. If `` is not present in the `` object, it will either return `[defaultValueIfNotPresent]` or throw an error if there the default value parameter was ommitted. - `.has()` -This method will return boolean `true` if the `` is present in the `` object or boolean `false` if not. +This method will return `true` if the `` is present in the `` object or `false` if not. - `.set(, )` -This method will replace the contents of `` for the `` object with ``; +This method will replace the in-memory contents of `` for the `` object with ``. + +- `.persist(, )` +This method will replace the in-memory contents of `` for the `` object with `` and will [eventually persist](#eventualpersistence) it on disk. + +- `.enablePersistence([minInterval = 120[, overrideMemory = true]])` +This method enables persistence on ``. +You can set the minimum interval between disk writes using the `[minInterval]` parameter. +If `overrideMemory` is `true`, it will also merge the in-memory config with the data loaded from the persistence file at the time persistence is enabled. It will load the persistence file on instance creation if [global persistence](#mautoenable) setting in _on_. + +- `.disablePersistence()` +This method disables persistence on ``. ## Advanced Usage @@ -142,7 +179,7 @@ You can also load multiple configs if ever needed: ```javascript AutoEnvConfig = require('autoenvconfig'); -//load the right env based on "path" config value +//load the right env based on "_path" config value let rightConfig = AutoEnvConfig.load(); rightConfig.get('key.in.right.file'); @@ -159,6 +196,8 @@ Note that you can call `load` directly on the package or on any `AutoEnvConfig` ## Release History +* [1.0.0](https://github.com/dnunes/autoenvconfig/releases/tag/v1.0.0) Added local [eventual persistence](#eventualpersistence)(beta) and basic unit tests this new feature; + * [0.1.6](https://github.com/dnunes/autoenvconfig/releases/tag/v0.1.6) Added unit tests for "[set](#mautoset)" methods and finished pending test for internal cache; * [0.1.5](https://github.com/dnunes/autoenvconfig/releases/tag/v0.1.5) Added "[set](#mautoset)" methods and started improving documentation for API. Missing unit tests on it; diff --git a/lib/AutoEnvConfig.js b/lib/AutoEnvConfig.js index f6b25e0..5958ebf 100644 --- a/lib/AutoEnvConfig.js +++ b/lib/AutoEnvConfig.js @@ -5,16 +5,24 @@ const , path = require('path') ; -let magicInstance = null; -let defaultEnvID = null; -let instancesCache = {}; +let default_schemaFileName = 'config.schema'; +let default_persistFileKey = '_persistFile'; +let p_magicInstance = null; +let p_defaultEnvID = null; +let p_instancesCache = {}; + +let p_persistenceActive = false; +let p_minInterval = 120; + + +//Helpers let _ = { 'ensureMagicInstance': function (instance) { - if (magicInstance) { return magicInstance; } + if (p_magicInstance) { return p_magicInstance; } if (!instance) { return publicFuncs.load(); } - magicInstance = instance; - return magicInstance; + p_magicInstance = instance; + return p_magicInstance; }, 'rootPath': path.resolve(path.dirname(require.main.filename)), @@ -22,17 +30,99 @@ let _ = { 'getConfigFilePath': function (id) { return path.resolve(_.envsPath, id); }, + 'getDefaultPersistenceFilePath': function (id) { + let configFileInfo = path.parse(id); + let persistenceFileName = configFileInfo['name'] + '.persist.json' + return path.resolve(_.envsPath, persistenceFileName); + }, + 'getDefaultEnvID': function () { + if (p_defaultEnvID) { return p_defaultEnvID; } + let fileList = fs.readdirSync(_.envsPath); + + let envConfig; + let envID = false; + fileList.some((currentFile) => { + if (path.extname(currentFile) != '.json') { return false; } + + try { + envConfig = _.loadReference(_.getConfigFilePath(currentFile)); + } catch (err) { + if (SyntaxError && err instanceof SyntaxError) { + //Use real debugging log here. + /* console.log( + 'There is a syntax error in your config file "'+ currentFile +'": '+ + '"'+ err.message +'". This file is being ignored by AutoEnvConfig.' + ); */ + return false; + } + throw err; + } + + if (envConfig['_path'] == _.rootPath) { + envID = path.basename(currentFile, '.json'); + return true; + } + }); + return (p_defaultEnvID = envID); + }, + + + //Generic Helpers + 'objType': function (obj) { + let t = typeof(obj); + if (t != 'object') { return t; } + let isArr = (obj instanceof Array); + return (isArr) ? 'array' : 'object'; + }, + 'supermerge': function (o1, o2) { let r = {}, k, v; + for (k in o1) { r[k] = o1[k]; } + for (k in o2) { v = o2[k]; + if (o1[k] && o1[k].constructor === Object && v.constructor === Object) { + r[k] = _.supermerge(o1[k], v); //array + array = merge + } else { r[k] = v; } //other combination = override + } return r; + }, + + //Specific Helpers + 'hasKeyInSchema': function (key, schema) { + if (typeof(schema['# '+ key]) != 'undefined') { return '# '+ key; } + if (typeof(schema['? '+ key]) != 'undefined') { return '? '+ key; } + return false; + }, + 'loadReference': function (filename) { + if (path.extname(filename) === '') { filename += '.json'; } + return JSON.parse(fs.readFileSync(filename, "utf8")); + }, + + //DRY Helpers + 'getSilently': function (envConfig, key, returnParent) { + let cur = envConfig; + let parent = envConfig; + let parts = key.split('.'); - 'getSchema': function () { - let name = 'config.schema'; - let schemaPath = _.getConfigFilePath(name); + let i = 0, n = parts.length; + let nextKey; + for (;i { - if (path.extname(currentFile) != '.json') { return false; } - - try { - envConfig = _.loadReference(_.getConfigFilePath(currentFile)); - } catch (err) { - if (SyntaxError && err instanceof SyntaxError) { - //Use real debugging log here. - /* console.log( - 'There is a syntax error in your config file "'+ currentFile +'": '+ - '"'+ err.message +'". This file is being ignored by AutoEnvConfig.' - ); */ - return false; - } - throw err; - } - - if (envConfig.path == _.rootPath) { - envID = currentFile; - return true; - } - }); - return (defaultEnvID = envID); + 'enablePersistence': function (minInterval = 120, affectMagic = true) { + p_minInterval = minInterval; + p_persistenceActive = true; + if (affectMagic && p_magicInstance) { + p_magicInstance.enablePersistence(minInterval); + } + }, + 'disablePersistence': function (affectMagic = true) { + p_persistenceActive = false; + if (affectMagic && p_magicInstance) { + p_magicInstance.disablePersistence(); + } }, + 'load': function (envID, forceNew) { + if (envID) { + let ext = path.extname(envID); + if (ext === '.json') { envID = envID.slice(0, -5); } + } + if (!envID) { envID = ''; } - if (!forceNew && instancesCache[envID]) { - return instancesCache[envID]; + if (!forceNew && p_instancesCache[envID]) { + return p_instancesCache[envID]; } let saveAsDefault = false; if (envID === '') { saveAsDefault = true; - envID = publicFuncs.getDefaultEnvID(); + envID = _.getDefaultEnvID(); if (!envID) { return false; } } - let autoEnvConfigInstance = new AutoEnvConfig(envID); + let autoEnvConfigInstance = new AutoEnvConfig(envID, p_persistenceActive, p_minInterval); if (saveAsDefault) { - instancesCache[''] = autoEnvConfigInstance; + p_instancesCache[''] = autoEnvConfigInstance; } if (!forceNew) { - instancesCache[envID] = autoEnvConfigInstance; + p_instancesCache[envID] = autoEnvConfigInstance; } _.ensureMagicInstance(autoEnvConfigInstance); return autoEnvConfigInstance; @@ -242,42 +302,175 @@ let publicFuncs = { 'has': function (key) { _.ensureMagicInstance(); - return magicInstance.has(key); + return p_magicInstance.has(key); }, 'get': function (key, def) { _.ensureMagicInstance(); - return magicInstance.get(key, def); + return p_magicInstance.get(key, def); }, 'set': function (key, value) { _.ensureMagicInstance(); - return magicInstance.set(key, value); + return p_magicInstance.set(key, value); + }, + 'persist': function (key, value) { + _.ensureMagicInstance(); + return p_magicInstance.persist(key, value); } }; -class AutoEnvConfig { - load(file) { return publicFuncs.load(file); } +class EventualPersistence { + constructor(path, minWriteInterval = 60) { + this._path = path; + this._minWriteInterval = minWriteInterval; - constructor(id) { - this.id = id; - let currentEnvFile; + this._timeout = null; + + let persistedData; try { - currentEnvFile = _.loadReference(_.getConfigFilePath(id)); + persistedData = _.loadReference(path); + } catch (err) { - if (SyntaxError && err instanceof SyntaxError) { + + if (err.code === 'ENOENT') { //Missing file. Try to create it. + try { + this._create(); + persistedData = {}; + } catch (err) { //Couldn't create it. Invalid path/wrong permissions? + throw new Error( + 'The persistence file "'+ path +'" does not exists and could not '+ + 'be created: "'+ err.message +'". Check the path and permissions.' + ); + } + + } else if (SyntaxError && err instanceof SyntaxError) { + //TODO: add option to ignore error and create a new file on error! + //if (false) { //purge the file and write "{}" on it. + // persistedData = {}; + //} + + //Syntax error on persistence file. This should never happen. throw new Error( - 'There is a syntax error in your config file "'+ id +'": '+ - '"'+ err.message +'". Please fix it and try again.' + 'There is a syntax error in your persistence file "'+ path +'". '+ + 'Maybe it got corrupted? Please fix or purge it and try again. '+ + 'Error message: "'+ err.message +'".' + ); + + } else { //Other errors + throw new Error( + 'Unknown error while loading your persistence file "'+ path +'". '+ + 'Error message: "'+ err.message +'".' ); } - throw err; } - let schema = _.getSchema(); - let defaultData = _.getCleanSchema(schema); - _.validateConfig(schema, currentEnvFile); - this.currentConfig = _.supermerge(defaultData, currentEnvFile); + this._persistedData = persistedData; + } + + _create() { + fs.writeFileSync(this._path, '{}'); + return true; + } + + getPersistedData() { return this._persistedData; } + + _setDirty() { + this._dirty = true; + this._persist(); + } + + _persist() { + //no need or already running + if (!this._dirty || this._running) { return false; } + //too early + if (this._timeout) { return false; } + + this._dirty = false; + this._running = true; + this._writeToDisk(); + } + + _writeToDisk() { + let contents = JSON.stringify(this.getPersistedData()); + fs.writeFile(this._path, contents, (error) => { + if (error) { return this._persistFailed(); } + else { return this._persistOK(); } + }); + } + _persistOK() { + this._running = false; + return this._throttle(); + } + _persistFailed() { + this._dirty = true; + this._running = false; + return this._throttle(); + } + + _throttle() { + this._timeout = setTimeout( + this._throttleRelease.bind(this), this._minWriteInterval *1000 + ); + } + _throttleRelease() { + this._timeout = null; + return this._persist(); + } + + update(key, value) { + let cur = this._persistedData; + let parts = key.split('.'), last = parts.pop(); + + let i = 0, n = parts.length, nextKey; + for (;i (http://dnunes.com)", "homepage": "http://dnunes.com/autoenvconfig/", @@ -38,11 +38,12 @@ ], "devDependencies": { "chai": "^3.5.0", - "coveralls": "^2.13.0", + "coveralls": "^2.13.1", "istanbul": "^0.4.5", - "mocha": "^3.2.0", + "mocha": "^3.4.2", "rewire": "^2.5.2", - "sinon": "^2.1.0" + "sinon": "^2.3.2" }, - "dependencies": {} + "dependencies": { + } } diff --git a/test/envs/config.schema b/test/envs/config.schema index bf3caaa..72f8e59 100644 --- a/test/envs/config.schema +++ b/test/envs/config.schema @@ -1,7 +1,9 @@ { + "# _path": "", + "? _persistFile": "", + "# id": "", - "# envtype": "", - "# path": "", + "? envtype": "", "# requiredKey": "", "? deep": { @@ -10,5 +12,5 @@ "? asWell": "otherValue" } }, - "# arr": [] + "? arr": [] } diff --git a/test/envs/config_noprefix.schema b/test/envs/config_noprefix.schema index f89ee64..df335ee 100644 --- a/test/envs/config_noprefix.schema +++ b/test/envs/config_noprefix.schema @@ -1,7 +1,8 @@ { + "# _path": "", + "# id": "", - "# envtype": "", - "# path": "", + "? envtype": "", "# requiredKey": "", "# deep": { @@ -10,5 +11,5 @@ "? asWell": "otherValue" } }, - "# arr": [] + "? arr": [] } diff --git a/test/envs/config_parseError.schema b/test/envs/config_parseError.schema index c645f92..2dc862b 100644 --- a/test/envs/config_parseError.schema +++ b/test/envs/config_parseError.schema @@ -1,7 +1,8 @@ { + "# _path": "INVALID", + "# id": INVALID :) - "# envtype": "INVALID", - "# path": "INVALID", + "? envtype": "INVALID", "# requiredKey": "" //there should be a comma here "# deep": { //also, no comments are allowed in json files @@ -10,5 +11,5 @@ "? asWell": "otherValue" } }, - "# arr": [] + "? arr": [] } diff --git a/test/envs/env1.json b/test/envs/env1.json index 6771d17..8397335 100644 --- a/test/envs/env1.json +++ b/test/envs/env1.json @@ -1,13 +1,7 @@ { + "_path": "/some/random/path", + "id": "env1", - "envtype": "staging", - "path": "/some/random/path", - "requiredKey": "value1", - "deep": { - "key": { - "supported": ":)" - } - }, - "arr": [] + "requiredKey": "value1" } diff --git a/test/envs/env2.json b/test/envs/env2.json index 48149cb..3c56763 100644 --- a/test/envs/env2.json +++ b/test/envs/env2.json @@ -1,13 +1,7 @@ { + "_path": "/some/random/path", + "id": "env2", - "envtype": "production", - "path": "/some/random/path", - "requiredKey": "value2", - "deep": { - "key": { - "supported": ":)" - } - }, - "arr": [] + "requiredKey": "value2" } diff --git a/test/envs/env_missingProperty.json b/test/envs/env_missingProperty.json index 2048483..a8fd74d 100644 --- a/test/envs/env_missingProperty.json +++ b/test/envs/env_missingProperty.json @@ -1,13 +1,12 @@ { + "_path": "/some/random/path", + "id": "envMissing", - "envtype": "production", - "path": "/some/random/path", "requiredKey": "missing", "deep": { "key": { } - }, - "arr": [] + } } diff --git a/test/envs/env_parseError.json b/test/envs/env_parseError.json index c9cf9eb..5da7589 100644 --- a/test/envs/env_parseError.json +++ b/test/envs/env_parseError.json @@ -1,13 +1,12 @@ { + "_path": "/some/random/path", + "id": "envParseError", - "envtype": "production", - "path": "/some/random/path", "requiredKey": "valueParseError" //there should be a comma here "deep": { //also, no comments are allowed in json files "key": { "supported": ":)" } - }, - "arr": [] + } } diff --git a/test/envs/env_persist_cantCreate.json b/test/envs/env_persist_cantCreate.json new file mode 100644 index 0000000..1ddbba9 --- /dev/null +++ b/test/envs/env_persist_cantCreate.json @@ -0,0 +1,8 @@ +{ + "_path": "/some/random/path", + "_persistFile": "/invalid/path.json", + + "id": "persist_cantCreate", + + "requiredKey": "cantCreate" +} diff --git a/test/envs/env_persist_existing.json b/test/envs/env_persist_existing.json new file mode 100644 index 0000000..2ca912e --- /dev/null +++ b/test/envs/env_persist_existing.json @@ -0,0 +1,8 @@ +{ + "_path": "/some/random/path", + "_persistFile": "./test/envs/persist/env_persist_existing.persist.json", + + "id": "persist_existing", + + "requiredKey": "not_loaded" +} diff --git a/test/envs/env_persist_invalid.json b/test/envs/env_persist_invalid.json new file mode 100644 index 0000000..40d870b --- /dev/null +++ b/test/envs/env_persist_invalid.json @@ -0,0 +1,8 @@ +{ + "_path": "/some/random/path", + "_persistFile": "./test/envs/persist/env_persist_invalid.persist.json", + + "id": "persist_invalid", + + "requiredKey": "invalid" +} diff --git a/test/envs/env_persist_specific.json b/test/envs/env_persist_specific.json new file mode 100644 index 0000000..d0dbd03 --- /dev/null +++ b/test/envs/env_persist_specific.json @@ -0,0 +1,8 @@ +{ + "_path": "/some/random/path", + "_persistFile": "./specificPath.persist.json", + + "id": "persist_specific", + + "requiredKey": "value1" +} diff --git a/test/envs/env_typeMismatch.json b/test/envs/env_typeMismatch.json index ac74e9a..aac2585 100644 --- a/test/envs/env_typeMismatch.json +++ b/test/envs/env_typeMismatch.json @@ -1,13 +1,11 @@ { + "_path": "/some/random/path", + "id": "envMismatch", "envtype": "staging", - "path": "/some/random/path", "requiredKey": "value1", "deep": { "key": "this should be an object" - - - }, - "arr": [] + } } diff --git a/test/envs/env_unexpectedProperty.json b/test/envs/env_unexpectedProperty.json index df584b1..487146a 100644 --- a/test/envs/env_unexpectedProperty.json +++ b/test/envs/env_unexpectedProperty.json @@ -1,7 +1,7 @@ { + "_path": "/some/random/path", + "id": "envUnexpected", - "envtype": "production", - "path": "/some/random/path", "requiredKey": "valueUnexpected", "deep": { @@ -9,6 +9,5 @@ "supported": ":)", "unexpected": "this is not on schema!" } - }, - "arr": [] + } } diff --git a/test/envs/magic.json b/test/envs/magic.json index 9ba99bd..6fffa7e 100644 --- a/test/envs/magic.json +++ b/test/envs/magic.json @@ -1,13 +1,7 @@ { + "_path": %curpath%, + "id": "magic", - "envtype": "local", - "path": %curpath%, - "requiredKey": "magic", - "deep": { - "key": { - "supported": ":)" - } - }, - "arr": [] + "requiredKey": "magic" } diff --git a/test/envs/persist/env_persist_existing.persist.json b/test/envs/persist/env_persist_existing.persist.json new file mode 100644 index 0000000..97dcf4b --- /dev/null +++ b/test/envs/persist/env_persist_existing.persist.json @@ -0,0 +1 @@ +{"requiredKey": "loaded_from_persistence"} diff --git a/test/envs/persist/env_persist_invalid.persist.json b/test/envs/persist/env_persist_invalid.persist.json new file mode 100644 index 0000000..e7f9756 --- /dev/null +++ b/test/envs/persist/env_persist_invalid.persist.json @@ -0,0 +1 @@ +{"invalid": "invalid"123} diff --git a/test/full.test.js b/test/full.test.js index 0f1f2a6..0374c12 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -24,7 +24,7 @@ let AutoEnvConfigClass ; -let replaceFiles = {}; +let leftoverFiles = [], replaceFiles = {}; //# Setup before(() => { @@ -38,9 +38,11 @@ before(() => { sandbox = sinon.sandbox.create(); - //This stub allows us to replace the path in the "magic.conf" file + //This stub allows us to force loading of specific files with special + //configurations without changing this module's code for consistent testing. let origReadFileSync = fs.readFileSync; - sandbox.stub(fs, 'readFileSync', function (filepath, encoding) { + + let stubbedReadFileSync = function (filepath, encoding) { let pathparts = filepath.split(path.sep); let filename = pathparts.pop(); if (replaceFiles[filename]) { //use invalid or different files for tests @@ -50,19 +52,26 @@ before(() => { filepath = pathparts.join(path.sep); let content = origReadFileSync(filepath, encoding); + //if it's the right file, let's replace the path to the current one if (filename == 'magic.json') { content = content.replace('%curpath%', JSON.stringify(mockRootPath)); } return content; - }); + }; + sandbox.stub(fs, 'readFileSync').callsFake(stubbedReadFileSync); }); //# Reset on each test afterEach(() => { - AutoEnvConfig._reset(); + //clear replace files list replaceFiles = {}; -}); //reset autoInstance and cache + //remove leftovers + leftoverFiles.forEach((file) => fs.unlinkSync(file)); + leftoverFiles = []; + //reset all internal state + AutoEnvConfig._reset(); +}); //# Cleanup after(() => { @@ -115,13 +124,13 @@ describe('Load Error Handling', function() { expect(fn).to.throw(expectedErrMessage); }); - it('magic "module.load" returns false when there is no matching env path', function () { + it('"module.load" returns false when there is no matching env path', function () { replaceFiles = {'magic.json': 'env1.json'}; let magicInstance = AutoEnvConfig.load(); expect(magicInstance).to.be.false; }); - it('magic "module.load" should use "defaultEnvID" cache even with "forceNew" flag', function () { + it('"module.load" should use "defaultEnvID" cache even with "forceNew" flag', function () { var readdirSyncSpy = sinon.spy(fs, 'readdirSync'); AutoEnvConfig.load('', 'forceNew'); AutoEnvConfig.load('', 'forceNew'); @@ -130,6 +139,24 @@ describe('Load Error Handling', function() { readdirSyncSpy.restore(); expect(readdirSyncSpy.callCount).to.be.equal(1); }); + + it('throws exception when it cannot create persistence file', function () { + let fn = function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.load('env_persist_cantCreate'); + }; + let expectedErrMessage = 'The persistence file "/invalid/path.json" does not exists and could not be created'; + expect(fn).to.throw(expectedErrMessage); + }); + + it('throws exception when the persistence file content is invalid', function () { + let fn = function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.load('env_persist_invalid'); + }; + let expectedErrMessage = 'There is a syntax error in your persistence file'; + expect(fn).to.throw(expectedErrMessage); + }); }); @@ -162,6 +189,57 @@ describe('Specific Loading', function() { }); +describe('Singleton-by-default behavior', function() { + it('will reuse the same instance by default', function () { + let specificInstance = AutoEnvConfig.load('env1'); + let specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('value1'); + //replace in-memory + specificInstance.set('requiredKey', 'set_by_code'); + specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('set_by_code'); + //load. internall cache should return the same instance + let specificInstanceCopy = AutoEnvConfig.load('env1'); + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + expect(specificKeyCopy).to.be.equal('set_by_code'); + }); + + it('will reuse the same instance when two IDs resolve to the same env file', function () { + let specificInstance = AutoEnvConfig.load('env1'); + let specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('value1'); + specificInstance.set('requiredKey', 'set_by_code'); + let specificInstanceCopy = AutoEnvConfig.load('env1.json'); + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + expect(specificKeyCopy).to.be.equal('set_by_code'); + }); + + it('will use a new instance if "forceNew" flag is passed to "load" method', function () { + let specificInstance = AutoEnvConfig.load('env1'); + let specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('value1'); + specificInstance.set('requiredKey', 'set_by_code'); + specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('set_by_code'); + let specificInstanceCopy = AutoEnvConfig.load('env1', 'forceNew'); + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + expect(specificKeyCopy).to.be.equal('value1'); + }); + + it('will use a new instance if "forceNew" flag is passed to "load" method when two IDs resolve to the same env file', function () { + let specificInstance = AutoEnvConfig.load('env1'); + let specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('value1'); + specificInstance.set('requiredKey', 'set_by_code'); + specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('set_by_code'); + let specificInstanceCopy = AutoEnvConfig.load('env1.json', 'forceNew'); + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + expect(specificKeyCopy).to.be.equal('value1'); + }); +}); + + describe('Magic Loading', function() { it('"module.load()" should use ', function () { let magicInstance = AutoEnvConfig.load(); @@ -170,6 +248,11 @@ describe('Magic Loading', function() { expect(magicKey).to.be.equal('magic'); }); + it('"module.load()" ID should not include ".json"', function () { + let magicInstance = AutoEnvConfig.load(); + expect(magicInstance.id).to.be.equal('magic'); + }); + it('"module.get" should use ', function () { let magicKey = AutoEnvConfig.get('requiredKey'); expect(magicKey).to.be.equal('magic'); @@ -195,74 +278,208 @@ describe('Magic Loading', function() { }); }); + describe('Instance Load and Magic Load equivalence', function() { it('"module.load()" object should be the same as "module.load(defaultEnv)" object', function () { - let magicInstance = AutoEnvConfig.load(); - let specificInstance = AutoEnvConfig.load('magic.json'); + let magicInstance = AutoEnvConfig.load(); + let specificInstance = AutoEnvConfig.load('magic'); expect(magicInstance).to.be.equal(specificInstance); }); it('"module.has" should be the same as "instance.has"', function () { - let magicInstance = AutoEnvConfig.load(); + let magicInstance = AutoEnvConfig.load(); let specificInstance = AutoEnvConfig.load('env1'); expect(magicInstance).to.not.be.equal(specificInstance); expect(magicInstance.has).to.be.equal(specificInstance.has); }); it('"module.get" should be the same as "instance.get"', function () { - let magicInstance = AutoEnvConfig.load(); + let magicInstance = AutoEnvConfig.load(); let specificInstance = AutoEnvConfig.load('env1'); expect(magicInstance).to.not.be.equal(specificInstance); expect(magicInstance.get).to.be.equal(specificInstance.get); }); it('"module.set" should be the same as "instance.set"', function () { - let magicInstance = AutoEnvConfig.load(); + let magicInstance = AutoEnvConfig.load(); let specificInstance = AutoEnvConfig.load('env1'); expect(magicInstance).to.not.be.equal(specificInstance); expect(magicInstance.set).to.be.equal(specificInstance.set); }); + it('"module.persist" should be the same as "instance.persist"', function () { + let magicInstance = AutoEnvConfig.load(); + let specificInstance = AutoEnvConfig.load('env1'); + expect(magicInstance).to.not.be.equal(specificInstance); + expect(magicInstance.persist).to.be.equal(specificInstance.persist); + }); }); + describe('Methods', function() { - it('magic "module.has(key)" should return true when key is present', function () { + it('"module.has(key)" should return true when key is present', function () { let hasMagicKey = AutoEnvConfig.has('requiredKey'); expect(hasMagicKey).to.be.true; }); - it('magic "module.has(key)" should return false when key is not present', function () { + it('"module.has(key)" should return false when key is not present', function () { let hasMagicKey = AutoEnvConfig.has('nonexistentKey'); expect(hasMagicKey).to.be.false; }); - it('magic "module.get(key)" should return value when key is present', function () { + it('"module.get(key)" should return value when key is present', function () { let magicKey = AutoEnvConfig.get('requiredKey'); expect(magicKey).to.be.equal('magic'); }); - it('magic "module.get(key)" should throw when key is not present', function () { + it('"module.get(key)" should throw when key is not present', function () { let fn = function () { AutoEnvConfig.get('nonexistentKey'); }; - expect(fn).to.throw('Can\'t find key "nonexistentKey" on current env config ("magic.json") and there was no default on function call!'); + expect(fn).to.throw('Can\'t find key "nonexistentKey" on current env config ("magic") and there was no default on function call!'); }); - it('magic "module.get(key, default)" should return value when default is supplied and key is present', function () { + it('"module.get(key, default)" should return "key value" even when _default_ is supplied if _key_ is present', function () { let magicKey = AutoEnvConfig.get('requiredKey', 'defaultValue'); expect(magicKey).to.be.equal('magic'); }); - it('magic "module.get(key, default)" should return default it is supplied and key is not present', function () { + it('"module.get(key, default)" should return "default" when it is supplied and _key_ is not present', function () { let magicKey = AutoEnvConfig.get('nonexistentKey', 'defaultValue'); expect(magicKey).to.be.equal('defaultValue'); }); - it('magic "module.set(key, value)" should update the value when the key does exist', function () { + it('"module.set(key, value)" should update the value when _key_ exists', function () { AutoEnvConfig.set('requiredKey', 'updated'); let magicKey = AutoEnvConfig.get('requiredKey'); expect(magicKey).to.be.equal('updated'); }); - it('magic "module.set(key, value)" should update the value of deep keys when the key does exist', function () { + it('"module.set(key, value)" should update the value of deep keys when _key_ exists', function () { AutoEnvConfig.set('deep.key.supported', 'updated'); let magicKey = AutoEnvConfig.get('deep.key.supported'); expect(magicKey).to.be.equal('updated'); }); - it('magic "module.set(key, value)" should throw when new value have different type', function () { + it('"module.set(key, value)" should throw when _value_ type mismatch', function () { let fn = function () { AutoEnvConfig.set('deep.key', 'updated'); }; expect(fn).to.throw('Env config key "deep.key" must be of type "object" ("string" received)'); }); - it('magic "module.set(key, value)" should throw when a key in not present in schema', function () { + it('"module.set(key, value)" should throw when _key_ is not present in schema', function () { let fn = function () { AutoEnvConfig.set('nonexistentKey'); }; - expect(fn).to.throw('Can\'t find key "nonexistentKey" on current env config ("magic.json").'); + expect(fn).to.throw('Can\'t find key "nonexistentKey" on current env config ("magic").'); + }); + it('"module.persist(key, value)" should throw when called in a disabled persistence setting', function () { + let fn = function () { + AutoEnvConfig.set('requiredKey', 'ok'); + AutoEnvConfig.persist('requiredKey', 'fail'); + }; + expect(fn).to.throw('The current instance of AutoEnvConfig was not set to have persistence activated!'); + }); + it('"instance.persist(key, value)" should throw when called in an instance created with persistence but disabled later on', function () { + let fn = function () { + AutoEnvConfig.enablePersistence(); + let specificInstance = AutoEnvConfig.load('env1'); + specificInstance.set('requiredKey', 'ok'); + specificInstance.persist('requiredKey', 'ok'); + specificInstance.disablePersistence(); + specificInstance.persist('requiredKey', 'fail'); + }; + expect(fn).to.throw('The current instance of AutoEnvConfig was not set to have persistence activated!'); + leftoverFiles = ['test/envs/env1.persist.json']; + }); +}); + + +describe('Persistence Files', function() { + it('create a persistence file in default location when persistence is enabled and no special settings are in place', function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.load('env1'); + expect(fs.readFileSync('test/envs/env1.persist.json').toString()).to.be.equal('{}'); + leftoverFiles = ['test/envs/env1.persist.json']; + }); + + it('create a persistence file in the specified location when persistence is enabled with a specific path', function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.load('env_persist_specific'); + expect(fs.readFileSync('specificPath.persist.json').toString()).to.be.equal('{}'); + leftoverFiles = ['specificPath.persist.json']; }); + it('NOT create a persistence file when persistence is disabled', function () { + AutoEnvConfig.load('env1'); + expect(fs.existsSync('test/envs/env1.persist.json')).to.be.false; + }); +}); + + +describe('Global and Instance Persistence settings', function() { + it('disabling persistence on global setting should also disable it on Magic Instance', function () { + let fn = function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.set('requiredKey', 'ok'); + AutoEnvConfig.persist('requiredKey', 'ok'); + //disabled globally. also disabled it on magic instance. + AutoEnvConfig.disablePersistence(); + AutoEnvConfig.persist('requiredKey', 'ok'); + }; + expect(fn).to.throw('The current instance of AutoEnvConfig was not set to have persistence activated!'); + leftoverFiles = ['test/envs/magic.persist.json']; + }); + + it('you can also disable global persistence setting without affecting Magic Instance', function () { + let fn = function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.set('requiredKey', 'ok'); + AutoEnvConfig.persist('requiredKey', 'ok'); + AutoEnvConfig.disablePersistence(false); + AutoEnvConfig.persist('requiredKey', 'ok'); + }; + expect(fn).to.not.throw(); + leftoverFiles = ['test/envs/magic.persist.json']; + }); + + it('do NOT load persisted data when persistence is disabled', function () { + AutoEnvConfig.load('env_persist_existing'); + let specificKey = AutoEnvConfig.get('requiredKey'); + expect(specificKey).to.be.equal('not_loaded'); + }); + + it('load persisted data on load when persistence is globally enabled', function () { + AutoEnvConfig.enablePersistence(); + AutoEnvConfig.load('env_persist_existing'); + let specificKey = AutoEnvConfig.get('requiredKey'); + expect(specificKey).to.be.equal('loaded_from_persistence'); + }); + + it('load persisted data when persistence is enabled only later in the code execution', function () { + AutoEnvConfig.load('env_persist_existing'); + let specificKey = AutoEnvConfig.get('requiredKey'); + expect(specificKey).to.be.equal('not_loaded'); + //now enable persistence and retry + AutoEnvConfig.enablePersistence(); + specificKey = AutoEnvConfig.get('requiredKey'); + expect(specificKey).to.be.equal('loaded_from_persistence'); + }); + + it('enable persistence in an instance without overwriting current in-memory config', function () { + let specificInstance = AutoEnvConfig.load('env_persist_existing'); + specificInstance.enablePersistence(); //overwrite + let specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('loaded_from_persistence'); + //load new instance and enable persistence without override + let specificInstanceCopy = AutoEnvConfig.load('env_persist_existing', 'forceNew'); + specificInstanceCopy.enablePersistence(120, false); //don't overwrite + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + expect(specificKeyCopy).to.be.equal('not_loaded'); + }); + + it('do NOT persist data when using "set" method even with persistence enabled', function () { + AutoEnvConfig.enablePersistence(); + let specificInstance = AutoEnvConfig.load('env_persist_existing'); + //check low level implementation as well. + var internalWriteCall = sinon.spy(specificInstance.eventualPersistence, '_writeToDisk'); + let specificKey = specificInstance.get('requiredKey'); + expect(specificInstance.get('requiredKey')).to.be.equal('loaded_from_persistence'); + //persistence activated, but replace in-memory only! + specificInstance.set('requiredKey', 'replaced_by_code'); + //check that the in-memory value was changed + specificKey = specificInstance.get('requiredKey'); + expect(specificKey).to.be.equal('replaced_by_code'); + //check number of calls to persist the file + internalWriteCall.restore(); + expect(internalWriteCall.callCount).to.be.equal(0); + //load new instance, which will re-read the persistence data + let specificInstanceCopy = AutoEnvConfig.load('env_persist_existing', 'forceNew'); + specificInstanceCopy.enablePersistence(120, false); //don't overwrite + let specificKeyCopy = specificInstanceCopy.get('requiredKey'); + //it should not be changed! + expect(specificKeyCopy).to.be.equal('loaded_from_persistence'); + }); });