Spaces:
Sleeping
Sleeping
| /* | |
| Copyright 2022 the JSDoc Authors. | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| https://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| 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; | |