UNPKG

30.7 kBJavaScriptView Raw
1/* eslint-disable new-cap */
2'use strict';
3
4var _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
6function _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
8var Search = require('scour-search');
9var assign = require('object-assign');
10var buildExtensions = require('./build_extensions');
11var normalizeKeypath = require('../utilities/normalize_keypath');
12var utils = require('../utilities');
13var negate = require('./negate');
14var sortValues = require('../utilities/sort_values');
15var 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
58function 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
71scour.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
965assign(scour, utils);
966
967/**
968 * Internal: decorates collection functions
969 */
970
971function 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
982function 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
999function 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
1010module.exports = scour;
\No newline at end of file