@seedalpha/store
v2.3.18
Published
Store
Downloads
3
Readme
Store
Observable store
Changelog
2.3.8
- bugfix on updating existing item in create method
2.3.7
- bugfix on calling setter of null $el in vm
2.3.6
- bugfix on null cursor
2.3.5
- bugfix on de-duplication with
_id
2.3.4
- clone
_id
toid
on items creation in collection
2.3.3
- bugfix in vue.js plugin: stop watching property changes of destoryed VMs
2.3.2
- bugfix on newly created items
2.3.1
- bugfix on duplicated items
2.3.0
- select API
- coverage
2.2.2
- dont act on parent
change
event afterteardown
2.2.1
- bugfix on remove
2.2.0
- map API
- coverage
2.1.0
- merge API (+ uniqueness)
- sort API
- limit API
- documentation
- coverage
2.0.0
- refactor store (from functional to oop)
- new
scope
API - query chaining
- sorting
- pagination via
limit + offset
- cursor
teardown
- documentation
- remove
inject
(usecreate
instead) - use
id
instead of_id
- partial updates are now default (no overloading anymore)
- cursors now cache results (dont re-execute queries)
- store links respond to cursor teardown (unwatched when cursor is destroyed)
1.5.1
- Refactor the way to update cursor when properties change
- Function
inject
will replace existing object in the collection by_id
1.5.0
- Move mixin from created to ready
- Look for cursor query changes and cursor watch with new query
1.4.0
- Link API
1.3.0
- Vue.js mixin plugin
1.2.0
- Plugin API
1.1.0
- Count API
1.0.2
- Avoid deadlock when emitting store events
- Fix tests
1.0.1
- Updated spec
- Full test coverage
- Updated documentation
1.0.0
- Define initial spec
Prerequisites
$ npm set registry http://npm.sandbox.elasticseed.net
$ npm set always-auth true
$ npm login
Installation
$ npm install seed-store
Usage
Store
var Store = require('seed-store');
var store1 = new Store();
var store2 = Store();
Key-value access
var store = new Store();
store.set('a.b.c', 5);
var value = store.get('a.b.c'); // 5
store.on('a.b.c', function(value) {
// value === 10
});
store.set('a.b.c', 10);
store.off('a.b.c');
Collection
var store = new Store();
var articles = store.collection('articles');
// find
var published = articles.find({ publishedAt: { $exists: true } });
published.value(); // []
articles.create({ title: 'Hello', publishedAt: new Date() });
published.value(); // [{ title: 'Hello', publi... }]
// findOne
var article = articles.findOne({ title: 'Hello' }).value();
article.title === 'Hello';
// exists
var exists = articles.exists({ title: { $in: ['Hello'] } }).value();
exists === true;
// count
var count = articles.count({ title: { $in: ['Hello'] } }).value();
count === 1;
// create
articles.create({ title: 'World' });
// update
articles.update({ title: 'Hello' }, { title: 'Piu' }, true); // query, object, partial
// remove
articles.remove({ title: { $exists: true } });
Queries
Most of queries are powered by an excellent sift
library. But that's not all, standard mongoose sorting and pagination are implemented as well
var top = this.store.collection('articles').find({ rank: { $exits: true } }, { limit: 5, offset: 0, sort: '-rank' });
top.value(); // [...]
Scopes
Scopes are isolated sub-collections. Once record is created, updated, removed or queried from scope, parent collection is modified. Once parent collection is modified from outside, scope change
is triggered.
var references = store.collection('references');
var stream = store.collection('references').scope('stream', function() {
return this.find({ scope: ['stream] });
});
Chaining
Collection scopes could be chained to provide a better, more semantic API (inspired by Rails AREL)
var store = new Store();
var people = store.collection('people');
var senior = people.find({ age: { $gt: 40 }});
var seniorDevs = senior.find({ occupation: 'dev' });
Names scopes
Named scopes are cached, and could be accessed via collections
var people = store.collection('people');
people.scope('male', function() {
return this.find({ gender: 'male' });
});
var males = people.scope('male');
males.value(); // [...]
Optimistic updates
Store emit events, when data inside collections is modified, by subscribing to store events, its possible to intersect data, send it to the server and modify collections on server response accordingly. Data in collections is updated immediately.
var memberships = store.collection('memberships');
store.on('memberships:create', function(objects) {
api.createMembership(objects[0].group, function(err, result) {
if (err) { // server fault, we need to remove existing optimistic record from collection
memberships.remove(objects[0]);
} else {
memberships.update(objects[0], result); // **snap**, we just overloaded exiting record
}
});
});
var mCursor = memberships.findOne();
mCursor.value() // undefined
memberships.create({ group: 123 });
mCursor.value() // { group: 123, _id: '_TEMP_ID_' }
// waiting for server to update record (just an example)
setTimeout(function() {
mCrusor.value() // { _id: '12312323eddf34434', group: 123, createdAt: ... }
}, 1000);
Plugins
Store implements .use
pattern for plugins
log.js
exports = module.exports = function(store, options) {
options = options || { debug: false };
store.log = function() {
if (options.debug) {
var args = Array.prototype.slice.call(arguments);
args.unshift('store:');
console.log.apply(console, args);
}
}
}
app.js
var store = new Store();
var log = require('./log');
store.use(log, { debug: true });
store.log('Hello world!'); // 'store: Hello world!'
Links
Links are helpers methods that evaluate cursors and update key-value store.
store.link('hasUnreadNotifications', function() {
return store.collection('notifications').exists({ read: false });
});
store.get('hasUnreadNotifications'); // false
store.collection('notifications').create({ read: false, message: 'Friend request pending' });
store.get('hasUnreadNotifications'); //true
Lifecycle Events
Store
, Collection
and Node
all implement event emitter api. Store emits {collection_name}:[create,update,remove]
. Collection and Node, both emit 'change'.
Store API
#new
create a new store
var store = new Store();
#get({String} path):*
get a value from store, using a path
var value = store.get('a.b.c');
#set({String} path, {*} value): *
set a value to store, using a path
store.set('app.im.preview', true);
#on({String} event, {Function} callback):Store
subscribe to a store event
store.on('app.im.preview', function(value) {
if (value) {
// open preview
} else {
// close preview
}
});
#off({String} event [, {Function} callback]):Store
unsubscribe from a store event
store.off('app.im.preview', cb);
store.off('a.b.c');
store.off(); // unsubscribe from all store events
#use({Function} plugin[, {Object} options]):Store
use a plugin
store.use(function(instance, options) {
store === instance;
options.message === 'World';
store.hello = function() {
console.log('Hello ' + options.message);
}
}, { message: 'World' });
store.hello(); // 'Hello World'
#link({String} path, {Function} fn):Store
link cursor to a key-value store and watch for it's changes
store.link('account', function() {
return store.collection('users').findOne({ _id: '123' });
});
store.get('account'); // { _id: '123', usename: 'John' };
store.collection('users').update({ _id: '123' }, { username: 'Jonathan' }, true);
store.get('account'); // { _id: '123', usename: 'Jonathan' };
#collection({String} name):Collection
create a new (or get cached) store collection
var references = store.collection('references');
Collection API
#create({Array|Object} object)
Insert an object into collection
var articles = store.collection('articles');
var article = { title: 'abc' };
articles.create(article);
// an `id` will be assigned if there is no one
article.id // "12398137492432"
#update({Object} query, {Object} object)
Find objects in collection and update them.
If partial update is set to false
, object will be overwrited.
If partial update is set to true
, object will be extended.
var articles = store.collection('articles');
articles.update({ title: 'Hello' }, { title: 'World' }, true);
#remove([{Object} query])
Remove all objects in collection that satisfy a given query.
var articles = store.collection('articles');
articles.remove({ title: { $in: ['Hello'] }});
#find([{Object} query][, {Object} options]):Node
Get a cursor to all objects in collection that satisfy a given query
var articles = store.collection('articles');
var cursor = articles.find({ publishedAt: { $exists: true }}, { limit: 10, offset: 10, sort: '-createdAt' });
cursor.value(); // []
#findOne([{Object} query][, {Object} options]):Node
Get a cursor to a first object in collection that satisfies a given query
var articles = store.collection('articles');
var cursor = articles.findOne({ publishedAt: { $exists: true }});
cursor.value(); // { title: '...' }
#exists([{Object} query][, {Object} options]):Node
Get a cursor to a value that indicates if there is an object in collection that satisfies a given query
var articles = store.collection('articles');
var cursor = articles.exists({ publishedAt: { $exists: true }});
cursor.value(); // true
#count([{Object} query][, {Object} options]):Node
Get a cursor to a value that indicates a number of an objects in collection that satisfy a given query
var articles = store.collection('articles');
var cursor = articles.count({ publishedAt: { $exists: true }});
cursor.value(); // 4
#sort({String} field):Node
Mongoose-like sorting for collection records. Could be chained like everything else.
var people = this.store.collection('people').sort('firstname'); // ordered by firstname
var ranked = this.store.collection('ratings').sort('-rank'); // reverse ordered by rank
#limit({Number} limit[, {Number} offset=0]):Node
Standalone limit query (useful with merge)
var ten = this.store.collection('people').sort('name').limit(10);
var tenAfterTen = this.store.collection('ratings').sort('rank').limit(10, 10);
#map({Function|String} path):Node
Aggregate cursor using String
path or Function
mapping function
var ids = this.store.collection('references').map('source.resourceId');
var size = this.store.collection('articles').map(function(item) {
return item.length;
});
ids.value(); // [...]
size.value(); // [...]
#select({String} fields):Node
Include/exclude object paths before return.
// will exclude sensitive data from response
var users = this.store
.collection('users')
.find()
.select('-password -hash -salt');
// will include only selected fields (excluding everything else)
var posts = this.store
.collection('posts')
.find()
.select('title author summary publishedAt');
id
property is always included in response
#merge([{Node} scope][, {Node} scope ...]):Node
Merges and deduplicates multiple scopes together. Check tests for more examples.
var references = this.store.collection('references');
var stream = references.scope('stream', function() {
return this.merge(
references.scope('bookmarks'),
references.scope('following'),
references.scope('groups'),
references.scope('company'),
references.scope('owned')
).order('-createdAt').limit(20);
});
Cursor API
Cursors are represented in Node
class, they share same API with Collections
, apart mutation methods, eg. can't call create
, update
, remove
on a cursor.
#value({Boolean} force=false)
Calculate and return cursor value
var cursor = articles.find();
cursor.value(); // [...], will execute a query and return all articles in collection
cursor.value(); // [...], will not re-execute the query, will return cached results
cursor.value(true); // [...], will force re-execute query
#watch({Function} fn):Function
Will execute fn
every time collection emits change
var cursor = articles.find();
var unwatch = cursor.watch(function() {
cursor.value(); // [{...}, {...}]
});
articles.create([{ title: 'Hello }, { title: 'World }]);
unwatch();
#teardown()
Will remove cursor data
, parent
and all event listeners.
Development
$ git clone [email protected]:seedalpha/store.git
$ cd store
$ npm install
$ npm test
$ npm run coverage
$ open coverage.html
License
SeedAlpha ©2015