End-of-life
July 2020: Thank you to all the users and contributors who have made this project possible. Moving forward, please consider alternatives such as immer.
scour.js
Traverse objects and arrays immutably
Scour is a general-purpose library for dealing with JSON trees.
As a simple utility with a broad purpose, it can be used to solve many problems. Use it to:
- Manage your Redux datastore.
- Provide a model layer to access data in your single-page app. →
- Navigate a large JSON tree easily.
- Rejoice in having a lightweight alternative to Immutable.js. (Compare)
Install
npm install --save-exact scourjs
window.scour // non commonjs
const scour = require('scourjs') // commonjs/node
import scour from 'scourjs' // es6 modules
Features
Calling scour(object)
returns a wrapper that you can use to traverse object
.
Use get() to retrieve values.
data =
{ users:
{ 1: { name: 'john' },
2: { name: 'shane', confirmed: true },
3: { name: 'barry', confirmed: true } } }
scour(data).get('users', '1', 'name') // => 'john'
Traversal
Use go() to dig into the structure. It will return another scour
wrapper scoped to that object.
data =
{ users:
{ admins:
{ bob: { logged_in: true },
sue: { logged_in: false } } } }
users = scour(data).go('users') // => [scour (admins)]
admins = scour(data).go('users', 'admins') // => [scour (bob, sue)]
admins.go('bob').get('logged_in') // => true
Chaining
scour()
provides a wrapper that can be used to chain methods. This is inspired by Underscore and Lodash.
scour(data)
.go('users')
.filter({ admin: true })
.value
Immutable modifications
Use set() to update values. Scout treats all data as immutable, so this
doesn't modify your original data
, but gets you a new one with the
modifications made.
data = scour(data)
.set(['users', '1', 'updated_at'], +new Date())
.value
// => { users:
// { 1: { name: 'john', updated_at: 1450667171188 },
// 2: { name: 'shane', confirmed: true },
// 3: { name: 'barry', confirmed: true } } }
Advanced traversing
Use filter() to filter results with advanced querying.
users = scour(data).go('users')
users
.filter({ confirmed: true })
.at(0)
.get('name') // => 'shane'
Models
Use use() to add your own methods to certain keypaths. This makes them behave like models.
See a detailed example to learn more.
Sample data
data =
{ artists:
{ 1: { first_name: 'Louie', last_name: 'Armstrong' },
2: { first_name: 'Miles', last_name: 'Davis' } } }
Your models
Root = {
artists () { return this.go('artists') }
}
Artist = {
fullname () {
return this.get('first_name') + ' ' + this.get('last_name')
}
}
Using with scour
db = scour(data)
.use({
'': Root,
'artists.*': Artist
})
db.artists().find({ name: 'Miles' }).fullname()
//=> 'Miles Davis'
API
scour
scour(object)
Returns a scour instance wrapping object
.
scour(obj)
It can be called on any Object or Array. (In fact, it can be called on anything, but is only generally useful for Objects and Arrays.)
data = { menu: { visible: true, position: 'left' } }
scour(data).get('menu.visible')
list = [ { id: 2 }, { id: 5 }, { id: 12 } ]
scour(list).get('0.id')
Chaining: You can use it to start method chains. In fact, the intended use is to keep your root scour object around, and chain from this.
db = scour({ menu: { visible: true, position: 'left' } })
// Elsewhere:
menu = db.go('menu')
menu.get('visible')
Properties: It the root, value and keypath properties.
s = scour(obj)
s.root // => [scour object]
s.value // => raw data (that is, `obj`)
s.keypath // => string array
Accessing the value: You can access the raw data using value.
db = scour(data)
db.value // => same as `data`
db.go('users').value // => same as `data.users`
Chaining methods
These methods are used to traverse nested structures. All these methods return scour instances, making them suitable for chaining.
On null values
Note that undefined
, false
and null
values are still scour-wrapped
when returned from go(), at() and [find()].
list = [ { name: 'Homer' }, { name: 'Bart' } ]
scour(list).at(4) // => [ scour undefined ]
scour(list).at(4).value // => undefined
This is done so that you can chain methods safely even when something is null. This behavior is consistent with what you'd expect with jQuery.
data = { users: { ... } }
db = scour(data)
db.go('blogposts').map((post) => post.get('title'))
// => []
go
go(keypath...)
Navigates down to a given keypath
. Always returns a scour instance.
Rules on null values apply.
data =
{ users:
{ 12: { name: 'steve', last: 'jobs' },
23: { name: 'bill', last: 'gates' } } }
scour(data).go('users') // => [scour (users)]
scour(data).go('users', '12') // => [scour (name, last)]
scour(data).go('users', '12').get('name') // => 'steve'
Dot notation: Keypaths can be given in dot notation or as an array. These statements are equivalent.
scour(data).go('users.12')
scour(data).go('users', '12')
scour(data).go(['users', '12'])
Non-objects: If you use it on a non-object or non-array value, it will still be returned as a scour instance. This is not likely what you want; use get() instead.
attr = scour(data).go('users', '12', 'name')
attr // => [scour object]
attr.value // => 'steve'
attr.keypath // => ['users', '12', 'name']
at
at(index)
Returns the item at index
. This differs from go
as this searches by
index, not by key. This returns a the raw value, unlike getAt(). Rules
on null values apply.
users =
{ 12: { name: 'steve' },
23: { name: 'bill' } }
scour(users).at(0) // => [scour { name: 'steve' }]
scour(users).get(12) // => [scour { name: 'steve' }]
getAt
getAt(index)
Returns the item at index
. This differs from get
as this searches by
index, not by key. This returns a the raw value, unlike at().
(Since v0.5)
users =
{ 12: { name: 'steve' },
23: { name: 'bill' } }
scour(users).at(0) // => [scour { name: 'steve' }]
scour(users).getAt(0) // => { name: 'steve' }
filter
filter(conditions)
Sifts through the values and returns a set that matches given
conditions
. Supports simple objects, MongoDB-style
queries, and functions.
scour(data).filter({ name: 'Moe' })
scour(data).filter({ name: { $in: ['Larry', 'Curly'] })
scour(data).filter((item) => item.get('name') === 'Moe')
Filter by object:
If you pass an object as a condition, filter()
will check if that object
coincides with the objects in the collection.
scour(data).filter({ name: 'Moe' })
Filter by function:
You may pass a function as a parameter. In this case, the item
being
passed to the callback will be a scour-wrapped object. The result
will also be a scour-wrapped object, making it chainable.
scour(data)
.filter((item, key) => +item.get('price') > 200)
.sortBy('price')
.first()
Advanced queries: MongoDB-style queries are supported as provided by sift.js. For reference, see MongoDB Query Operators.
scour(products).filter({ price: { $gt: 200 })
scour(articles).filter({ published_at: { $not: null }})
Arrays or objects: Both arrays and array-like objects are supported. In this example below, an object will be used as the input.
devices =
{ 1: { id: 1, name: 'Phone', mobile: true },
2: { id: 2, name: 'Tablet', mobile: true },
3: { id: 3, name: 'Desktop', mobile: false } }
scour(devices).filter({ mobile: true }).len()
// => 2
Also see scour.filter() for the unwrapped version.
reject
reject(conditions)
Inverse of filter() -- see filter()
documentation for details.
find
find(conditions)
Returns the first value that matches conditions
. Supports MongoDB-style
queries. For reference, see MongoDB Query Operators. Also
see filter(), as this is functionally-equivalent to the first result of
filter()
. Rules on null values apply.
scour(data).find({ name: 'john' })
scour(data).find({ name: { $in: ['moe', 'larry'] })
first
first()
Returns the first result as a scour-wrapped object. This is equivalent to at(0).
last
last()
Returns the first result as a scour-wrapped object. This is equivalent
to at(len() - 1)
: see at() and len().
sortBy
sortBy(condition)
Sorts a collection. Returns a scour-wrapped object suitable for chaining. Like other chainable methods, this works on arrays as well as objects. (Since v0.8)
data =
{ 0: { name: 'Wilma' },
1: { name: 'Barney' },
2: { name: 'Fred' } }
scour(data).sortBy('name').value
// { 1: { name: 'Barney' },
// 2: { name: 'Fred' },
// 0: { name: 'Wilma' } }
Conditions:
The given condition can be a string or a function. When it's given as a
function, the item
being passed is a scour-wrapped object, just like
in forEach() (et al). These two examples below are
functionally-equivalent.
scour(data).sortBy('name')
scour(data).sortBy((item) => item.get('name'))
You may also define nested keys in dot-notation:
scour(data).sortBy('user.name')
Reading methods
For retrieving data.
get
get(keypath...)
Returns data in a given keypath
.
data =
{ users:
{ 12: { name: 'steve' },
23: { name: 'bill' } } }
scour(data).get('users') // => same as data.users
scour(data).go('users').value // => same as data.users
Dot notation:
Like go(), the keypath
can be given in dot notation.
scour(data).get('books.featured.name')
scour(data).get('books', 'featured', 'name')
len
len()
Returns the length of the object or array. For objects, it returns the number of keys.
users =
{ 12: { name: 'steve' },
23: { name: 'bill' } }
names = scour(users).len() // => 2
toArray
toArray()
Returns an array. If the the value is an object, it returns the values of
that object. If the value is an array, it returns it as is. Also aliased
as values()
.
users =
{ 12: { name: 'steve' },
23: { name: 'bill' } }
names = scour(users).toArray()
// => [ {name: 'steve'}, {name: 'bill'} ]
keys
keys()
Returns keys. If the value is an array, this returns the array's indices. Also see toArray() to retrieve the values instead.
Writing methods
These are methods for modifying an object/array tree immutably. Note that all these functions are immutable--it will not modify existing data, but rather spawn new objects with the modifications done on them.
set
set(keypath, value)
Sets values immutably. Returns a copy of the same object (scour-wrapped) with the modifications applied.
data = { bob: { name: 'Bob' } }
db = scour(data)
db = db.set([ 'bob', 'name' ], 'Robert')
// db.value == { bob: { name: 'Robert' } }
Immutability: This is an immutable function, and will return a new object. It won't modify your original object.
profile = scour({ name: 'John' })
profile2 = profile.set('email', '[email protected]')
profile.value // => { name: 'John' }
profile2.value // => { name: 'John', email: '[email protected]' }
Using within a scope: Be aware that using all writing methods (set(), del(), extend()) on scoped objects (ie, made with go()) will spawn a new root object. If you're keeping a reference to the root object, you'll need to update it accordingly.
db = scour(data)
book = db.go('book')
book.root === db // correct so far
book = book.set('title', 'IQ84')
book = book.del('sale_price')
book.root !== db // `root` has been updated
Dot notation: Like go() and get(), the keypath can be given in dot notation or an array.
scour(data).set('menu.left.visible', true)
scour(data).set(['menu', 'left', 'visible'], true)
del
del(keypath)
Deletes values immutably. Returns a copy of the same object (scour-wrapped) with the modifications applied.
Like set(), the keypath can be given in dot notation or an array.
scour(data).del('menu.left.visible')
scour(data).del(['menu', 'left', 'visible'])
See set() for more information on working with immutables.
extend
extend(objects...)
Extends the data with more values. Returns a scour-wrapped object. Just like Object.assign, you may pass multiple objects to the parameters.
data = { a: 1, b: 2 }
data2 = scour(data).extend({ c: 3 })
data2 // => [scour { a: 1, b: 2, c: 3 }]
data2.value // => { a: 1, b: 2, c: 3 }
When used with anything non-object, it will be overridden.
data = {}
db = scour(data)
db = db.go('state').extend({ pressed: true }).root
db.value // => { state: { pressed: true } }
See set() for more information on working with immutables.
Utility methods
For stuff.
use
use(extensions)
Extends functionality for certain keypaths with custom methods. See Extensions example for examples.
data =
{ users:
{ 12: { name: 'steve', surname: 'jobs' },
23: { name: 'bill', surname: 'gates' } } }
extensions = {
'users.*': {
fullname () {
return this.get('name') + ' ' + this.get('surname')
}
}
}
scour(data)
.use(extensions)
.get('users', 12)
.fullname() // => 'bill gates'
Extensions format:
The parameter extension
is an object, with keys being keypath globs, and
values being properties to be extended.
.use({
'books.*': { ... },
'authors.*': { ... },
'publishers.*': { ... }
})
Extending root: To bind properties to the root method, use an empty string as the keypath.
.use({
'': {
users() { return this.go('users') },
authors() { return this.go('authors') }
}
})
Keypath filtering:
You can use glob-like *
and **
to match parts of a keypath. A *
will
match any one segment, and **
will match one or many segments. Here are
some examples:
users.*
- will matchusers.1
, but notusers.1.photos
users.**
- will matchusers.1.photos
users.*.photos
- will matchusers.1.photos
**
will match anything
When using outside root:
Any extensions in a scoped object (ie, made with go()) will be used relative
to it. For instance, if you define an extension to admins.*
inside
.go('users')
, it will affect `users.
data = { users: { john: { } }
db = scour(data)
users = db.go('users')
.use({ '*': { hasName () { return !!this.get('name') } })
users.go('john').hasName() // works
While this is supported, it is not recommended: these extensions will not propagate back to the root, and any objects taken from the root will not have those extensions applied to them.
users.go('john').hasName() // works
db.go('users.john').hasName() // doesn't work
index
index(keypath, field)
Sets up indices to improve filter() performance. (Since v0.12)
keypath
(String | Array) - the keypath of the collection.field
(String) - the name of the field to be indexed.
data =
{ users:
{ 1: { name: 'John Creamer' },
2: { name: 'Stephane K' } } }
db = scour(data).index('users', 'name')
db.filter({ name: 'Stephane K' })
Doing this will add an index in the root (acccessible via
scour().indices
) to make searches faster for certain filter() queries.
Any writing actions (set(), extend(), del()) will automatically
update the index.
See scour-search for more information on indexing.
toJSON
toJSON()
Returns the value for serialization. This allows JSON.stringify()
to
work with scour
-wrapped objects. The name of this method is a bit
confusing, as it doesn't actually return a JSON string — but I'm afraid
that it's the way that the JavaScript API for JSON.stringify works.
equal
equal(other)
Checks for equality between two Scour-wrapped objects.
a = scour(data)
b = scour(data)
a.equal(b) // => true
Iteration methods
These methods are generally useful for collections. These methods can work with either arrays or array-like objects, such as below.
subjects =
{ 1: { id: 1, title: 'Math', level: 101 },
2: { id: 2, title: 'Science', level: 103 },
3: { id: 3, title: 'History', level: 102 } }
Values:
For all these functions, The items passed onto the callbacks is a
scour-wrapped object. Use item.value
or this
to access the raw
values.
scour(subjects).forEach((subject, key) => {
console.log(subject.get('title'))
})
Return values: For methods that return values (such as map(), the returned results is not a scour-wrapped object, and isn't suitable for chaining.
scour(subjects).map((subject, key) => {
return subject.get('title') + ' ' + subject.get('level')
})
// => [ 'Math 101', 'Science 103', 'History 102' ]
forEach
forEach(function(item, key, index))
Loops through each item. Supports both arrays and objects. The rules specified in Iteration methods apply.
users =
{ 12: { name: 'steve' },
23: { name: 'bill' } }
scour(users).each((user, key) => {
console.log(user.get('name'))
})
The values passed onto the function are:
item
- the value; always a scour object.key
- the key.index
- the index.
each
each(fn)
Alias for forEach.
map
map(function(item, key))
Loops through each item and returns an array based on the iterator's return values. Supports both arrays and objects. The rules specified in Iteration methods apply.
users =
{ 12: { name: 'Steve' },
23: { name: 'Bill' } }
names = scour(users).map((user, key) => user.get('name'))
// => [ 'Steve', 'Bill' ]
mapObject
mapObject(function(val, key))
Creates a new Object
with with the results of calling a provided function
on every element in this array. Works like Array#map, but also works on
objects as well as arrays, and it returns an object instead.
The rules specified in Iteration methods apply.
See scour.mapObject() for details and the non-wrapped version.
indexedMap
indexedMap(function(val, key))
Creates a new Object
with with the results of calling a provided function
returning the keys and values for the new object.
The rules specified in Iteration methods apply.
See scour.indexedMap() for details and the non-wrapped version.
reset
reset(value, options)
Returns a clone with the value
replaced. The new instance will
retain the same properties, so things like use() extensions are carried
over.
db = scour({ name: 'hello' })
db.value //=> { name: 'hello' }
db = db.reset({})
db.value // => {}
This is useful for, say, using Scour with Redux and implementing an action to reset the state back to empty.
Attributes
These attributes are available to scour instances.
value
value
The raw value being wrapped. You can use this to terminate a chained call.
users =
[ { name: 'john', admin: true },
{ name: 'kyle', admin: false } ]
scour(users)
.filter({ admin: true })
.value
// => [ { name: 'john', admin: true } ]
root
root
A reference to the root scour instance.
Everytime you traverse using go(), a new scour object is spawned that's
scoped to a keypath. Each of these scour objects have a root
attribute
that's a reference to the top-level scour object.
db = scour(...)
photos = db.go('photos')
photos.root // => same as `db`
This allows you to return to the root when needed.
db = scour(...)
artist = db.go('artists', '9328')
artist.root.go('albums').find({ artist_id: artist.get('id') })
keypath
keypath
An array of strings representing each step in how deep the current scope is relative to the root. Each time you traverse using go(), a new scour object is spawned.
db = scour(...)
users = db.go('users')
users.keypath // => ['users']
admins = users.go('admins')
admins.keypath // => ['users', 'admins']
user = admins.go('23')
user.keypath // => ['users', 'admins', '23']
Utility functions
These are utilities that don't need a wrapped object.
scour.get
scour.get(object, keypath)
Gets a keypath from an object.
data = { users: { bob: { name: 'john' } } }
result = get(data, ['users', 'bob', 'name'])
// => 'robert'
This is also available as require('scourjs/utilities/get')
.
scour.set
scour.set(object, keypath, value)
Sets a keypath
into an object
immutably.
data = { users: { bob: { name: 'john' } } }
result = set(data, ['users', 'bob', 'name'], 'robert')
// => { users: { bob: { name: 'robert' } } }
This is also available as require('scourjs/utilities/set')
.
scour.del
scour.del(object, keypath)
Deletes a keypath
from an object
immutably.
data = { users: { bob: { name: 'robert' } } }
result = del(data, ['users', 'bob', 'name'])
// => { users: { bob: {} } }
This is also available as require('scourjs/utilities/del')
.
scour.extendIn
scour.extendIn(object, keypath, extensions...)
Extends a keypath
from an object
immutably.
data = { users: { bob: { name: 'robert' } } }
result = extendIn(data, ['users', 'bob'], { email: '[email protected]' })
// => { users: { bob: { name: 'robert', email: '[email protected]' } } }
This is also available as require('scourjs/utilities/extend_in')
.
scour.each
scour.each(iterable, fn)
Iterates through iterable
, either an object or an array. This is an
implementation of Array#forEach that also works for objects. The callback
fn
will be invoked with two parameters: currentValue
and key
, just
like Array#forEach
.
This is also available as require('scourjs/utilities/each')
.
scour.map
scour.map(iterable, fn)
Creates a new Array
with with the results of calling a provided function
on every element in this array. Works like Array#map, but also works on
objects as well as arrays.
The callback fn
will be invoked with two parameters: currentValue
and
key
, just like Array#map.
This is also available as require('scourjs/utilities/map')
.
scour.mapObject
scour.mapObject(iterable, fn)
Creates a new Object
with with the results of calling a provided function
on every element in this array. Works like Array#map, but also works on
objects as well as arrays, and it returns an object instead.
The callback fn
will be invoked with two parameters: currentValue
and
key
, just like Array#map.
object = { a: 20, b: 30, c: 40 }
result = scour.mapObject(object, (val, key) => {
return '$' + val + '.00'
})
// => { a: '$20.00', b: '$30.00', c: '$40.00' }
This is also available as require('scourjs/utilities/map_object')
.
scour.indexedMap
scour.indexedMap(iterable, fn)
Creates a new Object
with with the results of calling a provided function
returning the keys and values for the new object.
The callback fn
will be invoked with two parameters: currentValue
and
key
, just like Array#map.
The callback fn
should return an array with two elements: with result[0]
being the key, and result[1]
being the value. These are what the new
object will be constructed with.
The iterable
parameter can be an object or an array. This works like
Array#map
, but also works on objects as well as arrays.
list = ['Fred', 'Barney', 'Wilma']
object = scour.indexedMap(list, (val, key) => {
var newkey = val.substr(0, 1)
return [ newkey, val ]
})
// => { f: 'Fred', b: 'Barney', w: 'Wilma' }
This is also available as require('scourjs/utilities/indexed_map')
.
scour.filter
scour.filter(iterable, function(val, key), [isArray])
Creates a new Array or Object with all elements that pass the test implemented by the provided function.
Works like Array#filter, but will return an object if an object is also passed.
The optional isArray
argument, when passed true
, will always make this
return an Array
. If false
, it will always be an Object
. Leave it
undefined
for the default behavior.
This is also available as require('scourjs/utilities/filter')
.
scour.sortBy
scour.sortBy(iterable, criteria)
Sorts by a given criteria.
list = [ { name: 'Fred' }, { name: 'Barney' }, { name: 'Wilma' } ]
scour.sortBy(list, 'name')
This is also available as require('scourjs/utilities/sort_by')
.
Thanks
scour © 2015+, Rico Sta. Cruz. Released under the MIT License.
Authored and maintained by Rico Sta. Cruz with help from contributors (list).
ricostacruz.com  · GitHub @rstacruz  · Twitter @rstacruz