@seedalpha/store

2.3.18 • Public • Published

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 to id 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 after teardown

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 (use create 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 git@github.com:seedalpha/store.git
$ cd store
$ npm install
$ npm test
$ npm run coverage
$ open coverage.html

License

SeedAlpha ©2015

Readme

Keywords

Package Sidebar

Install

npm i @seedalpha/store

Weekly Downloads

0

Version

2.3.18

License

©2015 SeedAlpha

Unpacked Size

64.8 kB

Total Files

15

Last publish

Collaborators

  • tom.raggett.trcuk.com
  • ac_inc