Haha-Yes/node_modules/enmap/src/index.js
2018-09-06 15:02:53 +02:00

1226 lines
46 KiB
JavaScript

// Lodash should probably be a core lib but hey, it's useful!
const _ = require('lodash');
// Custom error codes with stack support.
const Err = require('./error.js');
// Native imports
const { resolve, sep } = require('path');
const fs = require('fs');
// Symbols are used to create "private" methods.
// https://medium.com/front-end-hacking/private-methods-in-es6-and-writing-your-own-db-b2e30866521f
const _mathop = Symbol('mathop');
const _getHighestAutonum = Symbol('getHighestAutonum');
const _check = Symbol('check');
const _validateName = Symbol('validateName');
const _fetchCheck = Symbol('fetchCheck');
const _parseData = Symbol('parseData');
const _readyCheck = Symbol('readyCheck');
const _clone = Symbol('clone');
const _init = Symbol('init');
/**
* A enhanced Map structure with additional utility methods.
* Can be made persistent
* @extends {Map}
*/
class Enmap extends Map {
constructor(iterable, options = {}) {
if (!iterable || typeof iterable[Symbol.iterator] !== 'function') {
options = iterable || {};
iterable = null;
}
super(iterable);
let cloneLevel;
if (options.cloneLevel) {
const accepted = ['none', 'shallow', 'deep'];
if (!accepted.includes(options.cloneLevel)) throw new Err('Unknown Clone Level. Options are none, shallow, deep. Default is deep.', 'EnmapOptionsError');
cloneLevel = options.cloneLevel; // eslint-disable-line prefer-destructuring
} else {
cloneLevel = 'deep';
}
// Object.defineProperty ensures that the property is "hidden" when outputting
// the enmap in console. Only actual map entries are shown using this method.
Object.defineProperty(this, 'cloneLevel', {
value: cloneLevel,
writable: true,
enumerable: false,
configurable: false
});
if (options.name) {
// better-sqlite-pool is better than directly using better-sqlite3 for multi-process purposes.
// required only here because otherwise non-persistent database still need to install it!
const { Pool } = require('better-sqlite-pool');
Object.defineProperty(this, 'persistent', {
value: true,
writable: false,
enumerable: false,
configurable: false
});
if (!options.dataDir) {
if (!fs.existsSync('./data')) {
fs.mkdirSync('./data');
}
}
const dataDir = resolve(process.cwd(), options.dataDir || 'data');
const pool = new Pool(`${dataDir}${sep}enmap.sqlite`);
Object.defineProperties(this, {
name: {
value: options.name,
writable: true,
enumerable: false,
configurable: false
},
fetchAll: {
value: !_.isNil(options.fetchAll) ? options.fetchAll : true,
writable: true,
enumerable: false,
configurable: false
},
pool: {
value: pool,
writable: false,
enumerable: false,
configurable: false
},
autoFetch: {
value: !_.isNil(options.autoFetch) ? options.autoFetch : true,
writable: true,
enumerable: false,
configurable: false
},
defer: {
value: new Promise((res) =>
Object.defineProperty(this, 'ready', {
value: res,
writable: false,
enumerable: false,
configurable: false
})),
writable: false,
enumerable: false,
configurable: false
}
});
this[_validateName]();
this[_init](pool);
} else {
Object.defineProperty(this, 'name', {
value: 'MemoryEnmap',
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(this, 'isReady', {
value: true,
writable: false,
enumerable: false,
configurable: false
});
}
}
/**
* Sets a value in Enmap.
* @param {string|number} key Required. The key of the element to add to The Enmap.
* @param {*} val Required. The value of the element to add to The Enmap.
* If the Enmap is persistent this value MUST be stringifiable as JSON.
* @param {string} path Optional. The path to the property to modify inside the value object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @example
* // Direct Value Examples
* enmap.set('simplevalue', 'this is a string');
* enmap.set('isEnmapGreat', true);
* enmap.set('TheAnswer', 42);
* enmap.set('IhazObjects', { color: 'black', action: 'paint', desire: true });
* enmap.set('ArraysToo', [1, "two", "tree", "foor"])
*
* // Settings Properties
* enmap.set('IhazObjects', 'color', 'blue'); //modified previous object
* enmap.set('ArraysToo', 2, 'three'); // changes "tree" to "three" in array.
* @returns {Enmap} The enmap.
*/
set(key, val, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(key) || !['String', 'Number'].includes(key.constructor.name)) {
throw new Err('Enmap require keys to be strings or numbers.', 'EnmapKeyTypeError');
}
let data = super.get(key);
const oldValue = super.has(key) ? data : null;
if (!_.isNil(path)) {
_.set(data, path, val);
} else {
data = val;
}
if (_.isFunction(this.changedCB)) {
this.changedCB(key, oldValue, data);
}
if (this.persistent) {
this.db.prepare(`INSERT OR REPLACE INTO ${this.name} (key, value) VALUES (?, ?);`).run(key, JSON.stringify(data));
}
return super.set(key, this[_clone](data));
}
/**
* Retrieves a key from the enmap. If fetchAll is false, returns a promise.
* @param {string|number} key The key to retrieve from the enmap.
* @param {string} path Optional. The property to retrieve from the object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @example
* const myKeyValue = enmap.get("myKey");
* console.log(myKeyValue);
*
* const someSubValue = enmap.get("anObjectKey", "someprop.someOtherSubProp");
* @return {*} The value for this key.
*/
get(key, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
if (!_.isNil(path)) {
this[_check](key, 'Object');
const data = super.get(key);
return _.get(data, path);
}
return super.get(key);
}
/**
* Retrieves the number of rows in the database for this enmap, even if they aren't fetched.
* @return {integer} The number of rows in the database.
*/
get count() {
const data = this.db.prepare(`SELECT count(*) FROM '${this.name}';`).get();
return data['count(*)'];
}
/**
* Retrieves all the indexes (keys) in the database for this enmap, even if they aren't fetched.
* @return {array<string>} Array of all indexes (keys) in the enmap, cached or not.
*/
get indexes() {
const rows = this.db.prepare(`SELECT key FROM '${this.name}';`).get();
return rows.map(row => row.key);
}
/**
* Migrates an Enmap from version 3 or lower to a Version 4 enmap, which is locked to sqlite backend only.
* Version 4 uses a different way of storing data, so is not directly compatible with version 3 data.
* Note that this migration also makes the data unuseable with version 3, so it should only be used to migrate once.
* @example
* // This example migrates from enmap-mongo to the new format.
* // Assumes: npm install enmap@3.1.4 enmap-sqlite@latest enmap-mongo@latest
* const Enmap = require("enmap");
* const Provider = require("enmap-mongo");
* const SQLite = require("enmap-sqlite");
*
* let options = {
* name: 'test',
* dbName: 'enmap',
* url: 'mongodb://username:password@localhost:27017/enmap'
* };
*
* const source = new Provider(options);
* const target = new SQLite({"name": "points"});
*
* Enmap.migrate(source, target);
* @param {Provider} source A valid Enmap provider. Can be any existing provider.
* @param {Provider} target An SQLite Enmap Provider. Cannot work without enmap-sqlite as the target.
*/
static async migrate(source, target) {
if (!source || !target) throw `Both source and target are required.`;
if (source.constructor.name !== 'EnmapProvider') throw new Err('Source must be a valid Enmap Provider (not an initialized enmap)');
if (target.constructor.name !== 'EnmapProvider') throw new Err('Target must be a valid Enmap Provider (not an initialized enmap)');
const sourceMap = new Enmap({ provider: source });
const targetMap = new Enmap({ fetchAll: false, provider: target });
await sourceMap.defer;
await targetMap.defer;
if (!targetMap.db.pool.path.includes('enmap.sqlite')) {
throw new Err('Target enmap is not an sqlite database. The migrate method is only to migrate from a 3.0 enmap to 4.0 sqlite enmap!');
}
const insertArray = [];
sourceMap.keyArray().forEach(key => {
insertArray.push(targetMap.db.set(key, JSON.stringify(sourceMap.get(key))));
});
await Promise.all(insertArray);
return;
}
/**
* Fetches every key from the persistent enmap and loads them into the current enmap value.
* @return {Enmap} The enmap containing all values.
*/
fetchEverything() {
this[_readyCheck]();
const rows = this.db.prepare(`SELECT * FROM ${this.name};`).all();
for (const row of rows) {
super.set(row.key, this[_parseData](row.value));
}
return this;
}
/**
* Force fetch one or more key values from the enmap. If the database has changed, that new value is used.
* @param {string|number} keyOrKeys A single key or array of keys to force fetch from the enmap database.
* @return {Enmap} The Enmap, including the new fetched value(s).
*/
fetch(keyOrKeys) {
this[_readyCheck]();
if (_.isArray(keyOrKeys)) {
const data = this.db.prepare(`SELECT * FROM ${this.name} WHERE key IN (${'?, '.repeat(keyOrKeys.length).slice(0, -2)})`).all(keyOrKeys);
for (const row of data) {
super.set(row.key, this[_parseData](row.value));
}
return this;
} else {
const data = this.db.prepare(`SELECT * FROM ${this.name} WHERE key = ?;`).get(keyOrKeys);
if (!data) return null;
super.set(keyOrKeys, this[_parseData](data.value));
return this[_parseData](data.value);
}
}
/**
* Removes a key or keys from the cache - useful when disabling autoFetch.
* @param {*} keyOrArrayOfKeys A single key or array of keys to remove from the cache.
* @returns {Enmap} The enmap minus the evicted keys.
*/
evict(keyOrArrayOfKeys) {
if (_.isArray(keyOrArrayOfKeys)) {
keyOrArrayOfKeys.forEach(key => super.delete(key));
} else {
super.delete(keyOrArrayOfKeys);
}
return this;
}
/**
* Generates an automatic numerical key for inserting a new value.
* This is a "weak" method, it ensures the value isn't duplicated, but does not
* guarantee it's sequential (if a value is deleted, another can take its place).
* Useful for logging, but not much else.
* @example
* enmap.set(enmap.autonum(), "This is a new value");
* @return {number} The generated key number.
*/
autonum() {
this[_fetchCheck]('internal::autonum', true);
const start = this.get('internal::autonum') || 0;
const highest = this[_getHighestAutonum](start);
this.set('internal::autonum', highest);
return highest;
}
/**
* Function called whenever data changes within Enmap after the initial load.
* Can be used to detect if another part of your code changed a value in enmap and react on it.
* @example
* enmap.changed((keyName, oldValue, newValue) => {
* console.log(`Value of ${keyName} has changed from: \n${oldValue}\nto\n${newValue});
* });
* @param {Function} cb A callback function that will be called whenever data changes in the enmap.
*/
changed(cb) {
this.changedCB = cb;
}
/**
* Shuts down the database. WARNING: USING THIS MAKES THE ENMAP UNUSEABLE. You should
* only use this method if you are closing your entire application.
* Note that honestly I've never had to use this, shutting down the app without a close() is fine.
* @return {Promise<*>} The promise of the database closing operation.
*/
close() {
this[_readyCheck]();
return this.pool.close();
}
/**
* Modify the property of a value inside the enmap, if the value is an object or array.
* This is a shortcut to loading the key, changing the value, and setting it back.
* @param {string|number} key Required. The key of the element to add to The Enmap or array.
* This value MUST be a string or number.
* @param {*} path Required. The property to modify inside the value object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @param {*} val Required. The value to apply to the specified property.
* @returns {Enmap} The enmap.
*/
setProp(key, path, val) {
this[_readyCheck]();
if (_.isNil(path)) throw new Err(`No path provided to set a property in "${key}" of enmap "${this.name}"`, 'EnmapPathError');
return this.set(key, val, path);
}
/**
* Push to an array value in Enmap.
* @param {string|number} key Required. The key of the array element to push to in Enmap.
* This value MUST be a string or number.
* @param {*} val Required. The value to push to the array.
* @param {string} path Optional. The path to the property to modify inside the value object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @param {boolean} allowDupes Optional. Allow duplicate values in the array (default: false).
* @example
* // Assuming
* enmap.set("simpleArray", [1, 2, 3, 4]);
* enmap.set("arrayInObject", {sub: [1, 2, 3, 4]});
*
* enmap.push("simpleArray", 5); // adds 5 at the end of the array
* enmap.push("arrayInObject", "five", "sub"); adds "five" at the end of the sub array
* @returns {Enmap} The enmap.
*/
push(key, val, path = null, allowDupes = false) {
this[_readyCheck]();
this[_fetchCheck](key);
this[_check](key, 'Array', path);
const data = super.get(key);
if (!_.isNil(path)) {
const propValue = _.get(data, path);
if (!allowDupes && propValue.indexOf(val) > -1) return this;
propValue.push(val);
_.set(data, path, propValue);
} else {
if (!allowDupes && data.indexOf(val) > -1) return this;
data.push(val);
}
return this.set(key, data);
}
/**
* Push to an array element inside an Object or Array element in Enmap.
* @param {string|number} key Required. The key of the element.
* This value MUST be a string or number.
* @param {*} path Required. The name of the array property to push to.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @param {*} val Required. The value push to the array property.
* @param {boolean} allowDupes Allow duplicate values in the array (default: false).
* @returns {Enmap} The enmap.
*/
pushIn(key, path, val, allowDupes = false) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(path)) throw new Err(`No path provided to push a value in "${key}" of enmap "${this.name}"`, 'EnmapPathError');
return this.push(key, val, path, allowDupes);
}
// AWESOME MATHEMATICAL METHODS
/**
* Executes a mathematical operation on a value and saves it in the enmap.
* @param {string|number} key The enmap key on which to execute the math operation.
* @param {string} operation Which mathematical operation to execute. Supports most
* math ops: =, -, *, /, %, ^, and english spelling of those operations.
* @param {number} operand The right operand of the operation.
* @param {string} path Optional. The property path to execute the operation on, if the value is an object or array.
* @example
* // Assuming
* points.set("number", 42);
* points.set("numberInObject", {sub: { anInt: 5 }});
*
* points.math("number", "/", 2); // 21
* points.math("number", "add", 5); // 26
* points.math("number", "modulo", 3); // 2
* points.math("numberInObject", "+", 10, "sub.anInt");
*
* @return {Map} The EnMap.
*/
math(key, operation, operand, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
this[_check](key, 'Number', path);
if (_.isNil(path)) {
if (operation === 'random' || operation === 'rand') {
return this.set(key, Math.round(Math.random() * operand));
}
return this.set(key, this[_mathop](this.get(key), operation, operand));
} else {
const data = this.get(key);
const propValue = _.get(data, path);
if (operation === 'random' || operation === 'rand') {
return this.set(key, Math.round(Math.random() * propValue), path);
}
return this.set(key, this[_mathop](propValue, operation, operand), path);
}
}
/**
* Increments a key's value or property by 1. Value must be a number, or a path to a number.
* @param {string|number} key The enmap key where the value to increment is stored.
* @param {string} path Optional. The property path to increment, if the value is an object or array.
* @example
* // Assuming
* points.set("number", 42);
* points.set("numberInObject", {sub: { anInt: 5 }});
*
* points.inc("number"); // 43
* points.inc("numberInObject", "sub.anInt"); // {sub: { anInt: 6 }}
* @return {Map} The EnMap.
*/
inc(key, path = null) {
this[_readyCheck]();
this[_check](key, 'Number', path);
if (_.isNil(path)) {
let val = this.get(key);
return this.set(key, ++val);
} else {
const data = this.get(key);
let propValue = _.get(data, path);
_.set(data, path, ++propValue);
return this.set(key, data);
}
}
/**
* Decrements a key's value or property by 1. Value must be a number, or a path to a number.
* @param {string|number} key The enmap key where the value to decrement is stored.
* @param {string} path Optional. The property path to decrement, if the value is an object or array.
* @example
* // Assuming
* points.set("number", 42);
* points.set("numberInObject", {sub: { anInt: 5 }});
*
* points.dec("number"); // 41
* points.dec("numberInObject", "sub.anInt"); // {sub: { anInt: 4 }}
* @return {Map} The EnMap.
*/
dec(key, path = null) {
this[_readyCheck]();
this[_check](key, 'Number', path);
if (_.isNil(path)) {
let val = this.get(key);
return this.set(key, --val);
} else {
const data = this.get(key);
let propValue = _.get(data, path);
_.set(data, path, --propValue);
return this.set(key, data);
}
}
/**
* Returns the specific property within a stored value. If the key does not exist or the value is not an object, throws an error.
* @param {string|number} key Required. The key of the element to get from The Enmap.
* @param {*} path Required. The property to retrieve from the object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @return {*} The value of the property obtained.
*/
getProp(key, path) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(path)) throw new Err(`No path provided get a property from "${key}" of enmap "${this.name}"`, 'EnmapPathError');
return this.get(key, path);
}
/**
* Returns the key's value, or the default given, ensuring that the data is there.
* This is a shortcut to "if enmap doesn't have key, set it, then get it" which is a very common pattern.
* @param {string|number} key Required. The key you want to make sure exists.
* @param {*} defaultvalue Required. The value you want to save in the database and return as default.
* @example
* // Simply ensure the data exists (for using property methods):
* enmap.ensure("mykey", {some: "value", here: "as an example"});
* enmap.has("mykey"); // always returns true
* enmap.get("mykey", "here") // returns "as an example";
*
* // Get the default value back in a variable:
* const settings = mySettings.ensure("1234567890", defaultSettings);
* console.log(settings) // enmap's value for "1234567890" if it exists, otherwise the defaultSettings value.
* @return {*} The value from the database for the key, or the default value provided for a new key.
*/
ensure(key, defaultvalue) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(defaultvalue)) throw new Err(`No default value provided on ensure method for "${key}" in "${this.name}"`, 'EnmapArgumentError');
if (super.has(key)) return super.get(key);
this.set(key, defaultvalue);
return defaultvalue;
}
/* BOOLEAN METHODS THAT CHECKS FOR THINGS IN ENMAP */
/**
* Returns whether or not the key exists in the Enmap.
* @param {string|number} key Required. The key of the element to add to The Enmap or array.
* This value MUST be a string or number.
* @param {string} path Optional. The property to verify inside the value object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @example
* if(enmap.has("myKey")) {
* // key is there
* }
*
* if(!enmap.has("myOtherKey", "oneProp.otherProp.SubProp")) return false;
* @returns {boolean}
*/
has(key, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
if (!_.isNil(path)) {
this[_check](key, 'Object');
const data = super.get(key);
return _.has(data, path);
}
return super.has(key);
}
/**
* Returns whether or not the property exists within an object or array value in enmap.
* @param {string|number} key Required. The key of the element to check in the Enmap or array.
* @param {*} path Required. The property to verify inside the value object or array.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @return {boolean} Whether the property exists.
*/
hasProp(key, path) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(path)) throw new Err(`No path provided to check for a property in "${key}" of enmap "${this.name}"`, 'EnmapPathError');
return this.has(key, path);
}
/**
* Deletes a key in the Enmap.
* @param {string|number} key Required. The key of the element to delete from The Enmap.
* @param {string} path Optional. The name of the property to remove from the object.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @returns {Enmap} The enmap.
*/
delete(key, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
const oldValue = this.get(key);
if (!_.isNil(path)) {
let data = this.get(key);
path = _.toPath(path);
const last = path.pop();
const propValue = path.length ? _.get(data, path) : data;
if (_.isArray(propValue)) {
propValue.splice(last, 1);
} else {
delete propValue[last];
}
if (path.length) {
_.set(data, path, propValue);
} else {
data = propValue;
}
this.set(key, data);
} else {
super.delete(key);
if (this.persistent) {
return this.db.prepare(`DELETE FROM ${this.name} WHERE key = '${key}'`).run();
}
if (typeof this.changedCB === 'function') {
this.changedCB(key, oldValue, null);
}
}
return this;
}
/**
* Delete a property from an object or array value in Enmap.
* @param {string|number} key Required. The key of the element to delete the property from in Enmap.
* @param {*} path Required. The name of the property to remove from the object.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
*/
deleteProp(key, path) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(path)) throw new Err(`No path provided to delete a property in "${key}" of enmap "${this.name}"`, 'EnmapPathError');
this.delete(key, path);
}
/**
* Deletes everything from the enmap. If persistent, clears the database of all its data for this table.
*/
deleteAll() {
this[_readyCheck]();
if (this.persistent) {
this.db.prepare(`DELETE FROM ${this.name};`).run();
}
super.clear();
}
clear() { return this.deleteAll(); }
/**
* Remove a value in an Array or Object element in Enmap. Note that this only works for
* values, not keys. Complex values such as objects and arrays will not be removed this way.
* @param {string|number} key Required. The key of the element to remove from in Enmap.
* This value MUST be a string or number.
* @param {*} val Required. The value to remove from the array or object.
* @param {string} path Optional. The name of the array property to remove from.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3".
* If not presents, removes directly from the value.
* @return {Map} The EnMap.
*/
remove(key, val, path = null) {
this[_readyCheck]();
this[_fetchCheck](key);
this[_check](key, ['Array', 'Object']);
const data = super.get(key);
if (!_.isNil(path)) {
const propValue = _.get(data, path);
if (_.isArray(propValue)) {
propValue.splice(propValue.indexOf(val), 1);
_.set(data, path, propValue);
} else if (_.isObject(propValue)) {
_.delete(data, `${path}.${val}`);
}
} else if (_.isArray(data)) {
const index = data.indexOf(val);
data.splice(index, 1);
} else if (_.isObject(data)) {
delete data[val];
}
return this.set(key, data);
}
/**
* Remove a value from an Array or Object property inside an Array or Object element in Enmap.
* Confusing? Sure is.
* @param {string|number} key Required. The key of the element.
* This value MUST be a string or number.
* @param {*} path Required. The name of the array property to remove from.
* Can be a path with dot notation, such as "prop1.subprop2.subprop3"
* @param {*} val Required. The value to remove from the array property.
* @return {Map} The EnMap.
*/
removeFrom(key, path, val) {
this[_readyCheck]();
this[_fetchCheck](key);
if (_.isNil(path)) throw new Err(`No path provided to remove an array element in "${key}" of enmap "${this.name}"`, 'EnmapPathError');
return this.remove(key, val, path);
}
/**
* Initialize multiple Enmaps easily.
* @param {Array<string>} names Array of strings. Each array entry will create a separate enmap with that name.
* @param {Object} options Options object to pass to the provider. See provider documentation for its options.
* @example
* // Using local variables and the mongodb provider.
* const Enmap = require('enmap');
* const { settings, tags, blacklist } = Enmap.multi(['settings', 'tags', 'blacklist']);
*
* // Attaching to an existing object (for instance some API's client)
* const Enmap = require("enmap");
* Object.assign(client, Enmap.multi(["settings", "tags", "blacklist"]));
*
* @returns {Array<Map>} An array of initialized Enmaps.
*/
static multi(names, options = {}) {
if (!names.length || names.length < 1) {
throw new Err('"names" argument must be an array of string names.', 'EnmapTypeError');
}
const returnvalue = {};
for (const name of names) {
const enmap = new Enmap(Object.assign(options, { name }));
returnvalue[name] = enmap;
}
return returnvalue;
}
/* INTERNAL (Private) METHODS */
/*
* Internal Method. Initializes the enmap depending on given values.
* @param {Map} pool In order to set data to the Enmap, one must be provided.
* @returns {Promise} Returns the defer promise to await the ready state.
*/
async [_init](pool) {
Object.defineProperty(this, 'db', {
value: await pool.acquire(),
writable: false,
enumerable: false,
configurable: false
});
if (this.db) {
Object.defineProperty(this, 'isReady', {
value: true,
writable: false,
enumerable: false,
configurable: false
});
} else {
throw new Err('Database Could Not Be Opened', 'EnmapDBConnectionError');
}
const table = this.db.prepare(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name = '${this.name}';`).get();
if (!table['count(*)']) {
this.db.prepare(`CREATE TABLE ${this.name} (key text PRIMARY KEY, value text)`).run();
this.db.pragma('synchronous = 1');
this.db.pragma('journal_mode = wal');
}
if (this.fetchAll) {
await this.fetchEverything();
}
this.ready();
return this.defer;
}
/*
* INTERNAL method used by autonum().
* Loops on incremental numerical values until it finds a free key
* of that value in the Enamp.
* @param {Integer} start The starting value to look for.
* @return {Integer} The first non-existant value found.
*/
[_getHighestAutonum](start = 0) {
this[_readyCheck]();
let highest = start;
while (this.has(highest)) {
highest++;
}
return highest;
}
/*
* INTERNAL method to verify the type of a key or property
* Will THROW AN ERROR on wrong type, to simplify code.
* @param {string|number} key Required. The key of the element to check
* @param {string} type Required. The javascript constructor to check
* @param {string} path Optional. The dotProp path to the property in the object enmap.
*/
[_check](key, type, path = null) {
if (!this.has(key)) throw new Err(`The key "${key}" does not exist in the enmap "${this.name}"`);
if (!type) return;
if (!_.isArray(type)) type = [type];
if (!_.isNil(path)) {
this[_check](key, 'Object');
const data = super.get(key);
if (!type.includes(_.get(data, path).constructor.name)) {
throw new Err(`The property "${path}" in key "${key}" is not of type "${type.join('" or "')}" in the enmap "${this.name}" (key was of type "${_.get(data, path).constructor.name}")`);
}
} else if (!type.includes(this.get(key).constructor.name)) {
throw new Err(`The key "${key}" is not of type "${type.join('" or "')}" in the enmap "${this.name}" (key was of type "${this.get(key).constructor.name}")`);
}
}
/*
* INTERNAL method to execute a mathematical operation. Cuz... javascript.
* And I didn't want to import mathjs!
* @param {number} base the lefthand operand.
* @param {string} op the operation.
* @param {number} opand the righthand operand.
* @return {number} the result.
*/
[_mathop](base, op, opand) {
if (base == undefined || op == undefined || opand == undefined) throw new Err('Math Operation requires base and operation', 'TypeError');
switch (op) {
case 'add' :
case 'addition' :
case '+' :
return base + opand;
case 'sub' :
case 'subtract' :
case '-' :
return base - opand;
case 'mult' :
case 'multiply' :
case '*' :
return base * opand;
case 'div' :
case 'divide' :
case '/' :
return base / opand;
case 'exp' :
case 'exponent' :
case '^' :
return Math.pow(base, opand);
case 'mod' :
case 'modulo' :
case '%' :
return base % opand;
}
return null;
}
/**
* Internal method used to validate persistent enmap names (valid Windows filenames)
* @private
*/
[_validateName]() {
this.name = this.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
}
/*
* Internal Method. Verifies if a key needs to be fetched from the database.
* If persistent enmap and autoFetch is on, retrieves the key.
* @param {string|number} key The key to check or fetch.
*/
[_fetchCheck](key, force = false) {
if (force) {
this.fetch(key);
return;
}
if (super.has(key)) return;
if (!this.persistent || !this.autoFetch) return;
this.fetch(key);
}
/*
* Internal Method. Parses JSON data.
* Reserved for future use (logical checking)
* @param {*} data The data to check/parse
* @returns {*} An object or the original data.
*/
[_parseData](data) {
return JSON.parse(data);
}
/*
* Internal Method. Clones a value or object with the enmap's set clone level.
* @param {*} data The data to clone.
* @return {*} The cloned value.
*/
[_clone](data) {
if (this.cloneLevel === 'none') return data;
if (this.cloneLevel === 'shallow') return _.clone(data);
if (this.cloneLevel === 'deep') return _.cloneDeep(data);
throw new Err('Invalid cloneLevel. What did you *do*, this shouldn\'t happen!', 'EnmapOptionsError');
}
/*
* Internal Method. Verifies that the database is ready, assuming persistence is used.
*/
[_readyCheck]() {
if (!this.isReady) throw new Err('Database is not ready. Refer to the readme to use enmap.defer', 'EnmapReadyError');
}
/*
BELOW IS DISCORD.JS COLLECTION CODE
Per notes in the LICENSE file, this project contains code from Amish Shah's Discord.js
library. The code is from the Collections object, in discord.js version 11.
All below code is sourced from Collections.
https://github.com/discordjs/discord.js/blob/stable/src/util/Collection.js
*/
/**
* Creates an ordered array of the values of this Enmap.
* The array will only be reconstructed if an item is added to or removed from the Enmap,
* or if you change the length of the array itself. If you don't want this caching behaviour,
* use `Array.from(enmap.values())` instead.
* @returns {Array}
*/
array() {
return Array.from(this.values());
}
/**
* Creates an ordered array of the keys of this Enmap
* The array will only be reconstructed if an item is added to or removed from the Enmap,
* or if you change the length of the array itself. If you don't want this caching behaviour,
* use `Array.from(enmap.keys())` instead.
* @returns {Array}
*/
keyArray() {
return Array.from(this.keys());
}
/**
* Obtains random value(s) from this Enmap. This relies on {@link Enmap#array}.
* @param {number} [count] Number of values to obtain randomly
* @returns {*|Array<*>} The single value if `count` is undefined,
* or an array of values of `count` length
*/
random(count) {
let arr = this.array();
if (count === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
if (arr.length === 0) return [];
const rand = new Array(count);
arr = arr.slice();
for (let i = 0; i < count; i++) {
rand[i] = [arr.splice(Math.floor(Math.random() * arr.length), 1)];
}
return rand;
}
/**
* Obtains random key(s) from this Enmap. This relies on {@link Enmap#keyArray}
* @param {number} [count] Number of keys to obtain randomly
* @returns {*|Array<*>} The single key if `count` is undefined,
* or an array of keys of `count` length
*/
randomKey(count) {
let arr = this.keyArray();
if (count === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
if (arr.length === 0) return [];
const rand = new Array(count);
arr = arr.slice();
for (let i = 0; i < count; i++) {
rand[i] = [arr.splice(Math.floor(Math.random() * arr.length), 1)];
}
return rand;
}
/**
* Searches for all items where their specified property's value is identical to the given value
* (`item[prop] === value`).
* @param {string} prop The property to test against
* @param {*} value The expected value
* @returns {Array}
* @example
* enmap.findAll('username', 'Bob');
*/
findAll(prop, value) {
if (typeof prop !== 'string') throw new TypeError('Key must be a string.');
if (typeof value === 'undefined') throw new Error('Value must be specified.');
const results = [];
for (const item of this.values()) {
if (item[prop] === value) results.push(item);
}
return results;
}
/**
* Searches for a single item where its specified property's value is identical to the given value
* (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to
* [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find).
* <warn>All Enmap used in Discord.js are mapped using their `id` property, and if you want to find by id you
* should use the `get` method. See
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.</warn>
* @param {string|Function} propOrFn The property to test against, or the function to test with
* @param {*} [value] The expected value - only applicable and required if using a property for the first argument
* @returns {*}
* @example
* enmap.find('username', 'Bob');
* @example
* enmap.find(val => val.username === 'Bob');
*/
find(propOrFn, value) {
if (typeof propOrFn === 'string') {
if (typeof value === 'undefined') throw new Error('Value must be specified.');
for (const item of this.values()) {
if (item[propOrFn] === value) return item;
}
return null;
} else if (typeof propOrFn === 'function') {
for (const [key, val] of this) {
if (propOrFn(val, key, this)) return val;
}
return null;
}
throw new Error('First argument must be a property string or a function.');
}
/* eslint-disable max-len */
/**
* Searches for the key of a single item where its specified property's value is identical to the given value
* (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to
* [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex).
* @param {string|Function} propOrFn The property to test against, or the function to test with
* @param {*} [value] The expected value - only applicable and required if using a property for the first argument
* @returns {*}
* @example
* enmap.findKey('username', 'Bob');
* @example
* enmap.findKey(val => val.username === 'Bob');
*/
/* eslint-enable max-len */
findKey(propOrFn, value) {
if (typeof propOrFn === 'string') {
if (typeof value === 'undefined') throw new Error('Value must be specified.');
for (const [key, val] of this) {
if (val[propOrFn] === value) return key;
}
return null;
} else if (typeof propOrFn === 'function') {
for (const [key, val] of this) {
if (propOrFn(val, key, this)) return key;
}
return null;
}
throw new Error('First argument must be a property string or a function.');
}
/**
* Searches for the existence of a single item where its specified property's value is identical to the given value
* (`item[prop] === value`).
* <warn>Do not use this to check for an item by its ID. Instead, use `enmap.has(id)`. See
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) for details.</warn>
* @param {string} prop The property to test against
* @param {*} value The expected value
* @returns {boolean}
* @example
* if (enmap.exists('username', 'Bob')) {
* console.log('user here!');
* }
*/
exists(prop, value) {
return Boolean(this.find(prop, value));
}
/**
* Identical to
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),
* but returns a Enmap instead of an Array.
* @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {Enmap}
*/
filter(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
const results = new Enmap();
for (const [key, val] of this) {
if (fn(val, key, this)) results.set(key, val);
}
return results;
}
/**
* Identical to
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
* @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {Array}
*/
filterArray(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
const results = [];
for (const [key, val] of this) {
if (fn(val, key, this)) results.push(val);
}
return results;
}
/**
* Identical to
* [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
* @param {Function} fn Function that produces an element of the new array, taking three arguments
* @param {*} [thisArg] Value to use as `this` when executing function
* @returns {Array}
*/
map(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
const arr = new Array(this.size);
let i = 0;
for (const [key, val] of this) arr[i++] = fn(val, key, this);
return arr;
}
/**
* Identical to
* [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).
* @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {boolean}
*/
some(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
for (const [key, val] of this) {
if (fn(val, key, this)) return true;
}
return false;
}
/**
* Identical to
* [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every).
* @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {boolean}
*/
every(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
for (const [key, val] of this) {
if (!fn(val, key, this)) return false;
}
return true;
}
/**
* Identical to
* [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce).
* @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`,
* and `enmap`
* @param {*} [initialValue] Starting value for the accumulator
* @returns {*}
*/
reduce(fn, initialValue) {
let accumulator;
if (typeof initialValue !== 'undefined') {
accumulator = initialValue;
for (const [key, val] of this) accumulator = fn(accumulator, val, key, this);
} else {
let first = true;
for (const [key, val] of this) {
if (first) {
accumulator = val;
first = false;
continue;
}
accumulator = fn(accumulator, val, key, this);
}
}
return accumulator;
}
/**
* Creates an identical shallow copy of this Enmap.
* @returns {Enmap}
* @example const newColl = someColl.clone();
*/
clone() {
return new this.constructor(this);
}
/**
* Combines this Enmap with others into a new Enmap. None of the source Enmaps are modified.
* @param {...Enmap} enmaps Enmaps to merge
* @returns {Enmap}
* @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl);
*/
concat(...enmaps) {
const newColl = this.clone();
for (const coll of enmaps) {
for (const [key, val] of coll) newColl.set(key, val);
}
return newColl;
}
/**
* Checks if this Enmap shares identical key-value pairings with another.
* This is different to checking for equality using equal-signs, because
* the Enmaps may be different objects, but contain the same data.
* @param {Enmap} enmap Enmap to compare with
* @returns {boolean} Whether the Enmaps have identical contents
*/
equals(enmap) {
if (!enmap) return false;
if (this === enmap) return true;
if (this.size !== enmap.size) return false;
return !this.find((value, key) => {
const testVal = enmap.get(key);
return testVal !== value || (testVal === undefined && !enmap.has(key));
});
}
}
module.exports = Enmap;
/**
* @external forEach
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach}
*/
/**
* @external keys
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys}
*/
/**
* @external values
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values}
*/