Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

baobab

Package Overview
Dependencies
Maintainers
1
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

baobab - npm Package Compare versions

Comparing version 0.4.4 to 1.0.0-rc1

CHANGELOG.md

2

bower.json
{
"name": "baobab",
"main": "build/baobab.min.js",
"version": "0.4.4",
"version": "1.0.0-rc1",
"homepage": "https://github.com/Yomguithereal/baobab",

@@ -6,0 +6,0 @@ "author": {

@@ -14,25 +14,10 @@ /**

// Should the tree clone data when giving it back to the user?
clone: false,
// Facets registration
facets: {},
// Which cloning function should the tree use?
cloningFunction: null,
// Validation specifications
validate: null,
// Should cursors be singletons?
cursorSingletons: true,
// Maximum records in the tree's history
maxHistory: 0,
// Collection of react mixins to merge with the tree's ones
mixins: [],
// Should the tree shift its internal reference when applying mutations?
shiftReferences: false,
// Custom typology object to use along with the validation utilities
typology: null,
// Validation specifications
validate: null
// Validation behaviour 'rollback' or 'notify'
validationBehavior: 'rollback'
};

@@ -12,3 +12,3 @@ /**

Object.defineProperty(Baobab, 'version', {
value: '0.4.4'
value: '1.0.0-rc1'
});

@@ -15,0 +15,0 @@

{
"name": "baobab",
"version": "0.4.4",
"version": "1.0.0-rc1",
"description": "JavaScript data tree with cursors.",
"main": "index.js",
"dependencies": {
"emmett": "^2.1.2",
"typology": "^0.3.1"
"emmett": "^3.0.0"
},

@@ -19,6 +18,4 @@ "devDependencies": {

"gulp-uglify": "^1.0.2",
"jsdom": "^3.1.0",
"lodash.clonedeep": "^3.0.0",
"lodash": "^3.6.0",
"mocha": "^2.0.1",
"react": "^0.13.0",
"vinyl-buffer": "^1.0.0",

@@ -25,0 +22,0 @@ "vinyl-source-stream": "^1.1.0"

@@ -9,6 +9,8 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab)

It can be paired with **React** easily through [mixins](#react-mixins) to provide a centralized model holding your application's state.
It aims at providing a centralized model holding an application's state and can be paired with **React** easily through mixins or higher order components (available [here](https://github.com/Yomguithereal/baobab-react)).
For a concise introduction about the library and how it can be used by a React/Flux application, you can head toward **@christianalfoni**'s [article](http://christianalfoni.github.io/javascript/2015/02/06/plant-a-baobab-tree-in-your-flux-application.html) on the subject.
For a concise introduction about the library and how it can be used in a React/Flux application, you can head toward **@christianalfoni**'s [article](http://christianalfoni.github.io/javascript/2015/02/06/plant-a-baobab-tree-in-your-flux-application.html) on the subject.
**Fun fact**: A [Baobab](http://en.wikipedia.org/wiki/Adansonia_digitata), or *Adansonia digitata*, is a very big and magnificient African tree.
## Summary

@@ -24,3 +26,2 @@

* [Events](#events)
* [React mixins](#react-mixins)
* [Advanced](#advanced)

@@ -30,8 +31,9 @@ * [Polymorphisms](#polymorphisms)

* [Options](#options)
* [Facets](#facets)
* [History](#history)
* [Update specifications](#update-specifications)
* [Chaining mutations](#chaining-mutations)
* [Cursor combinations](#cursor-combinations)
* [Data validation](#data-validation)
* [Common pitfalls](#common-pitfalls)
* [Philosophy](#philosophy)
* [Migration](#migration)
* [Contribution](#contribution)

@@ -63,8 +65,8 @@ * [License](#license)

If you want to use **Baobab** with node.js or browserify, you can use npm.
If you want to use **Baobab** with node.js/io.js or browserify/webpack etc., you can use npm.
```sh
npm install baobab
npm install baobab@1.0.0-rc1
# Or for the latest dev version
# Or if you need the latest dev version
npm install git+https://github.com/Yomguithereal/baobab.git

@@ -82,5 +84,7 @@ ```

```js
bower install baobab
bower install baobab@1.0.0-rc1
```
The library (as a standalone) currently weights ~20ko minified and ~6ko gzipped.
## Usage

@@ -92,3 +96,3 @@

Creating a *baobab* is as simple as instantiating it with an initial data set (note that only objects or array should be given).
Creating a tree is as simple as instantiating *Baobab* with an initial data set.

@@ -141,6 +145,20 @@ ```js

Rather, the tree will stack and merge every update order you give it and will only commit them at the next frame or next tick in node.
Rather, the tree will stack and merge every update order you give it and will only commit them later on.
This enables the tree to perform efficient mutations and to be able to notify any relevant cursors that the data they are watching over has changed.
**Important**: Note that the tree will shift the references of the objects it stores in order to enable *immutabley* comparisons between one version of the state and another (this is especially useful when using things as such as React's [PureRenderMixin](https://facebook.github.io/react/docs/pure-render-mixin.html)).
*Example*
```js
var tree = new Baobab({hello: 'world'});
var initialState = tree.get();
tree.set('hello', 'monde'});
// After asynchronous update...
assert(initialState !== tree.get());
```
##### Tree level

@@ -165,3 +183,3 @@

```js
cursor.edit({hello: 'world'});
cursor.set({hello: 'world'});
```

@@ -173,2 +191,5 @@

cursor.set('hello', 'world');
// Nested path
cursor.set(['one', 'two'], 'world');
```

@@ -179,3 +200,3 @@

```js
cursor.remove();
cursor.unset();
```

@@ -187,2 +208,5 @@

cursor.unset('hello');
// Nested path
cursor.unset(['one', 'two']);
```

@@ -192,7 +216,15 @@

Obviously this will fail if target data is not an array.
Obviously this will fail if the value at cursor is not an array.
```js
cursor.push('purple');
// Pushing several values
cursor.push(['purple', 'orange']);
// At key
cursor.push('list', 'orange')
// Nested path
cursor.push(['one', 'two'], 'orange');
```

@@ -202,21 +234,80 @@

Obviously this will fail if target data is not an array.
Obviously this will fail if the value at cursor is not an array.
```js
cursor.unshift('purple');
// Unshifting several values
cursor.unshift(['purple', 'orange']);
// At key
cursor.unshift('list', 'orange')
// Nested path
cursor.unshift(['one', 'two'], 'orange');
```
*Splicing an array*
Obviously this will fail if the value at cursor is not an array.
```js
cursor.splice([1, 1]);
// Applying splice n times with different arguments
cursor.splice([[1, 2], [3, 2, 'hello']]);
// At key
cursor.splice('list', [1, 1])
// Nested path
cursor.splice(['one', 'two'], [1, 1]);
```
*Applying a function*
```js
cursor.apply(function(currentData) {
var inc = function(currentData) {
return currentData + 1;
});
};
cursor.apply(inc);
// At key
cursor.apply('number', inc)
// Nested path
cursor.apply(['one', 'two'], 'orange');
```
*Chaining functions through composition*
For more details about this particular point, check [this](#chaining-mutations).
```js
var inc = function(currentData) {
return currentData + 1;
};
cursor.chain(inc);
// At key
cursor.chain('number', inc)
// Nested path
cursor.chain(['one', 'two'], 'orange');
```
*Shallowly merging objects*
Obviously this will fail if the value at cursor is not an object.
```js
cursor.merge({hello: 'world'});
// At key
cursor.merge('object', {hello: 'world'})
// Nested path
cursor.apply(['one', 'two'], {hello: 'world'});
```

@@ -228,3 +319,3 @@

Note however that only relevant cursors will be notified of data change.
Note however that **only** relevant cursors will be notified of a change.

@@ -272,3 +363,8 @@ Events can be bound to either the tree or cursors using the `on` method.

```js
tree.on('update', fn);
tree.on('update', function(e) {
var affectedPaths = e.data.log,
previousState = e.data.previousState;
//...
});
```

@@ -278,6 +374,8 @@

Will fire if a data-validation specification was passed at instance and if new data does not abide by those specifications. For more information about this, see the [data validation](#data-validation) part of the documentation.
Will fire if the `validate` function (see [options](#options)) returned an error for the current update.
```js
tree.on('invalid', fn);
tree.on('invalid', function(e) {
console.log(e.data.error);
});
```

@@ -289,3 +387,3 @@

Will fire if data watched by cursor has updated.
Will fire if data watched over by the cursor has updated.

@@ -306,3 +404,3 @@ ```js

Will fire if the cursor is irrelevant but becomes relevant again.
Will fire if the cursor was irrelevant but becomes relevant again.

@@ -317,166 +415,2 @@ ```js

#### React mixins
A *baobab* tree can easily be used as a UI model keeping the whole application state.
It is then really simple to bind this centralized model to React components by using the library's built-in mixins. Those will naturally bind components to one or more cursors watching over parts of the main state so they can update only when relevant data has been changed.
This basically makes the `shouldComponentUpdate` method useless in most of cases and ensures that your components will only re-render if they need to because of data changes.
##### Tree level
You can bind a React component to a Baoba tree cursors. Cursors bound to a component will have their values attached to `this.state.cursor` (or `this.state.cursors` if registering multiple cursors).
###### Single cursor binding:
Registering a cursor is as simple as defining a path.
```jsx
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursor: ['users'],
render: function() {
// Cursor data is then available either through:
var data = this.cursor.get();
// Or
var data = this.state.cursor;
return <ul>{data.map(function(username) {
return <li>{username}</li>;
})}</ul>;
}
});
```
You can also use a function returning a cursor. This allows the component to be bound to a cursor passed through `this.props`, for instance.
```jsx
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserGroupList = React.createClass({
mixins: [tree.mixin],
cursor: function() {
return this.props.usersCursor;
},
render: function() {
return <ul>{this.cursor.get().map(function(username) {
return <li>{username}</li>;
})}</ul>;
}
}
var users = tree.select('users');
React.render(
<UserGroupList usersCursor={users}/>,
document.getElementById('mount-point')
);
```
###### Multiple cursors binding:
Similarly, to bind several cursors to a component, you can bind your component to a list of cursors.
```jsx
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: [['users'], ['information', 'title']],
render: function() {
return (
<div>
<h1>{this.cursors[1].get()}</h1>
<ul>{this.cursors[0].get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
```
To access cursors in an easier way, you can also bind your component to a map of cursors.
```jsx
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: {
users: ['users'],
title: ['information', 'title']
},
render: function() {
return (
<div>
<h1>{this.cursors.title.get()}</h1>
<ul>{this.cursors.users.get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
```
Same with a function:
```jsx
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: function() {
return {
users: this.props.usersCursor,
title: ['information', 'title']
}
},
render: function() {
return (
<div>
<h1>{this.cursors.title.get()}</h1>
<ul>{this.cursors.users.get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
var users = tree.select('users');
React.render(
<UserList usersCursor={users}/>,
document.getElementById('mount-point')
);
```
### Advanced

@@ -493,2 +427,3 @@

colors: ['blue', 'yellow', 'green'],
currentColor: 1,
items: [{id: 'one', value: 'Hey'}, {id: 'two', value: 'Ho'}]

@@ -504,6 +439,6 @@ }

// Retrieving data
colorsCursor.get(1)
colorsCursor.get(1);
>>> 'yellow'
paletteCursor.get('colors', 2)
paletteCursor.get('colors', 2);
>>> 'green'

@@ -529,2 +464,13 @@

>>> 'Hey'
// Retrieving or selecting data by using the value of another cursor
var currentColorCursor = tree.select('colors', {$cursor: ['currentColor']});
var currentColor = tree.get('colors', {$cursor: ['currentColor']});
// Creating a blank tree
var blankTree = new Baobab();
// You despise "new"?
var tree = Baobab();
```

@@ -573,3 +519,3 @@

var rootCursor = tree.root();
var rootCursor = tree.root;
// or

@@ -579,3 +525,3 @@ var rootCursor = cursor.root();

*Check information about the cursor's location in the tree*
*Checking information about the cursor's location in the tree*

@@ -605,4 +551,3 @@ ```js

{
maxHistory: 5,
clone: true
autoCommit: false
}

@@ -614,14 +559,110 @@ )

* **asynchronous** *boolean* [`true`]: should the tree delay the update to the next frame or fire them synchronously?
* **clone** *boolean* [`false`]: by default, the tree will give access to references. Set to `true` to clone data when retrieving it from the tree if you feel paranoid and know you might mutate the references by accident or need a cloned object to handle.
* **cloningFunction** *function*: the library's cloning method is minimalist on purpose and won't cover edgy cases. You remain free to pass your own more complex cloning function to the tree if needed.
* **cursorSingletons** *boolean* [`true`]: by default, a *baobab* tree stashes the created cursor so only one would be created by path. You can override this behaviour by setting `cursorSingletons` to `false`.
* **maxHistory** *number* [`0`]: max number of records the tree is allowed to store within its internal history.
* **mixins** *array*: optional mixins to merge with baobab's ones. Recommending the [pure render](http://facebook.github.io/react/docs/pure-render-mixin.html) one from react.
* **shiftReferences** *boolean* [`false`]: tell the tree to shift references of the objects it updates so that functions performing shallow comparisons (such as the one used by the `PureRenderMixin`, for instance), can assess that data changed.
* **typology** *Typology|object*: a custom typology to be used to validate the tree's data.
* **validate** *object*: a [typology](https://github.com/jacomyal/typology) schema ensuring the tree's data is valid.
* **facets** *object*: a collection of facets to register when the tree is istantiated. For more information, see [facets](#facets).
* **validate** *function*: a function in charge of validating the tree whenever it updates. See below for an example of such function.
* **validationBehavior** *string* [`rollback`]: validation behavior of the tree. If `rollback`, the tree won't apply the current update and fire an `invalid` event while `notify` will only emit the event and let the tree enter the invalid state anyway.
*Validation function*
```js
function validationFunction(previousState, newState, affectedPaths) {
// Peform validation here and return an error if
// the tree is invalid
if (!valid)
return new Error('Invalid tree because of reasons.');
}
var tree = new Baobab({...}, {validate: validationFunction});
```
#### Facets
Facets can be considered as a "view" on the data of your tree (a filtered version of an array stored within your tree, for instance).
They watch over some paths of your tree and will update their cached data only when needed. As for cursors, you can also listen to their updates.
Facets can be defined at the tree's instantiation likewise:
```js
var tree = new Baobab(
// Data
{
projects: [
{
id: 1,
name: 'Tezcatlipoca',
user: 'John'
},
{
id: 2,
name: 'Huitzilopochtli',
user: 'John'
},
{
id: 3,
name: 'Tlaloc',
user: 'Jack'
}
],
currentProjectId: 1
},
// Options
{
facets: {
// Name of your facet
currentProject: {
// Cursors bound to your facet
// If any of the paths listed below fire
// an update, so will the facet.
cursors: {
id: ['currentProjectId'],
projects: ['projects']
},
get: function(data) {
// 'data' is the value of your mapped cursors
// Just return the wanted value
// Here, we use lodash to return the current's project
// data based on its id
return _.find(data.projects, {id: data.id});
}
},
// Other example
filteredProjects: {
cursors: {
projects: ['projects']
},
get: function(data) {
return data.projects.filter(function(p) {
return p.user === 'John';
});
}
},
}
}
)
```
You can then access facets' data and listen to their changes thusly:
```js
var facet = tree.facets.currentProject;
// Getting value (cached and only computed if needed)
facet.get();
// Listening
facet.on('update', function() {
console.log('New value:', facet.get());
});
```
#### History
A *baobab* tree, given you instantiate it with the correct option, is able to record *n* of its passed states so you can go back in time whenever you want.
**Baobab** lets you record the state of any cursor so you can seamlessly implement undo/redo features.

@@ -631,32 +672,77 @@ *Example*

```js
var baobab = new Baobab({name: 'Maria'}, {maxHistory: 1});
// Asynchronous tree so that examples are simpler
var baobab = new Baobab({colors: ['blue']}, {asynchronous: false}),
cursor = baobab.select('colors');
baobab.set('name', 'Isabella');
// Starting to record state, with 10 records maximum
cursor.startRecording(10);
// On next frame, when update has been committed
baobab.get('name')
>>> 'Isabella'
baobab.undo();
baobab.get('name')
>>> 'Maria'
cursor.push('yellow');
cursor.push('purple');
cursor.push('orange');
cursor.get();
>>> ['blue', 'yellow', 'purple', 'orange']
cursor.undo();
cursor.get();
>>> ['blue', 'yellow', 'purple']
cursor.undo(2);
cursor.get();
>>> ['blue']
```
*Related Methods*
*Starting recording*
Default max number of records is 5.
```js
// Check whether our tree hold records
baobab.hasHistory();
>>> true
cursor.startRecording(maxNbOfRecords);
```
// Retrieving history records
baobab.getHistory();
*Stoping recording*
```js
cursor.stopRecording();
```
*Undoing*
```js
cursor.undo();
cursor.undo(nbOfSteps);
```
*Clearing history*
```js
cursor.clearHistory();
```
*Checking if the cursor has an history*
```js
cursor.hasHistory();
```
*Checking whether the cursor is currently recording*
```js
cursor.recording;
```
*Retrieving the cursor's history*
```js
cursor.getHistory();
```
#### Update specifications
If you ever need to specify complex updates without resetting the whole subtree you are acting on, for readability or performance reasons, you remain free to use **Baobab**'s internal update specifications.
If you ever need to specify complex updates without replacing the whole subtree you are acting on, for readability or performance reasons, you remain free to use **Baobab**'s internal update specifications.
Those are widely inspired by React's immutable [helpers](http://facebook.github.io/react/docs/update.html), themselves inspired by [MongoDB](http://www.mongodb.org/)'s ones and can be used through `tree.update` and `cursor.update`.
Those are widely inspired by React's immutable [helpers](http://facebook.github.io/react/docs/update.html) and can be used through `tree.update` or `cursor.update`.
*Specifications*
**Specifications**

@@ -672,2 +758,3 @@ Those specifications are described by a JavaScript object that follows the nested structure you are trying to update and applying dollar-prefixed commands at leaf level.

* `$unshift`
* `$splice`
* `$merge`

@@ -739,115 +826,52 @@ * `$unset`

#### Cursor combinations
#### Common pitfalls
At times, you might want to listen to updates concerning a logical combination of cursors. For instance, you might want to know when two cursors both updated or when either one or the other did.
*Immutable behaviour*
You can build cursor combination likewise:
TL;DR: Don't mutate things in your *baobab* tree. Let the tree handle its own mutations.
```js
// Simple "or" combination
var combination = cursor1.or(cursor2);
For performance and size reasons *baobab* does not (yet?) use an immutable data structure. However, because it aims at producing a one-way data flow for your application state (like **React** would at component level), it must be used like an immutable data structure.
// Simple "and" combination
var combination = cursor1.and(cursor2);
For this reason, don't be surprised if you mutate things and break your tree.
// Complex combination
var combination = cursor1.or(cursor2).or(cursor3).and(cursor4);
// Listening to events
combination.on('update', handler);
// Releasing a combination to avoid leaks
combination.release();
```
#### Data validation
Given you pass the correct parameters, a baobab tree is able to check whether its data is valid or not against the supplied specification.
This specification must be written in the [typology](https://github.com/jacomyal/typology) library's style.
*Example*
```js
var baobab = new Baobab(
// This is bad:
var users = tree.get('users');
users[0].name = 'Jonathan';
// Initial state
{
hello: 'world',
colors: ['yellow', 'blue'],
counters: {
users: 3,
groups: 1
}
},
// Parameters
{
validate: {
hello: '?string',
colors: ['string'],
counters: {
users: 'number',
groups: 'number'
}
}
}
);
// If one updates the tree and does not respect the validation specification
baobab.set('hello', 42);
// Then the tree will fire an 'invalid' event containing a list of errors
baobab.on('invalid', function(e) {
console.log(e.data.errors);
});
// This is also bad:
var o = {hello: 'world'};
tree.set('key', o);
o.hello = 'other world';
```
#### Common pitfalls
## Philosophy
*Controlled input state*
**UIs as pure functions**
If you need to store a react controlled input's state into a baobab tree, remember you have to commit changes synchronously through the `commit` method if you don't want to observe nasty cursor jumps.
UIs should be, as far as possible, considered as pure functions. Baobab is just a way to provide the needed arguments, i.e. the data representing your app's state, to such a function.
```jsx
var tree = new Boabab({inputValue: null});
Considering your UIs like pure functions comes along with collateral advantages like easy undo/redo features, state storing (just save your tree in the `localStorage` and here you go) and easy isomorphism.
var Input = React.createClass({
mixins: [tree.mixin],
cursor: ['inputValue'],
onChange: function(e) {
var newValue = e.target.value;
**Only data should enter the tree**
// If one edits the tree normally, i.e. asynchronously, the cursor will hop
this.cursor.edit(newValue);
You shouldn't try to shove anything else than raw data into the tree. The tree hasn't been conceived to hold classes or fancy indexes with many circular references and cannot perform its magic on it. But, probably such magic is not desirable for those kind of abstractions anyway.
// One has to commit synchronously the update for the input to work correctly
this.cursor.edit(newValue);
this.tree.commit();
},
render: function() {
return <input onChange={this.onChange} value={this.cursor.get()} />;
}
});
```
That is to say the data you insert into the tree should logically be JSON-serializable else you might be missing the point.
*Immutable behaviour*
## Migration
TL;DR: Don't mutate things in your *baobab* tree. Let the tree handle its own mutations.
**From v0.4.x to 1.0.0**
For performance and size reasons *baobab* does not (yet?) use an immutable data structure. However, because it aims at producing a one-way data flow for your application state (like **React** would at component level), it must be used like an immutable data structure.
A lot of changes occurred between `0.4.x` and `1.0.0`. Most notable changes being the following ones:
For this reason, don't be surprised if you mutate things and break your tree.
* The tree now shift references by default.
* React integration has improved and is now handled by [baobab-react](https://github.com/Yomguithereal/baobab-react).
* `cursor.edit` and `cursor.remove` have been replaced by `cursor.set` and `cursor.unset` single argument polymorphisms.
* A lot of options (now unnecessary) have been dropped.
* Validation is no longer handled by [`typology`](https://github.com/jacomyal/typology) so you can choose you own validation system and so the library can remain lighter.
* Some new features such as: `$splice`, facets and so on...
```js
// This is bad:
var users = tree.get('users');
users[0].name = 'Jonathan';
For more information, see the [changelog](./CHANGELOG.md).
// This is also bad:
var o = {hello: 'world'};
tree.set('key', o);
o.hello = 'other world';
```
## Contribution

@@ -854,0 +878,0 @@

@@ -9,7 +9,7 @@ /**

EventEmitter = require('emmett'),
Typology = require('typology'),
Watcher = require('./watcher.js'),
Facet = require('./facet.js'),
helpers = require('./helpers.js'),
update = require('./update.js'),
merge = require('./merge.js'),
mixins = require('./mixins.js'),
defaults = require('../defaults.js'),

@@ -42,3 +42,2 @@ type = require('./type.js');

this.options = helpers.shallowMerge(defaults, opts);
this._cloner = this.options.cloningFunction || helpers.deepClone;

@@ -48,29 +47,26 @@ // Privates

this._future = undefined;
this._history = [];
this._cursors = {};
this._identity = '[object Baobab]';
// Internal typology
this.typology = this.options.typology ?
(this.options.typology instanceof Typology ?
this.options.typology :
new Typology(this.options.typology)) :
new Typology();
// Properties
this.data = helpers.deepClone(initialData);
this.root = this.select([]);
this.facets = {};
// Internal validation
this.validate = this.options.validate || null;
// Boostrapping root cursor's methods
function bootstrap(name) {
this[name] = function() {
var r = this.root[name].apply(this.root, arguments);
return r instanceof Cursor ? this : r;
};
}
if (this.validate)
try {
this.typology.check(initialData, this.validate, true);
}
catch (e) {
e.message = '/' + e.path.join('/') + ': ' + e.message;
throw e;
}
['get', 'set', 'unset', 'update'].forEach(bootstrap.bind(this));
// Properties
this.data = this._cloner(initialData);
// Facets
if (!type.Object(this.options.facets))
throw Error('Baobab: invalid facets.');
// Mixin
this.mixin = mixins.baobab(this);
for (var k in this.options.facets)
this.addFacet(k, this.options.facets[k]);
}

@@ -81,87 +77,13 @@

/**
* Private prototype
*/
Baobab.prototype._archive = function() {
if (this.options.maxHistory <= 0)
return;
var record = {
data: this._cloner(this.data)
};
// Replacing
if (this._history.length === this.options.maxHistory) {
this._history.pop();
}
this._history.unshift(record);
return record;
};
/**
* Prototype
*/
Baobab.prototype.commit = function(referenceRecord) {
var self = this,
log;
if (referenceRecord) {
// Override
this.data = referenceRecord.data;
log = referenceRecord.log;
}
else {
// Shifting root reference
if (this.options.shiftReferences)
this.data = helpers.shallowClone(this.data);
// Applying modification (mutation)
var record = this._archive();
log = update(this.data, this._transaction, this.options);
if (record)
record.log = log;
}
if (this.validate) {
var errors = [],
l = log.length,
d,
i;
for (i = 0; i < l; i++) {
d = helpers.getIn(this.validate, log[i]);
if (!d)
continue;
try {
this.typology.check(this.get(log[i]), d, true);
}
catch (e) {
e.path = log[i].concat((e.path || []));
errors.push(e);
}
}
if (errors.length)
this.emit('invalid', {errors: errors});
}
// Resetting
this._transaction = {};
if (this._future)
this._future = clearTimeout(this._future);
// Baobab-level update event
this.emit('update', {
log: log
});
Baobab.prototype.addFacet = function(name, definition) {
this.facets[name] = this.createFacet(definition);
return this;
};
Baobab.prototype.createFacet = function(definition) {
return new Facet(this, definition);
};
Baobab.prototype.select = function(path) {

@@ -178,3 +100,3 @@ if (!path)

// Casting to array
path = !type.Array(path) ? [path] : path;
path = [].concat(path);

@@ -187,90 +109,25 @@ // Complex path?

if (complex)
solvedPath = helpers.solvePath(this.data, path);
solvedPath = helpers.solvePath(this.data, path, this);
// Registering a new cursor or giving the already existing one for path
if (!this.options.cursorSingletons) {
return new Cursor(this, path);
}
else {
var hash = path.map(function(step) {
if (type.Function(step))
return complexHash('fn');
else if (type.Object(step))
return complexHash('ob');
else
return step;
}).join('λ');
var hash = path.map(function(step) {
if (type.Function(step))
return complexHash('fn');
else if (type.Object(step))
return complexHash('ob');
else
return step;
}).join('|λ|');
if (!this._cursors[hash]) {
var cursor = new Cursor(this, path, solvedPath, hash);
this._cursors[hash] = cursor;
return cursor;
}
else {
return this._cursors[hash];
}
if (!this._cursors[hash]) {
var cursor = new Cursor(this, path, solvedPath, hash);
this._cursors[hash] = cursor;
return cursor;
}
};
Baobab.prototype.root = function() {
return this.select([]);
};
Baobab.prototype.reference = function(path) {
var data;
if (arguments.length > 1)
path = helpers.arrayOf(arguments);
if (!type.Path(path))
throw Error('Baobab.get: invalid path.');
return helpers.getIn(
this.data, type.String(path) || type.Number(path) ? [path] : path
);
};
Baobab.prototype.get = function() {
var ref = this.reference.apply(this, arguments);
return this.options.clone ? this._cloner(ref) : ref;
};
Baobab.prototype.clone = function(path) {
return this._cloner(this.reference.apply(this, arguments));
};
Baobab.prototype.set = function(key, val) {
if (arguments.length < 2)
throw Error('Baobab.set: expects a key and a value.');
var spec = {};
if (type.Array(key)) {
var path = helpers.solvePath(this.data, key);
if (!path)
throw Error('Baobab.set: could not solve dynamic path.');
spec = helpers.pathObject(path, {$set: val});
}
else {
spec[key] = {$set: val};
return this._cursors[hash];
}
return this.update(spec);
};
Baobab.prototype.unset = function(key) {
if (!key && key !== 0)
throw Error('Baobab.unset: expects a valid key to unset.');
var spec = {};
spec[key] = {$unset: true};
return this.update(spec);
};
Baobab.prototype.update = function(spec) {
Baobab.prototype.stack = function(spec) {
var self = this;

@@ -298,29 +155,69 @@

Baobab.prototype.hasHistory = function() {
return !!this._history.length;
};
Baobab.prototype.commit = function() {
var self = this;
Baobab.prototype.getHistory = function() {
return this._history;
// Applying modifications
var result = update(this.data, this._transaction, this.options);
var oldData = this.data;
// Resetting
this._transaction = {};
if (this._future)
this._future = clearTimeout(this._future);
// Validate?
var validate = this.options.validate,
behavior = this.options.validationBehavior;
if (typeof validate === 'function') {
var error = validate.call(this, oldData, result.data, result.log);
if (error instanceof Error) {
this.emit('invalid', {error: error});
if (behavior === 'rollback')
return this;
}
}
// Switching tree's data
this.data = result.data;
// Baobab-level update event
this.emit('update', {
log: result.log,
previousState: oldData
});
return this;
};
Baobab.prototype.undo = function() {
if (!this.hasHistory())
throw Error('Baobab.undo: no history recorded, cannot undo.');
Baobab.prototype.watch = function(paths) {
if (!type.Array(paths) ||
paths.some(function(p) {
return !type.Path(p);
}))
throw Error('Baobab.watch: invalid paths.');
var lastRecord = this._history.shift();
this.commit(lastRecord);
return new Watcher(this, [].concat(paths));
};
Baobab.prototype.release = function() {
var k;
delete this.data;
delete this._transaction;
delete this._history;
// Releasing cursors
for (var k in this._cursors)
for (k in this._cursors)
this._cursors[k].release();
delete this._cursors;
// Releasing facets
for (k in this.facets)
this.facets[k].release();
delete this.facets;
// Killing event emitter

@@ -334,5 +231,9 @@ this.kill();

Baobab.prototype.toJSON = function() {
return this.reference();
return this.get();
};
Baobab.prototype.toString = function() {
return this._identity;
};
/**

@@ -339,0 +240,0 @@ * Export

@@ -8,5 +8,4 @@ /**

var EventEmitter = require('emmett'),
Combination = require('./combination.js'),
mixins = require('./mixins.js'),
helpers = require('./helpers.js'),
defaults = require('../defaults.js'),
type = require('./type.js');

@@ -30,4 +29,9 @@

this.hash = hash;
this.relevant = this.reference() !== undefined;
this.archive = null;
this.recording = false;
this.undoing = false;
// Privates
this._identity = '[object Cursor]';
// Complex path?

@@ -37,5 +41,23 @@ this.complexPath = !!solvedPath;

// Relevant?
this.relevant = this.get() !== undefined;
// Root listeners
function update(previousState) {
if (self.recording && !self.undoing) {
// Handle archive
var data = helpers.getIn(previousState, self.solvedPath, self.tree),
record = helpers.deepClone(data);
self.archive.add(record);
}
self.undoing = false;
return self.emit('update');
}
this.updateHandler = function(e) {
var log = e.data.log,
previousState = e.data.previousState,
shouldFire = false,

@@ -46,34 +68,18 @@ c, p, l, m, i, j;

if (self.complexPath)
self.solvedPath = helpers.solvePath(self.tree.data, self.path);
self.solvedPath = helpers.solvePath(self.tree.data, self.path, self.tree);
// If selector listens at tree, we fire
if (!self.path.length)
return self.emit('update');
return update(previousState);
// Checking update log to see whether the cursor should update.
outer:
for (i = 0, l = log.length; i < l; i++) {
c = log[i];
if (self.solvedPath)
shouldFire = helpers.solveUpdate(log, [self.solvedPath]);
for (j = 0, m = c.length; j < m; j++) {
p = c[j];
// If path is not relevant to us, we break
if (p !== '' + self.solvedPath[j])
break;
// If we reached last item and we are relevant, we fire
if (j + 1 === m || j + 1 === self.solvedPath.length) {
shouldFire = true;
break outer;
}
}
}
// Handling relevancy
var data = self.reference() !== undefined;
var data = self.get() !== undefined;
if (self.relevant) {
if (data && shouldFire) {
self.emit('update');
update(previousState);
}

@@ -88,3 +94,3 @@ else if (!data) {

self.emit('relevant');
self.emit('update');
update(previousState);
self.relevant = true;

@@ -95,11 +101,6 @@ }

// Making mixin
this.mixin = mixins.cursor(this);
// Lazy binding
var bound = false,
regularOn = this.on,
regularOnce = this.once;
var bound = false;
var lazyBind = function() {
this._lazyBind = function() {
if (bound)

@@ -111,11 +112,7 @@ return;

this.on = function() {
lazyBind();
return regularOn.apply(this, arguments);
};
this.on = helpers.before(this._lazyBind, this.on.bind(this));
this.once = helpers.before(this._lazyBind, this.once.bind(this));
this.once = function() {
lazyBind();
return regularOnce.apply(this, arguments);
};
if (this.complexPath)
this._lazyBind();
}

@@ -133,3 +130,3 @@

Cursor.prototype.isLeaf = function() {
return type.Primitive(this.reference());
return type.Primitive(this.get());
};

@@ -190,3 +187,3 @@

if (last + 1 === this.up().reference().length)
if (last + 1 === this.up().get().length)
return null;

@@ -203,3 +200,3 @@

var list = this.up().reference();
var list = this.up().get();

@@ -212,3 +209,3 @@ return this.tree.select(this.solvedPath.slice(0, -1).concat(list.length - 1));

if (!(this.reference() instanceof Array))
if (!(this.get() instanceof Array))
return null;

@@ -226,138 +223,146 @@

if (type.Step(path))
return this.tree.get(this.solvedPath.concat(path));
else
return this.tree.get(this.solvedPath);
};
var fullPath = this.solvedPath.concat(
[].concat(path || path === 0 ? path : [])
);
Cursor.prototype.reference = function(path) {
if (arguments.length > 1)
path = helpers.arrayOf(arguments);
if (type.Step(path))
return this.tree.reference(this.solvedPath.concat(path));
else
return this.tree.reference(this.solvedPath);
return helpers.getIn(this.tree.data, fullPath, this.tree);
};
Cursor.prototype.clone = function(path) {
if (arguments.length > 1)
path = helpers.arrayOf(arguments);
if (type.Step(path))
return this.tree.clone(this.solvedPath.concat(path));
else
return this.tree.clone(this.solvedPath);
};
/**
* Update
*/
Cursor.prototype.set = function(key, val) {
if (arguments.length < 2)
throw Error('baobab.Cursor.set: expecting at least key/value.');
function pathPolymorphism(method, allowedType, key, val) {
if (arguments.length > 5)
throw Error('baobab.Cursor.' + method + ': too many arguments.');
var data = this.reference();
if (arguments.length < 4) {
val = key;
key = [];
}
if (typeof data !== 'object')
throw Error('baobab.Cursor.set: trying to set key to a non-object.');
key = key || [];
var spec = {};
// Splice exception
if (method === 'splice' &&
!type.Splicer(val)) {
if (type.Array(val))
val = [val];
else
throw Error('baobab.Cursor.splice: incorrect value.');
}
if (type.Array(key)) {
var path = helpers.solvePath(data, key);
// Checking value validity
if (allowedType && !allowedType(val))
throw Error('baobab.Cursor.' + method + ': incorrect value.');
if (!path)
throw Error('baobab.Cursor.set: could not solve dynamic path.');
var path = [].concat(key),
solvedPath = helpers.solvePath(this.get(), path, this.tree);
spec = helpers.pathObject(path, {$set: val});
}
else {
spec[key] = {$set: val};
}
if (!solvedPath)
throw Error('baobab.Cursor.' + method + ': could not solve dynamic path.');
return this.update(spec);
};
var leaf = {};
leaf['$' + method] = val;
Cursor.prototype.edit = function(val) {
return this.update({$set: val});
};
var spec = helpers.pathObject(solvedPath, leaf);
return spec;
}
function makeUpdateMethod(command, type) {
Cursor.prototype[command] = function() {
var spec = pathPolymorphism.bind(this, command, type).apply(this, arguments);
return this.update(spec);
};
}
makeUpdateMethod('set');
makeUpdateMethod('apply', type.Function);
makeUpdateMethod('chain', type.Function);
makeUpdateMethod('push');
makeUpdateMethod('unshift');
makeUpdateMethod('merge', type.Object);
makeUpdateMethod('splice');
Cursor.prototype.unset = function(key) {
if (!key && key !== 0)
throw Error('baobab.Cursor.unset: expects a valid key to unset.');
if (key === undefined && this.isRoot())
throw Error('baobab.Cursor.unset: cannot remove root node.');
if (typeof this.reference() !== 'object')
throw Error('baobab.Cursor.set: trying to set key to a non-object.');
var spec = pathPolymorphism.bind(this, 'unset', null).apply(this, [key, true]);
var spec = {};
spec[key] = {$unset: true};
return this.update(spec);
};
Cursor.prototype.remove = function() {
if (this.isRoot())
throw Error('baobab.Cursor.remove: cannot remove root node.');
Cursor.prototype.update = function(key, spec) {
if (arguments.length < 2) {
this.tree.stack(helpers.pathObject(this.solvedPath, key));
return this;
}
return this.update({$unset: true});
};
// Solving path
var path = [].concat(key),
solvedPath = helpers.solvePath(this.get(), path, this.tree);
Cursor.prototype.apply = function(fn) {
if (typeof fn !== 'function')
throw Error('baobab.Cursor.apply: argument is not a function.');
if (!solvedPath)
throw Error('baobab.Cursor.update: could not solve dynamic path.');
return this.update({$apply: fn});
this.tree.stack(helpers.pathObject(this.solvedPath.concat(solvedPath), spec));
return this;
};
Cursor.prototype.chain = function(fn) {
if (typeof fn !== 'function')
throw Error('baobab.Cursor.chain: argument is not a function.');
/**
* History
*/
Cursor.prototype.startRecording = function(maxRecords) {
maxRecords = maxRecords || 5;
return this.update({$chain: fn});
};
if (maxRecords < 1)
throw Error('baobab.Cursor.startRecording: invalid maximum number of records.');
Cursor.prototype.push = function(value) {
if (!(this.reference() instanceof Array))
throw Error('baobab.Cursor.push: trying to push to non-array value.');
if (this.archive)
return this;
if (arguments.length > 1)
return this.update({$push: helpers.arrayOf(arguments)});
else
return this.update({$push: value});
// Lazy bind
this._lazyBind();
this.archive = helpers.archive(maxRecords);
this.recording = true;
return this;
};
Cursor.prototype.unshift = function(value) {
if (!(this.reference() instanceof Array))
throw Error('baobab.Cursor.push: trying to push to non-array value.');
if (arguments.length > 1)
return this.update({$unshift: helpers.arrayOf(arguments)});
else
return this.update({$unshift: value});
Cursor.prototype.stopRecording = function() {
this.recording = false;
return this;
};
Cursor.prototype.merge = function(o) {
if (!type.Object(o))
throw Error('baobab.Cursor.merge: trying to merge a non-object.');
Cursor.prototype.undo = function(steps) {
steps = steps || 1;
if (!type.Object(this.reference()))
throw Error('baobab.Cursor.merge: trying to merge into a non-object.');
if (!this.recording)
throw Error('baobab.Cursor.undo: cursor is not recording.');
this.update({$merge: o});
if (!type.PositiveInteger(steps))
throw Error('baobab.Cursor.undo: expecting a positive integer.');
var record = this.archive.back(steps);
if (!record)
throw Error('boabab.Cursor.undo: cannot find a relevant record (' + steps + ' back).');
this.undoing = true;
return this.set(record);
};
Cursor.prototype.update = function(spec) {
this.tree.update(helpers.pathObject(this.solvedPath, spec));
return this;
Cursor.prototype.hasHistory = function() {
return !!(this.archive && this.archive.get().length);
};
/**
* Combination
*/
Cursor.prototype.or = function(otherCursor) {
return new Combination('or', this, otherCursor);
Cursor.prototype.getHistory = function() {
return this.archive ? this.archive.get() : [];
};
Cursor.prototype.and = function(otherCursor) {
return new Combination('and', this, otherCursor);
Cursor.prototype.clearHistory = function() {
this.archive = null;
return this;
};

@@ -381,2 +386,3 @@

delete this.solvedPath;
delete this.archive;

@@ -391,7 +397,7 @@ // Killing emitter

Cursor.prototype.toJSON = function() {
return this.reference();
return this.get();
};
type.Cursor = function (value) {
return value instanceof Cursor;
Cursor.prototype.toString = function() {
return this._identity;
};

@@ -398,0 +404,0 @@

@@ -14,2 +14,22 @@ /**

// Decorate a function by applying something before it
function before(decorator, fn) {
return function() {
decorator();
fn.apply(null, arguments);
};
}
// Non-mutative splice function
function splice(array, index, nb /*, &elements */) {
var elements = arrayOf(arguments).slice(3);
index = +index;
nb = +nb;
return array
.slice(0, index)
.concat(array.slice(index + nb).concat(elements));
}
// Shallow merge

@@ -155,6 +175,7 @@ function shallowMerge(o1, o2) {

// Retrieve nested objects
function getIn(object, path) {
function getIn(object, path, tree) {
path = path || [];
var c = object,
p,
i,

@@ -174,6 +195,17 @@ l;

else if (typeof path[i] === 'object') {
if (!type.Array(c))
if (tree && '$cursor' in path[i]) {
if (!type.Path(path[i].$cursor))
throw Error('baobab.getIn: $cursor path must be an array.');
p = tree.get(path[i].$cursor);
c = c[p];
}
else if (!type.Array(c)) {
return;
}
c = firstByComparison(c, path[i]);
else {
c = firstByComparison(c, path[i]);
}
}

@@ -189,3 +221,3 @@ else {

// Solve a complex path
function solvePath(object, path) {
function solvePath(object, path, tree) {
var solvedPath = [],

@@ -210,8 +242,20 @@ c = object,

else if (typeof path[i] === 'object') {
if (!type.Array(c))
if (tree && '$cursor' in path[i]) {
if (!type.Path(path[i].$cursor))
throw Error('baobab.getIn: $cursor path must be an array.');
p = tree.get(path[i].$cursor);
solvedPath.push(p);
c = c[p];
}
else if (!type.Array(c)) {
return;
}
idx = indexByComparison(c, path[i]);
solvedPath.push(idx);
c = c[idx];
else {
idx = indexByComparison(c, path[i]);
solvedPath.push(idx);
c = c[idx];
}
}

@@ -227,2 +271,36 @@ else {

// Determine whether an update should fire for the given paths
// NOTES: 1) if performance becomes an issue, the threefold loop can be
// simplified to become a complex twofold one.
// 2) a regex version could also work but I am not confident it would be
// faster.
function solveUpdate(log, paths) {
var i, j, k, l, m, n, p, c, s;
// Looping through possible paths
for (i = 0, l = paths.length; i < l; i++) {
p = paths[i];
// Looping through logged paths
for (j = 0, m = log.length; j < m; j++) {
c = log[j];
// Looping through steps
for (k = 0, n = c.length; k < n; k++) {
s = c[k];
// If path is not relevant, we break
if (s != p[k])
break;
// If we reached last item and we are relevant
if (k + 1 === n || k + 1 === p.length)
return true;
}
}
}
return false;
}
// Return a fake object relative to the given path

@@ -246,2 +324,3 @@ function pathObject(path, spec) {

// Shim used for cross-compatible event emitting extension
function inherits(ctor, superCtor) {

@@ -255,4 +334,30 @@ ctor.super_ = superCtor;

// Archive
function archive(size) {
var records = [];
return {
add: function(record) {
records.unshift(record);
if (records.length > size)
records.length = size;
},
back: function(steps) {
var record = records[steps - 1];
if (record)
records = records.slice(steps);
return record;
},
get: function() {
return records;
}
};
}
module.exports = {
archive: archive,
arrayOf: arrayOf,
before: before,
deepClone: deepClone,

@@ -265,3 +370,5 @@ shallowClone: shallowClone,

pathObject: pathObject,
solvePath: solvePath
solvePath: solvePath,
solveUpdate: solveUpdate,
splice: splice
};

@@ -11,103 +11,60 @@ /**

// Helpers
function hasKey(o, key) {
return key in (o || {});
}
var COMMANDS = ['$unset', '$set', '$merge', '$apply'];
function conflict(a, b, key) {
return hasKey(a, key) && hasKey(b, key);
// TODO: delete every keys
function only(o, n, keep) {
COMMANDS.forEach(function(c) {
if (keep !== c)
delete o[c];
});
o[keep] = n[keep];
}
// Main function
function merge() {
var res = {},
current,
next,
l = arguments.length,
i,
k;
// TODO: use a better way than shallow cloning b?
function merge(a, b) {
var o = helpers.shallowClone(b || {}),
k,
i;
for (i = l - 1; i >= 0; i--) {
COMMANDS.forEach(function(c) {
if (a[c])
only(o, a, c);
});
// Upper $set/$apply... and conflicts
// When solving conflicts, here is the priority to apply:
// -- 0) $unset
// -- 1) $set
// -- 2) $merge
// -- 3) $apply
// -- 4) $chain
if (arguments[i].$unset) {
delete res.$set;
delete res.$apply;
delete res.$merge;
res.$unset = arguments[i].$unset;
}
else if (arguments[i].$set) {
delete res.$apply;
delete res.$merge;
delete res.$unset;
res.$set = arguments[i].$set;
continue;
}
else if (arguments[i].$merge) {
delete res.$set;
delete res.$apply;
delete res.$unset;
res.$merge = arguments[i].$merge;
continue;
}
else if (arguments[i].$apply){
delete res.$set;
delete res.$merge;
delete res.$unset;
res.$apply = arguments[i].$apply;
continue;
}
else if (arguments[i].$chain) {
delete res.$set;
delete res.$merge;
delete res.$unset;
if (a.$chain) {
COMMANDS.slice(0, -1).forEach(function(c) {
delete o[c];
});
if (res.$apply)
res.$apply = helpers.compose(res.$apply, arguments[i].$chain);
else
res.$apply = arguments[i].$chain;
continue;
}
if (o.$apply)
o.$apply = helpers.compose(o.$apply, a.$chain);
else
o.$apply = a.$chain;
}
for (k in arguments[i]) {
current = res[k];
next = arguments[i][k];
if (a.$splice || o.$splice) {
o.$splice = [].concat(o.$splice || []).concat(a.$splice || []);
}
if (current && type.Object(next)) {
if (a.$push || o.$push) {
o.$push = [].concat(o.$push || []).concat(a.$push || []);
}
// $push conflict
if (conflict(current, next, '$push')) {
if (type.Array(current.$push))
current.$push = current.$push.concat(next.$push);
else
current.$push = [current.$push].concat(next.$push);
}
if (a.$unshift || o.$unshift) {
o.$unshift = [].concat(a.$unshift || []).concat(o.$unshift || []);
}
// $unshift conflict
else if (conflict(current, next, '$unshift')) {
if (type.Array(next.$unshift))
current.$unshift = next.$unshift.concat(current.$unshift);
else
current.$unshift = [next.$unshift].concat(current.$unshift);
}
for (k in a) {
// No conflict
else {
res[k] = merge(next, current);
}
}
else {
res[k] = next;
}
}
if (type.Object(a[k]))
o[k] = merge(a[k], o[k]);
else if (k[0] !== '$')
o[k] = a[k];
}
return res;
return o;
}
module.exports = merge;

@@ -10,106 +10,90 @@ /**

*/
var type = {};
// Not reusing methods as it will just be an extra
// call on the stack
var type = function (value) {
if (Array.isArray(value)) {
return 'array';
} else if (typeof value === 'object' && value !== null) {
return 'object';
} else if (typeof value === 'string') {
return 'string';
} else if (typeof value === 'number') {
return 'number';
} else if (typeof value === 'boolean') {
return 'boolean';
} else if (typeof value === 'function') {
return 'function';
} else if (value === null) {
return 'null';
} else if (value === undefined) {
return 'undefined';
} else if (value instanceof Date) {
return 'date';
} else {
return 'invalid';
}
};
/**
* Helpers
*/
function anyOf(value, allowed) {
return allowed.some(function(t) {
return type[t](value);
});
}
type.Array = function (value) {
/**
* Simple types
*/
type.Array = function(value) {
return Array.isArray(value);
};
type.Object = function (value) {
return !Array.isArray(value) && typeof value === 'object' && value !== null;
type.Object = function(value) {
return value &&
typeof value === 'object' &&
!Array.isArray(value) &&
!(value instanceof Function);
};
type.String = function (value) {
type.String = function(value) {
return typeof value === 'string';
};
type.Number = function (value) {
type.Number = function(value) {
return typeof value === 'number';
};
type.Boolean = function (value) {
return typeof value === 'boolean';
type.PositiveInteger = function(value) {
return typeof value === 'number' && value > 0 && value % 1 === 0;
};
type.Function = function (value) {
type.Function = function(value) {
return typeof value === 'function';
};
type.Primitive = function (value) {
type.Primitive = function(value) {
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
};
type.Date = function (value) {
type.Date = function(value) {
return value instanceof Date;
};
type.Step = function (value) {
var valueType = type(value);
var notValid = ['null', 'undefined', 'invalid', 'date'];
return notValid.indexOf(valueType) === -1;
/**
* Complex types
*/
type.NonScalar = function(value) {
return type.Object(value) || type.Array(value);
};
// Should undefined be allowed?
type.Path = function (value) {
var types = ['object', 'string', 'number', 'function', 'undefined'];
type.Splicer = function(value) {
return type.Array(value) &&
value.every(type.Array);
};
type.Path = function(value) {
var allowed = ['String', 'Number', 'Function', 'Object'];
if (type.Array(value)) {
for (var x = 0; x < value.length; x++) {
if (types.indexOf(type(value[x])) === -1) {
return false;
}
}
} else {
return types.indexOf(type(value)) >= 0;
return value.every(function(step) {
return anyOf(step, allowed);
});
}
return true;
else {
return anyOf(value, allowed);
}
};
// string|number|array|cursor|function
type.MixinCursor = function (value) {
var allowedValues = ['string', 'number', 'array', 'function'];
return allowedValues.indexOf(type(value)) >= 0 || type.Cursor(value);
type.MixinCursor = function(value) {
return anyOf(value, ['String', 'Number', 'Array', 'Function', 'Cursor']);
};
// array|object|function
type.MixinCursors = function (value) {
var allowedValues = ['array', 'object', 'function'];
return allowedValues.indexOf(type(value)) >= 0;
type.MixinCursors = function(value) {
return anyOf(value, ['Object', 'Array', 'Function']);
};
// Already know this is an array
type.ComplexPath = function (value) {
var complexTypes = ['object', 'function'];
for (var x = 0; x < value.length; x++) {
if (complexTypes.indexOf(type(value[x])) >= 0) {
return true;
}
}
return false;
type.ComplexPath = function(value) {
return value.some(function(step) {
return anyOf(step, ['Object', 'Function']);
});
};
module.exports = type;

@@ -11,17 +11,6 @@ /**

var COMMANDS = {};
[
'$set',
'$push',
'$unshift',
'$apply',
'$merge'
].forEach(function(c) {
COMMANDS[c] = true;
});
// Helpers
function makeError(path, message) {
var e = new Error('baobab.update: ' + message + ' at path /' +
path.toString());
path.slice(1).join('/'));

@@ -32,144 +21,139 @@ e.path = path;

// Core function
function update(target, spec, opts) {
opts = opts || {shiftReferences: false};
module.exports = function(data, spec, opts) {
opts = opts || {};
if (!type.Object(data) && !type.Array(data))
throw Error('baobab.update: invalid target.');
var log = {};
// Closure mutating the internal object
(function mutator(o, spec, path, parent) {
path = path || [];
// Shifting root
data = {root: helpers.shallowClone(data)};
var hash = path.join('λ'),
// Closure performing the updates themselves
var mutator = function(o, spec, path, parent) {
path = path || ['root'];
var hash = path.join('|λ|'),
lastKey = path[path.length - 1],
fn,
h,
k,
v;
for (k in spec) {
if (COMMANDS[k]) {
v = spec[k];
var leafLevel = Object.keys(spec).some(function(k) {
return !!~['$set', '$push', '$unshift', '$splice', '$unset', '$merge', '$apply'].indexOf(k);
});
// Logging update
log[hash] = true;
if (leafLevel) {
log[hash] = true;
// TODO: this could be before in the recursion
// Applying
switch (k) {
case '$push':
if (!type.Array(o))
throw makeError(path, 'using command $push to a non array');
for (k in spec) {
if (!type.Array(v))
o.push(v);
else
o.push.apply(o, v);
break;
case '$unshift':
if (!type.Array(o))
throw makeError(path, 'using command $unshift to a non array');
// $unset
if (k === '$unset') {
var olderKey = path[path.length - 2];
if (!type.Array(v))
o.unshift(v);
else
o.unshift.apply(o, v);
break;
}
}
else {
h = hash ? hash + 'λ' + k : k;
if (!type.Object(parent[olderKey]))
throw makeError(path.slice(0, -1), 'using command $unset on a non-object');
if ('$unset' in (spec[k] || {})) {
parent[olderKey] = helpers.shallowClone(o);
delete parent[olderKey][lastKey];
// Logging update
log[h] = true;
if (type.Array(o)) {
if (!opts.shiftReferences)
o.splice(k, 1);
else
parent[path[path.length - 1]] = o.slice(0, +k).concat(o.slice(+k + 1));
}
else {
delete o[k];
}
break;
}
else if ('$set' in (spec[k] || {})) {
v = spec[k].$set;
// Logging update
log[h] = true;
o[k] = v;
// $set
if (k === '$set') {
v = spec.$set;
o[lastKey] = v;
break;
}
else if ('$apply' in (spec[k] || {}) || '$chain' in (spec[k] || {})) {
// TODO: this should not happen likewise.
fn = spec[k].$apply || spec[k].$chain;
// $apply
if (k === '$apply') {
fn = spec.$apply;
if (typeof fn !== 'function')
throw makeError(path.concat(k), 'using command $apply with a non function');
throw makeError(path, 'using command $apply with a non function');
// Logging update
log[h] = true;
o[k] = fn.call(null, o[k]);
o[lastKey] = fn.call(null, o[lastKey]);
break;
}
else if ('$merge' in (spec[k] || {})) {
v = spec[k].$merge;
if (!type.Object(o[k]))
throw makeError(path.concat(k), 'using command $merge on a non-object');
// $merge
if (k === '$merge') {
v = spec.$merge;
// Logging update
log[h] = true;
o[k] = helpers.shallowMerge(o[k], v);
if (!type.Object(o[lastKey]) || !type.Object(v))
throw makeError(path, 'using command $merge with a non object');
o[lastKey] = helpers.shallowMerge(o[lastKey], v);
break;
}
else if (opts.shiftReferences &&
('$push' in (spec[k] || {}) ||
'$unshift' in (spec[k] || {}))) {
if ('$push' in (spec[k] || {})) {
v = spec[k].$push;
if (!type.Array(o[k]))
throw makeError(path.concat(k), 'using command $push to a non array');
o[k] = o[k].concat(v);
}
if ('$unshift' in (spec[k] || {})) {
v = spec[k].$unshift;
// $splice
if (k === '$splice') {
v = spec.$splice;
if (!type.Array(o[k]))
throw makeError(path.concat(k), 'using command $unshift to a non array');
o[k] = (v instanceof Array ? v : [v]).concat(o[k]);
}
if (!type.Array(o[lastKey]))
throw makeError(path, 'using command $push to a non array');
// Logging update
log[h] = true;
v.forEach(function(args) {
o[lastKey] = helpers.splice.apply(null, [o[lastKey]].concat(args));
});
}
else {
// If nested object does not exist, we create it
if (typeof o[k] === 'undefined')
o[k] = {};
// $push
if (k === '$push') {
v = spec.$push;
// Shifting reference
if (opts.shiftReferences)
o[k] = helpers.shallowClone(o[k]);
if (!type.Array(o[lastKey]))
throw makeError(path, 'using command $push to a non array');
// Recur
// TODO: fix this horrendous behaviour.
mutator(
o[k],
spec[k],
path.concat(k),
o
);
o[lastKey] = o[lastKey].concat(v);
}
// $unshift
if (k === '$unshift') {
v = spec.$unshift;
if (!type.Array(o[lastKey]))
throw makeError(path, 'using command $unshift to a non array');
o[lastKey] = [].concat(v).concat(o[lastKey]);
}
}
}
})(target, spec);
else {
for (k in spec) {
return Object.keys(log).map(function(hash) {
return hash.split('λ');
});
}
// If nested object does not exist, we create it
if (typeof o[lastKey][k] === 'undefined')
o[lastKey][k] = {};
else
o[lastKey][k] = helpers.shallowClone(o[lastKey][k]);
// Exporting
module.exports = update;
// Recur
mutator(
o[lastKey],
spec[k],
path.concat(k),
o
);
}
}
};
mutator(data, spec);
// Returning data and path log
return {
data: data.root,
// SHIFT LOG
log: Object.keys(log).map(function(hash) {
return hash.split('|λ|').slice(1);
})
};
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc