@slimio/config
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -0,0 +0,0 @@ /// <reference types="node" /> |
{ | ||
"name": "@slimio/config", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "SlimIO Reactive JSON Config loaded", | ||
@@ -17,3 +17,4 @@ "main": "index.js", | ||
"engines": { | ||
"node": ">=8.11.1" | ||
"npm": ">=6.0.0", | ||
"node": ">=10.1.0" | ||
}, | ||
@@ -34,3 +35,4 @@ "keywords": [ | ||
"@escommunity/minami": "^1.0.0", | ||
"@slimio/eslint-config": "^1.1.3", | ||
"@slimio/eslint-config": "^1.1.5", | ||
"@types/node": "^10.3.4", | ||
"ava": "^0.25.0", | ||
@@ -37,0 +39,0 @@ "eslint": "^4.19.1", |
@@ -20,4 +20,6 @@ # Config | ||
Create a simple config file for your project (take this example) | ||
## Usage example | ||
Create a simple json file for your project (As below) | ||
```json | ||
@@ -31,3 +33,3 @@ { | ||
Install and use our package like this to recover values (with commonjs). | ||
Now create a new Configuration instance and read it | ||
@@ -37,9 +39,89 @@ ```js | ||
async function main() { | ||
const myConfig = new Config("./path/to/config.json"); | ||
await myConfig.read(); | ||
const cfg = new Config("./path/to/config.json"); | ||
cfg.read().then(() => { | ||
console.log(cfg.get("loglevel")); // stdout: 5 | ||
}).catch(console.error); | ||
``` | ||
console.log(myConfig.get("loglevel")); | ||
## API | ||
### constructor<T>(configFilePath: string, options?: Config.ConstructorOptions) | ||
Create a new Configuration instance | ||
```js | ||
const options = { autoReload: true }; | ||
const cfg = new Config("./path/to/file.json", options); | ||
``` | ||
Available options are | ||
```ts | ||
interface ConstructorOptions { | ||
createOnNoEntry?: boolean; | ||
writeOnSet?: boolean; | ||
autoReload?: boolean; | ||
reloadDelay?: number; | ||
defaultSchema?: object; | ||
} | ||
main().catch(console.error); | ||
``` | ||
### read(defaultPayload?: T): Promise<this>; | ||
Will trigger and read the local configuration (on disk). | ||
```js | ||
const cfg = new Config("./path/to/file.json"); | ||
assert.equal(cfg.configHasBeenRead, false); // true | ||
await cfg.read(); | ||
assert.equal(cfg.configHasBeenRead, true); // true | ||
``` | ||
Retriggering the method will made an hot-reload of all properties. For a cold reload you will have to close the configuration before. | ||
### setupAutoReload(): void; | ||
Setup hot reload (with a file watcher). This method is automatically triggered if the Configuration has been created with the option `autoReload` set to true. | ||
### get<H>(fieldPath: string): H | ||
Get a value from a key (field path). | ||
For example, image a json file with a `foo` field | ||
```js | ||
const cfg = new Config("./path/to/file.json"); | ||
await cfg.read(); | ||
const fooValue = cfg.get("foo"); | ||
``` | ||
### set<H>(fieldPath: string, fieldValue: H): void; | ||
Set a given field in the configuration | ||
```js | ||
const cfg = new Config("./config.json", { | ||
createOnNoEntry: true | ||
}); | ||
await cfg.read({ foo: "bar" }); | ||
cfg.set("foo", "hello world!"); | ||
await cfg.writeOnDisk(); | ||
``` | ||
### observableOf(fieldPath: string): ObservableLike; | ||
Observe a given configuration key with an Observable object! | ||
```js | ||
const { writeFile } = require("fs").promises; | ||
const cfg = new Config("./config.json", { | ||
autoReload: true, | ||
createOnNoEntry: true | ||
}); | ||
await cfg.read({ foo: "bar" }); | ||
// Observe initial and next value(s) of foo | ||
cfg.observableOf("foo").subscribe(console.log); | ||
// Re-write local config file | ||
const newPayload = { foo: "world" }; | ||
await writeFile("./config.json", JSON.stringify(newPayload, null, 4)); | ||
``` | ||
### writeOnDisk(): Promise<void> | ||
Write the configuration on the disk | ||
### close(): Promise<void> | ||
Close (and write on disk) the configuration (it will close the watcher and clean all active observers). |
// Require Node.JS core packages | ||
const { parse, extname } = require("path"); | ||
const { promisify } = require("util"); | ||
const Events = require("events"); | ||
const events = require("events"); | ||
const { | ||
access, | ||
readFile, | ||
writeFile, | ||
promises: { | ||
access, | ||
readFile, | ||
writeFile | ||
}, | ||
constants: { R_OK, W_OK } | ||
} = require("fs"); | ||
// Require third-party NPM package(s) | ||
// Require Third-party NPM package(s) | ||
const watcher = require("node-watch"); | ||
@@ -21,15 +22,8 @@ const is = require("@sindresorhus/is"); | ||
// Require internal dependencie(s) | ||
// Require Internal dependencie(s) | ||
const { formatAjvErrors } = require("./utils"); | ||
// FS Async Wrapper | ||
const FSAsync = { | ||
access: promisify(access), | ||
readFile: promisify(readFile), | ||
writeFile: promisify(writeFile) | ||
}; | ||
// Private Config Accessors | ||
const payload = Symbol(); | ||
const schema = Symbol(); | ||
const payload = Symbol("payload"); | ||
const schema = Symbol("schema"); | ||
@@ -39,2 +33,3 @@ /** | ||
* @classdesc Reactive JSON Config loader class | ||
* @extends events | ||
* @template T | ||
@@ -54,6 +49,8 @@ * | ||
* | ||
* @event reload | ||
* | ||
* @author Thomas GENTILHOMME <gentilhomme.thomas@gmail.com> | ||
* @version 0.1.0 | ||
*/ | ||
class Config extends Events { | ||
class Config extends events { | ||
@@ -68,10 +65,21 @@ /** | ||
* @param {Object=} options.defaultSchema Optional default Schema | ||
* @param {Number=} [options.reloadDelay=1000] Hot reload delay | ||
* @param {Number=} [options.reloadDelay=1000] Hot reload delay (in milliseconds) | ||
* | ||
* @throws {TypeError} | ||
* @throws {Error} | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
* const cfgOptions = { | ||
* autoReload: true, | ||
* createOnNoEntry: true, | ||
* writeOnSet: true, | ||
* reloadDelay: 2000 | ||
* }; | ||
* const cfgM = new Config("./path/to/config.json", cfgOptions); | ||
*/ | ||
constructor(configFilePath, options = {}) { | ||
constructor(configFilePath, options = Object.create(null)) { | ||
super(); | ||
if (is(configFilePath) !== "string") { | ||
if (!is.string(configFilePath)) { | ||
throw new TypeError("Config.constructor->configFilePath should be typeof <string>"); | ||
@@ -112,6 +120,14 @@ } | ||
* @desc Get a payload Object clone (or null if the configuration has not been read yet) | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
* const cfg = new Config("./path/to/config.json"); | ||
* await cfg.read(); | ||
* const configContent = cfg.payload; | ||
* console.log(JSON.stringify(configContent, null, 2)); | ||
*/ | ||
get payload() { | ||
if (!this.configHasBeenRead) { | ||
return null; | ||
return Object.create(null); | ||
} | ||
@@ -131,10 +147,25 @@ | ||
* @throws {TypeError} | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
* const cfg = new Config("./path/to/config.json"); | ||
* await cfg.read(); | ||
* | ||
* // Assign a new cfg (payload). It should match the cfg Schema (if there is any) | ||
* try { | ||
* cfg.payload = { | ||
* foo: "bar" | ||
* }; | ||
* } | ||
* catch (error) { | ||
* // handle error here! | ||
* } | ||
*/ | ||
set payload(newPayload) { | ||
if (!this.configHasBeenRead) { | ||
throw new Error( | ||
"Config.payload - cannot set a new payload when the config has not been read yet!" | ||
); | ||
// eslint-disable-next-line max-len | ||
throw new Error("Config.payload - cannot set a new payload when the config has not been read yet!"); | ||
} | ||
if (is(newPayload) !== "Object") { | ||
if (!is.object(newPayload)) { | ||
throw new TypeError("Config.payload->newPayload should be typeof <Object>"); | ||
@@ -165,2 +196,4 @@ } | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
@@ -182,4 +215,3 @@ * const myConfig = new Config("./path/to/config.json", { | ||
// Declare scoped variable(s) to the top | ||
let JSONConfig; | ||
let JSONSchema; | ||
let JSONConfig, JSONSchema; | ||
@@ -189,6 +221,4 @@ // Get and parse the JSON Configuration file (if exist). | ||
try { | ||
await FSAsync.access(this.configFile, R_OK | W_OK); | ||
JSONConfig = JSON.parse( | ||
await FSAsync.readFile(this.configFile) | ||
); | ||
await access(this.configFile, R_OK | W_OK); | ||
JSONConfig = JSON.parse(await readFile(this.configFile)); | ||
} | ||
@@ -199,6 +229,6 @@ catch (err) { | ||
} | ||
JSONConfig = is(defaultPayload) === "Object" ? | ||
JSONConfig = is.object(defaultPayload) ? | ||
defaultPayload : | ||
is.nullOrUndefined(this[payload]) ? {} : this.payload; | ||
await FSAsync.writeFile(this.configFile, JSON.stringify(JSONConfig, null, 4)); | ||
is.nullOrUndefined(this[payload]) ? Object.create(null) : this.payload; | ||
await writeFile(this.configFile, JSON.stringify(JSONConfig, null, 4)); | ||
} | ||
@@ -209,6 +239,4 @@ | ||
try { | ||
await FSAsync.access(this.schemaFile, R_OK); | ||
JSONSchema = JSON.parse( | ||
await FSAsync.readFile(this.schemaFile) | ||
); | ||
await access(this.schemaFile, R_OK); | ||
JSONSchema = JSON.parse(await readFile(this.schemaFile)); | ||
} | ||
@@ -241,2 +269,4 @@ catch (err) { | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @throws {Error} | ||
@@ -246,6 +276,7 @@ */ | ||
if (!this.configHasBeenRead) { | ||
throw new Error( | ||
"Config.setupAutoReaload - cannot setup autoReload when the config has not been read yet!" | ||
); | ||
// eslint-disable-next-line max-len | ||
throw new Error("Config.setupAutoReaload - cannot setup autoReload when the config has not been read yet!"); | ||
} | ||
// Return if autoReload is already actived | ||
if (this.autoReloadActivated) { | ||
@@ -256,3 +287,3 @@ return; | ||
this.autoReloadActivated = true; | ||
this.watcher = watcher(this.configFile, { delay: this.reloadDelay }, async(evt, name) => { | ||
this.watcher = watcher(this.configFile, { delay: this.reloadDelay }, async() => { | ||
await this.read(); | ||
@@ -275,2 +306,4 @@ this.emit("reload"); | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
@@ -291,7 +324,6 @@ * const myConfig = new Config("./path/to/config.json", { | ||
if (!this.configHasBeenRead) { | ||
throw new Error( | ||
"Config.get - Unable to get a key, the configuration has not been initialized yet!" | ||
); | ||
// eslint-disable-next-line max-len | ||
throw new Error("Config.get - Unable to get a key, the configuration has not been initialized yet!"); | ||
} | ||
if (is(fieldPath) !== "string") { | ||
if (!is.string(fieldPath)) { | ||
throw new TypeError("Config.get->fieldPath should be typeof <string>"); | ||
@@ -311,2 +343,4 @@ } | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
@@ -339,5 +373,7 @@ * const myConfig = new Config("./config.json", { | ||
observableOf(fieldPath) { | ||
const fieldValue = this.get(fieldPath); | ||
return new Observable((observer) => { | ||
// Retrieve the field value first | ||
const fieldValue = this.get(fieldPath); | ||
return new Observable((observer) => { | ||
// Send it as first Observed value! | ||
observer.next(fieldValue); | ||
@@ -361,2 +397,4 @@ this.subscriptionObservers.push([fieldPath, observer]); | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
@@ -383,13 +421,17 @@ * const myConfig = new Config("./config.json", { | ||
if (!this.configHasBeenRead) { | ||
throw new Error( | ||
"Config.set - Unable to set a key, the configuration has not been initialized yet!" | ||
); | ||
// eslint-disable-next-line max-len | ||
throw new Error("Config.set - Unable to set a key, the configuration has not been initialized yet!"); | ||
} | ||
if (is(fieldPath) !== "string") { | ||
if (!is.string(fieldPath)) { | ||
throw new TypeError("Config.set->fieldPath should be typeof <string>"); | ||
} | ||
// Setup the new cfg by using the getter/setter payload | ||
this.payload = set(this.payload, fieldPath, fieldValue); | ||
// If writeOnSet option is actived, writeOnDisk at the next loop iteration (lazy) | ||
if (this.writeOnSet) { | ||
process.nextTick(this.writeOnDisk.bind(this)); | ||
setImmediate(() => { | ||
this.writeOnDisk().catch(console.error); | ||
}); | ||
} | ||
@@ -406,2 +448,11 @@ } | ||
* @throws {Error} | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
* // Config can be created with the option `writeOnSet` that enable cfg auto-writing on disk after every set! | ||
* const cfg = new Config("./path/to/config.json"); | ||
* await cfg.read(); | ||
* cfg.set("field.path", "value"); | ||
* await cfg.writeOnDisk(); | ||
*/ | ||
@@ -413,4 +464,4 @@ async writeOnDisk() { | ||
await FSAsync.access(this.configFile, W_OK); | ||
await FSAsync.writeFile(this.configFile, JSON.stringify(this[payload], null, 4)); | ||
await access(this.configFile, W_OK); | ||
await writeFile(this.configFile, JSON.stringify(this[payload], null, 4)); | ||
} | ||
@@ -421,11 +472,18 @@ | ||
* @method close | ||
* @desc Close the configuration (it will close the watcher and all active observers). | ||
* @desc Close (and write on disk) the configuration (it will close the watcher and clean all active observers). | ||
* @memberof Config# | ||
* @returns {Promise<void>} | ||
* | ||
* @throws {Error} | ||
* | ||
* @version 0.1.0 | ||
* | ||
* @example | ||
* const cfg = new Config("./path/to/config.json"); | ||
* await cfg.read(); | ||
* await cfg.close(); | ||
*/ | ||
async close() { | ||
if (!this.configHasBeenRead) { | ||
throw new Error( | ||
"Config.close - Cannot close unreaded configuration" | ||
); | ||
throw new Error("Config.close - Cannot close unreaded configuration"); | ||
} | ||
@@ -437,5 +495,9 @@ if (this.autoReloadActivated && !this.watcher.isClosed()) { | ||
// Write the Configuration on the disk to be safe | ||
await this.writeOnDisk(); | ||
for (const [, subscriptionObservers] of this.subscriptionObservers) { | ||
// Complete all observers | ||
for (const [index, subscriptionObservers] of this.subscriptionObservers) { | ||
subscriptionObservers.complete(); | ||
this.subscriptionObservers.splice(index, 1); | ||
} | ||
@@ -453,2 +515,3 @@ this.configHasBeenRead = false; | ||
// Export class | ||
module.exports = Config; |
@@ -11,2 +11,3 @@ /** | ||
* @function formatAjvErrors | ||
* @memberof utils# | ||
* @desc format ajv errors | ||
@@ -13,0 +14,0 @@ * @param {ajv.ErrorObject[]} ajvErrors Array of ajv error Object |
/* eslint no-new: off */ | ||
/* eslint max-len: off */ | ||
// Require Dependencies! | ||
// Require Node.JS Dependencies | ||
const { | ||
writeFile, | ||
unlink | ||
} = require("fs").promises; | ||
// Require Third-Party Dependencies! | ||
const avaTest = require("ava"); | ||
const is = require("@sindresorhus/is"); | ||
// Require Internal Dependencies | ||
const Config = require("../src/config.class"); | ||
const { formatAjvErrors } = require("../src/utils.js"); | ||
const { promisify } = require("util"); | ||
const { | ||
access, | ||
readFile, | ||
writeFile, | ||
unlink | ||
} = require("fs"); | ||
// FS Async Wrapper | ||
const FSAsync = { | ||
access: promisify(access), | ||
readFile: promisify(readFile), | ||
writeFile: promisify(writeFile), | ||
unlink: promisify(unlink) | ||
}; | ||
const configSchemaJSON = { | ||
@@ -46,10 +39,10 @@ title: "Config", | ||
for (let num = 1; num < count + 1; num++) { | ||
toDelete.push(FSAsync.unlink(`./test/config${num}.schema.json`)); | ||
toDelete.push(FSAsync.unlink(`./test/config${num}.json`)); | ||
toDelete.push(unlink(`./test/config${num}.schema.json`)); | ||
toDelete.push(unlink(`./test/config${num}.json`)); | ||
} | ||
toDelete.push(FSAsync.unlink("./test/basicConfig.json")); | ||
toDelete.push(FSAsync.unlink("./test/basicConfig3.json")); | ||
toDelete.push(FSAsync.unlink("./test/basicConfig4.json")); | ||
toDelete.push(FSAsync.unlink("./test/defaultSchemaConfig1.json")); | ||
toDelete.push(FSAsync.unlink("./test/defaultSchemaConfig2.json")); | ||
toDelete.push(unlink("./test/basicConfig.json")); | ||
toDelete.push(unlink("./test/basicConfig3.json")); | ||
toDelete.push(unlink("./test/basicConfig4.json")); | ||
toDelete.push(unlink("./test/defaultSchemaConfig1.json")); | ||
toDelete.push(unlink("./test/defaultSchemaConfig2.json")); | ||
@@ -61,7 +54,7 @@ await Promise.all(toDelete); | ||
const num = ++count; | ||
await FSAsync.writeFile( | ||
await writeFile( | ||
`./test/config${num}.schema.json`, | ||
JSON.stringify(configSchemaJSON, null, 4) | ||
); | ||
await FSAsync.writeFile( | ||
await writeFile( | ||
`./test/config${num}.json`, | ||
@@ -156,3 +149,3 @@ JSON.stringify(configJSON, null, 4) | ||
}); | ||
await FSAsync.writeFile(`./test/config${num}.json`, JSON.stringify(newConfigJSON, null, 4)); | ||
await writeFile(`./test/config${num}.json`, JSON.stringify(newConfigJSON, null, 4)); | ||
}); | ||
@@ -193,3 +186,3 @@ await config.close(); | ||
const payload = config.payload; | ||
test.is(payload, null); | ||
test.deepEqual(payload, Object.create(null)); | ||
}); | ||
@@ -229,3 +222,3 @@ | ||
}; | ||
await FSAsync.writeFile( | ||
await writeFile( | ||
"./test/defaultSchemaConfig1.json", | ||
@@ -248,3 +241,3 @@ JSON.stringify(configJSON, null, 4) | ||
avaTest("Default Schema", async(test) => { | ||
await FSAsync.writeFile( | ||
await writeFile( | ||
"./test/defaultSchemaConfig2.json", | ||
@@ -349,3 +342,3 @@ JSON.stringify(configJSON, null, 4) | ||
await config.read(configJSON); | ||
await FSAsync.unlink("./test/basicConfig4.json"); | ||
await unlink("./test/basicConfig4.json"); | ||
await config.read(); | ||
@@ -352,0 +345,0 @@ await config.close(); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
156456
23
1433
125
0
6