UNPKG

20.4 kBJavaScriptView Raw
1/* eslint-disable new-cap */
2'use strict'
3
4const sift = require('sift')
5const assign = require('object-assign')
6const buildExtensions = require('./lib/build_extensions')
7const normalizeKeypath = require('./lib/normalize_keypath')
8
9/**
10 * scour : scour(object)
11 * Returns a scour instance wrapping `object`.
12 *
13 * scour(obj)
14 *
15 * It can be called on any Object or Array. (In fact, it can be called on
16 * anything, but is only generally useful for Objects and Arrays.)
17 *
18 * data = { menu: { visible: true, position: 'left' } }
19 * scour(data).get('menu.visible')
20 *
21 * list = [ { id: 2 }, { id: 5 }, { id: 12 } ]
22 * scour(list).get('0.id')
23 *
24 * __Chaining__:
25 * You can use it to start method chains. In fact, the intended use is to keep
26 * your root [scour] object around, and chain from this.
27 *
28 * db = scour({ menu: { visible: true, position: 'left' } })
29 *
30 * // Elsewhere:
31 * menu = db.go('menu')
32 * menu.get('visible')
33 *
34 * __Properties__:
35 * It the [root], [value] and [keypath] properties.
36 *
37 * s = scour(obj)
38 * s.root // => [scour object]
39 * s.value // => raw data (that is, `obj`)
40 * s.keypath // => string array
41 *
42 * __Accessing the value:__
43 * You can access the raw data using [value].
44 *
45 * db = scour(data)
46 * db.value // => same as `data`
47 * db.go('users').value // => same as `data.users`
48 */
49
50function scour (value, options) {
51 if (!(this instanceof scour)) return new scour(value, options)
52 this.value = value
53
54 this.root = options && options.root || this
55 this.keypath = options && options.keypath || []
56 this.extensions = options && options.extensions || []
57
58 // Apply any property extensions
59 if (this.extensions.length) this.applyExtensions()
60}
61
62/**
63 * Attributes:
64 * (Section) These attributes are available to [scour] instances.
65 */
66
67/**
68 * value : value
69 * The raw value being wrapped. You can use this to terminate a chained call.
70 *
71 * users =
72 * [ { name: 'john', admin: true },
73 * { name: 'kyle', admin: false } ]
74 *
75 * scour(users)
76 * .filter({ admin: true })
77 * .value
78 * // => [ { name: 'john', admin: true } ]
79 */
80
81/**
82 * root : root
83 * A reference to the root [scour] instance.
84 * Everytime you traverse using [go()], a new [scour] object is spawned that's
85 * scoped to a keypath. Each of these [scour] objects have a `root` attribute
86 * that's a reference to the top-level [scour] object.
87 *
88 * db = scour(...)
89 *
90 * photos = db.go('photos')
91 * photos.root // => same as `db`
92 *
93 * This allows you to return to the root when needed.
94 *
95 * db = scour(...)
96 * artist = db.go('artists', '9328')
97 * artist.root.go('albums').find({ artist_id: artist.get('id') })
98 */
99
100/**
101 * keypath : keypath
102 * An array of strings representing each step in how deep the current scope is
103 * relative to the root. Each time you traverse using [go()], a new [scour]
104 * object is spawned.
105 *
106 * db = scour(...)
107 *
108 * users = db.go('users')
109 * users.keypath // => ['users']
110 *
111 * admins = users.go('admins')
112 * admins.keypath // => ['users', 'admins']
113 *
114 * user = admins.go('23')
115 * user.keypath // => ['users', 'admins', '23']
116 */
117
118scour.prototype = {
119 /**
120 * Traversal methods:
121 * (Section) For traversing. All these methods return [scour] instances,
122 * making them suitable for chaining.
123 */
124
125 /**
126 * go : go(keypath...)
127 * Navigates down to a given `keypath`. Always returns a [scour] instance.
128 *
129 * data =
130 * { users:
131 * { 12: { name: 'steve', last: 'jobs' },
132 * 23: { name: 'bill', last: 'gates' } } }
133 *
134 * scour(data).go('users') // => [scour (users)]
135 * scour(data).go('users', '12') // => [scour (name, last)]
136 * scour(data).go('users', '12').get('name') // => 'steve'
137 *
138 * __Dot notation:__
139 * Keypaths can be given in dot notation or as an array. These statements are
140 * equivalent.
141 *
142 * scour(data).go('users.12')
143 * scour(data).go('users', '12')
144 * scour(data).go(['users', '12'])
145 *
146 * __Non-objects:__
147 * If you use it on a non-object or non-array value, it will still be
148 * returned as a [scour] instance. This is not likely what you want; use
149 * [get()] instead.
150 *
151 * attr = scour(data).go('users', '12', 'name')
152 * attr // => [scour object]
153 * attr.value // => 'steve'
154 * attr.keypath // => ['users', '12', 'name']
155 */
156
157 go () {
158 const keypath = normalizeKeypath(arguments, true)
159 const result = this.get.apply(this, keypath)
160 return this._get(result, keypath, true)
161 },
162
163 /**
164 * Returns the item at `index`. This differs from `go` as this searches by
165 * index, not by key.
166 *
167 * users =
168 * { 12: { name: 'steve' },
169 * 23: { name: 'bill' } }
170 *
171 * scour(users).at(0) // => [scour { name: 'steve' }]
172 * scour(users).get(12) // => [scour { name: 'steve' }]
173 */
174
175 at (index) {
176 if (Array.isArray(this.value)) {
177 return this._get(this.value[index], [ '' + index ])
178 }
179
180 const key = this.keys()[index]
181 return this._get(this.value[key], [ '' + key ])
182 },
183
184 /**
185 * Sifts through the values and returns a set that matches given
186 * `conditions`. Supports functions, simple objects, and MongoDB-style
187 * queries.
188 *
189 * [query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
190 *
191 * scour(data).filter({ name: 'john' })
192 * scour(data).filter({ name: { $in: ['moe', 'larry'] })
193 *
194 * MongoDB-style queries are supported as provided by [sift.js]. For
195 * reference, see [MongoDB Query Operators][query-ops].
196 *
197 * scour(products).filter({ price: { $gt: 200 })
198 * scour(articles).filter({ published_at: { $not: null }})
199 */
200
201 filter (conditions) {
202 const results = sift(conditions, this.value)
203 return this._get(results, [])
204 },
205
206 /**
207 * Returns the first value that matches `conditions`. Supports MongoDB-style
208 * queries. For reference, see [MongoDB Query Operators][query-ops]. Also
209 * see [filter()], as this is functionally-equivalent to the first result of
210 * `filter()`.
211 *
212 * [query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
213 *
214 * scour(data).find({ name: 'john' })
215 * scour(data).find({ name: { $in: ['moe', 'larry'] })
216 */
217
218 find (conditions) {
219 const key = sift.keyOf(conditions, this.value)
220 if (typeof key === 'undefined') return
221 return this._get(this.value[key], [key])
222 },
223
224 /**
225 * Returns the first result as a [scour]-wrapped object. This is equivalent
226 * to [at(0)](#at).
227 */
228
229 first () {
230 return this.at(0)
231 },
232
233 /**
234 * Returns the first result as a [scour]-wrapped object. This is equivalent
235 * to `at(len() - 1)`: see [at()] and [len()].
236 */
237
238 last () {
239 return this.at(this.len() - 1)
240 },
241
242 /**
243 * Reading methods:
244 * (Section) For retrieving data.
245 */
246
247 /**
248 * get : get(keypath...)
249 * Returns data in a given `keypath`.
250 *
251 * data =
252 * { users:
253 * { 12: { name: 'steve' },
254 * 23: { name: 'bill' } } }
255 *
256 * scour(data).get('users') // => same as data.users
257 * scour(data).go('users').value // => same as data.users
258 *
259 * __Dot notation:__
260 * Like [go()], the `keypath` can be given in dot notation.
261 *
262 * scour(data).get('books.featured.name')
263 * scour(data).get('books', 'featured', 'name')
264 */
265
266 get () {
267 let result = this.value
268 const keypath = normalizeKeypath(arguments, true)
269
270 for (let i = 0, len = keypath.length; i < len; i++) {
271 result = result[keypath[i]]
272 if (!result) return
273 }
274
275 return result
276 },
277
278 /**
279 * Returns the length of the object or array. For objects, it returns the
280 * number of keys.
281 *
282 * users =
283 * { 12: { name: 'steve' },
284 * 23: { name: 'bill' } }
285 *
286 * names = scour(users).len() // => 2
287 */
288
289 len () {
290 if (!this.value) return 0
291 if (Array.isArray(this.value)) return this.value.length
292 return this.keys().length
293 },
294
295 /**
296 * Returns an array. If the the value is an object, it returns the values of
297 * that object.
298 *
299 * users =
300 * { 12: { name: 'steve' },
301 * 23: { name: 'bill' } }
302 *
303 * names = scour(users).toArray()
304 * // => [ {name: 'steve'}, {name: 'bill'} ]
305 */
306
307 toArray () {
308 if (Array.isArray(this.value)) return this.value
309 var result = []
310 scour.each(this.value, (val, key) => result.push(val))
311 return result
312 },
313
314 /**
315 * Alias for `toArray()`.
316 */
317
318 values () {
319 return this.toArray()
320 },
321
322 /**
323 * Returns keys. If the value is an array, this returns the array's indices.
324 */
325
326 keys () {
327 return Object.keys(this.value)
328 },
329
330 /**
331 * Writing methods:
332 * (Section) These are methods for modifying an object/array tree immutably.
333 * Note that all these functions are immutable--it will not modify existing
334 * data, but rather spawn new objects with the modifications done on them.
335 */
336
337 /**
338 * Sets values immutably. Returns a copy of the same object ([scour]-wrapped)
339 * with the modifications applied.
340 *
341 * data = { bob: { name: 'Bob' } }
342 * db = scour(data)
343 * db.set([ 'bob', 'name' ], 'Robert')
344 * // db.value == { bob: { name: 'Robert' } }
345 *
346 * __Immutability:__
347 * This is an immutable function, and will return a new object. It won't
348 * modify your original object.
349 *
350 * profile = scour({ name: 'John' })
351 * profile2 = profile.set('email', 'john@gmail.com')
352 *
353 * profile.value // => { name: 'John' }
354 * profile2.value // => { name: 'John', email: 'john@gmail.com' }
355 *
356 * __Using within a scope:__
357 * Be aware that using all writing methods ([set()], [del()], [extend()]) on
358 * scoped objects (ie, made with [go()]) will spawn a new [root] object. If
359 * you're keeping a reference to the root object, you'll need to update it
360 * accordingly.
361 *
362 * db = scour(data)
363 * book = db.go('book')
364 * book.root === db // correct so far
365 *
366 * book = book.set('title', 'IQ84')
367 * book = book.del('sale_price')
368 * book.root !== db // `root` has been updated
369 *
370 * __Dot notation:__
371 * Like [go()] and [get()], the keypath can be given in dot notation or an
372 * array.
373 *
374 * scour(data).set('menu.left.visible', true)
375 * scour(data).set(['menu', 'left', 'visible'], true)
376 */
377
378 set (keypath, value) {
379 keypath = normalizeKeypath(keypath)
380
381 if (this.root !== this) {
382 return this.root
383 .set(this.keypath.concat(keypath), value).go(this.keypath)
384 }
385
386 const result = scour.set(this.value, keypath, value)
387 return this.replace(result, { root: null })
388 },
389
390 /**
391 * Deletes values immutably. Returns a copy of the same object
392 * ([scour]-wrapped) with the modifications applied.
393 *
394 * Like [set()], the keypath can be given in dot notation or an
395 * array.
396 *
397 * scour(data).del('menu.left.visible')
398 * scour(data).del(['menu', 'left', 'visible'])
399 *
400 * See [set()] for more information on working with immutables.
401 */
402
403 del (keypath) {
404 keypath = normalizeKeypath(keypath)
405
406 if (this.root !== this) {
407 return this.root.del(this.keypath.concat(keypath)).go(this.keypath)
408 }
409
410 const result = scour.del(this.value, keypath)
411 return this.replace(result, { root: null })
412 },
413
414 /**
415 * extend : extend(objects...)
416 * Extends the data with more values. Returns a [scour]-wrapped object. Only
417 * supports objects; arrays and non-objects will return undefined. Just like
418 * [Object.assign], you may pass multiple objects to the parameters.
419 *
420 * data = { a: 1, b: 2 }
421 * data2 = scour(data).extend({ c: 3 })
422 *
423 * data2 // => [scour { a: 1, b: 2, c: 3 }]
424 * data2.value // => { a: 1, b: 2, c: 3 }
425 *
426 * See [set()] for more information on working with immutables.
427 */
428
429 extend () {
430 if (typeof this.value !== 'object' || Array.isArray(this.value)) return
431 var result = {}
432 assign(result, this.value)
433 for (var i = 0, len = arguments.length; i < len; i++) {
434 if (typeof arguments[i] !== 'object') return
435 assign(result, arguments[i])
436 }
437
438 if (this.root !== this) {
439 return this.root.set(this.keypath, result).go(this.keypath)
440 }
441
442 return this.replace(result)
443 },
444
445 /**
446 * Utility methods:
447 * (Section) For stuff.
448 */
449
450 /**
451 * use : use(extensions)
452 * Extends functionality for certain keypaths with custom methods.
453 * See [Extensions example] for examples.
454 *
455 * data =
456 * { users:
457 * { 12: { name: 'steve', surname: 'jobs' },
458 * 23: { name: 'bill', surname: 'gates' } } }
459 *
460 * extensions = {
461 * 'users.*': {
462 * fullname () {
463 * return this.get('name') + ' ' + this.get('surname')
464 * }
465 * }
466 * }
467 *
468 * scour(data)
469 * .use(extensions)
470 * .get('users', 12)
471 * .fullname() // => 'bill gates'
472 *
473 * __Extensions format:__
474 * The parameter `extension` is an object, with keys being keypath globs, and
475 * values being properties to be extended.
476 *
477 * .use({
478 * 'books.*': { ... },
479 * 'authors.*': { ... },
480 * 'publishers.*': { ... }
481 * })
482 *
483 * __Extending root:__
484 * To bind properties to the root method, use an empty string as the keypath.
485 *
486 * .use({
487 * '': {
488 * users() { return this.go('users') },
489 * authors() { return this.go('authors') }
490 * }
491 * })
492 *
493 * __Keypath filtering:__
494 * You can use glob-like `*` and `**` to match parts of a keypath. A `*` will
495 * match any one segment, and `**` will match one or many segments. Here are
496 * some examples:
497 *
498 * - `users.*` - will match `users.1`, but not `users.1.photos`
499 * - `users.**` - will match `users.1.photos`
500 * - `users.*.photos` - will match `users.1.photos`
501 * - `**` will match anything
502 *
503 * __When using outside root:__
504 * Any extensions in a scoped object (ie, made with [go()]) will be used relative
505 * to it. For instance, if you define an extension to `admins.*` inside
506 * `.go('users')`, it will affect `users.
507 *
508 * data = { users: { john: { } }
509 * db = scour(data)
510 *
511 * users = db.go('users')
512 * .use({ '*': { hasName () { return !!this.get('name') } })
513 *
514 * users.go('john').hasName() // works
515 *
516 * While this is supported, it is *not* recommended: these extensions will not
517 * propagate back to the root, and any objects taken from the root will not
518 * have those extensions applied to them.
519 *
520 * users.go('john').hasName() // works
521 * db.go('users.john').hasName() // doesn't work
522 */
523
524 use (spec) {
525 const extensions = buildExtensions(this.keypath, spec)
526 if (this.root === this) {
527 return this.replace(this.value, { extensions, root: null })
528 } else {
529 // Spawn a new `root` with the extensions applied
530 return this.root
531 .replace(this.root.value, { extensions, root: null })
532 .replace(this.value, { keypath: this.keypath })
533 }
534 },
535
536 /**
537 * Returns the value for serialization. This allows `JSON.stringify()` to
538 * work with `scour`-wrapped objects. The name of this method is a bit
539 * confusing, as it doesn't actually return a JSON string — but I'm afraid
540 * that it's the way that the JavaScript API for [JSON.stringify] works.
541 *
542 * [JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior
543 */
544
545 toJSON () {
546 return this.value
547 },
548
549 valueOf () {
550 return this.value
551 },
552
553 toString () {
554 return `[scour (${this.keys().join(', ')})]`
555 },
556
557 /**
558 * Iteration methods:
559 * (Section) For traversing.
560 */
561
562 /**
563 * forEach : forEach(function(item, key))
564 * Loops through each item. Supports both arrays and objects. The `item`s
565 * passed to the function will be returned as a [scour] instance.
566 *
567 * users =
568 * { 12: { name: 'steve' },
569 * 23: { name: 'bill' } }
570 *
571 * scour(users).each((user, key) => {
572 * console.log(user.get('name'))
573 * })
574 *
575 * The values passed onto the function are:
576 *
577 * - `item` - the value; always a scour object.
578 * - `key` - the key.
579 *
580 * The value being passed onto the function is going to be a [scour] object.
581 * Use `item.value` or `this` to access the raw values.
582 */
583
584 forEach (fn) {
585 scour.each(this.value, (val, key) => {
586 fn.call(val, this._get(val, [key]), key)
587 })
588 return this
589 },
590
591 /**
592 * Alias for [forEach](#foreach).
593 */
594
595 each (fn) {
596 return this.forEach(fn)
597 },
598
599 /**
600 * map : map(function(item, key))
601 * Loops through each item and returns an array based on the iterator's
602 * return values. Supports both arrays and objects. The `item`s passed to
603 * the function will be returned as a [scour] instance.
604 *
605 * users =
606 * { 12: { name: 'steve' },
607 * 23: { name: 'bill' } }
608 *
609 * names = scour(users).map((user, key) => user.get('name'))
610 * // => [ 'steve', 'bill' ]
611 */
612
613 map: thisify(require('./utilities/map')),
614
615 /**
616 * Internal: spawns an instance with a given data and keypath.
617 */
618
619 _get (result, keypath) {
620 if (typeof result === 'undefined' || result === null) return result
621 return this.replace(result, {
622 keypath: this.keypath.concat(keypath)
623 })
624 },
625
626 /**
627 * Internal: Returns a clone with the `value` replaced. The new instance will
628 * retain the same properties, so things like [use()] extensions are carried
629 * over. You may pass additional `options`.
630 *
631 * db = scour(data)
632 * db = db.replace(newData)
633 *
634 * I don't think this will be useful outside internal use.
635 */
636
637 replace (value, options) {
638 const op = options || {}
639 return new scour(value || this.value, {
640 root:
641 typeof op.root !== 'undefined' ? op.root : this.root,
642 keypath:
643 typeof op.keypath !== 'undefined' ? op.keypath : this.keypath,
644 extensions: typeof op.extensions !== 'undefined'
645 ? this.extensions.concat(op.extensions)
646 : this.extensions
647 })
648 },
649
650 /**
651 * Internal: applies extensions
652 */
653
654 applyExtensions () {
655 var path = this.keypath.join('.')
656
657 this.extensions.forEach((extension) => {
658 // extension is [ RegExp, properties object ]
659 if (extension[0].test(path)) assign(this, extension[1])
660 })
661 }
662}
663
664/**
665 * Utility functions:
666 * (Section) These are utilities that don't need a wrapped object.
667 */
668
669/**
670 * scour.set : scour.set(object, keypath, value)
671 * Sets a `keypath` into an `object` immutably.
672 *
673 * data = { users: { bob: { name: 'john' } } }
674 *
675 * result = set(data, ['users', 'bob', 'name'], 'robert')
676 * // => { users: { bob: { name: 'robert' } } }
677 *
678 * This is also available as `require('scourjs/utilities/set')`.
679 */
680
681scour.set = require('./utilities/set')
682
683/**
684 * scour.del : scour.del(object, keypath)
685 * Deletes a `keypath` from an `object` immutably.
686 *
687 * data = { users: { bob: { name: 'robert' } } }
688 * result = del(data, ['users', 'bob', 'name'])
689 *
690 * // => { users: { bob: {} } }
691 *
692 * This is also available as `require('scourjs/utilities/del')`.
693 */
694
695scour.del = require('./utilities/del')
696
697/**
698 * scour.each : scour.each(iterable, fn)
699 * Iterates through `iterable`, either an object or an array. This is an
700 * implementation of `Array.forEach` that also works for objects.
701 *
702 * This is also available as `require('scourjs/utilities/each')`.
703 */
704
705scour.each = require('./utilities/each')
706
707/**
708 * scour.map : scour.map(iterable, fn)
709 * Works like Array#map, but also works on objects.
710 *
711 * This is also available as `require('scourjs/utilities/map')`.
712 */
713
714scour.map = require('./utilities/map')
715
716/**
717 * Internal: decorates collection functions
718 */
719
720function thisify (fn) {
721 return function () {
722 return fn.bind(null, this.forEach.bind(this)).apply(this, arguments)
723 }
724}
725
726module.exports = scour