const _ = require('lodash'); // Install shim for Object.hasOwn() if necessary. /* istanbul ignore next */ if (!Object.hasOwn) { Object.hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); } const IS_UNDEFINED = { isUndefined: true, }; function addToSelection(salty, item, i) { salty._selectedItems.push(item); salty._selectedIndexes.push(i); } function selectAllItems(salty) { salty._selectedItems = salty._items.slice(); salty._selectedIndexes = [...Array(salty._items.length).keys()]; } function createSelection(salty) { salty._selectedIndexes = []; salty._selectedItems = []; } function eraseSelection(salty) { salty._selectedIndexes = null; salty._selectedItems = null; } function finderWithFunction(salty, func) { createSelection(salty); salty._items.forEach((item, i) => { if (func.bind(item)()) { addToSelection(salty, item, i); } }); return salty; } function finderWithMatcher(salty, ...args) { let item; let matches; const len = salty._items.length; const matcher = _.defaults({}, ...args); const matcherKeys = Object.keys(matcher); let matcherValue; // Fast path if matcher object was empty or missing; that means "select all." if (matcherKeys.length === 0) { selectAllItems(salty); } else { createSelection(salty); for (let i = 0; i < len; i++) { matches = true; item = salty._items[i]; for (const key of matcherKeys) { matcherValue = matcher[key]; if (_.isMatch(matcherValue, IS_UNDEFINED)) { matches = _.isUndefined(item[key]); } else if (Array.isArray(matcherValue)) { if (!matcherValue.includes(item[key])) { matches = false; } } else if (matcherValue !== item[key]) { matches = false; } if (matches === false) { break; } } if (matches) { addToSelection(salty, item, i); } } } return salty; } function finder(salty, ...args) { if (args.length && _.isFunction(args[0])) { return finderWithFunction(salty, args[0]); } else { return finderWithMatcher(salty, ...args); } } function isNullOrUndefined(value) { return _.isNull(value) || _.isUndefined(value); } function sorter(items, keys) { keys = keys.split(',').map((key) => key.trim()); keys.reverse(); for (const key of keys) { // TaffyDB has an unusual approach to sorting that can yield surprising results. Rather than // duplicate that approach, we use the following sort order, which is close enough to TaffyDB // and is easier to reason about: // // 1. Non-null, non-undefined values, in standard sort order // 2. Null values // 3. Explicitly undefined values: key is present, value is undefined // 4. Implicitly undefined values: key is not present items.sort((a, b) => { const aValue = a[key]; const bValue = b[key]; if (isNullOrUndefined(aValue) || isNullOrUndefined(bValue)) { // Null and undefined come after all other values. if (!isNullOrUndefined(aValue)) { return -1; } if (!isNullOrUndefined(bValue)) { return 1; } // Null comes before undefined. if (_.isNull(aValue) && _.isUndefined(bValue)) { return -1; } if (_.isUndefined(aValue) && _.isNull(bValue)) { return 1; } // Explicitly undefined comes before implicitly undefined. if (_.isUndefined(aValue) && _.isUndefined(bValue)) { if (Object.hasOwn(a, key)) { return -1; } if (Object.hasOwn(b, key)) { return 1; } } // Both values are null, or both values are undefined. return 0; } // Neither value is null or undefined, so just use standard sort order. if (aValue < bValue) { return -1; } if (aValue > bValue) { return 1; } return 0; }); } return true; } function makeDb(salty) { /* Selections are persisted a bit differently in TaffyDB and Salty. Consider the following: ```js let db2 = db({ a: 1 }); db2.remove(); db().get; db2.get(); ``` In TaffyDB, `db` and `db2` track selected items separately. `db().get()` returns all items; `db2.get()` returns an empty array. In Salty, `db` and `db2` are the same object, so they share information about selected items. `db().get()` returns all items; `db2.get()` also returns all items, because the selection from `db().get()` remains active. */ const db = (...args) => finder(salty, ...args); db.sort = (keys) => sorter(salty._items, keys); return db; } class Salty { constructor(items) { this._items = items ? items.slice() : []; eraseSelection(this); return makeDb(this); } each(func) { this._selectedItems.forEach((item, i) => func(item, i)); return this; } get() { return this._selectedItems.slice(); } remove() { let removedItems = 0; if (this._selectedIndexes && this._selectedIndexes.length) { removedItems = this._selectedIndexes.length; this._items = this._items.filter((_item, i) => !this._selectedIndexes.includes(i)); } // Make the selection empty so that calling `get()` returns an empty array. createSelection(this); return removedItems; } } module.exports = Salty;