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