1 | /* eslint-disable new-cap */
|
2 | ;
|
3 |
|
4 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
|
5 |
|
6 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
7 |
|
8 | var Search = require('scour-search');
|
9 | var assign = require('object-assign');
|
10 | var buildExtensions = require('./build_extensions');
|
11 | var normalizeKeypath = require('../utilities/normalize_keypath');
|
12 | var utils = require('../utilities');
|
13 | var negate = require('./negate');
|
14 | var sortValues = require('../utilities/sort_values');
|
15 | var toFunction = require('to-function');
|
16 |
|
17 | /**
|
18 | * scour : scour(object)
|
19 | * Returns a scour instance wrapping `object`.
|
20 | *
|
21 | * scour(obj)
|
22 | *
|
23 | * It can be called on any Object or Array. (In fact, it can be called on
|
24 | * anything, but is only generally useful for Objects and Arrays.)
|
25 | *
|
26 | * data = { menu: { visible: true, position: 'left' } }
|
27 | * scour(data).get('menu.visible')
|
28 | *
|
29 | * list = [ { id: 2 }, { id: 5 }, { id: 12 } ]
|
30 | * scour(list).get('0.id')
|
31 | *
|
32 | * __Chaining__:
|
33 | * You can use it to start method chains. In fact, the intended use is to keep
|
34 | * your root [scour] object around, and chain from this.
|
35 | *
|
36 | * db = scour({ menu: { visible: true, position: 'left' } })
|
37 | *
|
38 | * // Elsewhere:
|
39 | * menu = db.go('menu')
|
40 | * menu.get('visible')
|
41 | *
|
42 | * __Properties__:
|
43 | * It the [root], [value] and [keypath] properties.
|
44 | *
|
45 | * s = scour(obj)
|
46 | * s.root // => [scour object]
|
47 | * s.value // => raw data (that is, `obj`)
|
48 | * s.keypath // => string array
|
49 | *
|
50 | * __Accessing the value:__
|
51 | * You can access the raw data using [value].
|
52 | *
|
53 | * db = scour(data)
|
54 | * db.value // => same as `data`
|
55 | * db.go('users').value // => same as `data.users`
|
56 | */
|
57 |
|
58 | function scour(value, options) {
|
59 | if (!(this instanceof scour)) return new scour(value, options);
|
60 | this.value = value;
|
61 |
|
62 | this.root = options && options.root || this;
|
63 | this.keypath = options && options.keypath || [];
|
64 | this.extensions = options && options.extensions || [];
|
65 | this.indices = options && options.indices || undefined;
|
66 |
|
67 | // Apply any property extensions
|
68 | if (this.extensions.length) this.applyExtensions();
|
69 | }
|
70 |
|
71 | scour.prototype = {
|
72 | /**
|
73 | * Chaining methods:
|
74 | * (Section) These methods are used to traverse nested structures. All these
|
75 | * methods return [scour] instances, making them suitable for chaining.
|
76 | *
|
77 | * #### On null values
|
78 | * Note that `undefined`, `false` and `null` values are still [scour]-wrapped
|
79 | * when returned from [go()], [at()] and [find()].
|
80 | *
|
81 | * list = [ { name: 'Homer' }, { name: 'Bart' } ]
|
82 | *
|
83 | * scour(list).at(4) // => [ scour undefined ]
|
84 | * scour(list).at(4).value // => undefined
|
85 | *
|
86 | * This is done so that you can chain methods safely even when something is null.
|
87 | * This behavior is consistent with what you'd expect with jQuery.
|
88 | *
|
89 | * data = { users: { ... } }
|
90 | * db = scour(data)
|
91 | *
|
92 | * db.go('blogposts').map((post) => post.get('title'))
|
93 | * // => []
|
94 | */
|
95 |
|
96 | /**
|
97 | * go : go(keypath...)
|
98 | * Navigates down to a given `keypath`. Always returns a [scour] instance.
|
99 | * Rules [on null values] apply.
|
100 | *
|
101 | * data =
|
102 | * { users:
|
103 | * { 12: { name: 'steve', last: 'jobs' },
|
104 | * 23: { name: 'bill', last: 'gates' } } }
|
105 | *
|
106 | * scour(data).go('users') // => [scour (users)]
|
107 | * scour(data).go('users', '12') // => [scour (name, last)]
|
108 | * scour(data).go('users', '12').get('name') // => 'steve'
|
109 | *
|
110 | * __Dot notation:__
|
111 | * Keypaths can be given in dot notation or as an array. These statements are
|
112 | * equivalent.
|
113 | *
|
114 | * scour(data).go('users.12')
|
115 | * scour(data).go('users', '12')
|
116 | * scour(data).go(['users', '12'])
|
117 | *
|
118 | * __Non-objects:__
|
119 | * If you use it on a non-object or non-array value, it will still be
|
120 | * returned as a [scour] instance. This is not likely what you want; use
|
121 | * [get()] instead.
|
122 | *
|
123 | * attr = scour(data).go('users', '12', 'name')
|
124 | * attr // => [scour object]
|
125 | * attr.value // => 'steve'
|
126 | * attr.keypath // => ['users', '12', 'name']
|
127 | */
|
128 |
|
129 | go: function go() {
|
130 | var keypath = normalizeKeypath(arguments, true);
|
131 | var result = this.get.apply(this, keypath);
|
132 | return this._get(result, keypath);
|
133 | },
|
134 |
|
135 | /**
|
136 | * Internal: gathers multiple keys; not used yet
|
137 | */
|
138 |
|
139 | gather: function gather(keypaths) {
|
140 | var _this = this;
|
141 |
|
142 | var result;
|
143 | if (Array.isArray(this.value)) {
|
144 | result = keypaths.map(function (key, val) {
|
145 | return _this.get(key);
|
146 | });
|
147 | } else {
|
148 | result = utils.indexedMap(keypaths, function (key) {
|
149 | return [key, _this.get(key)];
|
150 | });
|
151 | }
|
152 | return this.reset(result);
|
153 | },
|
154 |
|
155 | /**
|
156 | * Returns the item at `index`. This differs from `go` as this searches by
|
157 | * index, not by key. This returns a the raw value, unlike [getAt()]. Rules
|
158 | * [on null values] apply.
|
159 | *
|
160 | * users =
|
161 | * { 12: { name: 'steve' },
|
162 | * 23: { name: 'bill' } }
|
163 | *
|
164 | * scour(users).at(0) // => [scour { name: 'steve' }]
|
165 | * scour(users).get(12) // => [scour { name: 'steve' }]
|
166 | */
|
167 |
|
168 | at: function at(index) {
|
169 | if (Array.isArray(this.value)) {
|
170 | return this._get(this.value[index], ['' + index]);
|
171 | }
|
172 |
|
173 | var key = this.keys()[index];
|
174 | return this._get(key && this.value[key], ['' + key]);
|
175 | },
|
176 |
|
177 | /**
|
178 | * Returns the item at `index`. This differs from `get` as this searches by
|
179 | * index, not by key. This returns a the raw value, unlike [at()].
|
180 | * *(Since v0.5)*
|
181 | *
|
182 | * users =
|
183 | * { 12: { name: 'steve' },
|
184 | * 23: { name: 'bill' } }
|
185 | *
|
186 | * scour(users).at(0) // => [scour { name: 'steve' }]
|
187 | * scour(users).getAt(0) // => { name: 'steve' }
|
188 | */
|
189 |
|
190 | getAt: function getAt(index) {
|
191 | if (Array.isArray(this.value)) return this.value[index];
|
192 | var key = this.keys()[index];
|
193 | return key && this.value[key];
|
194 | },
|
195 |
|
196 | /**
|
197 | * Sifts through the values and returns a set that matches given
|
198 | * `conditions`. Supports simple objects, MongoDB-style
|
199 | * queries, and functions.
|
200 | *
|
201 | * scour(data).filter({ name: 'Moe' })
|
202 | * scour(data).filter({ name: { $in: ['Larry', 'Curly'] })
|
203 | * scour(data).filter((item) => item.get('name') === 'Moe')
|
204 | *
|
205 | * __Filter by object:__
|
206 | * If you pass an object as a condition, `filter()` will check if that object
|
207 | * coincides with the objects in the collection.
|
208 | *
|
209 | * scour(data).filter({ name: 'Moe' })
|
210 | *
|
211 | * __Filter by function:__
|
212 | * You may pass a function as a parameter. In this case, the `item` being
|
213 | * passed to the callback will be a [scour]-wrapped object. The result
|
214 | * will also be a [scour]-wrapped object, making it chainable.
|
215 | *
|
216 | * scour(data)
|
217 | * .filter((item, key) => +item.get('price') > 200)
|
218 | * .sortBy('price')
|
219 | * .first()
|
220 | *
|
221 | * __Advanced queries:__
|
222 | * MongoDB-style queries are supported as provided by [sift.js]. For
|
223 | * reference, see [MongoDB Query Operators][query-ops].
|
224 | *
|
225 | * scour(products).filter({ price: { $gt: 200 })
|
226 | * scour(articles).filter({ published_at: { $not: null }})
|
227 | *
|
228 | * __Arrays or objects:__
|
229 | * Both arrays and array-like objects are supported. In this example below,
|
230 | * an object will be used as the input.
|
231 | *
|
232 | * devices =
|
233 | * { 1: { id: 1, name: 'Phone', mobile: true },
|
234 | * 2: { id: 2, name: 'Tablet', mobile: true },
|
235 | * 3: { id: 3, name: 'Desktop', mobile: false } }
|
236 | *
|
237 | * scour(devices).filter({ mobile: true }).len()
|
238 | * // => 2
|
239 | *
|
240 | * Also see [scour.filter()] for the unwrapped version.
|
241 | *
|
242 | * [query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
|
243 | */
|
244 |
|
245 | filter: function filter(conditions) {
|
246 | if (!this.value) return this.reset([]);
|
247 | if (typeof conditions === 'function') {
|
248 | return this.filterByFunction(conditions);
|
249 | }
|
250 |
|
251 | var idx = this.indices && this.indices[this.keypath.join('.')] || Search(this.value);
|
252 |
|
253 | return this.reset(idx.filter(conditions));
|
254 | },
|
255 | filterByFunction: function filterByFunction(fn) {
|
256 | var isArray = Array.isArray(this.value);
|
257 | var result;
|
258 |
|
259 | if (isArray) {
|
260 | result = [];
|
261 | this.each(function (val, key) {
|
262 | return fn(val, key) && result.push(val.value);
|
263 | });
|
264 | } else {
|
265 | result = {};
|
266 | this.each(function (val, key) {
|
267 | if (fn(val, key)) result[key] = val.value;
|
268 | });
|
269 | }
|
270 |
|
271 | return this.reset(result);
|
272 | },
|
273 |
|
274 | /**
|
275 | * Inverse of [filter()] -- see `filter()` documentation for details.
|
276 | */
|
277 |
|
278 | reject: function reject(conditions) {
|
279 | if (!this.value) return this.reset([]);
|
280 | if (typeof conditions === 'function') {
|
281 | return this.filterByFunction(negate(conditions));
|
282 | } else {
|
283 | return this.filter({ $not: conditions });
|
284 | }
|
285 | },
|
286 |
|
287 | /**
|
288 | * Returns the first value that matches `conditions`. Supports MongoDB-style
|
289 | * queries. For reference, see [MongoDB Query Operators][query-ops]. Also
|
290 | * see [filter()], as this is functionally-equivalent to the first result of
|
291 | * `filter()`. Rules [on null values] apply.
|
292 | *
|
293 | * [query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
|
294 | *
|
295 | * scour(data).find({ name: 'john' })
|
296 | * scour(data).find({ name: { $in: ['moe', 'larry'] })
|
297 | */
|
298 |
|
299 | find: function find(conditions) {
|
300 | var value = this.value;
|
301 | var key = Search(value).indexOf(conditions);
|
302 | if (key === -1) return;
|
303 | return this._get(value[key], [key]);
|
304 | },
|
305 |
|
306 | /**
|
307 | * Returns the first result as a [scour]-wrapped object. This is equivalent
|
308 | * to [at(0)](#at).
|
309 | */
|
310 |
|
311 | first: function first() {
|
312 | return this.at(0);
|
313 | },
|
314 |
|
315 | /**
|
316 | * Returns the first result as a [scour]-wrapped object. This is equivalent
|
317 | * to `at(len() - 1)`: see [at()] and [len()].
|
318 | */
|
319 |
|
320 | last: function last() {
|
321 | var len = this.len();
|
322 | return this.at(len - 1);
|
323 | },
|
324 |
|
325 | /**
|
326 | * Sorts a collection. Returns a [scour]-wrapped object suitable for
|
327 | * chaining. Like other chainable methods, this works on arrays as well as
|
328 | * objects. *(Since v0.8)*
|
329 | *
|
330 | * data =
|
331 | * { 0: { name: 'Wilma' },
|
332 | * 1: { name: 'Barney' },
|
333 | * 2: { name: 'Fred' } }
|
334 | *
|
335 | * scour(data).sortBy('name').value
|
336 | * // { 1: { name: 'Barney' },
|
337 | * // 2: { name: 'Fred' },
|
338 | * // 0: { name: 'Wilma' } }
|
339 | *
|
340 | * __Conditions:__
|
341 | * The given condition can be a string or a function. When it's given as a
|
342 | * function, the `item` being passed is a [scour]-wrapped object, just like
|
343 | * in [forEach()] (et al). These two examples below are
|
344 | * functionally-equivalent.
|
345 | *
|
346 | * scour(data).sortBy('name')
|
347 | * scour(data).sortBy((item) => item.get('name'))
|
348 | *
|
349 | * You may also define nested keys in dot-notation:
|
350 | *
|
351 | * scour(data).sortBy('user.name')
|
352 | */
|
353 |
|
354 | sortBy: function sortBy(condition) {
|
355 | if (!this.value) return this.reset([]);
|
356 | var values;
|
357 |
|
358 | if (typeof condition === 'string') {
|
359 | var key = condition;
|
360 | condition = toFunction(key);
|
361 | // don't use `this.map` or `this.each` so we skip `new scour()`
|
362 | values = utils.map(this.value, function (value, key, index) {
|
363 | return {
|
364 | key: key, value: value, criteria: condition(value, key), index: index
|
365 | };
|
366 | });
|
367 | } else {
|
368 | values = this.map(function (value, key, index) {
|
369 | return {
|
370 | key: key, value: value.value, criteria: condition(value, key), index: index
|
371 | };
|
372 | });
|
373 | }
|
374 |
|
375 | var sorted = sortValues(values, Array.isArray(this.value));
|
376 | return this.reset(sorted);
|
377 | },
|
378 |
|
379 | /**
|
380 | * Reading methods:
|
381 | * (Section) For retrieving data.
|
382 | */
|
383 |
|
384 | /**
|
385 | * get : get(keypath...)
|
386 | * Returns data in a given `keypath`.
|
387 | *
|
388 | * data =
|
389 | * { users:
|
390 | * { 12: { name: 'steve' },
|
391 | * 23: { name: 'bill' } } }
|
392 | *
|
393 | * scour(data).get('users') // => same as data.users
|
394 | * scour(data).go('users').value // => same as data.users
|
395 | *
|
396 | * __Dot notation:__
|
397 | * Like [go()], the `keypath` can be given in dot notation.
|
398 | *
|
399 | * scour(data).get('books.featured.name')
|
400 | * scour(data).get('books', 'featured', 'name')
|
401 | */
|
402 |
|
403 | get: function get() {
|
404 | if (!this.value) return;
|
405 | var keypath = normalizeKeypath(arguments, true);
|
406 | return utils.get(this.value, keypath);
|
407 | },
|
408 |
|
409 | /**
|
410 | * Returns the length of the object or array. For objects, it returns the
|
411 | * number of keys.
|
412 | *
|
413 | * users =
|
414 | * { 12: { name: 'steve' },
|
415 | * 23: { name: 'bill' } }
|
416 | *
|
417 | * names = scour(users).len() // => 2
|
418 | */
|
419 |
|
420 | len: function len() {
|
421 | if (!this.value) return 0;
|
422 | if (Array.isArray(this.value)) return this.value.length;
|
423 | return this.keys().length;
|
424 | },
|
425 |
|
426 | /**
|
427 | * Returns an array. If the the value is an object, it returns the values of
|
428 | * that object. If the value is an array, it returns it as is. Also aliased
|
429 | * as `values()`.
|
430 | *
|
431 | * users =
|
432 | * { 12: { name: 'steve' },
|
433 | * 23: { name: 'bill' } }
|
434 | *
|
435 | * names = scour(users).toArray()
|
436 | * // => [ {name: 'steve'}, {name: 'bill'} ]
|
437 | */
|
438 |
|
439 | toArray: function toArray() {
|
440 | if (Array.isArray(this.value)) return this.value;
|
441 | return scour.map(this.value, function (val, key) {
|
442 | return val;
|
443 | });
|
444 | },
|
445 | values: function values() {
|
446 | return this.toArray();
|
447 | },
|
448 |
|
449 | /**
|
450 | * Returns keys. If the value is an array, this returns the array's indices.
|
451 | * Also see [toArray()] to retrieve the values instead.
|
452 | */
|
453 |
|
454 | keys: function keys() {
|
455 | if (!this.value) return [];
|
456 | return Object.keys(this.value);
|
457 | },
|
458 |
|
459 | /**
|
460 | * Writing methods:
|
461 | * (Section) These are methods for modifying an object/array tree immutably.
|
462 | * Note that all these functions are immutable--it will not modify existing
|
463 | * data, but rather spawn new objects with the modifications done on them.
|
464 | */
|
465 |
|
466 | /**
|
467 | * Sets values immutably. Returns a copy of the same object ([scour]-wrapped)
|
468 | * with the modifications applied.
|
469 | *
|
470 | * data = { bob: { name: 'Bob' } }
|
471 | * db = scour(data)
|
472 | * db.set([ 'bob', 'name' ], 'Robert')
|
473 | * // db.value == { bob: { name: 'Robert' } }
|
474 | *
|
475 | * __Immutability:__
|
476 | * This is an immutable function, and will return a new object. It won't
|
477 | * modify your original object.
|
478 | *
|
479 | * profile = scour({ name: 'John' })
|
480 | * profile2 = profile.set('email', 'john@gmail.com')
|
481 | *
|
482 | * profile.value // => { name: 'John' }
|
483 | * profile2.value // => { name: 'John', email: 'john@gmail.com' }
|
484 | *
|
485 | * __Using within a scope:__
|
486 | * Be aware that using all writing methods ([set()], [del()], [extend()]) on
|
487 | * scoped objects (ie, made with [go()]) will spawn a new [root] object. If
|
488 | * you're keeping a reference to the root object, you'll need to update it
|
489 | * accordingly.
|
490 | *
|
491 | * db = scour(data)
|
492 | * book = db.go('book')
|
493 | * book.root === db // correct so far
|
494 | *
|
495 | * book = book.set('title', 'IQ84')
|
496 | * book = book.del('sale_price')
|
497 | * book.root !== db // `root` has been updated
|
498 | *
|
499 | * __Dot notation:__
|
500 | * Like [go()] and [get()], the keypath can be given in dot notation or an
|
501 | * array.
|
502 | *
|
503 | * scour(data).set('menu.left.visible', true)
|
504 | * scour(data).set(['menu', 'left', 'visible'], true)
|
505 | */
|
506 |
|
507 | set: function set(keypath, value) {
|
508 | keypath = normalizeKeypath(keypath);
|
509 |
|
510 | if (this.root !== this) {
|
511 | return this.root.set(this.keypath.concat(keypath), value).go(this.keypath);
|
512 | }
|
513 |
|
514 | // use .valueOf() to denature any scour-wrapping or String() or whatnot
|
515 | var result = scour.set(this.value || {}, keypath, value.valueOf());
|
516 |
|
517 | // Update indices, if any
|
518 | var indices = updateIndices(this.indices, result, keypath);
|
519 |
|
520 | return this.reset(result, { root: null, indices: indices });
|
521 | },
|
522 |
|
523 | /**
|
524 | * Deletes values immutably. Returns a copy of the same object
|
525 | * ([scour]-wrapped) with the modifications applied.
|
526 | *
|
527 | * Like [set()], the keypath can be given in dot notation or an
|
528 | * array.
|
529 | *
|
530 | * scour(data).del('menu.left.visible')
|
531 | * scour(data).del(['menu', 'left', 'visible'])
|
532 | *
|
533 | * See [set()] for more information on working with immutables.
|
534 | */
|
535 |
|
536 | del: function del(keypath) {
|
537 | if (!this.value) return this;
|
538 | keypath = normalizeKeypath(keypath);
|
539 |
|
540 | if (this.root !== this) {
|
541 | return this.root.del(this.keypath.concat(keypath)).go(this.keypath);
|
542 | }
|
543 |
|
544 | var result = scour.del(this.value, keypath);
|
545 | var indices = updateIndices(this.indices, result, keypath);
|
546 | return this.reset(result, { root: null, indices: indices });
|
547 | },
|
548 |
|
549 | /**
|
550 | * extend : extend(objects...)
|
551 | * Extends the data with more values. Returns a [scour]-wrapped object. Just
|
552 | * like [Object.assign], you may pass multiple objects to the parameters.
|
553 | *
|
554 | * data = { a: 1, b: 2 }
|
555 | * data2 = scour(data).extend({ c: 3 })
|
556 | *
|
557 | * data2 // => [scour { a: 1, b: 2, c: 3 }]
|
558 | * data2.value // => { a: 1, b: 2, c: 3 }
|
559 | *
|
560 | * When used with anything non-object, it will be overridden.
|
561 | *
|
562 | * data = {}
|
563 | * db = scour(data)
|
564 | * db = db.go('state').extend({ pressed: true }).root
|
565 | *
|
566 | * db.value // => { state: { pressed: true } }
|
567 | *
|
568 | * See [set()] for more information on working with immutables.
|
569 | */
|
570 |
|
571 | extend: function extend() {
|
572 | var result = {};
|
573 | if (_typeof(this.value) === 'object' && !Array.isArray(this.value)) {
|
574 | assign(result, this.value);
|
575 | }
|
576 |
|
577 | for (var i = 0, len = arguments.length; i < len; i++) {
|
578 | if (_typeof(arguments[i]) !== 'object') return;
|
579 | assign(result, arguments[i]);
|
580 | }
|
581 |
|
582 | if (this.root !== this) {
|
583 | return this.root.set(this.keypath, result).go(this.keypath);
|
584 | }
|
585 |
|
586 | // TODO
|
587 | var indices = updateAllIndices(this.indices, result);
|
588 | return this.reset(result, { root: false, indices: indices });
|
589 | },
|
590 |
|
591 | /**
|
592 | * Utility methods:
|
593 | * (Section) For stuff.
|
594 | */
|
595 |
|
596 | /**
|
597 | * use : use(extensions)
|
598 | * Extends functionality for certain keypaths with custom methods.
|
599 | * See [Extensions example] for examples.
|
600 | *
|
601 | * data =
|
602 | * { users:
|
603 | * { 12: { name: 'steve', surname: 'jobs' },
|
604 | * 23: { name: 'bill', surname: 'gates' } } }
|
605 | *
|
606 | * extensions = {
|
607 | * 'users.*': {
|
608 | * fullname () {
|
609 | * return this.get('name') + ' ' + this.get('surname')
|
610 | * }
|
611 | * }
|
612 | * }
|
613 | *
|
614 | * scour(data)
|
615 | * .use(extensions)
|
616 | * .get('users', 12)
|
617 | * .fullname() // => 'bill gates'
|
618 | *
|
619 | * __Extensions format:__
|
620 | * The parameter `extension` is an object, with keys being keypath globs, and
|
621 | * values being properties to be extended.
|
622 | *
|
623 | * .use({
|
624 | * 'books.*': { ... },
|
625 | * 'authors.*': { ... },
|
626 | * 'publishers.*': { ... }
|
627 | * })
|
628 | *
|
629 | * __Extending root:__
|
630 | * To bind properties to the root method, use an empty string as the keypath.
|
631 | *
|
632 | * .use({
|
633 | * '': {
|
634 | * users() { return this.go('users') },
|
635 | * authors() { return this.go('authors') }
|
636 | * }
|
637 | * })
|
638 | *
|
639 | * __Keypath filtering:__
|
640 | * You can use glob-like `*` and `**` to match parts of a keypath. A `*` will
|
641 | * match any one segment, and `**` will match one or many segments. Here are
|
642 | * some examples:
|
643 | *
|
644 | * - `users.*` - will match `users.1`, but not `users.1.photos`
|
645 | * - `users.**` - will match `users.1.photos`
|
646 | * - `users.*.photos` - will match `users.1.photos`
|
647 | * - `**` will match anything
|
648 | *
|
649 | * __When using outside root:__
|
650 | * Any extensions in a scoped object (ie, made with [go()]) will be used relative
|
651 | * to it. For instance, if you define an extension to `admins.*` inside
|
652 | * `.go('users')`, it will affect `users.
|
653 | *
|
654 | * data = { users: { john: { } }
|
655 | * db = scour(data)
|
656 | *
|
657 | * users = db.go('users')
|
658 | * .use({ '*': { hasName () { return !!this.get('name') } })
|
659 | *
|
660 | * users.go('john').hasName() // works
|
661 | *
|
662 | * While this is supported, it is *not* recommended: these extensions will not
|
663 | * propagate back to the root, and any objects taken from the root will not
|
664 | * have those extensions applied to them.
|
665 | *
|
666 | * users.go('john').hasName() // works
|
667 | * db.go('users.john').hasName() // doesn't work
|
668 | */
|
669 |
|
670 | use: function use(spec) {
|
671 | var extensions = buildExtensions(this.keypath, spec);
|
672 | if (this.root === this) {
|
673 | return this.reset(this.value, { extensions: extensions, root: null });
|
674 | } else {
|
675 | // Spawn a new `root` with the extensions applied
|
676 | return this.root.reset(this.root.value, { extensions: extensions, root: null }).reset(this.value, { keypath: this.keypath });
|
677 | }
|
678 | },
|
679 |
|
680 | /**
|
681 | * index : index(keypath, field)
|
682 | * Sets up indices to improve [filter()] performance. *(Since v0.12)*
|
683 | *
|
684 | * - `keypath` *(String | Array)* - the keypath of the collection.
|
685 | * - `field` *(String)* - the name of the field to be indexed.
|
686 | *
|
687 | * data =
|
688 | * { users:
|
689 | * { 1: { name: 'John Creamer' },
|
690 | * 2: { name: 'Stephane K' } } }
|
691 | *
|
692 | * db = scour(data).index('users', 'name')
|
693 | * db.filter({ name: 'Stephane K' })
|
694 | *
|
695 | * Doing this will add an index in the root (acccessible via
|
696 | * `scour().indices`) to make searches faster for certain [filter()] queries.
|
697 | * Any writing actions ([set()], [extend()], [del()]) will automatically
|
698 | * update the index.
|
699 | *
|
700 | * See [scour-search] for more information on indexing.
|
701 | *
|
702 | * [scour-search]: https://github.com/rstacruz/scour-search
|
703 | */
|
704 |
|
705 | index: function index(keypath, field) {
|
706 | keypath = normalizeKeypath(keypath);
|
707 | if (this.root !== this) return this.root.index(keypath);
|
708 |
|
709 | var oldIndices = this.indices || {};
|
710 | var keypathStr = keypath.join('.');
|
711 |
|
712 | var indices = assign({}, oldIndices, _defineProperty({}, keypathStr, (oldIndices[keypathStr] || Search(this.get(keypath) || {})).index(field)));
|
713 |
|
714 | // TODO remove ||{}
|
715 | return this.reset(this.value, { indices: indices, root: null });
|
716 | },
|
717 |
|
718 | /**
|
719 | * Returns the value for serialization. This allows `JSON.stringify()` to
|
720 | * work with `scour`-wrapped objects. The name of this method is a bit
|
721 | * confusing, as it doesn't actually return a JSON string — but I'm afraid
|
722 | * that it's the way that the JavaScript API for [JSON.stringify] works.
|
723 | *
|
724 | * [JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior
|
725 | */
|
726 |
|
727 | toJSON: function toJSON() {
|
728 | return this.value;
|
729 | },
|
730 | valueOf: function valueOf() {
|
731 | return this.value;
|
732 | },
|
733 |
|
734 | /**
|
735 | * Checks for equality between two Scour-wrapped objects.
|
736 | *
|
737 | * a = scour(data)
|
738 | * b = scour(data)
|
739 | *
|
740 | * a.equal(b) // => true
|
741 | */
|
742 |
|
743 | equal: function equal(other) {
|
744 | return this.value === other.value && this.keypath.join('.') === other.keypath.join('.');
|
745 | },
|
746 | toString: function toString() {
|
747 | return '[scour (' + this.keys().join(', ') + ')]';
|
748 | },
|
749 |
|
750 | /**
|
751 | * Iteration methods:
|
752 | * (Section) These methods are generally useful for collections. These
|
753 | * methods can work with either arrays or array-like objects, such as
|
754 | * below.
|
755 | *
|
756 | * subjects =
|
757 | * { 1: { id: 1, title: 'Math', level: 101 },
|
758 | * 2: { id: 2, title: 'Science', level: 103 },
|
759 | * 3: { id: 3, title: 'History', level: 102 } }
|
760 | *
|
761 | * __Values:__
|
762 | * For all these functions, The items passed onto the callbacks _is_ a
|
763 | * [scour]-wrapped object. Use `item.value` or `this` to access the raw
|
764 | * values.
|
765 | *
|
766 | * scour(subjects).forEach((subject, key) => {
|
767 | * console.log(subject.get('title'))
|
768 | * })
|
769 | *
|
770 | * __Return values:__
|
771 | * For methods that return values (such as [map()], the returned results _is
|
772 | * not_ a [scour]-wrapped object, and isn't suitable for chaining.
|
773 | *
|
774 | * scour(subjects).map((subject, key) => {
|
775 | * return subject.get('title') + ' ' + subject.get('level')
|
776 | * })
|
777 | * // => [ 'Math 101', 'Science 103', 'History 102' ]
|
778 | */
|
779 |
|
780 | /**
|
781 | * forEach : forEach(function(item, key, index))
|
782 | * Loops through each item. Supports both arrays and objects.
|
783 | * The rules specified in [Iteration methods] apply.
|
784 | *
|
785 | * users =
|
786 | * { 12: { name: 'steve' },
|
787 | * 23: { name: 'bill' } }
|
788 | *
|
789 | * scour(users).each((user, key) => {
|
790 | * console.log(user.get('name'))
|
791 | * })
|
792 | *
|
793 | * The values passed onto the function are:
|
794 | *
|
795 | * - `item` - the value; always a scour object.
|
796 | * - `key` - the key.
|
797 | * - `index` - the index.
|
798 | */
|
799 |
|
800 | forEach: function forEach(fn) {
|
801 | var _this2 = this;
|
802 |
|
803 | var index = 0;
|
804 | scour.each(this.value, function (val, key) {
|
805 | fn.call(val, _this2._get(val, [key]), key, index++);
|
806 | });
|
807 | return this;
|
808 | },
|
809 |
|
810 | /**
|
811 | * Alias for [forEach](#foreach).
|
812 | */
|
813 |
|
814 | each: function each(fn) {
|
815 | return this.forEach(fn);
|
816 | },
|
817 |
|
818 | /**
|
819 | * map : map(function(item, key))
|
820 | * Loops through each item and returns an array based on the iterator's
|
821 | * return values. Supports both arrays and objects.
|
822 | * The rules specified in [Iteration methods] apply.
|
823 | *
|
824 | * users =
|
825 | * { 12: { name: 'Steve' },
|
826 | * 23: { name: 'Bill' } }
|
827 | *
|
828 | * names = scour(users).map((user, key) => user.get('name'))
|
829 | * // => [ 'Steve', 'Bill' ]
|
830 | */
|
831 |
|
832 | map: thisify(utils.map),
|
833 |
|
834 | /**
|
835 | * mapObject : mapObject(function(val, key))
|
836 | * Creates a new `Object` with with the results of calling a provided function
|
837 | * on every element in this array. Works like [Array#map], but also works on
|
838 | * objects as well as arrays, and it returns an object instead.
|
839 | * The rules specified in [Iteration methods] apply.
|
840 | *
|
841 | * See [scour.mapObject()] for details and the non-wrapped version.
|
842 | */
|
843 |
|
844 | mapObject: thisify(utils.mapObject),
|
845 |
|
846 | /**
|
847 | * indexedMap : indexedMap(function(val, key))
|
848 | * Creates a new `Object` with with the results of calling a provided function
|
849 | * returning the keys and values for the new object.
|
850 | * The rules specified in [Iteration methods] apply.
|
851 | *
|
852 | * See [scour.indexedMap()] for details and the non-wrapped version.
|
853 | */
|
854 |
|
855 | indexedMap: thisify(utils.indexedMap),
|
856 |
|
857 | /**
|
858 | * Internal: spawns an instance with a given data and keypath.
|
859 | */
|
860 |
|
861 | _get: function _get(result, keypath) {
|
862 | return this.reset(result, {
|
863 | keypath: this.keypath.concat(keypath)
|
864 | });
|
865 | },
|
866 |
|
867 | /**
|
868 | * Returns a clone with the `value` replaced. The new instance will
|
869 | * retain the same properties, so things like [use()] extensions are carried
|
870 | * over.
|
871 | *
|
872 | * db = scour({ name: 'hello' })
|
873 | * db.value //=> { name: 'hello' }
|
874 | *
|
875 | * db = db.reset({})
|
876 | * db.value // => {}
|
877 | *
|
878 | * This is useful for, say, using Scour with [Redux] and implementing an
|
879 | * action to reset the state back to empty.
|
880 | */
|
881 |
|
882 | reset: function reset(value, options) {
|
883 | var op = options || {};
|
884 | return new scour(value, {
|
885 | root: typeof op.root !== 'undefined' ? op.root : this.root,
|
886 | keypath: typeof op.keypath !== 'undefined' ? op.keypath : this.keypath,
|
887 | indices: typeof op.indices !== 'undefined' ? op.indices : this.indices,
|
888 | extensions: typeof op.extensions !== 'undefined' ? this.extensions.concat(op.extensions) : this.extensions
|
889 | });
|
890 | },
|
891 |
|
892 | /**
|
893 | * Internal: applies extensions
|
894 | */
|
895 |
|
896 | applyExtensions: function applyExtensions() {
|
897 | var _this3 = this;
|
898 |
|
899 | var path = this.keypath.join('.');
|
900 |
|
901 | this.extensions.forEach(function (extension) {
|
902 | // extension is [ RegExp, properties object ]
|
903 | if (extension[0].test(path)) assign(_this3, extension[1]);
|
904 | });
|
905 | }
|
906 | };
|
907 |
|
908 | /**
|
909 | * Attributes:
|
910 | * (Section) These attributes are available to [scour] instances.
|
911 | */
|
912 |
|
913 | /**
|
914 | * value : value
|
915 | * The raw value being wrapped. You can use this to terminate a chained call.
|
916 | *
|
917 | * users =
|
918 | * [ { name: 'john', admin: true },
|
919 | * { name: 'kyle', admin: false } ]
|
920 | *
|
921 | * scour(users)
|
922 | * .filter({ admin: true })
|
923 | * .value
|
924 | * // => [ { name: 'john', admin: true } ]
|
925 | */
|
926 |
|
927 | /**
|
928 | * root : root
|
929 | * A reference to the root [scour] instance.
|
930 | * Everytime you traverse using [go()], a new [scour] object is spawned that's
|
931 | * scoped to a keypath. Each of these [scour] objects have a `root` attribute
|
932 | * that's a reference to the top-level [scour] object.
|
933 | *
|
934 | * db = scour(...)
|
935 | *
|
936 | * photos = db.go('photos')
|
937 | * photos.root // => same as `db`
|
938 | *
|
939 | * This allows you to return to the root when needed.
|
940 | *
|
941 | * db = scour(...)
|
942 | * artist = db.go('artists', '9328')
|
943 | * artist.root.go('albums').find({ artist_id: artist.get('id') })
|
944 | */
|
945 |
|
946 | /**
|
947 | * keypath : keypath
|
948 | * An array of strings representing each step in how deep the current scope is
|
949 | * relative to the root. Each time you traverse using [go()], a new [scour]
|
950 | * object is spawned.
|
951 | *
|
952 | * db = scour(...)
|
953 | *
|
954 | * users = db.go('users')
|
955 | * users.keypath // => ['users']
|
956 | *
|
957 | * admins = users.go('admins')
|
958 | * admins.keypath // => ['users', 'admins']
|
959 | *
|
960 | * user = admins.go('23')
|
961 | * user.keypath // => ['users', 'admins', '23']
|
962 | */
|
963 |
|
964 | // Export utilities
|
965 | assign(scour, utils);
|
966 |
|
967 | /**
|
968 | * Internal: decorates collection functions
|
969 | */
|
970 |
|
971 | function thisify(fn) {
|
972 | return function () {
|
973 | return fn.bind(null, this.forEach.bind(this)).apply(this, arguments);
|
974 | };
|
975 | }
|
976 |
|
977 | /**
|
978 | * Internal: in the root, given the new data `result`, do an update because
|
979 | * `keypath` was changed.
|
980 | */
|
981 |
|
982 | function updateIndices(indices, result, keypath) {
|
983 | if (!indices) return;
|
984 |
|
985 | for (var len = keypath.length, i = len; i >= 0; i--) {
|
986 | var newKeypath = keypath.slice(0, i);
|
987 | var keypathStr = newKeypath.join('.');
|
988 | if (indices[keypathStr]) {
|
989 | var newData = utils.get(result, newKeypath);
|
990 | var keys = i === len ? Object.keys(newData) : keypath[i];
|
991 |
|
992 | indices[keypathStr] = indices[keypathStr].reindex(newData, keys);
|
993 | }
|
994 | }
|
995 |
|
996 | return indices;
|
997 | }
|
998 |
|
999 | function updateAllIndices(indices, result) {
|
1000 | if (!indices) return;
|
1001 |
|
1002 | var keys = Object.keys(result);
|
1003 | for (var i = 0, len = keys.length; i < len; i++) {
|
1004 | indices = updateIndices(indices, result, [keys[i]]);
|
1005 | }
|
1006 |
|
1007 | return indices;
|
1008 | }
|
1009 |
|
1010 | module.exports = scour; |
\ | No newline at end of file |