Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

wootils

Package Overview
Dependencies
Maintainers
1
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wootils - npm Package Compare versions

Comparing version 3.0.4 to 4.0.0

CHANGELOG.md

11

browser/index.js

@@ -0,5 +1,10 @@

/**
* I'm using this extremely verbose syntax because it's the only way the transpilation process
* would recognize both 'imports' and 'exports'.
*
* @ignore
*/
const SimpleStorage = require('./simpleStorage');
module.exports = {
SimpleStorage,
};
module.exports.SimpleStorage = SimpleStorage;

@@ -1,22 +0,57 @@

const extend = require('extend');
const {
deepAssignWithShallowMerge,
deepAssignWithOverwrite,
} = require('../shared/deepAssign');
/**
* @module browser/simpleStorage
*/
/**
* @typedef {Object} SimpleStorageStorageLogger
* @property {?Function} warn Prints out a warning message. Either this or `warning` MUST be
* present.
* @property {?Function} warning Prints out a warning message. Either this or `warn` MUST be
* present.
* @callback SimpleStorageLoggerWarnFn
* @param {string} message The message to log the warning for.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Object} SimpleStorageLogger
* @property {?SimpleStorageLoggerWarnFn} warn Prints out a warning message. Either this or
* `warning` MUST be present.
* @property {?SimpleStorageLoggerWarnFn} warning Prints out a warning message. Either this or
* `warn` MUST be present.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {'local'|'session'|'temp'} SimpleStorageStorageType
* @enum {string}
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Object} SimpleStorageStorageTypes
* @property {SimpleStorageStorage} local The methods to work with `localStorage`.
* @property {SimpleStorageStorage} session The methods to work with `sessionStorage`.
* @property {SimpleStorageStorage} temp The methods to work with the _"temp storage"_.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Object} SimpleStorageStorageOptions
* @property {string} [name='simpleStorage'] A reference name for the storage.
* @property {string} [key='simpleStorage'] The key the class will use to
* store the data on the storage.
* @property {Array} [typePriority=['local', 'session', 'temp']] The priority list of types of
* storage the service will try to
* use when initialized.
* @property {string} [name='simpleStorage']
* A reference name for the storage.
* @property {string} [key='simpleStorage']
* The key the class will use to store the data on the storage.
* @property {SimpleStorageStorageType[]} [typePriority=['local', 'session', 'temp']]
* The priority list of types of storage the service will try to use when initialized.
* @parent module:browser/simpleStorage
*/
/**
* A utility type for all the differents dictionary {@link SimpleStorage} manages.
*
* @typedef {Object.<string,*>} SimpleStorageDictionary
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Object} SimpleStorageEntriesOptions

@@ -28,10 +63,11 @@ * @property {boolean} [enabled=false] Whether or not to use the entries

* storage will become an empty object.
* @property {Number} [expiration=3600] The amount of seconds relative to the
* @property {number} [expiration=3600] The amount of seconds relative to the
* current time that needs to pass in order to
* consider an entry expired.
* @property {Boolean} [deleteExpired=true] Whether or not to delete expired entries
* @property {boolean} [deleteExpired=true] Whether or not to delete expired entries
* (both when loading the storage and when
* trying to access the entries).
* @property {Boolean} [saveWhenDeletingExpired=true] Whether or not to sync the storage after
* @property {boolean} [saveWhenDeletingExpired=true] Whether or not to sync the storage after
* deleting an expired entry.
* @parent module:browser/simpleStorage
*/

@@ -41,3 +77,3 @@

* @typedef {Object} SimpleStorageOptions
* @property {Boolean} [initialize=true] Whether or not to initialize the
* @property {boolean} [initialize=true] Whether or not to initialize the
* service right from the constructor.

@@ -50,6 +86,6 @@ * It means that it will validate the

* the initialization.
* @property {Window} [window] The `window`/`global` object the class
* will use in order to access
* @property {Window} [window] The `window`/`global` object the
* class will use in order to access
* `localStorage` and `sessionStorage`.
* @property {?SimpleStorageStorageLogger} [logger] A custom logger to print out the
* @property {?SimpleStorageLogger} [logger] A custom logger to print out the
* warnings when the class needs to do a

@@ -61,9 +97,10 @@ * fallback to a different storage type.

* @property {SimpleStorageEntriesOptions} [entries] These are the options for customizing
* the way the service works with entries.
* By default, the class saves any kind
* of object on the storage, but by
* using entries you can access them by
* name and even define expiration time
* so they'll be removed after a while.
* @property {Object} [tempStorage={}] The `tempStorage` is the storage the
* the way the service works with
* entries. By default, the class saves
* any kind of object on the storage,
* but by using entries you can access
* them by name and even define
* expiration time so they'll be removed
* after a while.
* @property {SimpleStorageDictionary} [tempStorage={}] The `tempStorage` is the storage the
* class uses when none of the others

@@ -74,26 +111,31 @@ * are available. Is just a simple

* page), the data goes away.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Function} SimpleStorageStorageAvailableMethod
* @callback SimpleStorageStorageAvailableMethod
* @param {string} [fallbackFrom] If the storage is being used as a fallback from another one that
* is not available, this parameter will have its name.
* @return {boolean} Whether or not the storage is available.
* @returns {boolean} Whether or not the storage is available.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Function} SimpleStorageStorageGetMethod
* @callback SimpleStorageStorageGetMethod
* @param {string} key The key used by the class to save data on the storage.
* @return {Object} The contents from the storage.
* @returns {SimpleStorageDictionary} The contents from the storage.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Function} SimpleStorageStorageSetMethod
* @param {string} key The key used by the class to save data on the storage.
* @callback SimpleStorageStorageSetMethod
* @param {string} key The key used by the class to save data on the storage.
* @param {Object} value The data to save on the storage.
* @parent module:browser/simpleStorage
*/
/**
* @typedef {Function} SimpleStorageStorageDeleteMethod
* @callback SimpleStorageStorageDeleteMethod
* @param {string} key The key used by the class to save data on the storage.
* @parent module:browser/simpleStorage
*/

@@ -103,11 +145,12 @@

* @typedef {Object} SimpleStorageStorage
* @property {string} name The name of the storage.
* @property {SimpleStorageStorageAvailableMethod} available The method to check if the storage can
* be used or not.
* @property {SimpleStorageStorageGetMethod} get The method used to read from the
* storage.
* @property {SimpleStorageStorageSetMethod} set The method used to write on the
* storage.
* @property {SimpleStorageStorageDeleteMethod} delete The method used to delete data from
* the storage.
* @property {string} name The name of the storage.
* @property {SimpleStorageStorageAvailableMethod} isAvailable The method to check if the storage
* can be used or not.
* @property {SimpleStorageStorageGetMethod} get The method used to read from the
* storage.
* @property {SimpleStorageStorageSetMethod} set The method used to write on the
* storage.
* @property {SimpleStorageStorageDeleteMethod} delete The method used to delete data from
* the storage.
* @parent module:browser/simpleStorage
*/

@@ -119,2 +162,3 @@

* @property {Object} value The actual data for the entry.
* @parent module:browser/simpleStorage
*/

@@ -127,10 +171,11 @@

