Comparing version 3.0.4 to 4.0.0
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
5
8692
200
11
324100
18
44
1
- Removedjimple@^1.5.0
- Removedjimple@1.5.0(transitive)