123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- 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;
|