* data and even expiration time for it.
*
* @parent module:browser/simpleStorage
* @abstract
* @tutorial simpleStorage
*/
class SimpleStorage {
/**
* Class constructor.
* @param {SimpleStorageOptions} [options={}] The options to customize the class.
* @param {Partial<SimpleStorageOptions>} [options={}] The options to customize the class.
* @throws {Error} If instantiated without extending it.
* @abstract
*/

@@ -141,3 +186,3 @@ constructor(options = {}) {

throw new TypeError(
'SimpleStorage is an abstract class, it can\'t be instantiated directly'
'SimpleStorage is an abstract class, it can\'t be instantiated directly',
);

@@ -148,2 +193,3 @@ }

* the data.
*
* @type {SimpleStorageOptions}

@@ -175,6 +221,4 @@ * @access protected

* A dictionary with the storage types the class supports.
* @type {Object}
* @property {SimpleStorageStorage} local The methods to work with `localStorage`.
* @property {SimpleStorageStorage} session The methods to work with `sessionStorage`.
* @property {SimpleStorageStorage} temp The methods to work with the _"temp storage"_.
*
* @type {SimpleStorageStorageTypes}
* @access protected

@@ -208,2 +252,3 @@ */

* {@link SimpleStorageStorage} being used.
*
* @type {?SimpleStorageStorage}

@@ -216,3 +261,4 @@ * @access protected

* way you won't need to write/read/parse from the storage every time you need to do something.
* @type {Object}
*
* @type {SimpleStorageDictionary}
* @access protected

@@ -227,76 +273,143 @@ */

/**
* This method _"initializes" the class by validating custom options, loading the reference for
* the required storage and synchronizing the data with the storage.
* Adds a new entry to the class data, and if `save` is used, saves it into the storage.
*
* @param {string} key The entry key.
* @param {Object|Promise} value The entry value, or a {@link Promise} that resolves into
* the value.
* @param {boolean} [save=true] Whether or not the class should save the data into the
* storage.
* @returns {Object|Promise} If `value` is an {@link Object}, it will return the same object; but
* if `value` is a {@link Promise}, it will return the
* _"promise chain"_.
* @access protected
*/
_initialize() {
this._validateOptions();
this._storage = this._initializeStorage();
this._data = this._initializeStorageData();
_addEntry(key, value, save = true) {
return this._isPromise(value) ?
value.then((realValue) => this._addResolvedEntry(key, realValue, save)) :
this._addResolvedEntry(key, value, save);
}
/**
* This method is called when the storage is deleted or resetted and if entries are disabled.
* It can be used to define the initial value of the data the class saves on the storage.
* @return {Object}
* This is the real method behind `_addEntry`. It Adds a new entry to the class data and, if
* `save` is used, it also saves it into the storage.
* The reason that there are two methods for this is, is because `_addEntry` can receive a
* {@link Promise}, and in that case, this method gets called after it gets resolved.
*
* @param {string} key The entry key.
* @param {Object} value The entry value.
* @param {boolean} save Whether or not the class should save the data into the storage.
* @returns {Object} The same data that was saved.
* @access protected
*/
_getInitialData() {
return {};
_addResolvedEntry(key, value, save) {
this._data[key] = {
time: this._now(),
value: deepAssignWithShallowMerge(value),
};
if (save) {
this._save();
}
return value;
}
/**
* Access the data the class saves on the storage.
* @return {Object}
* Deletes the class data from the storage.
*
* @param {boolean} [reset=true] Whether or not to reset the data to the initial data
* (`_getInitialData`), if entries area disabled, or to an empty
* object, if they are enabled.
* @access protected
*/
_getData() {
return this._data;
_delete(reset = true) {
this._storage.delete(this._options.storage.key);
if (reset) {
this._setData(this._getInitialData(), false);
} else {
this._setData({}, false);
}
}
/**
* Overwrites the data reference the class has and, if `save` is used, it also saves it into
* Deletes an entry from the class data, and if `save` is used, the changes will be saved on
* the storage.
* @param {Object|Promise} data The new data, or a {@link Promise} that resolves into the
* new data.
* @param {boolean} [save=true] Whether or not the class should save the data into the
* storage.
* @return {Object\Promise} If `data` is an {@link Object}, it will return the same object; but
* if `data` is a {@link Promise}, it will return the _"promise chain"_.
*
* @param {string} key The entry key.
* @param {boolean} [save=true] Whether or not the class should save the data into the storage
* after deleting the entry.
* @returns {boolean} Whether or not the entry was deleted.
* @access protected
*/
_setData(data, save = true) {
return this._isPromise(data) ?
data.then((realData) => this._setResolvedData(realData, save)) :
this._setResolvedData(data, save);
_deleteEntry(key, save = true) {
const exists = this._hasEntry(key);
if (exists) {
delete this._data[key];
if (save) {
this._save();
}
}
return exists;
}
/**
* This is the real method behind `_setData`. It overwrites the data reference the class
* has and, if `save` is used, it also saves it into the storage.
* The reason that there are two methods for this is, is because `_setData` can receive a
* {@link Promise}, and in that case, this method gets called after it gets resolved.
* @param {Object} data The new data.
* @param {boolean} save Whether or not the class should save the data into the storage.
* @return {Object} The same data that was saved.
* Filters out a dictionary of entries by checking if they expired or not.
*
* @param {Object} entries A dictionary of key-value, where the value is a
* {@link SimpleStorageEntry}.
* @param {number} expiration The amount of seconds that need to have passed in order to
* consider an entry expired.
* @returns {Object} A new dictionary without the expired entries.
* @access protected
*/
_setResolvedData(data, save) {
this._data = this._copy(data);
if (save) {
this._save();
}
_deleteExpiredEntries(entries, expiration) {
const result = {};
const now = this._now();
Object.keys(entries).forEach((key) => {
const entry = entries[key];
if ((now - entry.time) < expiration) {
result[key] = entry;
}
});
return data;
return result;
}
/**
* Resets the data on the class; If entries are enabled, the data will become an empty
* {@link Object}; otherwise, it will call {@link this#_getInitialData}.
* @param {boolean} [save=true] Whether or not the class should save the data into the storage.
* Deletes an object from the `localStorage`.
*
* @param {string} key The object key.
* @access protected
*/
_resetData(save = true) {
const data = this._options.entries.enabled ? {} : this._getInitialData();
return this._setData(data, save);
_deleteFromLocalStorage(key) {
delete this._options.window.localStorage[key];
}
/**
* Deletes an object from the `sessionStorage`.
*
* @param {string} key The object key.
* @access protected
*/
_deleteFromSessionStorage(key) {
delete this._options.window.sessionStorage[key];
}
/**
* Deletes an object from the _"temp storage"_.
*
* @param {string} key The object key.
* @access protected
*/
_deleteFromTempStorage(key) {
delete this._options.tempStorage[key];
}
/**
* Gets the data the class saves on the storage.
*
* @returns {Object}
* @access protected
*/
_getData() {
return this._data;
}
/**
* Gets an entry from the storage dictionary.
*
* @param {string} key The entry key.
* @return {SimpleStorageEntry} Whatever is on the storage.
* @returns {?SimpleStorageEntry} Whatever is on the storage, or `null`.
* @throws {Error} If entries are not enabled.

@@ -327,4 +440,5 @@ * @access protected

* Gets the value of an entry.
*
* @param {string} key The entry key.
* @return {?Object}
* @returns {?SimpleStorageDictionary}
* @access protected

@@ -337,65 +451,48 @@ */

/**
* Adds a new entry to the class data, and if `save` is used, saves it into the storage.
* @param {string} key The entry key.
* @param {Object|Promise} value The entry value, or a {@link Promise} that resolves into
* the value.
* @param {boolean} [save=true] Whether or not the class should save the data into the
* storage.
* @return {Object\Promise} If `value` is an {@link Object}, it will return the same object; but
* if `value` is a {@link Promise}, it will return the
* _"promise chain"_.
* Gets an object from `localStorage`.
*
* @param {string} key The key used to save the object.
* @returns {?SimpleStorageDictionary}
* @access protected
*/
_addEntry(key, value, save = true) {
return this._isPromise(value) ?
value.then((realValue) => this._addResolvedEntry(key, realValue, save)) :
this._addResolvedEntry(key, value, save);
_getFromLocalStorage(key) {
const value = this._options.window.localStorage[key];
return value ? JSON.parse(value) : null;
}
/**
* This is the real method behind `_addEntry`. It Adds a new entry to the class data and, if
* `save` is used, it also saves it into the storage.
* The reason that there are two methods for this is, is because `_addEntry` can receive a
* {@link Promise}, and in that case, this method gets called after it gets resolved.
* @param {string} key The entry key.
* @param {Object} value The entry value.
* @param {boolean} save Whether or not the class should save the data into the storage.
* @return {Object} The same data that was saved.
* Gets an object from `sessionStorage`.
*
* @param {string} key The key used to save the object.
* @returns {?SimpleStorageDictionary}
* @access protected
*/
_addResolvedEntry(key, value, save) {
this._data[key] = {
time: this._now(),
value: this._copy(value),
};
if (save) {
this._save();
}
return value;
_getFromSessionStorage(key) {
const value = this._options.window.sessionStorage[key];
return value ? JSON.parse(value) : null;
}
/**
* Deletes an entry from the class data, and if `save` is used, the changes will be saved on
* the storage.
* @param {string} key The entry key.
* @param {boolean} [save=true] Whether or not the class should save the data into the storage
* after deleting the entry.
* @return {boolean} Whether or not the entry was deleted.
* Gets an object from the _"temp storage"_.
*
* @param {string} key The key used to save the object.
* @returns {?SimpleStorageDictionary}
* @access protected
*/
_deleteEntry(key, save = true) {
const exists = this._hasEntry(key);
if (exists) {
delete this._data[key];
if (save) {
this._save();
}
}
return exists;
_getFromTempStorage(key) {
return this._options.tempStorage[key];
}
/**
* This method is called when the storage is deleted or resetted and if entries are disabled.
* It can be used to define the initial value of the data the class saves on the storage.
*
* @returns {SimpleStorageDictionary}
* @access protected
*/
_getInitialData() {
return {};
}
/**
* Checks whether an entry exists or not.
*
* @param {string} key The entry key.
* @return {boolean}
* @returns {boolean}
* @access protected

@@ -407,94 +504,17 @@ */

/**
* Deletes the class data from the storage.
* @param {boolean} [reset=true] Whether or not to reset the data to the initial data
* (`_getInitialData`), if entries area disabled, or to an empty
* object, if they are enabled.
* This method _"initializes" the class by validating custom options, loading the reference for
* the required storage and synchronizing the data with the storage.
*
* @access protected
*/
_delete(reset = true) {
delete this._storage.delete(this._options.storage.key);
if (reset) {
this._setData(this._getInitialData(), false);
} else {
this._setData({}, false);
}
_initialize() {
this._validateOptions();
this._storage = this._initializeStorage();
this._data = this._initializeStorageData();
}
/**
* Saves the data from the class into the storage.
* @access protected
*/
_save() {
this._storage.set(this._options.storage.key, this._data);
}
/**
* Merges the class default options with the custom ones that can be sent to the constructor.
* The reason there's a method for this is because some of the options can be functions, and
* deep merges with functions can go wrong (and are expensive), so this methods takes out the
* functions first, does the merge and then adds them again.
* Similar to what it does for fuctions, it also takes out arrays: Merging arrays not always work
* as expected if the base array has some values already. Instead of the base values being
* overwritten, they are replaced with the amount of values specified on the _"overwrite array"_.
* Is easy to understand the reason, but nonetheless, it makes it confussing for an option to
* behave like that.
* @param {SimpleStorageOptions} defaults The class default options.
* @param {SimpleStorageOptions} custom The custom options sent to the constructor.
* @return {SimpleStorageOptions}
* @access protected
*/
_mergeOptions(defaults, custom) {
const newDefaults = Object.assign({}, defaults);
const newCustom = Object.assign({}, custom);
const fnOptions = {};
['window', 'logger', 'tempStorage'].forEach((fnOptionName) => {
fnOptions[fnOptionName] = newCustom[fnOptionName] || newDefaults[fnOptionName];
delete newDefaults[fnOptionName];
delete newCustom[fnOptionName];
});
let newStorageTypePriority;
if (newCustom.storage && newCustom.storage.typePriority) {
newStorageTypePriority = newCustom.storage.typePriority;
}
const newOptions = extend(
true,
newDefaults,
newCustom
);
Object.keys(fnOptions).forEach((fnOptionName) => {
newOptions[fnOptionName] = fnOptions[fnOptionName];
});
if (newStorageTypePriority) {
newOptions.storage.typePriority = newStorageTypePriority;
}
return newOptions;
}
/**
* Validates the class options before loading the storage and the data.
* @throws {Error} If either `storage.name` or `storage.key` are missing from the options.
* @throws {Error} If the options have a custom logger but it doesn't have `warn` nor `warning`
* methods.
* @access protected
*/
_validateOptions() {
const { storage, logger } = this._options;
const missing = ['name', 'key'].find((key) => typeof storage[key] !== 'string');
if (missing) {
throw new Error(`Missing required configuration setting: ${missing}`);
}
if (logger && (
typeof logger.warn !== 'function' &&
typeof logger.warning !== 'function'
)) {
throw new Error('The logger must implement a `warn` or `warning` method');
}
}
/**
* This method checks the list of priorities from the `storage.typePriority` option and tries
* to find the first available storage.
* @return {SimpleStorageStorage}
*
* @returns {SimpleStorageStorage}
* @throws {Error} If none of the storage options are available.

@@ -525,3 +545,4 @@ * @access protected

* to delete expired entries.
* @return {Object}
*
* @returns {Object}
* @access protected

@@ -542,71 +563,23 @@ */

/**
* Filters out a dictionary of entries by checking if they expired or not.
* @param {Object} entries A dictionary of key-value, where the value is a
* {@link SimpleStorageEntry}.
* @param {number} expiration The amount of seconds that need to have passed in order to
* consider an entry expired.
* @return {Object} A new dictionary without the expired entries.
* Checks whether `localStorage` is available or not.
*
* @param {string} [fallbackFrom] In case it's being used as a fallback, this will be the name
* of the storage that wasn't available.
* @returns {boolean}
* @access protected
*/
_deleteExpiredEntries(entries, expiration) {
const result = {};
const now = this._now();
Object.keys(entries).forEach((key) => {
const entry = entries[key];
if ((now - entry.time) < expiration) {
result[key] = entry;
}
});
return result;
}
/**
* Prints out a warning message. The method will first check if there's a custom logger (from
* the class options), otherwise, it will fallback to the `console` on the `window` option.
* @param {string} message The message to print out.
* @access protected
*/
_warn(message) {
const { logger } = this._options;
if (logger) {
if (logger.warning) {
logger.warning(message);
} else {
logger.warn(message);
}
} else {
// eslint-disable-next-line no-console
this._options.window.console.warn(message);
_isLocalStorageAvailable(fallbackFrom) {
if (fallbackFrom) {
this._warnStorageFallback(fallbackFrom, 'localStorage');
}
}
/**
* Makes a deep copy of an object.
* @param {Object|Array} obj The object to copy.
* @return {Object|Array}
* @access protected
*/
_copy(obj) {
let result;
if (Array.isArray(obj)) {
({ obj: result } = extend(true, {}, { obj }));
} else {
result = extend(true, {}, obj);
}
return result;
return !!this._options.window.localStorage;
}
/**
* Helper method to get the current timestamp in seconds.
* @return {number}
* Checks whether an object is a Promise or not.
*
* @param {*} obj The object to test.
* @returns {boolean}
* @access protected
*/
_now() {
return Math.floor(Date.now() / 1000);
}
/**
* Checkes whether an object is a Promise or not.
* @param {Object} obj The object to test.
* @return {boolean}
* @access protected
*/
_isPromise(obj) {

@@ -620,78 +593,111 @@ return (

/**
* Prints out a message saying that the class is doing a fallback from a storage to another
* one.
* @param {string} from The name of the storage that's not available.
* @param {string} to The name of the storage that will be used instead.
* Checks whether `sessionStorage` is available or not.
*
* @param {string} [fallbackFrom] In case it's being used as a fallback, this will be the name
* of the storage that wasn't available.
* @returns {boolean}
* @access protected
*/
_warnStorageFallback(from, to) {
this._warn(`${from} is not available; switching to ${to}`);
_isSessionStorageAvailable(fallbackFrom) {
if (fallbackFrom) {
this._warnStorageFallback(fallbackFrom, 'sessionStorage');
}
return !!this._options.window.sessionStorage;
}
/**
* Checks whether `localStorage` is available or not.
* This method is just here to comply with the {@link SimpleStorageStorage} _"interface"_ as
* the temp storage is always available.
*
* @param {string} [fallbackFrom] In case it's being used as a fallback, this will be the name
* of the storage that wasn't available.
* @return {boolean}
* @returns {boolean}
* @access protected
*/
_isLocalStorageAvailable(fallbackFrom) {
_isTempStorageAvailable(fallbackFrom) {
if (fallbackFrom) {
this._warnStorageFallback(fallbackFrom, 'localStorage');
this._warnStorageFallback(fallbackFrom, 'tempStorage');
}
return !!this._options.window.localStorage;
return true;
}
/**
* Gets an object from `localStorage`.
* @param {string} key The key used to save the object.
* @return {Object}
* Merges the class default options with the custom ones that can be sent to the constructor.
* The reason there's a method for this is because of a specific (edgy) use case: `tempStorage`
* can be a Proxy, and a Proxy without defined keys stops working after an
* `Object.assign`/spread.
*
* @param {SimpleStorageOptions} defaults The class default options.
* @param {SimpleStorageOptions} custom The custom options sent to the constructor.
* @returns {SimpleStorageOptions}
* @access protected
*/
_getFromLocalStorage(key) {
const value = this._options.window.localStorage[key];
return value ? JSON.parse(value) : null;
_mergeOptions(defaults, custom) {
const { tempStorage } = custom;
const options = deepAssignWithOverwrite(defaults, custom);
if (tempStorage) {
options.tempStorage = tempStorage;
}
return options;
}
/**
* Sets an object into the `localStorage`.
* @param {string} key The object key.
* @param {Object} value The object to save.
* Helper method to get the current timestamp in seconds.
*
* @returns {number}
* @access protected
*/
_setOnLocalStorage(key, value) {
this._options.window.localStorage[key] = JSON.stringify(value);
_now() {
return Math.floor(Date.now() / 1000);
}
/**
* Deletes an object from the `localStorage`.
* @param {string} key The object key.
* Resets the data on the class; If entries are enabled, the data will become an empty
* {@link object}; otherwise, it will call {@link this#_getInitialData}.
*
* @param {boolean} [save=true] Whether or not the class should save the data into the storage.
* @returns {Object}
* @access protected
*/
_deleteFromLocalStorage(key) {
delete this._options.window.localStorage[key];
_resetData(save = true) {
const data = this._options.entries.enabled ? {} : this._getInitialData();
return this._setData(data, save);
}
/**
* Checks whether `sessionStorage` is available or not.
* @param {string} [fallbackFrom] In case it's being used as a fallback, this will be the name
* of the storage that wasn't available.
* @return {boolean}
* Saves the data from the class into the storage.
*
* @access protected
*/
_isSessionStorageAvailable(fallbackFrom) {
if (fallbackFrom) {
this._warnStorageFallback(fallbackFrom, 'sessionStorage');
}
return !!this._options.window.sessionStorage;
_save() {
this._storage.set(this._options.storage.key, this._data);
}
/**
* Gets an object from `sessionStorage`.
* @param {string} key The key used to save the object.
* @return {Object}
* Overwrites the data reference the class has and, if `save` is used, it also saves it into
* the storage.
*
* @param {Object|Promise} data The new data, or a {@link Promise} that resolves into the
* new data.
* @param {boolean} [save=true] Whether or not the class should save the data into the
* storage.
* @returns {Object|Promise} If `data` is an {@link object}, it will return the same object; but
* if `data` is a {@link Promise}, it will return the _"promise chain"_.
* @access protected
*/
_getFromSessionStorage(key) {
const value = this._options.window.sessionStorage[key];
return value ? JSON.parse(value) : null;
_setData(data, save = true) {
return this._isPromise(data) ?
data.then((realData) => this._setResolvedData(realData, save)) :
this._setResolvedData(data, save);
}
/**
* Sets an object into the `localStorage`.
*
* @param {string} key The object key.
* @param {Object} value The object to save.
* @access protected
*/
_setOnLocalStorage(key, value) {
this._options.window.localStorage[key] = JSON.stringify(value);
}
/**
* Sets an object into the `sessionStorage`.
*
* @param {string} key The object key.

@@ -705,49 +711,83 @@ * @param {Object} value The object to save.

/**
* Deletes an object from the `sessionStorage`.
* @param {string} key The object key.
* Sets an object into the _"temp storage"_.
*
* @param {string} key The object key.
* @param {Object} value The object to save.
* @access protected
*/
_deleteFromSessionStorage(key) {
delete this._options.window.sessionStorage[key];
_setOnTempStorage(key, value) {
this._options.tempStorage[key] = value;
}
/**
* This method is just here to comply with the {@link SimpleStorageStorage} _"interface"_ as
* the temp storage is always available.
* @param {string} [fallbackFrom] In case it's being used as a fallback, this will be the name
* of the storage that wasn't available.
* @return {boolean}
* This is the real method behind `_setData`. It overwrites the data reference the class
* has and, if `save` is used, it also saves it into the storage.
* The reason that there are two methods for this is, is because `_setData` can receive a
* {@link Promise}, and in that case, this method gets called after it gets resolved.
*
* @param {Object} data The new data.
* @param {boolean} save Whether or not the class should save the data into the storage.
* @returns {Object} The same data that was saved.
* @access protected
*/
_isTempStorageAvailable(fallbackFrom) {
if (fallbackFrom) {
this._warnStorageFallback(fallbackFrom, 'tempStorage');
_setResolvedData(data, save) {
this._data = deepAssignWithShallowMerge(data);
if (save) {
this._save();
}
return true;
return data;
}
/**
* Gets an object from the _"temp storage"_.
* @param {string} key The key used to save the object.
* @return {Object}
* Validates the class options before loading the storage and the data.
*
* @throws {Error} If either `storage.name` or `storage.key` are missing from the options.
* @throws {Error} If the options have a custom logger but it doesn't have `warn` nor `warning`
* methods.
* @access protected
*/
_getFromTempStorage(key) {
return this._options.tempStorage[key];
_validateOptions() {
const { storage, logger } = this._options;
const missing = ['name', 'key'].find((key) => typeof storage[key] !== 'string');
if (missing) {
throw new Error(`Missing required configuration setting: ${missing}`);
}
if (logger && (
typeof logger.warn !== 'function' &&
typeof logger.warning !== 'function'
)) {
throw new Error('The logger must implement a `warn` or `warning` method');
}
}
/**
* Sets an object into the _"temp storage"_.
* @param {string} key The object key.
* @param {Object} value The object to save.
* Prints out a warning message. The method will first check if there's a custom logger (from
* the class options), otherwise, it will fallback to the `console` on the `window` option.
*
* @param {string} message The message to print out.
* @access protected
*/
_setOnTempStorage(key, value) {
this._options.tempStorage[key] = value;
_warn(message) {
const { logger } = this._options;
if (logger) {
if (logger.warning) {
logger.warning(message);
} else {
logger.warn(message);
}
} else {
// eslint-disable-next-line no-console
this._options.window.console.warn(message);
}
}
/**
* Deletes an object from the _"temp storage"_.
* @param {string} key The object key.
* Prints out a message saying that the class is doing a fallback from a storage to another
* one.
*
* @param {string} from The name of the storage that's not available.
* @param {string} to The name of the storage that will be used instead.
* @access protected
*/
_deleteFromTempStorage(key) {
delete this._options.tempStorage[key];
_warnStorageFallback(from, to) {
this._warn(`${from} is not available; switching to ${to}`);
}

@@ -754,0 +794,0 @@ }

const path = require('path');
const { provider } = require('jimple');
const ObjectUtils = require('../shared/objectUtils');
const { deepAssign } = require('../shared/deepAssign');
const { providerCreator } = require('../shared/jimpleFns');
/**
* @module node/appConfiguration
*/
/**
* @typedef {import('./environmentUtils').EnvironmentUtils} EnvironmentUtils
* @typedef {import('./rootRequire').RootRequireFn} RootRequireFn
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} AppConfigurationOptions
* @property {string} [defaultConfigurationName='default'] The name of the default
* configuration
* configuration.
* @property {string} [environmentVariable='APP_CONFIG'] The name of the variable it
* will read in order to
* determine which configuration
* to load
* to load.
* @property {string} [path='./config/[app-name]'] The path to the configurations

@@ -21,24 +37,52 @@ * directory, relative to the

* of the configuration.
* @parent module:node/appConfiguration
*/
/**
* @typedef {Object} AppConfigurationServiceMap
* @property {string|EnvironmentUtils} [environmentUtils]
* The name of the service for {@link EnvironmentUtils} or an instance of it. `environmentUtils` by
* default.
* @property {string|RootRequireFn} [rootRequire]
* The name of the service for {@link RootRequireFn} or an instance of it. `rootRequire` by default.
* @parent module:node/appConfiguration
*/
/**
* @typedef {Object} AppConfigurationProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link AppConfiguration}. Its default
* value is `appConfiguration`.
* @property {string} appName
* The name of the application.
* @property {Object} defaultConfiguration
* The service default configuration.
* @property {Partial<AppConfigurationOptions>} options
* Overwrites for the service customization options.
* @property {AppConfigurationServiceMap} services
* A dictionary with the services that need to be injected on the class.
* @parent module:node/appConfiguration
*/
/**
* This is a service to manage applications configurations. It takes care of loading, activating,
* switching and merging configuration files.
*
* @parent module:node/appConfiguration
* @tutorial appConfiguration
*/
class AppConfiguration {
/**
* Class constructor.
* @param {EnvironmentUtils} environmentUtils Required to read the environment
* variables and determine which
* configuration to use.
* @param {Function} rootRequire Necessary to be able to require the
* configuration files with paths
* relative to the app root directory.
* @param {string} [appName='app'] The name of the app using this
* service.
* It's also used as part of the name
* of the configuration files.
* @param {Object} [defaultConfiguration={}] The default configuration the others
* will extend.
* @param {AppConfigurationOptions} [options={}] Options to customize the service
* @param {EnvironmentUtils} environmentUtils
* Required to read the environment variables and determine which configuration to use.
* @param {RootRequireFn} rootRequire
* Necessary to be able to require the configuration files with paths relative to the app root
* directory.
* @param {string} [appName='app']
* The name of the app using this service. It's also used as part of the name of the
* configuration files.
* @param {Object} [defaultConfiguration={}]
* The default configuration the others will extend.
* @param {Partial<AppConfigurationOptions>} [options={}]
* Options to customize the service.
*/

@@ -50,20 +94,29 @@ constructor(

defaultConfiguration = {},
options = {}
options = {},
) {
/**
* A local reference for the `environmentUtils` service.
*
* @type {EnvironmentUtils}
* @access protected
* @ignore
*/
this.environmentUtils = environmentUtils;
this._environmentUtils = environmentUtils;
/**
* The function that allows the service to `require` a configuration file with a path relative
* to the app root directory.
* @type {Function}
*
* @type {RootRequireFn}
* @access protected
* @ignore
*/
this.rootRequire = rootRequire;
this._rootRequire = rootRequire;
/**
* The service customizable options.
*
* @type {AppConfigurationOptions}
* @access protected
* @ignore
*/
this.options = ObjectUtils.merge(true, {
this._options = ObjectUtils.merge({
defaultConfigurationName: 'default',

@@ -77,20 +130,80 @@ environmentVariable: 'APP_CONFIG',

* as keys.
* @type {Object}
*
* @type {Object.<string,Object>}
* @access protected
* @ignore
*/
this.configurations = {
[this.options.defaultConfigurationName]: defaultConfiguration,
this._configurations = {
[this._options.defaultConfigurationName]: defaultConfiguration,
};
/**
* The name of the active configuration.
*
* @type {string}
* @access protected
* @ignore
*/
this.activeConfiguration = this.options.defaultConfigurationName;
this._activeConfiguration = this._options.defaultConfigurationName;
/**
* Whether or not the configuration can be switched.
*
* @type {boolean}
* @access protected
* @ignore
*/
this.allowConfigurationSwitch = !!this.get('allowConfigurationSwitch');
this._allowConfigurationSwitch = !!this.get('allowConfigurationSwitch');
}
/**
* Gets a setting or settings from the active configuration.
*
* @example
* // To get a single setting
* const value = appConfiguration.get('some-setting');
*
* // To get multiple values
* const {
* settingOne,
* settingTwo,
* } = appConfiguration.get(['settingOne', 'settingTwo']);
*
* // Use paths
* const subValue = appConfiguration.get('settingOne.subSetting');
*
* @param {string|string[]} setting A setting path or a list of them.
* @param {boolean} [asArray=false] When `setting` is an Array, if this is `true`,
* instead of returning an object, it will return an
* array of settings.
* @returns {*}
*/
get(setting, asArray = false) {
let result;
if (Array.isArray(setting)) {
result = asArray ?
setting.map((name) => this.get(name)) :
setting.reduce(
(current, name) => ({ ...current, [name]: this.get(name) }),
{},
);
} else if (setting === 'name') {
result = this._activeConfiguration;
} else {
result = ObjectUtils.get(this.getConfig(), setting);
}
return result;
}
/**
* Gets a configuration settings. If no name is specified, it will return the settings of the
* active configuration.
*
* @param {string} [name=''] The name of the configuration.
* @returns {?Object}
*/
getConfig(name = '') {
const existing = this._configurations[(name || this._activeConfiguration)];
return existing ? ObjectUtils.copy(existing) : null;
}
/**
* Load a new configuration.
*
* @param {string} name The configuration name.

@@ -100,3 +213,3 @@ * @param {Object} settings The configuration settings.

* adding it.
* @return {Object} The settings of the new configuration.
* @returns {Object} The settings of the new configuration.
* @throws {Error} If the configuration tries to extend a configuration that doesn't exist.

@@ -106,3 +219,3 @@ */

// Get the name of the configuration it will extend.
const extendsFrom = settings.extends || this.options.defaultConfigurationName;
const extendsFrom = settings.extends || this._options.defaultConfigurationName;
// Get the settings of the configuration to extend.

@@ -117,3 +230,3 @@ const baseConfiguration = this.getConfig(extendsFrom);

true,
switchTo
switchTo,
);

@@ -128,3 +241,19 @@ } else {

/**
* Load a configuration from a file.
* Checks if there's a configuration name on the environment variable and if there is, try to load
* the configuration file for it.
*
* @returns {Object} The loaded configuration or an empty object if the variable was empty.
*/
loadFromEnvironment() {
const name = this._environmentUtils.get(this._options.environmentVariable);
let result = {};
if (name) {
result = this.loadFromFile(name);
}
return result;
}
/**
* Loads a configuration from a file.
*
* @param {string} name The name of the configuration.

@@ -136,3 +265,3 @@ * @param {boolean} [switchTo=true] If the service should switch to the new configuration

* configuration setting.
* @return {Object} The settings of the loaded configuration.
* @returns {Object} The settings of the loaded configuration.
* @throws {Error} If the configuration file can't be loaded.

@@ -142,5 +271,5 @@ */

// Format the name of the configuration file.
const filename = this.options.filenameFormat.replace(/\[name\]/g, name);
const filename = this._options.filenameFormat.replace(/\[name\]/g, name);
// Build the path to the configuration file.
const filepath = path.join(this.options.path, filename);
const filepath = path.join(this._options.path, filename);

@@ -150,3 +279,3 @@ let settings = {};

try {
settings = this.rootRequire(filepath);
settings = this._rootRequire(filepath);
} catch (error) {

@@ -157,3 +286,3 @@ throw new Error(`The configuration file couldn't be loaded: ${filepath}`);

// Get the name of the configuration it will extend.
const extendsFrom = settings.extends || this.options.defaultConfigurationName;
const extendsFrom = settings.extends || this._options.defaultConfigurationName;
// Get the base configuration from either the service or by loading it.

@@ -166,3 +295,3 @@ const baseConfiguration = this.getConfig(extendsFrom) || this.loadFromFile(extendsFrom, false);

checkSwitchFlag,
switchTo
switchTo,
);

@@ -173,84 +302,6 @@ // Return the loaded configuration.

/**
* Check if there's a configuration name on the environment variable and if there is, try to load
* the configuration file for it.
* @return {Object} The loaded configuration or an empty object if the variable was empty.
*/
loadFromEnvironment() {
const name = this.environmentUtils.get(this.options.environmentVariable);
let result = {};
if (name) {
result = this.loadFromFile(name);
}
return result;
}
/**
* Get a configuration settings. If no name is specified, it will return the settings of the
* active configuration.
* @param {string} [name=''] The name of the configuration.
* @return {Object}
*/
getConfig(name = '') {
return this.configurations[(name || this.activeConfiguration)];
}
/**
* Overwrites all the settings for a configuration. If the name is not specified, it will
* overwrite the active configuration.
* @param {Object} config The new configuration settings.
* @param {string} [name=''] The name of the configuration.
* @param {boolean} [merge=true] Whether or not to merge the new settings with the existing
* ones.
* @return {Object} The updated configuration.
*/
setConfig(config, name = '', merge = true) {
const key = (name || this.activeConfiguration);
this.configurations[key] = merge ?
ObjectUtils.merge(this.configurations[key], config) :
config;
return this.configurations[key];
}
/**
* Get a setting or settings from the active configuration.
* @example
* // To get a single setting
* const value = appConfiguration.get('some-setting');
*
* // To get multiple values
* const {
* settingOne,
* settingTwo,
* } = appConfiguration.get(['settingOne', 'settingTwo']);
*
* // Use paths
* const subValue = appConfiguration.get('settingOne.subSetting');
*
* @param {string|Array} setting A setting path or a list of them.
* @param {boolean} [asArray=false] When `setting` is an Array, if this is `true`, instead
* of returning an object, it will return an array of
* settings.
* @return {*}
*/
get(setting, asArray = false) {
let result;
if (Array.isArray(setting)) {
result = asArray ?
setting.map((name) => this.get(name)) :
setting.reduce(
(current, name) => Object.assign({}, current, {
[name]: this.get(name),
}),
{}
);
} else if (setting === 'name') {
result = this.activeConfiguration;
} else {
result = ObjectUtils.get(this.getConfig(), setting);
}
return result;
}
/**
* Set the value of a setting or settings from the active configuration.
* Sets the value of a setting or settings from the active configuration.
* If both the current and the new value of a setting are objects, then instead of overwriting
* it, the method will merge them.
*
* @example

@@ -264,6 +315,8 @@ * // To set a single setting value

* })
* @param {string|Object} setting The name of the setting to update or a dictionary of settings
* and their values.
* @param {*} value The value of the setting. This is only used when `setting` is
* a string.
*
* @param {string|Object.<string,*>} setting The name of the setting to update or a dictionary of
* settings and their values.
* @param {*} [value] The value of the setting. This is only used when
* `setting` is a string.
* @throws {Error} If `setting` is not a dictionary and `value` is undefined.
*/

@@ -288,24 +341,35 @@ set(setting, value) {

/**
* Check whether the service can switch configurations or not.
* @return {boolean}
* Overwrites all the settings for a configuration. If the name is not specified, it will
* overwrite the active configuration.
*
* @param {Object} config The new configuration settings.
* @param {string} [name=''] The name of the configuration.
* @param {boolean} [merge=true] Whether or not to merge the new settings with the existing
* ones.
* @returns {Object} The updated configuration.
*/
canSwitch() {
return this.allowConfigurationSwitch;
setConfig(config, name = '', merge = true) {
const key = (name || this._activeConfiguration);
this._configurations[key] = merge ?
ObjectUtils.merge(this._configurations[key], config) :
config;
return this._configurations[key];
}
/**
* Switch to a different configuration. If the configuration is not registered, it will try to
* Switchs to a different configuration. If the configuration is not registered, it will try to
* load from a file.
*
* @param {string} name The new of the configuration to switch to.
* @param {boolean} [force=false] A way to force the service to switch even if the
* `allowConfigurationSwitch` property if `false`.
* @return {Object} The new active configuration.
* @returns {Object} The new active configuration.
* @throws {Error} If `force` is `false` and the `allowConfigurationSwitch` property is `false`.
*/
switch(name, force = false) {
if (!this.canSwitch() && !force) {
if (!this._allowConfigurationSwitch && !force) {
throw new Error(`You can't switch the configuration to '${name}', the feature is disabled`);
} else if (!this.configurations[name]) {
} else if (!this._configurations[name]) {
this.loadFromFile(name, true, false);
} else {
this.activeConfiguration = name;
this._activeConfiguration = name;
}

@@ -316,3 +380,37 @@

/**
* The name of the active configuration.
*
* @type {string}
*/
get activeConfiguration() {
return this._activeConfiguration;
}
/**
* Whether or not the active configuration can be switched.
*
* @type {boolean}
*/
get canSwitch() {
return this._allowConfigurationSwitch;
}
/**
* A dictionary with all the loaded configurations. It uses the names of the configurations
* as keys.
*
* @type {Object.<string,Object>}
*/
get configurations() {
return ObjectUtils.copy(this._configurations);
}
/**
* The service customizable options.
*
* @type {AppConfigurationOptions}
*/
get options() {
return { ...this._options };
}
/**
* Add a new configuration to the service.
*
* @param {string} name The name of the new configuration.

@@ -324,4 +422,4 @@ * @param {Object} settings The configuration settings.

* after adding it.
* @access protected
* @ignore
* @access protected
*/

@@ -333,6 +431,6 @@ _addConfiguration(name, settings, checkSwitchFlag, switchTo) {

if (checkSwitchFlag && typeof newSettings.allowConfigurationSwitch === 'boolean') {
this.allowConfigurationSwitch = newSettings.allowConfigurationSwitch;
this._allowConfigurationSwitch = newSettings.allowConfigurationSwitch;
}
this.configurations[name] = newSettings;
this._configurations[name] = newSettings;
if (switchTo) {

@@ -344,34 +442,49 @@ this.switch(name, true);

/**
* Generates a `Provider` with an already defined name, default configuration and options.
* @example
* // Generate the provider
* const provider = appConfiguration('my-app', {
* birthday: '25-09-2015',
* });
* // Register it on the container
* container.register(provider);
* // Getting access to the service instance
* const appConfiguration = container.get('appConfiguration');
* @param {string} [appName] The name of the app.
* @param {Object} [defaultConfiguration] The service default configuration.
* @param {Object} [options] Options to customize the service.
* @return {Provider}
* The service provider to register an instance of {@link AppConfiguration} on the container.
*
* @type {ProviderCreatorWithOptions<AppConfigurationProviderOptions>}
* @tutorial appConfiguration
*/
const appConfiguration = (
appName,
defaultConfiguration,
options
) => provider((app) => {
app.set('appConfiguration', () => new AppConfiguration(
app.get('environmentUtils'),
app.get('rootRequire'),
appName,
defaultConfiguration,
options
));
const appConfiguration = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'appConfiguration', () => {
/**
* @type {AppConfigurationProviderOptions}
* @ignore
*/
const useOptions = deepAssign(
{
services: {
environmentUtils: 'environmentUtils',
rootRequire: 'rootRequire',
},
},
options,
);
const services = Object.keys(useOptions.services)
.reduce(
(acc, key) => {
const value = useOptions.services[key];
const service = typeof value === 'string' ?
app.get(value) :
value;
return {
...acc,
[key]: service,
};
},
{},
);
return new AppConfiguration(
services.environmentUtils,
services.rootRequire,
useOptions.appName,
useOptions.defaultConfiguration,
useOptions.options,
);
});
});
module.exports = {
AppConfiguration,
appConfiguration,
};
module.exports.AppConfiguration = AppConfiguration;
module.exports.appConfiguration = appConfiguration;

@@ -1,29 +0,65 @@

const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
/**
* @module node/environmentUtils
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} EnvironmentUtilsProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link EnvironmentUtils}. Its default
* value is `environmentUtils`.
*
* @parent module:node/environmentUtils
*/
/**
* A simple service to avoid calling `process.env` on multiples places of an app.
*
* @parent module:node/environmentUtils
* @tutorial environmentUtils
*/
class EnvironmentUtils {
/**
* Class constructor.
*/
constructor() {
/**
* The current `NODE_ENV`. If the variable is empty, the value will be `development`.
*
* @type {string}
* @access protected
* @ignore
*/
this.env = this.get('NODE_ENV', 'development');
this._env = this.get('NODE_ENV', 'development');
/**
* Whether or not the environment is production.
*
* @type {boolean}
* @access protected
* @ignore
*/
this.production = this.env === 'production';
this._production = this.env === 'production';
}
/**
* Checks whether an environment variable exists or not.
*
* @param {string} name The name of the variable.
* @returns {boolean}
*/
exists(name) {
// eslint-disable-next-line no-process-env
return typeof process.env[name] !== 'undefined';
}
/**
* Gets the value of an environment variable.
*
* @param {string} name The name of the variable.
* @param {string} [defaultValue=''] A fallback value in case the variable is `undefined`
* @param {string} [defaultValue=''] A fallback value in case the variable is `undefined`.
* @param {boolean} [required=false] If the variable is required and `undefined`, it will throw
* an error.
* @return {string}
* @throws {Error} if `required` is set to `true` and the variable is `undefined`.
* @returns {string}
* @throws {Error} If `required` is set to `true` and the variable is `undefined`.
*/

@@ -47,7 +83,8 @@ get(name, defaultValue = '', required = false) {

* Sets the value of an environment variable.
* @param {string} name The name of the variable
*
* @param {string} name The name of the variable.
* @param {string} value The value of the variable.
* @param {string} [overwrite=false] If the variable already exists, the method won't overwrite
* it, unless you set this parameter to `true`.
* @return {boolean} Whether or not the variable was set.
* @returns {boolean} Whether or not the variable was set.
*/

@@ -67,35 +104,41 @@ set(name, value, overwrite = false) {

/**
* Checks whether an environment variable exists or not.
* @param {string} name The name of the variable.
* @return {boolean}
* Whether or not the environment is for development.
*
* @type {boolean}
*/
exists(name) {
// eslint-disable-next-line no-process-env
return typeof process.env[name] !== 'undefined';
get development() {
return !this._production;
}
/**
* Check whether or not the environment is for development.
* @return {boolean}
* The current `NODE_ENV`. If the variable is empty, the value will be `development`.
*
* @type {string}
* @access protected
* @ignore
*/
get development() {
return !this.production;
get env() {
return this._env;
}
/**
* Whether or not the environment is production.
*
* @type {boolean}
* @access protected
* @ignore
*/
get production() {
return this._production;
}
}
/**
* The service provider that once registered on the app container will set an instance of
* `EnvironmentUtils` as the `environmentUtils` service.
* @example
* // Register it on the container
* container.register(environmentUtils);
* // Getting access to the service instance
* const environmentUtils = container.get('environmentUtils');
* @type {Provider}
* The service provider to register an instance of {@link EnvironmentUtils} on the container.
*
* @type {ProviderCreatorWithOptions<EnvironmentUtilsProviderOptions>}
* @tutorial environmentUtils
*/
const environmentUtils = provider((app) => {
app.set('environmentUtils', () => new EnvironmentUtils());
const environmentUtils = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'environmentUtils', () => new EnvironmentUtils());
});
module.exports = {
EnvironmentUtils,
environmentUtils,
};
module.exports.EnvironmentUtils = EnvironmentUtils;
module.exports.environmentUtils = environmentUtils;

@@ -1,9 +0,46 @@

const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssignWithShallowMerge } = require('../shared/deepAssign');
/**
* @module node/errorHandler
*/
/**
* @typedef {import('./logger').Logger} Logger
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} ErrorHandlerServiceMap
* @property {string[]|string|Logger} [logger]
* A list of loggers' service names from which the service will try to find the first available,
* a specific service name, or an instance of {@link Logger}.
* @parent module:node/errorHandler
*/
/**
* @typedef {Object} ErrorHandlerProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link ErrorHandler}. Its default value
* is `errorHandler`.
* @property {boolean} exitOnError
* Whether or not to exit the process after receiving an error.
* @property {ErrorHandlerServiceMap} services
* A dictionary with the services that need to be injected on the class.
* @parent module:node/errorHandler
*/
/**
* An error handler that captures uncaught exceptions and unhandled rejections in order to log
* them with detail.
*
* @parent module:node/errorHandler
* @tutorial errorHandler
*/
class ErrorHandler {
/**
* Class constructor.
* @param {Logger} appLogger To log the detail of the erros.

@@ -16,15 +53,24 @@ * @param {boolean} [exitOnError=true] Whether or not to exit the process after receiving an

* A local reference for the `appLogger` service.
*
* @type {Logger}
* @access protected
* @ignore
*/
this.appLogger = appLogger;
this._appLogger = appLogger;
/**
* Whether or not to exit the process after receiving an error.
*
* @type {boolean}
* @access protected
* @ignore
*/
this.exitOnError = exitOnError;
this._exitOnError = exitOnError;
/**
* The list of events this handler will listen for in order to catch errors.
* @type {Array}
*
* @type {string[]}
* @access protected
* @ignore
*/
this.eventsNames = [
this._eventsNames = [
'uncaughtException',

@@ -35,2 +81,3 @@ 'unhandledRejection',

* Bind the handler method so it can be used on the calls to `process`.
*
* @ignore

@@ -41,21 +88,6 @@ */

/**
* Starts listening for unhandled errors.
*/
listen() {
this.eventsNames.forEach((eventName) => {
process.on(eventName, this.handler);
});
}
/**
* Stops listening for unhandled errors.
*/
stopListening() {
this.eventsNames.forEach((eventName) => {
process.removeListener(eventName, this.handler);
});
}
/**
* This is called by the process listeners when an uncaught exception is thrown or a rejected
* promise is not handled. It logs the error on detail.
* The process exits when after logging an error.
*
* @param {Error} error The unhandled error.

@@ -65,5 +97,5 @@ */

// If the logger is configured to show the time...
if (this.appLogger.showTime) {
if (this._appLogger.showTime) {
// ...just send the error.
this.appLogger.error(error);
this._appLogger.error(error);
} else {

@@ -78,7 +110,7 @@ // ...otherwise, get the time on a readable format.

// Log the new message with the exception.
this.appLogger.error(message, error);
this._appLogger.error(message, error);
}
// Check if it should exit the process.
if (this.exitOnError) {
if (this._exitOnError) {
// eslint-disable-next-line no-process-exit

@@ -88,40 +120,89 @@ process.exit(1);

}
/**
* Starts listening for unhandled errors.
*/
listen() {
this._eventsNames.forEach((eventName) => {
process.on(eventName, this.handler);
});
}
/**
* Stops listening for unhandled errors.
*/
stopListening() {
this._eventsNames.forEach((eventName) => {
process.removeListener(eventName, this.handler);
});
}
/**
* Whether or not the process will exit after receiving an error.
*
* @type {boolean}
*/
get exitOnError() {
return this._exitOnError;
}
}
/**
* Generates a `Provider` with an already defined flag to exit or not the process when after
* handling an error.
* @param {boolean} [exitOnError] Whether or not to exit the process after receiving an error.
* @return {Provider}
* The service provider to register an instance of {@link ErrorHandler} on the container.
*
* @throws {Error} If `services.logger` specifies a service that doesn't exist or if it's a falsy
* value.
* @type {ProviderCreatorWithOptions<ErrorHandlerProviderOptions>}
* @tutorial errorHandler
*/
const errorHandlerWithOptions = (exitOnError) => provider((app) => {
app.set('errorHandler', () => {
let logger = null;
try {
logger = app.get('logger');
} catch (ignore) {
logger = app.get('appLogger');
const errorHandler = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'errorHandler', () => {
/**
* @type {ErrorHandlerProviderOptions}
* @ignore
*/
const useOptions = deepAssignWithShallowMerge(
{
services: {
logger: ['logger', 'appLogger'],
},
},
options,
);
const { logger } = useOptions.services;
/**
* @type {?Logger}
* @ignore
*/
let useLogger;
if (Array.isArray(logger)) {
useLogger = logger.reduce(
(acc, name) => {
let nextAcc;
if (acc) {
nextAcc = acc;
} else {
try {
nextAcc = app.get(name);
} catch (ignore) {
nextAcc = null;
}
}
return nextAcc;
},
null,
);
} else if (typeof logger === 'string') {
useLogger = app.get(logger);
} else {
useLogger = logger;
}
return new ErrorHandler(
logger,
exitOnError
);
if (!useLogger) {
throw new Error('No logger service was found');
}
return new ErrorHandler(useLogger, useOptions.exitOnError);
});
});
/**
* The service provider that once registered on the app container will set an instance of
* `ErrorHandler` as the `errorHandler` service.
* @example
* // Register it on the container
* container.register(errorHandler);
* // Getting access to the service instance
* const errorHandler = container.get('errorHandler');
* @type {Provider}
*/
const errorHandler = errorHandlerWithOptions();
module.exports = {
ErrorHandler,
errorHandlerWithOptions,
errorHandler,
};
module.exports.ErrorHandler = ErrorHandler;
module.exports.errorHandler = errorHandler;

@@ -0,1 +1,8 @@

/**
* I'm using this extremely verbose syntax because it's the only way the transpilation process
* would recognize both 'imports' and 'exports'.
*
* @ignore
*/
const { AppConfiguration } = require('./appConfiguration');

@@ -9,10 +16,8 @@ const { EnvironmentUtils } = require('./environmentUtils');

module.exports = {
AppConfiguration,
EnvironmentUtils,
ErrorHandler,
Logger,
packageInfo,
PathUtils,
rootRequire,
};
module.exports.AppConfiguration = AppConfiguration;
module.exports.EnvironmentUtils = EnvironmentUtils;
module.exports.ErrorHandler = ErrorHandler;
module.exports.Logger = Logger;
module.exports.packageInfo = packageInfo;
module.exports.PathUtils = PathUtils;
module.exports.rootRequire = rootRequire;
const colors = require('colors/safe');
const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssign } = require('../shared/deepAssign');
/**
* @module node/logger
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} PackageInfo
* @property {string} [nameForCLI] A specific name to use on the logger; it overwrites `name`.
* @property {string} name The package name.
*/
/**
* @typedef {Object} AppLoggerServiceMap
* @property {string|PackageInfo} [packageInfo]
* The name of the service that containers the information of the `package.json`. `packageInfo` by
* default.
* @parent module:node/logger
*/
/**
* @typedef {Object} AppLoggerProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link Logger} with the package name as
* prefix. Its default value is `appLogger`.
* @property {AppLoggerServiceMap} services
* A dictionary with the services that need to be injected.
* @property {boolean} [showTime]
* Whether or not to show the time on each message.
* @parent module:node/logger
*/
/**
* @typedef {Object} LoggerProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link Logger}. Its default value is
* `logger`.
* @property {string} [messagesPrefix]
* A prefix to include in front of all the messages.
* @property {boolean} [showTime]
* Whether or not to show the time on each message.
* @parent module:node/logger
*/
/**
* This can be either a message to log, or an array where the first item is the message and the
* second one is the color it should be used to log it.
*
* @example
* logger.log('hello world');
* // It will log 'hello world' with the default color.
* logger.log(['hello world', 'red'])
* // It will log 'hello world' in red.
*
* @typedef {string|string[]} LoggerLine
* @parent module:node/logger
*/
/**
* @typedef {string|LoggerLine[]} LoggerMessage
* @parent module:node/logger
*/
/**
* A utility service to log messages on the console.
*
* @parent module:node/logger
* @tutorial logger
*/
class Logger {
/**
* Class constructor.
* @param {string} [messagesPrefix=''] A prefix to include in front of all the messages.

@@ -15,38 +85,35 @@ * @param {boolean} [showTime=false] Whether or not to show the time on each message.

* The prefix to include in front of all the messages.
*
* @type {string}
* @access protected
* @ignore
*/
this.messagesPrefix = messagesPrefix;
this._messagesPrefix = messagesPrefix;
/**
* Whether or not to show the time on each message.
*
* @type {boolean}
* @access protected
* @ignore
*/
this.showTime = showTime;
this._showTime = showTime;
/**
* An alias for the {@link Logger#warning} method.
*
* @type {Function}
* @see {@link Logger#warning}
*/
this.warn = this.warning.bind(this);
}
/**
* Logs a warning (yellow) message or messages on the console.
* @param {string|Array} message A single message of a list of them. See the `log()` documentation
* to see all the supported properties for the `message` parameter.
*/
warning(message) {
this.log(message, 'yellow');
}
/**
* Logs a success (green) message or messages on the console.
* @param {string|Array} message A single message of a list of them. See the `log()` documentation
* to see all the supported properties for the `message` parameter.
*/
success(message) {
this.log(message, 'green');
}
/**
* Logs an error (red) message or messages on the console.
* @param {string|Array|Error} message A single message of a list of them. See the
* `log()` documentation to see all the supported
* properties for the `message` parameter. Different
* from the other log methods, you can use an
* `Error` object and the method will take care of
* extracting the message and the stack information.
* @param {Object} [exception=null] If the exception has a `stack` property, the
* method will log each of the stack calls using
* `info()`.
*
* @param {LoggerMessage|Error} message
* A single message of a list of them. See the `log()` documentation to see all the supported
* properties for the `message` parameter. Different from the other log methods, you can use an
* `Error` object and the method will take care of extracting the message and the stack
* information.
* @param {Object} [exception=null]
* If the exception has a `stack` property, the method will log each of the stack calls using
* `info()`.
*/

@@ -74,4 +141,5 @@ error(message, exception = null) {

* Logs an information (gray) message or messages on the console.
* @param {string|Array} message A single message of a list of them. See the `log()` documentation
* to see all the supported properties for the `message` parameter.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log}
*/

@@ -83,2 +151,3 @@ info(message) {

* Logs a message with an specific color on the console.
*
* @example

@@ -93,12 +162,13 @@ * // Simple

* CLILogger.log([
* 'Ph\'nglu',
* 'mglw\'nafh',
* ['Cthulhu', 'green'],
* ['R\'lyeh wgah\'nagl fhtagn', 'red']
* 'Ph\'nglu',
* 'mglw\'nafh',
* ['Cthulhu', 'green'],
* ['R\'lyeh wgah\'nagl fhtagn', 'red']
* ], 'grey');
*
* @param {string|Array} message A text message to log or a list of them.
* @param {string} color Optional. The color of the message (the default is 'white').
* This can be overwritten line by line when the message is an
* array, take a look at the example.
* @param {LoggerMessage} message A text message to log or a list of them.
* @param {string} [color='raw'] Optional. The color of the message (the default is
* the terminal default). This can be overwritten line by
* line when the message is an array, take a look at the
* example.
*/

@@ -124,4 +194,5 @@ log(message, color = 'raw') {

* Prefixes a message with the text sent to the constructor and, if enabled, the current time.
*
* @param {string} text The text that needs the prefix.
* @return {string}
* @returns {string}
*/

@@ -152,9 +223,44 @@ prefix(text) {

/**
* Logs a success (green) message or messages on the console.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log}
*/
success(message) {
this.log(message, 'green');
}
/**
* Logs a warning (yellow) message or messages on the console.
*
* @param {LoggerMessage} message A single message of a list of them.
* @see {@link Logger#log}
*/
warning(message) {
this.log(message, 'yellow');
}
/**
* The prefix to include in front of all the messages.
*
* @type {string}
*/
get messagesPrefix() {
return this._messagesPrefix;
}
/**
* Whether or not to show the time on each message.
*
* @type {boolean}
*/
get showTime() {
return this._showTime;
}
/**
* Gets a function to modify the color of a string. The reason for this _"proxy method"_ is that
* the `colors` module doesn't have a `raw` option and the alternative would've been adding a few
* `if`s on the `log` method.
*
* @param {string} name The name of the color.
* @return {Function} A function that receives a string and returns it colored.
* @returns {Function} A function that receives a string and returns it colored.
* @access protected
* @ignore
* @access protected
*/

@@ -166,61 +272,54 @@ _color(name) {

/**
* Generates a `Provider` with an already defined message prefix and time setting.
* @example
* // Generate the provider
* const provider = loggerWithOptions('my-prefix', true);
* // Register it on the container
* container.register(provider);
* // Getting access to the service instance
* const logger = container.get('logger');
* @param {string} [messagesPrefix] A prefix to include in front of all the messages.
* @param {boolean} [showTime] Whether or not to show the time on each message.
* @return {Provider}
* The service provider to register an instance of {@link Logger} on the container.
*
* @type {ProviderCreatorWithOptions<LoggerProviderOptions>}
* @tutorial logger
*/
const loggerWithOptions = (messagesPrefix, showTime) => provider((app) => {
app.set('logger', () => new Logger(messagesPrefix, showTime));
const logger = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'logger', () => new Logger(
options.messagesPrefix,
options.showTime,
));
});
/**
* The service provider that once registered on the app container will set an instance of
* `Logger` as the `logger` service.
* @example
* // Register it on the container
* container.register(logger);
* // Getting access to the service instance
* const logger = container.get('logger');
* @type {Provider}
* The service provider to register an instance of {@link Logger} with the package name as
* messages prefix on the container.
*
* @type {ProviderCreatorWithOptions<AppLoggerProviderOptions>}
* @tutorial logger
*/
const logger = loggerWithOptions();
/**
* Generates a `Provider` with an already defined time setting and that uses the `packageInfo`
* service in order to retrieve the name of the project and use it as messages prefix.
* @param {boolean} [showTime] Whether or not to show the time on each message.
* @return {Provider}
*/
const appLoggerWithOptions = (showTime) => provider((app) => {
app.set('appLogger', () => {
const packageInfo = app.get('packageInfo');
const prefix = packageInfo.nameForCLI || packageInfo.name;
return new Logger(prefix, showTime);
const appLogger = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'appLogger', () => {
/**
* @type {AppLoggerProviderOptions}
* @ignore
*/
const useOptions = deepAssign(
{
services: {
packageInfo: 'packageInfo',
},
},
options,
);
const { packageInfo } = useOptions.services;
/**
* @type {PackageInfo}
* @ignore
*/
const usePackageInfo = typeof packageInfo === 'string' ?
app.get(packageInfo) :
packageInfo;
const prefix = usePackageInfo.nameForCLI || usePackageInfo.name;
return new Logger(
prefix,
useOptions.showTime,
);
});
});
/**
* The service provider that once registered on the app container will set an instance of
* `Logger` as the `appLogger` service. The difference with the regular `logger` is that this one
* uses the `packageInfo` service in order to retrieve the name of the project and use it as
* messages prefix.
* @example
* // Register it on the container
* container.register(appLogger);
* // Getting access to the service instance
* const appLogger = container.get('appLogger');
* @type {Provider}
*/
const appLogger = appLoggerWithOptions();
module.exports = {
Logger,
loggerWithOptions,
logger,
appLoggerWithOptions,
appLogger,
};
module.exports.Logger = Logger;
module.exports.logger = logger;
module.exports.appLogger = appLogger;
const fs = require('fs-extra');
const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssign } = require('../shared/deepAssign');
/**
* Returns the contents of the project `package.json`.
* @module node/packageInfo
*/
/**
* @typedef {import('./pathUtils').PathUtils} PathUtils
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} PackageInfoServiceMap
* @property {string|PathUtils} [pathUtils]
* The name of the service for {@link PathUtils} or an instance of it. `pathUtils` by default.
* @parent module:node/packageInfo
*/
/**
* @typedef {Object} PackageInfoProviderOptions
* @property {string} serviceName
* The name that will be used to register the result of
* {@link module:node/packageInfo~packageInfo|packageInfo}. Its default value is `packageInfo`.
* @property {PackageInfoServiceMap} services
* A dictionary with the services that need to be injected on the function.
* @parent module:node/packageInfo
*/
/**
* Gets the contents of the implementation's `package.json`.
*
* @param {PathUtils} pathUtils To build the path to the `package.json`.
* @return {Object}
* @returns {Object.<string,*>}
* @tutorial packageInfo
* @todo This should be `async`, or at least have an async alternative.
*/
const packageInfo = (pathUtils) => fs.readJsonSync(pathUtils.join('package.json'));
/**
* The service provider that once registered on the app container will set the result of
* `packageInfo()` as the `packageInfo` service.
* @example
* // Register it on the container
* container.register(packageInfoProvider);
* // Getting access to the service value
* const packageInfo = container.get('packageInfo');
* @type {Provider}
* {@link module:node/packageInfo~packageInfo|packageInfo} as a service.
*
* @type {ProviderCreatorWithOptions<PackageInfoProviderOptions>}
* @tutorial packageInfo
*/
const packageInfoProvider = provider((app) => {
app.set('packageInfo', () => packageInfo(app.get('pathUtils')));
const packageInfoProvider = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'packageInfo', () => {
/**
* @type {PackageInfoProviderOptions}
* @ignore
*/
const useOptions = deepAssign(
{
services: {
pathUtils: 'pathUtils',
},
},
options,
);
const { pathUtils } = useOptions.services;
const usePathUtils = typeof pathUtils === 'string' ?
app.get(pathUtils) :
pathUtils;
return packageInfo(usePathUtils);
});
});
module.exports = {
packageInfo,
packageInfoProvider,
};
module.exports.packageInfo = packageInfo;
module.exports.packageInfoProvider = packageInfoProvider;
const path = require('path');
const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
/**
* @module node/pathUtils
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} PathUtilsProviderOptions
* @property {string} serviceName The name that will be used to register an instance
* of {@link PathUtils}. Its default value is `pathUtils`.
* @property {string} [home] The path to the new home location.
* @parent module:node/pathUtils
*/
/**
* A utility services to manage paths on a project. It allows for path building relatives to
* the project root or from where the app executable is located.
*
* @parent module:node/pathUtils
* @tutorial pathUtils
*/
class PathUtils {
/**
* Class constructor.
* @param {string} [home=''] The location of the project's `home`(root) directory. By default

@@ -16,10 +36,16 @@ * it uses `process.cwd()`.

* The root path from where the app is being executed.
*
* @type {string}
* @access protected
* @ignore
*/
this.path = process.cwd();
this._path = process.cwd();
/**
* A dictionary of different path locations.
* @type {Object}
*
* @type {Object.<string,string>}
* @access protected
* @ignore
*/
this.locations = {};
this._locations = {};

@@ -30,3 +56,4 @@ this._addAppLocation();

/**
* Add a new location.
* Adds a new location.
*
* @param {string} name The reference name.

@@ -53,5 +80,6 @@ * @param {string} locationPath The path of the location. It must be inside the path from the

/**
* Get a location path by its name.
* Gets a location path by its name.
*
* @param {string} name The location name.
* @return {string}
* @returns {string}
* @throws {Error} If there location hasn't been added.

@@ -68,7 +96,17 @@ */

/**
* Build a path using a location path as base.
* @param {string} location The location name.
* @param {Array} paths The rest of the path components to join.
* @return {string}
* Alias to `joinFrom` that uses the `home` location by default.
*
* @param {...string} paths The rest of the path components to join.
* @returns {string}
*/
join(...paths) {
return this.joinFrom('home', ...paths);
}
/**
* Builds a path using a location path as base.
*
* @param {string} location The location name.
* @param {...string} paths The rest of the path components to join.
* @returns {string}
*/
joinFrom(location, ...paths) {

@@ -79,12 +117,13 @@ const locationPath = this.getLocation(location);

/**
* Alias to `joinFrom` that uses the `home` location by default.
* @param {Array} paths The rest of the path components to join.
* @return {string}
* The path to the directory where the app executable is located.
*
* @type {string}
*/
join(...paths) {
return this.joinFrom('home', ...paths);
get app() {
return this.getLocation('app');
}
/**
* Get the project root path.
* @return {string}
* The project root path.
*
* @type {string}
*/

@@ -95,12 +134,26 @@ get home() {

/**
* Get the path to the directory where the app executable is located.
* @return {string}
* A dictionary of different path locations.
*
* @type {Object.<string,string>}
* @access protected
* @ignore
*/
get app() {
return this.getLocation('app');
get locations() {
return this._locations;
}
/**
* Find and register the location for the app executable directory.
* The root path from where the app is being executed.
*
* @type {string}
* @access protected
* @ignore
*/
get path() {
return this._path;
}
/**
* Finds and register the location for the app executable directory.
*
* @access protected
* @ignore
*/

@@ -119,33 +172,14 @@ _addAppLocation() {

}
/**
* Generates a `Provider` with an already defined `home` location.
* @example
* // Generate the provider
* const provider = pathUtilsWithHome('my-path');
* // Register it on the container
* container.register(provider);
* // Getting access to the service instance
* const pathUtils = container.get('pathUtils');
* @param {string} [home] The path to the new home location.
* @return {Provider}
* The service provider to register an instance of {@link PathUtils} on the container.
*
* @type {ProviderCreatorWithOptions<PathUtilsProviderOptions>}
* @tutorial pathUtils
*/
const pathUtilsWithHome = (home) => provider((app) => {
app.set('pathUtils', () => new PathUtils(home));
const pathUtils = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'pathUtils', () => new PathUtils(options.home));
});
/**
* The service provider that once registered on the app container will set an instance of
* `PathUtils` as the `pathUtils` service.
* @example
* // Register it on the container
* container.register(pathUtils);
* // Getting access to the service instance
* const pathUtils = container.get('pathUtils');
* @type {Provider}
*/
const pathUtils = pathUtilsWithHome();
module.exports = {
PathUtils,
pathUtils,
pathUtilsWithHome,
};
module.exports.PathUtils = PathUtils;
module.exports.pathUtils = pathUtils;
const { appConfiguration } = require('./appConfiguration');
const { environmentUtils } = require('./environmentUtils');
const { errorHandlerWithOptions, errorHandler } = require('./errorHandler');
const {
loggerWithOptions,
logger,
appLoggerWithOptions,
appLogger,
} = require('./logger');
const { errorHandler } = require('./errorHandler');
const { logger, appLogger } = require('./logger');
const { packageInfoProvider } = require('./packageInfo');
const { pathUtils, pathUtilsWithHome } = require('./pathUtils');
const { pathUtils } = require('./pathUtils');
const { rootRequireProvider } = require('./rootRequire');
module.exports = {
appConfiguration,
environmentUtils,
errorHandlerWithOptions,
errorHandler,
loggerWithOptions,
logger,
appLoggerWithOptions,
appLogger,
packageInfo: packageInfoProvider,
pathUtils,
pathUtilsWithHome,
rootRequire: rootRequireProvider,
};
module.exports.appConfiguration = appConfiguration;
module.exports.environmentUtils = environmentUtils;
module.exports.errorHandler = errorHandler;
module.exports.logger = logger;
module.exports.appLogger = appLogger;
module.exports.packageInfo = packageInfoProvider;
module.exports.pathUtils = pathUtils;
module.exports.rootRequire = rootRequireProvider;

@@ -1,6 +0,50 @@

const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssign } = require('../shared/deepAssign');
/**
* @module node/rootRequire
*/
/**
* @typedef {import('./pathUtils').PathUtils} PathUtils
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/
/**
* @typedef {Object} RootRequireServiceMap
* @property {string|PathUtils} [pathUtils]
* The name of the service for {@link PathUtils} or an instance of it. `pathUtils` by default.
* @parent module:node/rootRequire
*/
/**
* @typedef {Object} RootRequireProviderOptions
* @property {string} serviceName
* The name that will be used to register the result of
* {@link module:node/rootRequire~rootRequire|rootRequire}. Its default value is `rootRequire`.
* @property {RootRequireServiceMap} services
* A dictionary with the services that need to be injected on the function.
* @parent module:node/rootRequire
*/
/**
* Exactly like `require`, but th epath is relative to the project root directory.
*
* @callback RootRequireFn
* @param {string} path The path to the file, relative to the project root directory.
* @returns {Object}
* @parent module:node/rootRequire
* @tutorial rootRequire
*/
/**
* Generates a function to require a file relative to the project root directory.
*
* @param {PathUtils} pathUtils To build the path to the files it will `require`.
* @return {Function(string):*}
* @returns {RootRequireFn}
* @tutorial rootRequire
*/

@@ -13,17 +57,32 @@ const rootRequire = (pathUtils) => (path) =>

* The service provider that once registered on the app container will set the result of
* `rootRequire(pathUtils)` as the `rootRequire` service.
* @example
* // Register it on the container
* container.register(rootRequireProvider);
* // Getting access to the service instance
* const rootRequire = container.get('rootRequire');
* @type {Provider}
* {@link module:node/rootRequire~rootRequire|rootRequire} as a service.
*
* @type {ProviderCreatorWithOptions<RootRequireProviderOptions>}
* @tutorial rootRequire
*/
const rootRequireProvider = provider((app) => {
app.set('rootRequire', () => rootRequire(app.get('pathUtils')));
const rootRequireProvider = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'rootRequire', () => {
/**
* @type {RootRequireProviderOptions}
* @ignore
*/
const useOptions = deepAssign(
{
services: {
pathUtils: 'pathUtils',
},
},
options,
);
const { pathUtils } = useOptions.services;
const usePathUtils = typeof pathUtils === 'string' ?
app.get(pathUtils) :
pathUtils;
return rootRequire(usePathUtils);
});
});
module.exports = {
rootRequire,
rootRequireProvider,
};
module.exports.rootRequire = rootRequire;
module.exports.rootRequireProvider = rootRequireProvider;

@@ -5,3 +5,3 @@ {

"homepage": "https://homer0.github.io/wootils/",
"version": "3.0.4",
"version": "4.0.0",
"repository": "homer0/wootils",

@@ -11,3 +11,2 @@ "author": "Leonardo Apiwan (@homer0) <me@homer0.com>",

"dependencies": {
"jimple": "^1.5.0",
"fs-extra": "^9.0.1",

@@ -20,16 +19,20 @@ "colors": "^1.4.0",

"devDependencies": {
"eslint": "^7.3.1",
"eslint-plugin-homer0": "^4.0.0",
"jest-ex": "^9.0.0",
"jest-cli": "^26.1.0",
"jasmine-expect": "^4.0.3",
"@babel/preset-env": "7.10.3",
"@babel/core": "7.10.3",
"@babel/plugin-transform-runtime": "7.10.3",
"esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0",
"esdoc-node": "1.0.4",
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"5to6-codemod": "1.8.0",
"commitizen": "^4.1.2",
"cz-conventional-changelog": "^3.2.0",
"eslint": "^7.4.0",
"eslint-plugin-homer0": "^5.0.0",
"husky": "^4.2.5",
"jest": "^26.1.0",
"jscodeshift": "0.10.0",
"jsdoc": "^3.6.4",
"jsdoc-ts-utils": "^1.0.0",
"docdash": "homer0/docdash#semver:^2.0.0",
"leasot": "^11.0.0",
"lint-staged": "^10.2.11",
"coveralls": "^3.1.0"
"semantic-release": "^17.1.1"
},

@@ -40,15 +43,25 @@ "engine-strict": true,

},
"husky": {
"hooks": {
"pre-commit": "./utils/hooks/pre-commit",
"post-merge": "./utils/hooks/post-merge",
"prepare-commit-msg": "./utils/hooks/prepare-commit-msg"
}
},
"lint-staged": {
"*.js": "eslint"
},
"main": "src/index.js",
"scripts": {
"hooks": "./utils/hooks/install",
"test": "node ./utils/scripts/test.js",
"prepare": "./utils/scripts/prepare",
"test": "./utils/scripts/test",
"lint": "./utils/scripts/lint",
"lint:full": "./utils/scripts/lint-full",
"predocs": "node ./utils/scripts/docs-index.js",
"lint:all": "./utils/scripts/lint-all",
"docs": "./utils/scripts/docs",
"todo": "./utils/scripts/todo"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
# Wootils
[![Travis](https://img.shields.io/travis/homer0/wootils.svg?style=flat-square)](https://travis-ci.org/homer0/wootils)
[![GitHub Workflow Status (master)](https://img.shields.io/github/workflow/status/homer0/wootils/Test/master?style=flat-square)](https://github.com/homer0/wootils/actions?query=workflow%3ATest)
[![Coveralls github](https://img.shields.io/coveralls/github/homer0/wootils.svg?style=flat-square)](https://coveralls.io/github/homer0/wootils?branch=master)

@@ -12,3 +12,3 @@ [![David](https://img.shields.io/david/homer0/wootils.svg?style=flat-square)](https://david-dm.org/homer0/wootils)

The idea was to take all those small thing I'm always rewriting on every project and move them to a single package so I can not only stop copying & pasting them all over the place but also maintain them all together.
The idea was to take all those small thing I'm always rewriting on every project and move them to a single package so I can, not only stop copying & pasting them all over the place, but also maintain them all together.

@@ -20,10 +20,2 @@ There are two rules I followed when I had to decide what to put and what to keep somewhere else:

## Information
| - | - |
|--------------|--------------------------------------------------------------------|
| Package | wootils |
| Description | A set of Javascript utilities for building Node and browser apps. |
| Node Version | >= v10.0.0 |
## Usage

@@ -94,2 +86,8 @@

#### DeepAssign
Deep merge (and copy) of objects(`{}`) and `Array`s using native spread syntax.
[Read more about DeepAssign](./documents/shared/deepAssign.md)
#### deferred

@@ -113,2 +111,8 @@

#### Jimple Functions
A set of utility functions to generate resources that can be used on Jimple or abstractions created from it (like [Jimpex](https://yarnpkg.com/package/jimpex)).
[Read more about the Jimple Functions](./documents/shared/jimpleFns.md)
#### ObjectUtils

@@ -128,34 +132,67 @@

## Development
## ES Modules
Before doing anything, install the repository hooks:
All files are written using commonjs, as I targeted the oldest Node LTS and it doesn't support modules (without a flag) yet, but you can use it with ESM.
```bash
# You can either use npm or yarn, it doesn't matter
yarn run hooks
When the package gets published, an ESM version is generated on the path `/esm`. If you are using the latest version of Node, or a module bundler (like [projext](https://projextjs.com) :D), instead of requiring from the package's root path, you should do it from the `/esm` sub path:
```js
// commonjs
const ObjectUtils = require('wootils/shared/objectUtils');
// ESM
import ObjectUtils from 'wootils/esm/shared/objectUtils';
```
Since the next LTS to become "the oldest" is 12, which still uses the flag, I still have no plans on going with ESM by default.
## Development
### NPM/Yarn Tasks
| Task | Description |
|--------------------------|-------------------------------------|
| `yarn run hooks` | Install the GIT repository hooks. |
| `yarn test` | Run the project unit tests. |
| `yarn run lint` | Lint the modified files. |
| `yarn run lint:full` | Lint the project code. |
| `yarn run docs` | Generate the project documentation. |
| `yarn run todo` | List all the pending to-do's. |
| Task | Description |
|------------|--------------------------------------|
| `docs` | Generates the project documentation. |
| `lint` | Lints the staged files. |
| `lint:all` | Lints the entire project code. |
| `prepare` | Generates the project ESM version. |
| `test` | Runs the project unit tests. |
| `todo` | Lists all the pending to-do's. |
### Repository hooks
I use [`husky`](https://yarnpkg.com/package/husky) to automatically install the repository hooks so the code will be tested and linted before any commit and the dependencies updated after every merge.
The configuration is on the `husky` property of the `package.json` and the hooks' files are on `./utils/hooks`.
#### Commits convention
I use [conventional commits](https://www.conventionalcommits.org) with [`commitizen`](https://yarnpkg.com/package/commitizen) in order to support semantic releases. The one that sets it up is actually husky, that installs a script that runs commitizen on the `git commit` command.
The hook for this is on `./utils/hooks/prepare-commit-msg` and the configuration for comitizen is on the `config.commitizen` property of the `package.json`.
### Releases
I use [`semantic-release`](https://yarnpkg.com/package/semantic-release) and a GitHub action to automatically release on NPM everything that gets merged to master.
The configuration for `semantic-release` is on `./releaserc` and the workflow for the release is on `./.github/workflow/release.yml`.
### Testing
I use [Jest](https://facebook.github.io/jest/) with [Jest-Ex](https://yarnpkg.com/en/package/jest-ex) to test the project. The configuration file is on `./.jestrc`, the tests and mocks are on `./tests` and the script that runs it is on `./utils/scripts/test`.
I use [Jest](https://facebook.github.io/jest/) to test the project.
The configuration file is on `./.jestrc.json`, the tests are on `./tests` and the script that runs it is on `./utils/scripts/test`.
### Linting
I use [ESlint](http://eslint.org) to validate all our JS code. The configuration file for the project code is on `./.eslintrc` and for the tests on `./tests/.eslintrc` (which inherits from the one on the root), there's also an `./.eslintignore` to ignore some files on the process, and the script that runs it is on `./utils/scripts/lint`.
I use [ESlint](https://eslint.org) with [my own custom configuration](https://yarnpkg.com/package/eslint-plugin-homer0) to validate all the JS code and the JSDoc comments.
The configuration file for the project code is on `./.eslintrc` and the one for the tests is on `./tests/.eslintrc`, and there's also an `./.eslintignore` to exclude some files on the process. The script that runs it is on `./utils/scripts/lint`.
### Documentation
I use [ESDoc](http://esdoc.org) to generate HTML documentation for the project. The configuration file is on `./.esdocrc` and the script that runs it is on `./utils/scripts/docs`.
I use [JSDoc](https://jsdoc.app) to generate an HTML documentation site for the project.
The configuration file is on `./.jsdoc.js` and the script that runs it is on `./utils/scripts/docs`.
### To-Dos

@@ -167,4 +204,2 @@

You can work with this project on Windows, but it only works if you use [Yarn](https://yarnpkg.com/en/docs/install). The reason is that NPM on Windows doesn't allow you to use paths like `./scripts/something` on the `package.json` scripts, while Yarn does.
Another alternative if you are using Windows is to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10).
This project uses bash scripts for development, so if you want to develop on Windows, you need to do it with [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10).

@@ -5,28 +5,63 @@ const statuses = require('statuses');

/**
* @typedef {Function:(string,Object):Promise<Object,Error>} FetchClient
* @module shared/apiClient
*/
/**
* @typedef {Object} FetchOptions
* @property {string} method The request method.
* @property {Object} headers The request headers.
* @property {string} body The request body.
* @property {boolean} json Whether or not the response should _"JSON decoded"_.
* This kind of dictionary is used for building stuff like query string parameters and headers.
*
* @typedef {Object.<string,(string|number)>} APIClientParametersDictionary
* @parent module:shared/apiClient
*/
/**
* @typedef {Object} APIClientFetchOptions
* @property {string} [method] The request method.
* @property {APIClientParametersDictionary} [headers] The request headers.
* @property {string} [body] The request body.
* @property {boolean} [json ] Whether or not the response should _"JSON
* decoded"_.
* @parent module:shared/apiClient
*/
/**
* @callback APIClientFetchClient
* @param {string} url The request URL.
* @param {APIClientFetchOptions} [options] The request options.
* @returns {Promise<Response>}
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
* @parent module:shared/apiClient
*/
/**
* @typedef {APIClientFetchOptions & APIClientRequestOptionsProperties} APIClientRequestOptions
* @parent module:shared/apiClient
*/
/**
* @typedef {Object} APIClientRequestOptionsProperties
* @property {string} url The request URL.
* @augments APIClientFetchOptions
* @parent module:shared/apiClient
*/
/**
* @typedef {Object} APIClientEndpoint
* @property {String} path The path to the endpoint relative to the API entry point. It can
* include placeholders with the format `:placeholder-name` that are
* going to be replaced when the endpoint gets generated.
* @property {?Object} query A dictionary of query string parameters that will be added when the
* endpoint. If the value of a parameter is `null`, it won't be added.
* @property {string} path The path to the endpoint relative to the API
* entry point. It can include placeholders
* with the format `:placeholder-name` that are
* going to be replaced when the endpoint gets
* generated.
* @property {?APIClientParametersDictionary} query A dictionary of query string parameters that
* will be added when the endpoint. If the value
* of a parameter is `null`, it won't be added.
* @parent module:shared/apiClient
*/
/**
* @typedef {Object} APIClientEndpoints
* @property {string|APIClientEndpoints|APIClientEndpoint} [endpointName] A name for the endpoint
* that will be used to
* reference it on the
* `endpoint(...)` method.
* @typedef {(string|APIClientEndpoint)} APIClientEndpointValue
* @parent module:shared/apiClient
*/
/**
* @typedef {Object.<string,APIClientEndpointValue>} APIClientEndpoints
* @example

@@ -54,2 +89,4 @@ * {

* }
*
* @parent module:shared/apiClient
*/

@@ -59,13 +96,15 @@

* An API client with configurable endpoints.
*
* @parent module:shared/apiClient
* @tutorial APIClient
*/
class APIClient {
/**
* Class constructor.
* @param {String} url The API entry point.
* @param {APIClientEndpoints} endpoints A dictionary of named endpoints relative
* to the API entry point.
* @param {FetchClient} fetchClient The fetch function that makes the
* requests.
* @param {Object} [defaultHeaders={}] A dictionary of default headers to
* include on every request.
* @param {string} url The API entry point.
* @param {APIClientEndpoints} endpoints A dictionary of named endpoints
* relative to the API entry point.
* @param {APIClientFetchClient} fetchClient The fetch function that makes the
* requests.
* @param {APIClientParametersDictionary} [defaultHeaders={}] A dictionary of default headers
* to include on every request.
*/

@@ -75,135 +114,71 @@ constructor(url, endpoints, fetchClient, defaultHeaders = {}) {

* The API entry point.
* @type {String}
*
* @type {string}
* @access protected
* @ignore
*/
this.url = url;
this._url = url;
/**
* A dictionary of named endpoints relative to the API entry point.
* @type {Object}
* @property {string|APIClientEndpoint} [endpointName] The name of the endpoint.
*
* @type {Object.<string,(string|APIClientEndpoint)>}
* @access protected
* @ignore
*/
this.endpoints = this.flattenEndpoints(endpoints);
this._endpoints = ObjectUtils.flat(
endpoints,
'.',
'',
(ignore, value) => typeof value.path === 'undefined',
);
/**
* The fetch function that makes the requests.
* @type {FetchClient}
*
* @type {APIClientFetchClient}
* @access protected
* @ignore
*/
this.fetchClient = fetchClient;
this._fetchClient = fetchClient;
/**
* A dictionary of default headers to include on every request.
* @type {Object}
*
* @type {APIClientParametersDictionary}
* @access protected
* @ignore
*/
this.defaultHeaders = defaultHeaders;
this._defaultHeaders = defaultHeaders;
/**
* An authorization token to include on the requests.
* @type {String}
*
* @type {string}
* @access protected
* @ignore
*/
this.authorizationToken = '';
this._authorizationToken = '';
}
/**
* Takes a dictionary of endpoints and flatten them on a single level.
* The method just calls {@link ObjectUtils.flat}.
* @param {APIClientEndpoints} endpoints A dictionary of named endpoints.
* @return {Object}
*/
flattenEndpoints(endpoints) {
return ObjectUtils.flat(
endpoints,
'.',
'',
(ignore, value) => typeof value.path === 'undefined'
);
}
/**
* Sets a bearer token for all the requests.
* @param {String} [token=''] The new authorization token. If the value is empty, it will remove
* any token previously saved.
*/
setAuthorizationToken(token = '') {
this.authorizationToken = token;
}
/**
* Sets the default headers for the requests.
* @param {Object} [headers={}] The new default headers.
* @param {Boolean} [overwrite=true] If `false`, it will merge the new default headers with
* the current ones.
*/
setDefaultHeaders(headers = {}, overwrite = true) {
this.defaultHeaders = Object.assign(
{},
(overwrite ? {} : this.defaultHeaders),
headers
);
}
/**
* Makes a `GET` request.
* @param {String} url The request URL.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*/
get(url, options = {}) {
return this.fetch(Object.assign({ url }, options));
}
/**
* Makes a `HEAD` request.
* @param {String} url The request URL.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*/
head(url, options = {}) {
return this.get(url, Object.assign({}, options, { method: 'head' }));
}
/**
* Makes a `POST` request.
* @param {String} url The request URL.
* @param {Object} body The request body.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*/
post(url, body, options = {}) {
return this.fetch(Object.assign({
url,
body,
method: 'post',
}, options));
}
/**
* Makes a `PUT` request.
* @param {String} url The request URL.
* @param {Object} body The request body.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*/
put(url, body, options = {}) {
return this.post(url, body, Object.assign({}, options, { method: 'put' }));
}
/**
* Makes a `PATCH` request.
* @param {String} url The request URL.
* @param {Object} body The request body.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*/
patch(url, body, options = {}) {
return this.post(url, body, Object.assign({}, options, { method: 'patch' }));
}
/**
* Makes a `DELETE` request.
* @param {String} url The request URL.
* @param {Object} body The request body.
* @param {FetchOptions} [options={}] The request options.
* @return {Promise<Object,Error>}
*
* @param {string} url The request URL.
* @param {Object} body The request body.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
delete(url, body = {}, options = {}) {
return this.post(url, body, Object.assign({}, options, { method: 'delete' }));
return this.post(url, body, { method: 'delete', ...options });
}
/**
* Generates an endpoint URL.
* @param {String} name The name of the endpoint on the `endpoints` property.
* @param {Object} [parameters={}] A dictionary of values that will replace placeholders on the
* endpoint definition.
* @return {String}
*
* @param {string} name The name of the endpoint on the
* `endpoints` property.
* @param {APIClientParametersDictionary} [parameters={}] A dictionary of values that will
* replace placeholders on the endpoint
* definition.
* @throws {Error} If the endpoint doesn't exist on the `endpoints` property.
* @returns {string}
*/
endpoint(name, parameters = {}) {
// Get the endpoint information.
const info = this.endpoints[name];
const info = this._endpoints[name];
// Validate that the endpoint exists.

@@ -214,5 +189,5 @@ if (!info) {

// Get a new reference for the parameters.
const params = Object.assign({}, parameters);
const params = { ...parameters };
// If the endpoint is a string, format it into an object with `path`.
const endpoint = typeof info === 'string' ? { path: info } : info;
const endpoint = typeof info === 'string' ? { path: info, query: null } : info;
// Define the object that will have the query string.

@@ -249,3 +224,3 @@ const query = {};

// ...replace the placeholder with the value.
path = path.replace(placeholder, value);
path = path.replace(placeholder, `${value}`);
} else {

@@ -257,3 +232,3 @@ // ...otherwise, add it on the query string.

// Convert the URL into a `urijs` object.
const uri = urijs(`${this.url}/${path}`);
const uri = urijs(`${this._url}/${path}`);
// Loop and add all the query string parameters.

@@ -267,30 +242,22 @@ Object.keys(query).forEach((queryName) => {

/**
* Generates a dictionary of headers using the service `defaultHeaders` property as base.
* If a token was set using `setAuthorizationToken`, the method will add an `Authorization`
* header for the bearer token.
* @param {Object} [overwrites={}] Extra headers to add.
* @return {Object}
* Formats an error response into a proper Error object. This method should proabably be
* overwritten to accomodate the error messages for the API it's being used for.
*
* @param {Object} response A received response from a request.
* @param {?string} response.error An error message received on the response.
* @param {number} status The HTTP status of the response.
* @returns {Error}
*/
headers(overwrites = {}) {
const headers = Object.assign({}, this.defaultHeaders);
if (this.authorizationToken) {
headers.Authorization = `Bearer ${this.authorizationToken}`;
}
return Object.assign({}, headers, overwrites);
error(response, status) {
return new Error(`[${status}]: ${response.error}`);
}
/**
* Makes a request.
* @param {Object} options The request options.
* @param {string} options.url The request URL.
* @param {string} options.method The request method. `GET` by default.
* @param {Object} options.body A request body to send.
* @param {Object} options.headers The request headers.
* @param {boolean} options.json Whether or not the response should _"JSON decoded"_. `true`
* by default.
* @return {Promise<Object,Error>}
*
* @param {APIClientRequestOptions} options The request options.
* @returns {Promise<Response>}
*/
fetch(options) {
// Get a new reference of the request options.
const opts = Object.assign({}, options);
const opts = { ...options };
// Format the request method and check if it should use the default.

@@ -315,3 +282,3 @@ opts.method = opts.method ? opts.method.toUpperCase() : 'GET';

if (opts.headers) {
hasContentType = Object.keys(opts.headers)
hasContentType = !!Object.keys(opts.headers)
.find((name) => name.toLowerCase() === 'content-type');

@@ -337,3 +304,3 @@ } else {

// Make the request.
return this.fetchClient(url, opts)
return this._fetchClient(url, opts)
.then((response) => {

@@ -348,2 +315,4 @@ // Capture the response status.

* fails, it will return an empty object.
*
* @ignore
*/

@@ -369,11 +338,140 @@ nextStep = response.json().catch(() => ({}));

/**
* Formats an error response into a proper Error object.
* @param {Object} response A received response from a request.
* @return {Error}
* Makes a `GET` request.
*
* @param {string} url The request URL.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
error(response) {
return new Error(response.error);
get(url, options = {}) {
return this.fetch({ url, ...options });
}
/**
* Makes a `HEAD` request.
*
* @param {string} url The request URL.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
head(url, options = {}) {
return this.get(url, { ...options, method: 'head' });
}
/**
* Generates a dictionary of headers using the service `defaultHeaders` property as base.
* If a token was set using `setAuthorizationToken`, the method will add an `Authorization`
* header for the bearer token.
*
* @param {Object.<string,(string|number)>} [overwrites={}] Extra headers to add.
* @returns {Object.<string,(string|number)>}
*/
headers(overwrites = {}) {
const headers = { ...this._defaultHeaders };
if (this._authorizationToken) {
headers.Authorization = `Bearer ${this._authorizationToken}`;
}
return { ...headers, ...overwrites };
}
/**
* Makes a `PATCH` request.
*
* @param {string} url The request URL.
* @param {Object} body The request body.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
patch(url, body, options = {}) {
return this.post(url, body, { method: 'patch', ...options });
}
/**
* Makes a `POST` request.
*
* @param {string} url The request URL.
* @param {Object} body The request body.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
post(url, body, options = {}) {
return this.fetch({
url,
body,
method: 'post',
...options,
});
}
/**
* Makes a `PUT` request.
*
* @param {string} url The request URL.
* @param {Object} body The request body.
* @param {APIClientFetchOptions} [options={}] The request options.
* @returns {Promise<Response>}
*/
put(url, body, options = {}) {
return this.post(url, body, { method: 'put', ...options });
}
/**
* Sets a bearer token for all the requests.
*
* @param {string} [token=''] The new authorization token. If the value is empty, it will remove
* any token previously saved.
*/
setAuthorizationToken(token = '') {
this._authorizationToken = token;
}
/**
* Sets the default headers for the requests.
*
* @param {APIClientParametersDictionary} [headers={}] The new default headers.
* @param {boolean} [overwrite=true] If `false`, it will merge the new
* default headers with the current
* ones.
*/
setDefaultHeaders(headers = {}, overwrite = true) {
this._defaultHeaders = {
...(overwrite ? {} : this._defaultHeaders),
...headers,
};
}
/**
* An authorization token to include on the requests.
*
* @type {string}
*/
get authorizationToken() {
return this._authorizationToken;
}
/**
* A dictionary of default headers to include on every request.
*
* @type {APIClientParametersDictionary}
*/
get defaultHeaders() {
return { ...this._defaultHeaders };
}
/**
* A dictionary of named endpoints relative to the API entry point.
*
* @type {Object.<string,(string|APIClientEndpoint)>}
*/
get endpoints() {
return { ...this._endpoints };
}
/**
* The fetch function that makes the requests.
*
* @type {APIClientFetchClient}
*/
get fetchClient() {
return this._fetchClient;
}
/**
* The API entry point.
*
* @type {string}
*/
get url() {
return this._url;
}
}
module.exports = APIClient;
/**
* Create a deferred promise using a native Promise.
* @return {Object}
* @module shared/deferred
*/
/**
* @typedef {Object} DeferredPromise
* @property {Promise} promise The deferred promise.
* @property {Function} resolve The functon to resolve the promise.
* @property {Function} reject The functon to reject the promise.
* @property {Function} resolve The function to resolve the promise.
* @property {Function} reject The function to reject the promise.
* @parent module:shared/deferred
* @tutorial deferred
*/
/**
* Creates a deferred promise using a native {@link Promise}.
*
* @returns {DeferredPromise}
* @parent module:shared/deferred
* @tutorial deferred
*/
const deferred = () => {

@@ -9,0 +22,0 @@ let resolve;

/**
* @module shared/eventsHub
*/
/**
* When there's a one time subscription, a wrapper function is created with a special property
* to identify it and remove it once it gets triggered. The wrapper and the original function
* are stored in case `off` is called before the wrapper gets triggered; it will receive the
* original function, not the wrapper, so the class needs a way to map them together.
*
* @typedef {Object} EventsHubWrapperInfo
* @property {Function} wrapper The wrapper function that was created for the subscription.
* @property {Function} original The original listener that was sent.
* @ignore
*/
/**
* @callback EventsHubOnceWrapper
* @param {...*} args The parameters for the original listener.
* @returns {*}
* @property {boolean} [once=true] A flag so the class will identify the wrapper.
* @ignore
*/
/**
* A minimal implementation of an events handler service.
*
* @parent module:shared/eventsHub
* @tutorial eventsHub
*/
class EventsHub {
/**
* Class constructor.
* @ignore
*/
constructor() {
/**
* A dictionary of the events and their listeners.
* @type {Object}
*
* @type {Object.<string,Function[]>}
* @access protected
* @ignore
*/
this._events = {};
/**
* A dictionary of wrappers that were created for "one time subscriptions". This is used
* by the {@link EventsHub#off}: if it doesn't find the subscriber as it is, it will look
* for a wrapper and remove it.
*
* @type {Object.<string,EventsHubWrapperInfo[]>}
* @access protected
* @ignore
*/
this._onceWrappers = {};
}
/**
* Adds a new event listener.
* @param {String|Array} event An event name or a list of them.
* @param {Function} fn The listener function.
* @return {Function} An unsubscribe function to remove the listener or listeners.
* Emits an event and call all its listeners.
*
* @param {string|string[]} event An event name or a list of them.
* @param {...*} args A list of parameters to send to the listeners.
*/
on(event, fn) {
emit(event, ...args) {
const toClean = [];
const events = Array.isArray(event) ? event : [event];
events.forEach((name) => {
const subscribers = this.subscribers(name);
if (!subscribers.includes(fn)) {
subscribers.push(fn);
}
this.subscribers(name).forEach((subscriber) => {
subscriber(...args);
if (subscriber.once) {
toClean.push({
event: name,
fn: subscriber,
});
}
});
});
return () => this.off(event, fn);
toClean.forEach((info) => this.off(info.event, info.fn));
}
/**
* Adds an event listener that will only be executed once.
* @param {String|Array} event An event name or a list of them.
* @param {Function} fn The listener function.
* @return {Function} An unsubscribe function to remove the listener.
*/
once(event, fn) {
// eslint-disable-next-line no-param-reassign
fn.once = true;
return this.on(event, fn);
}
/**
* Removes an event listener.
* @param {String|Array} event An event name or a list of them.
* @param {Function} fn The listener function.
* @return {Boolean|Array} If `event` was a `string`, it will return whether or not the listener
* was found and removed; but if `event` was an `Array`, it will return
* a list of boolean values.
*
* @param {string|string[]} event An event name or a list of them.
* @param {Function} fn The listener function.
* @returns {boolean|boolean[]} If `event` was a `string`, it will return whether or not the
* listener was found and removed; but if `event` was an `Array`, it
* will return a list of boolean values.
*/

@@ -58,7 +89,32 @@ off(event, fn) {

const subscribers = this.subscribers(name);
const onceSubscribers = this._onceWrappers[name];
let found = false;
const index = subscribers.indexOf(fn);
let index = subscribers.indexOf(fn);
if (index > -1) {
found = true;
/**
* If the listener had the `once` flag, then it's a wrapper, so it needs to remove it
* from the wrappers list too.
*
* @ignore
*/
if (fn.once && onceSubscribers) {
const wrapperIndex = onceSubscribers.findIndex((item) => item.wrapper === fn);
onceSubscribers.splice(wrapperIndex, 1);
}
subscribers.splice(index, 1);
} else if (this._onceWrappers[name]) {
/**
* If it couldn't found the subscriber, maybe it's because it's the original listener
* of a wrapper.
*
* @ignore
*/
index = onceSubscribers.findIndex((item) => item.original === fn);
if (index > -1) {
found = true;
const originalIndex = subscribers.indexOf(onceSubscribers[index].original);
subscribers.splice(originalIndex, 1);
onceSubscribers.splice(index, 1);
}
}

@@ -72,30 +128,80 @@

/**
* Emits an event and call all its listeners.
* @param {String|Array} event An event name or a list of them.
* @param {Array} args A list of parameters to send to the listeners.
* Adds a new event listener.
*
* @param {string|string[]} event An event name or a list of them.
* @param {Function} fn The listener function.
* @returns {Function} An unsubscribe function to remove the listener or listeners.
*/
emit(event, ...args) {
const toClean = [];
on(event, fn) {
const events = Array.isArray(event) ? event : [event];
events.forEach((name) => {
this.subscribers(name).forEach((subscriber) => {
subscriber(...args);
if (subscriber.once) {
toClean.push({
event: name,
fn: subscriber,
});
const subscribers = this.subscribers(name);
if (!subscribers.includes(fn)) {
subscribers.push(fn);
}
});
return () => this.off(event, fn);
}
/**
* Adds an event listener that will only be executed once.
*
* @param {string|string[]} event An event name or a list of them.
* @param {Function} fn The listener function.
* @returns {Function} An unsubscribe function to remove the listener.
*/
once(event, fn) {
const events = Array.isArray(event) ? event : [event];
// Try to find an existing wrapper.
let wrapper = events.reduce(
(acc, name) => {
let nextAcc;
if (acc) {
// A previous iteration found a wrapper, so `continue`.
nextAcc = acc;
} else if (this._onceWrappers[name]) {
// A list of wrappers exists for the event, so, let's try an find one for this function.
const existing = this._onceWrappers[name].find((item) => item.original === fn);
if (existing) {
nextAcc = existing.wrapper;
} else {
nextAcc = null;
}
} else {
// The list didn't even exists, let's at least create it.
this._onceWrappers[name] = [];
nextAcc = null;
}
return nextAcc;
},
null,
);
// No wrapper was found, so let's create one.
if (!wrapper) {
/**
* A simple wrapper for the original listener.
*
* @type {EventsHubOnceWrapper}
*/
wrapper = (...args) => fn(...args);
wrapper.once = true;
events.forEach((name) => {
this._onceWrappers[name].push({
wrapper,
original: fn,
});
});
});
}
toClean.forEach((info) => this.off(info.event, info.fn));
return this.on(event, wrapper);
}
/**
* Reduce a target using an event. It's like emit, but the events listener return
* Reduces a target using an event. It's like emit, but the events listener return
* a modified (or not) version of the `target`.
* @param {String|Array} event An event name or a list of them.
* @param {*} target The variable to reduce with the listeners.
* @param {Array} args A list of parameters to send to the listeners.
* @return {*} A version of the `target` processed by the listeners.
*
* @param {string|string[]} event An event name or a list of them.
* @param {*} target The variable to reduce with the listeners.
* @param {...*} args A list of parameters to send to the listeners.
* @returns {*} A version of the `target` processed by the listeners.
*/

@@ -113,3 +219,3 @@ reduce(event, target, ...args) {

} else if (typeof result === 'object') {
processed = Object.assign({}, result);
processed = { ...result };
} else {

@@ -137,5 +243,6 @@ processed = result;

/**
* Get all the listeners for an event.
* @param {String} event The name of the event.
* @return {Array}
* Gets all the listeners for an event.
*
* @param {string} event The name of the event.
* @returns {Function[]}
*/

@@ -142,0 +249,0 @@ subscribers(event) {

/**
* Helper class to create a proxy for a promise in order to add custom properties.
* @module shared/extendPromise
*/
/**
* Helper class that creates a proxy for a {@link Promise} in order to add custom properties.
*
* The only reason this class exists is so it can "scope" the necessary methods to extend promise
* and avoid workarounds in order to declare them, as both methods need to call themselves
* recursively.
* @ignore
* The only reason this class exists is so it can "scope" the necessary methods to extend
* {@link Promise} and avoid workarounds in order to declare them, as they need to call
* themselves recursively.
*
* @parent module:shared/extendPromise
* @tutorial extendPromise
*/
class PromiseExtender {
/**
* @param {Promise} promise The promise to extend.
* @param {Object} properties A dictionary of custom properties to _inject_ in the promise
* chain.
* @param {Promise} promise The promise to extend.
* @param {Object.<string,*>} properties A dictionary of custom properties to _inject_ in the
* promise chain.
*/

@@ -18,2 +24,3 @@ constructor(promise, properties) {

* The proxied promise.
*
* @type {Proxy<Promise>}

@@ -27,2 +34,3 @@ * @access private

* The extended promise.
*
* @type {Proxy<Promise>}

@@ -39,8 +47,9 @@ */

* properties would be lost.
*
* @param {Promise} promise The promise to proxy.
* @param {Object} properties A dictionary of custom properties to _inject_ in the promise
* chain.
* @return {Proxy<Promise>}
* @throws {Error} if `promise` is not a valid instance of {@link Promise}.
* @throws {Error} if `properties` is not an object or if it doesn't have any properties.
* @returns {Proxy<Promise>}
* @throws {Error} If `promise` is not a valid instance of {@link Promise}.
* @throws {Error} If `properties` is not an object or if it doesn't have any properties.
* @access private

@@ -76,6 +85,7 @@ * @ignore

* can also be extended.
*
* @param {Function} fn The promise function to proxy.
* @param {Object} properties A dictionary of custom properties to _inject_ in the promise
* chain.
* @return {Proxy<Function>}
* @returns {Proxy<Function>}
* @access private

@@ -98,14 +108,16 @@ * @ignore

* `finally`s are added.
*
* @param {Promise} promise The promise to extend.
* @param {Object} properties A dictionary of custom properties to _inject_ in the promise
* chain.
* @throws {Error} if `promise` is not a valid instance of {@link Promise}.
* @throws {Error} if `properties` is not an object or if it doesn't have any properties.
* @return {Proxy<Promise>}
* @throws {Error} If `promise` is not a valid instance of {@link Promise}.
* @throws {Error} If `properties` is not an object or if it doesn't have any properties.
* @returns {Proxy<Promise>}
* @tutorial extendPromise
*/
const extendPromise = (
promise,
properties
properties,
) => (new PromiseExtender(promise, properties)).promise;
module.exports = extendPromise;

@@ -0,13 +1,44 @@

/**
* I'm using this extremely verbose syntax because it's the only way the transpilation process
* would recognize both 'imports' and 'exports'.
*
* @ignore
*/
const APIClient = require('./apiClient');
const {
DeepAssign,
deepAssign,
deepAssignWithConcat,
deepAssignWithOverwrite,
deepAssignWithShallowMerge,
} = require('./deepAssign');
const deferred = require('./deferred');
const EventsHub = require('./eventsHub');
const extendPromise = require('./extendPromise');
const {
resource,
resourceCreator,
resourcesCollection,
provider,
providerCreator,
providers,
} = require('./jimpleFns');
const ObjectUtils = require('./objectUtils');
module.exports = {
APIClient,
deferred,
EventsHub,
extendPromise,
ObjectUtils,
};
module.exports.APIClient = APIClient;
module.exports.DeepAssign = DeepAssign;
module.exports.deepAssign = deepAssign;
module.exports.deepAssignWithConcat = deepAssignWithConcat;
module.exports.deepAssignWithOverwrite = deepAssignWithOverwrite;
module.exports.deepAssignWithShallowMerge = deepAssignWithShallowMerge;
module.exports.deferred = deferred;
module.exports.EventsHub = EventsHub;
module.exports.extendPromise = extendPromise;
module.exports.resource = resource;
module.exports.resourceCreator = resourceCreator;
module.exports.resourcesCollection = resourcesCollection;
module.exports.provider = provider;
module.exports.providerCreator = providerCreator;
module.exports.providers = providers;
module.exports.ObjectUtils = ObjectUtils;
const extend = require('extend');
/**
* @module shared/objectUtils
*/
/**
* @callback ObjectUtilsShouldFlatFn
* @param {string} key The key for the property object that is being parsed.
* @param {Object} value The value of the property object that is being parsed.
* @returns {boolean} Whether or not the method should flat a sub object.
* @parent module:shared/objectUtils
*/
/**
* @typedef {Object.<string,string>} ObjectUtilsExtractPathsDictionary
* @parent module:shared/objectUtils
*/
/**
* @typedef {ObjectUtilsExtractPathsDictionary|string} ObjectUtilsExtractPath
* @parent module:shared/objectUtils
*/
/**
* A small collection of utility methods to work with objects.
*
* @parent module:shared/objectUtils
* @tutorial objectUtils
*/
class ObjectUtils {
/**
* @throws {Error} is called. This class is meant to be have only static methods.
* @ignore
* Creates a deep copy of a given object.
*
* @param {Object} target The object to copy.
* @returns {Object}
*/
constructor() {
throw new Error('ObjectUtils is a static class');
static copy(target) {
return this.merge(target);
}
/**
* This method makes a deep merge of a list of objects into a new one. The method also supports
* arrays.
* @example
* const objA = { a: 'first' };
* const objB = { b: 'second' };
* console.log(ObjectUtils.merge(objA, objB));
* // Will output { a: 'first', b: 'second' }
* @example
* const arrA = [{ a: 'first' }];
* const arrB = [{ b: 'second' }];
* console.log(ObjectUtils.merge(objA, objB));
* // Will output [{ a: 'first', b: 'second' }]
* @param {...{Object}} targets The objects to merge.
* @return {Object}
* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `dash-case` to `lowerCamelCase`.
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will
* use all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/
static merge(...targets) {
const [firstTarget] = targets;
const base = Array.isArray(firstTarget) ? [] : {};
return extend(true, base, ...targets);
static dashToLowerCamelKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])-([a-z])/g,
(fullMatch, firstLetter, secondLetter) => {
const newSecondLetter = secondLetter.toUpperCase();
return `${firstLetter}${newSecondLetter}`;
},
include,
exclude,
pathDelimiter,
);
}
/**
* Creates a deep copy of a given object.
* @param {Object} target The object to copy.
* @return {Object}
* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `dash-case` to `snake_case`.
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will
* use all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/
static copy(target) {
return this.merge(target);
static dashToSnakeKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])-([a-z])/g,
(fullMatch, firstLetter, secondLetter) => `${firstLetter}_${secondLetter}`,
include,
exclude,
pathDelimiter,
);
}
/**
* Returns the value of an object property using a path.
* Deletes a property of an object using a path.
*
* @example
* const obj = {
* const target = {
* propOne: {

@@ -51,102 +101,52 @@ * propOneSub: 'Charito!',

* };
* console.log(ObjectUtils.get(
* obj,
* console.log(ObjectUtils.delete(
* target,
* 'propOne.propOneSub'
* ));
* // Will output 'Charito!'
* // Will output { propTwo: '!!!' }
*
* @param {Object} target The object from where the property will be read.
* @param {string} objPath The path to the property.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path is
* invalid. If this is `false`, the method will silently
* fail and return `undefined`.
* @return {*}
* @throws {Error} If the path is invalid and `failWithError` is set to `true`.
* @param {Object} target The object from where the property will be
* removed.
* @param {string} objPath The path to the property.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components.
* @param {boolean} [cleanEmptyProperties=true] If this flag is `true` and after removing the
* property the parent object is empty, it will
* remove it recursively until a non empty parent
* object is found.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path
* is invalid. If this is `false`, the method will
* silently fail.
* @returns {Object} A copy of the original object with the removed property/properties.
*/
static get(target, objPath, pathDelimiter = '.', failWithError = false) {
const parts = objPath.split(pathDelimiter);
const first = parts.shift();
let currentElement = target[first];
if (typeof currentElement === 'undefined') {
if (failWithError) {
throw new Error(`There's nothing on '${objPath}'`);
}
} else if (parts.length) {
let currentPath = first;
parts.some((currentPart) => {
let breakLoop = false;
currentPath += `${pathDelimiter}${currentPart}`;
currentElement = currentElement[currentPart];
if (typeof currentElement === 'undefined') {
if (failWithError) {
throw new Error(`There's nothing on '${currentPath}'`);
} else {
breakLoop = true;
}
}
return breakLoop;
});
}
return currentElement;
}
/**
* Sets a property on an object using a path. If the path doesn't exist, it will be created.
* @example
* const target = {};
* console.log(ObjectUtils.set(target, 'some.prop.path', 'some-value'));
* // Will output { some: { prop: { path: 'some-value' } } }
*
* @param {Object} target The object where the property will be set.
* @param {string} objPath The path for the property.
* @param {*} value The value to set on the property.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path is
* invalid. If this is `false`, the method will silently
* fail and return `undefined`.
* @return {Object} A copy of the original object with the added property/properties.
* @throws {Error} If one of the path components is for a non-object property and
* `failWithError` is set to `true`.
*/
static set(
static delete(
target,
objPath,
value,
pathDelimiter = '.',
failWithError = false
cleanEmptyProperties = true,
failWithError = false,
) {
const parts = objPath.split(pathDelimiter);
const last = parts.pop();
let result = this.copy(target);
if (objPath.includes(pathDelimiter)) {
const parts = objPath.split(pathDelimiter);
const last = parts.pop();
let currentElement = result;
let currentPath = '';
parts.forEach((part) => {
currentPath += `${pathDelimiter}${part}`;
const element = currentElement[part];
const elementType = typeof element;
if (elementType === 'undefined') {
currentElement[part] = {};
currentElement = currentElement[part];
} else if (elementType === 'object') {
currentElement = currentElement[part];
} else {
const errorPath = currentPath.substr(pathDelimiter.length);
if (failWithError) {
throw new Error(
`There's already an element of type '${elementType}' on '${errorPath}'`
);
} else {
result = undefined;
}
}
});
if (result) {
currentElement[last] = value;
if (parts.length) {
const parentPath = parts.join(pathDelimiter);
const parentObj = this.get(
result,
parentPath,
pathDelimiter,
failWithError,
);
delete parentObj[last];
if (cleanEmptyProperties && !Object.keys(parentObj).length) {
result = this.delete(
result,
parentPath,
pathDelimiter,
cleanEmptyProperties,
failWithError,
);
}
} else {
result[objPath] = value;
delete result[last];
}

@@ -158,2 +158,3 @@

* Extracts a property or properties from an object in order to create a new one.
*
* @example

@@ -176,16 +177,15 @@ * const target = {

* // Will output { name: 'Rosario', age: 3, address: { planet: 'earth' } }
* @param {Object} target The object from where the
* property/properties will be extracted.
* @param {Array|Object|string} objPaths This can be a single path or a list of
* them. And for this method, the paths are
* not only strings but can also be an object
* with a single key, the would be the path
* to where to "do the extraction", and the
* value the path on the target object.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the
* path components.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the
* path is invalid. If this is `false`, the
* method will silently fail an empty object.
* @return {Object}
*
* @param {Object} target
* The object from where the property/properties will be extracted.
* @param {ObjectUtilsExtractPath|ObjectUtilsExtractPath[]} objPaths
* This can be a single path or a list of them. And for this method, the paths are not only
* strings but can also be an object with a single key, the would be the path to where to "do
* the extraction", and the value the path on the target object.
* @param {string} [pathDelimiter='.']
* The delimiter that will separate the path components.
* @param {boolean} [failWithError=false]
* Whether or not to throw an error when the path is invalid. If this is `false`, the method will
* silently fail an empty object.
* @returns {Object}
*/

@@ -232,67 +232,6 @@ static extract(target, objPaths, pathDelimiter = '.', failWithError = false) {

}
/**
* Deletes a property of an object using a path.
* @example
* const target = {
* propOne: {
* propOneSub: 'Charito!',
* },
* propTwo: '!!!',
* };
* console.log(ObjectUtils.delete(
* target,
* 'propOne.propOneSub'
* ));
* // Will output { propTwo: '!!!' }
*
* @param {Object} target The object from where the property will be
* removed.
* @param {string} objPath The path to the property.
* @param {String} [pathDelimiter='.'] The delimiter that will separate the path
* components.
* @param {Boolean} [cleanEmptyProperties=true] If this flag is `true` and after removing the
* property the parent object is empty, it will
* remove it recursively until a non empty parent
* object is found.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path
* is invalid. If this is `false`, the method will
* silently fail.
* @return {Object} A copy of the original object with the removed property/properties.
*/
static delete(
target,
objPath,
pathDelimiter = '.',
cleanEmptyProperties = true,
failWithError = false
) {
const parts = objPath.split(pathDelimiter);
const last = parts.pop();
let result = this.copy(target);
if (parts.length) {
const parentPath = parts.join(pathDelimiter);
const parentObj = this.get(
result,
parentPath,
pathDelimiter,
failWithError
);
delete parentObj[last];
if (cleanEmptyProperties && !Object.keys(parentObj).length) {
result = this.delete(
result,
parentPath,
pathDelimiter,
cleanEmptyProperties,
failWithError
);
}
} else {
delete result[last];
}
return result;
}
/**
* Flatterns an object properties into a single level dictionary.
*
* @example

@@ -308,18 +247,17 @@ * const target = {

*
* @param {Object} target The object to transform.
* @param {String} [pathDelimiter='.'] The delimiter that will separate
* the path components.
* @param {String} [prefix=''] A custom prefix to be added before
* the name of the properties. This
* can be used on custom cases and
* it's also used when the method
* calls itself in order to flattern
* a sub object.
* @param {?Function(String,*):Boolean} [shouldFlattern=null] A custom function that can be used
* in order to tell the method whether
* an Object or an Array property
* should be flattern or not. It will
* receive the key for the property
* and the Object/Array itself.
* @return {Object}
* @param {Object} target The object to transform.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the
* path components.
* @param {string} [prefix=''] A custom prefix to be added before the
* name of the properties. This can be
* used on custom cases and it's also
* used when the method calls itself in
* order to flattern a sub object.
* @param {?ObjectUtilsShouldFlatFn} [shouldFlattern=null] A custom function that can be used in
* order to tell the method whether an
* Object or an Array property should be
* flattern or not. It will receive the
* key for the property and the
* Object/Array itself.
* @returns {Object}
*/

@@ -339,3 +277,3 @@ static flat(target, pathDelimiter = '.', prefix = '', shouldFlattern = null) {

name,
shouldFlattern
shouldFlattern,
));

@@ -350,25 +288,5 @@ } else {

/**
* This method does the exact opposite from `flat`: It takes an already flattern object and
* restores it structure.
* @example
* const target = {
* 'propOne.propOneSub': 'Charito!
* propTwo: '!!!',
* };
* console.log(ObjectUtils.unflat(target);
* // Will output { propOne: { propOneSub: 'Charito!' }, 'propTwo': '!!!' }
*
* @param {Object} target The object to transform.
* @param {String} [pathDelimiter='.'] The delimiter that will separate the path components.
* @return {Object}
*/
static unflat(target, pathDelimiter = '.') {
return Object.keys(target).reduce(
(current, key) => this.set(current, key, target[key], pathDelimiter),
{}
);
}
/**
* Formats all the keys on an object using a way similar to `.replace(regexp, ...)` but that
* also works recursively and with _"object paths"_.
*
* @example

@@ -394,13 +312,13 @@ * const target = {

* @param {Function} replaceWith The callback the method will call when formatting a
* replace. Think of `searchExpression` and `replaceWith`
* as the parameters of a `.replace` call, where the
* object is the key.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* replacement. Think of `searchExpression` and
* `replaceWith` as the parameters of a `.replace` call,
* where the object is the key.
* @param {string[]} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
* @returns {Object}
*/

@@ -413,3 +331,3 @@ static formatKeys(

exclude = [],
pathDelimiter = '.'
pathDelimiter = '.',
) {

@@ -424,2 +342,4 @@ // First of all, get all the keys from the target.

* recursive call.
*
* @ignore
*/

@@ -485,2 +405,4 @@ const hasChildrenByKey = {};

* The `key` is set to `false` so it will be later removed using `.filter`.
*
* @ignore
*/

@@ -553,2 +475,4 @@ key = false;

* Basically: If it's a key, don't format it and don't make recursive calls for it.
*
* @ignore
*/

@@ -594,3 +518,3 @@ hasChildrenByKey[useExcludePath] = false;

subExcludeByKey[key] || [],
pathDelimiter
pathDelimiter,
);

@@ -601,34 +525,59 @@ } else {

// "Done", return the new object with the "new key" and the "new value".
return Object.assign({}, newObj, {
[newKey]: newValue,
});
return { ...newObj, [newKey]: newValue };
},
{}
{},
);
}
/**
* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `lowerCamelCase` to `snake_case`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
* Returns the value of an object property using a path.
*
* @example
* const obj = {
* propOne: {
* propOneSub: 'Charito!',
* },
* propTwo: '!!!',
* };
* console.log(ObjectUtils.get(
* obj,
* 'propOne.propOneSub'
* ));
* // Will output 'Charito!'
*
* @param {Object} target The object from where the property will be read.
* @param {string} objPath The path to the property.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path is
* invalid. If this is `false`, the method will silently
* fail and return `undefined`.
* @returns {*}
* @throws {Error} If the path is invalid and `failWithError` is set to `true`.
*/
static lowerCamelToSnakeKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])([A-Z])/g,
(fullMatch, firstLetter, secondLetter) => {
const newSecondLetter = secondLetter.toLowerCase();
return `${firstLetter}_${newSecondLetter}`;
},
include,
exclude,
pathDelimiter
);
static get(target, objPath, pathDelimiter = '.', failWithError = false) {
const parts = objPath.split(pathDelimiter);
const first = parts.shift();
let currentElement = target[first];
if (typeof currentElement === 'undefined') {
if (failWithError) {
throw new Error(`There's nothing on '${objPath}'`);
}
} else if (parts.length) {
let currentPath = first;
parts.some((currentPart) => {
let breakLoop = false;
currentPath += `${pathDelimiter}${currentPart}`;
currentElement = currentElement[currentPart];
if (typeof currentElement === 'undefined') {
if (failWithError) {
throw new Error(`There's nothing on '${currentPath}'`);
} else {
breakLoop = true;
}
}
return breakLoop;
});
}
return currentElement;
}

@@ -638,11 +587,12 @@ /**

* `lowerCamelCase` to `dash-case`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will
* use all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/

@@ -659,3 +609,3 @@ static lowerCamelToDashKeys(target, include = [], exclude = [], pathDelimiter = '.') {

exclude,
pathDelimiter
pathDelimiter,
);

@@ -665,38 +615,126 @@ }

* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `snake_case` to `lowerCamelCase`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
* `lowerCamelCase` to `snake_case`.
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will use
* all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/
static snakeToLowerCamelKeys(target, include = [], exclude = [], pathDelimiter = '.') {
static lowerCamelToSnakeKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])_([a-z])/g,
/([a-z])([A-Z])/g,
(fullMatch, firstLetter, secondLetter) => {
const newSecondLetter = secondLetter.toUpperCase();
return `${firstLetter}${newSecondLetter}`;
const newSecondLetter = secondLetter.toLowerCase();
return `${firstLetter}_${newSecondLetter}`;
},
include,
exclude,
pathDelimiter
pathDelimiter,
);
}
/**
* This method makes a deep merge of a list of objects into a new one. The method also supports
* arrays.
*
* @example
* const objA = { a: 'first' };
* const objB = { b: 'second' };
* console.log(ObjectUtils.merge(objA, objB));
* // Will output { a: 'first', b: 'second' }
*
* @example
* const arrA = [{ a: 'first' }];
* const arrB = [{ b: 'second' }];
* console.log(ObjectUtils.merge(objA, objB));
* // Will output [{ a: 'first', b: 'second' }]
*
* @param {...Object} targets The objects to merge.
* @returns {Object}
*/
static merge(...targets) {
const [firstTarget] = targets;
const base = Array.isArray(firstTarget) ? [] : {};
return extend(true, base, ...targets);
}
/**
* Sets a property on an object using a path. If the path doesn't exist, it will be created.
*
* @example
* const target = {};
* console.log(ObjectUtils.set(target, 'some.prop.path', 'some-value'));
* // Will output { some: { prop: { path: 'some-value' } } }
*
* @param {Object} target The object where the property will be set.
* @param {string} objPath The path for the property.
* @param {*} value The value to set on the property.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components.
* @param {boolean} [failWithError=false] Whether or not to throw an error when the path is
* invalid. If this is `false`, the method will silently
* fail and return `undefined`.
* @returns {Object} A copy of the original object with the added property/properties.
* @throws {Error} If one of the path components is for a non-object property and
* `failWithError` is set to `true`.
*/
static set(
target,
objPath,
value,
pathDelimiter = '.',
failWithError = false,
) {
let result = this.copy(target);
if (objPath.includes(pathDelimiter)) {
const parts = objPath.split(pathDelimiter);
const last = parts.pop();
let currentElement = result;
let currentPath = '';
parts.forEach((part) => {
currentPath += `${pathDelimiter}${part}`;
const element = currentElement[part];
const elementType = typeof element;
if (elementType === 'undefined') {
currentElement[part] = {};
currentElement = currentElement[part];
} else if (elementType === 'object') {
currentElement = currentElement[part];
} else {
const errorPath = currentPath.substr(pathDelimiter.length);
if (failWithError) {
throw new Error(
`There's already an element of type '${elementType}' on '${errorPath}'`,
);
} else {
result = undefined;
}
}
});
if (result) {
currentElement[last] = value;
}
} else {
result[objPath] = value;
}
return result;
}
/**
* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `snake_case` to `dash-case`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will
* use all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/

@@ -710,3 +748,3 @@ static snakeToDashKeys(target, include = [], exclude = [], pathDelimiter = '.') {

exclude,
pathDelimiter
pathDelimiter,
);

@@ -716,17 +754,18 @@ }

* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `dash-case` to `lowerCamelCase`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
* `snake_case` to `lowerCamelCase`.
*
* @param {Object} target The object for format.
* @param {string[]} [include=[]] A list of keys or paths where the transformation
* will be made. If not specified, the method will
* use all the keys from the object.
* @param {string[]} [exclude=[]] A list of keys or paths where the transformation
* won't be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path
* components for both `include` and `exclude`.
* @returns {Object}
*/
static dashToLowerCamelKeys(target, include = [], exclude = [], pathDelimiter = '.') {
static snakeToLowerCamelKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])-([a-z])/g,
/([a-z])_([a-z])/g,
(fullMatch, firstLetter, secondLetter) => {

@@ -738,30 +777,35 @@ const newSecondLetter = secondLetter.toUpperCase();

exclude,
pathDelimiter
pathDelimiter,
);
}
/**
* A shorthand method for {@link ObjectUtils.formatKeys} that transforms the keys from
* `dash-case` to `snake_case`.
* @param {Object} target The object for format.
* @param {Array} [include=[]] A list of keys or paths where the transformation will
* be made. If not specified, the method will use all the
* keys from the object.
* @param {Array} [exclude=[]] A list of keys or paths where the transformation won't
* be made.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components
* for both `include` and `exclude`.
* @return {Object}
* This method does the exact opposite from `flat`: It takes an already flattern object and
* restores it structure.
*
* @example
* const target = {
* 'propOne.propOneSub': 'Charito!',
* propTwo: '!!!',
* };
* console.log(ObjectUtils.unflat(target));
* // Will output { propOne: { propOneSub: 'Charito!' }, 'propTwo': '!!!' }
*
* @param {Object} target The object to transform.
* @param {string} [pathDelimiter='.'] The delimiter that will separate the path components.
* @returns {Object}
*/
static dashToSnakeKeys(target, include = [], exclude = [], pathDelimiter = '.') {
return this.formatKeys(
target,
/([a-z])-([a-z])/g,
(fullMatch, firstLetter, secondLetter) => `${firstLetter}_${secondLetter}`,
include,
exclude,
pathDelimiter
static unflat(target, pathDelimiter = '.') {
return Object.keys(target).reduce(
(current, key) => this.set(current, key, target[key], pathDelimiter),
{},
);
}
/**
* @throws {Error} If instantiated. This class is meant to be have only static methods.
*/
constructor() {
throw new Error('ObjectUtils is a static class');
}
}
module.exports = ObjectUtils;

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc