Maintaining a resource collection with ngResource, socket.io and $q - javascript

I am trying to create an AngularJS factory that maintains a collection of resources automatically by retrieving the initial items from the API and then listening for socket updates to keep the collection current.
angular.module("myApp").factory("myRESTFactory", function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q, $rootScope) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// manually add something to the collection
Factory.push = function(item) {
Factory.collection.push(item);
};
// search the collection for matching objects
Factory.find = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
resolve(_.where(Factory.collection, opts || {}));
});
});
};
// search the collection for a matching object
Factory.findOne = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
var item = _.findWhere(collection, opts || {});
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
resolve(Factory.collection[idx]);
});
});
};
// create a new item; save to API & collection
Factory.create = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.save(opts).$promise.then(function(item){
Factory.collection.push(item);
resolve(item);
});
});
});
};
Factory.update = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.update({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
resolve(item);
});
});
});
};
Factory.delete = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.delete({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection.splice(idx, 1);
resolve(item);
});
});
});
};
// new items received from the wire
Socket.on('new', function(item){
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx===-1) Factory.collection.push(item);
// this doesn't help
$rootScope.$apply();
});
Socket.on('update', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
// this doesn't help
$rootScope.$apply();
});
Socket.on('delete', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx!==-1) Factory.collection.splice(idx, 1);
});
return Factory;
});
My backend is solid and the socket messages come through correctly. However, the controllers don't respond to updates to the collection if any of the Factory methods are used.
i.e.
This works (responds to socket updates to the collection):
$scope.users = User.collection;
This does not work (it loads the user initially but is not aware of updates to the collection):
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
How can I get my controllers to respond to update to changes to the collection?
Update:
I was able to implement a workaround in the controller by changing this:
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
To this:
$scope.$watchCollection('users', function() {
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
});
However, nobody likes workarounds, especially when it involves redundant code in your controllers. I am adding a bounty to the question for the person who can solve this inside the Factory.

The solution is for the factory methods to return an empty object/array to be populated later (similar to the way ngResource works). Then attach the socket listeners to both those return objects/arrays and the main Factory.collection array.
angular.module("myApp").factory("myRESTFactory",
function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// This function attaches socket listeners to given array
// or object and automatically updates it based on updates
// from the websocket
var socketify = function(thing, opts){
// if attaching to array
// i.e. myRESTFactory.find({name: "John"})
// was used, returning an array
if(angular.isArray(thing)) {
Socket.on('new', function(item){
// push the object to the array only if it
// matches the query object
var matches = $filter('find')([item], opts);
if(matches.length){
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
if(idx===-1) thing.push(item);
}
});
Socket.on('update', function(item) {
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
var matches = $filter('find')([item], opts);
// if the object matches the query obj,
if(matches.length){
// and is already in the array
if(idx > -1){
// then update it
thing[idx] = item;
// otherwise
} else {
// add it to the array
thing.push(item);
}
// if the object doesn't match the query
// object anymore,
} else {
// and is currently in the array
if(idx > -1){
// then splice it out
thing.splice(idx, 1);
}
}
});
Socket.on('delete', function(item) {
...
});
// if attaching to object
// i.e. myRESTFactory.findOne({name: "John"})
// was used, returning an object
} else if (angular.isObject(thing)) {
Socket.on('update', function(item) {
...
});
Socket.on('delete', function(item) {
...
});
}
// attach the socket listeners to the factory
// collection so it is automatically maintained
// by updates from socket.io
socketify(Factory.collection);
// return an array of results that match
// the query object, opts
Factory.find = function(opts) {
// an empty array to hold matching results
var results = [];
// once the API responds,
Factory.collection.$promise.then(function(){
// see which items match
var matches = $filter('find')(Factory.collection, opts);
// and add them to the results array
for(var i = matches.length - 1; i >= 0; i--) {
results.push(matches[i]);
}
});
// attach socket listeners to the results
// array so that it is automatically maintained
socketify(results, opts);
// return results now. initially it is empty, but
// it will be populated with the matches once
// the api responds, as well as pushed, spliced,
// and updated since we socketified it
return results;
};
Factory.findOne = function(opts) {
var result = {};
Factory.collection.$promise.then(function(){
result = _.extend(result, $filter('findOne')(Factory.collection, opts));
});
socketify(result);
return result;
};
...
return Factory;
};
The reason this is so so great is that your controllers can be ridiculously simple yet powerful at the same time. For example,
$scope.users = User.find();
This returns an array of ALL users that you can use in your view; in an ng-repeat or something else. It will automatically be updated/spliced/pushed by updates from the socket and you don't need to do anything extra to get that. But wait, there's more.
$scope.users = User.find({status: "active"});
This will return an array of all active users. That array will also automatically be managed and filtered by our socketify function. So if a user is updated from "active" to "inactive", he is automatically spliced from the array. The inverse is also true; a user that gets updated from "inactive" to "active" is automatically added to the array.
The same is true for the other methods as well.
$scope.user = User.findOne({firstname: "Jon"});
If Jon's email changes, the object in the controller is updated. If his firstname changes to "Jonathan", $scope.user becomes an empty object. Better UX would be to do soft-delete or just mark the user deleted somehow, but that can be added later.
No $watch, $watchCollection, $digest, $broadcast, required--it just works.

Don't expose the collection property on Factory, keep it as a local variable.
Create a new exposed getter/setter on the Factory that proxies to and from the local variable.
Use the getter/setter Object internally in your find methods.
Something like this:
// internal variable
var collection = Resource.query();
// exposed 'proxy' object
Object.defineProperty(Factory, 'collection', {
get: function () {
return collection;
},
set: function (item) {
// If we got a finite Integer.
if (_.isFinite(item)) {
collection.splice(item, 1);
}
// Check if the given item is already in the collection.
var idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if (idx) {
// Update the item in the collection.
collection[idx] = item;
} else {
// Push the new item to the collection.
collection.push(item);
}
// Trigger the $digest cycle as a last step after modifying the collection.
// Can safely be moved to Socket listeners so as to not trigger unnecessary $digests from an angular function.
$rootScope.$digest();
}
});
/**
* Change all calls from 'Factory.collection.push(item)' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection[idx] = item' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection.splice(idx, 1) to
* 'Factory.collection = idx;'
*
*/
Now, seeing as how the non angular parties modify your collection (namely Sockets in this case), you will need to trigger a $digest cycle to reflect the new state of the collection.
If you are only ever interested in keeping the collection in sync in a single $scope (or multiple ones, but not cross-scope) I would attach said $scope's to the factory, and run the $digest there instead of $rootScope. That will save you a little bit of performance down the line.
here's a jsbin showcasing how the usage of an Object.getter will keep your collection in sync and allow you to find items recently added to the collection.
I've opted for setTimeout in the jsbin so as to not trigger automatic $digests through the usage of $interval.
Obviously the jsbin is very barebones; There's no promises being shuffled around, no socket connections. I only wanted to showcase how you can keep things in sync.
I will admit that Factory.collection = value looks whack, but you could hide that away with the help of wrapping functions to make it more pretty / read better.

Related

How to handle information needed from a promise for future saves and updates

I have an object I'm saving to an API. The API returns a unique identifier: id.
When I save with I get a promise which includes my new, API assigned, id.
Now the user may want to carry out other operations, which will require the id. For example, renaming the widget and re-saving, or adding further objects that point back to its id.
What are the practical and ideally straightforward options*?
*Other advice on stackoverflow I've read suggests using 'resolve' which would be fine if I was reverting to the router at this point. But I'm not at the moment.
Here's a simple example:
widget.saveThis = function() {
if ('id' in this) {
this.put();
} else {
var _this = this;
rest.all('widgets').post(this).then(function(result) {
// Copy the new properties I have received to this object.
// ignore the methods from restangular.
for (var key in result) {
if (typeof(result[key]) != 'function')
_this[key] = result[key];
}
p.refresh();
});
}
};
Where if save is pushed twice in a row we might get two copies of the object.
Imagine you have a service where you do the API Communication (maybe via REST?
"use strict";
(function() {
var module = angular.module('myModule.service');
module.factory('myService', function($http, $q) {
return {
/**
* save and get
*/
saveAndGet: function(myObject) {
var deferred = $q.defer();
$http.post(getContextPath()+'/rs/saveObj', JSON.stringify{myObject})
.success( function(data) {
deferred.resolve(data);
})
.error(function(response) {
deferred.reject(response);
});
return deferred.promise;
}
}
});
})();
now imagine you have a controller where you wait for the saving to be done:
"use strict";
(function() {
var module = angular.module('myModule.controller');
module.controller('MyController' , function($scope, myService) {
var myObj = //set it somehow
$scope.id; //needed for save the "new" id
myService.saveAndGet(myObj)
.then( function(result) {
// is called if no error occured
$scope.id = result.id;
)};
})();
and then image you have that backend (in java for example)
#POST
#Path("saveObj")
#Produces({"application/json"})
public Response createProjectComment(MyObj obj) {
// do something and create myNewId
if (myNewId == null) {
return Response.ok("error").build();
}
return Response.ok(myNewId).build();
}
that would be one way to solve your problem.

Angular loop through object array

I have the following function which creates an array of objects inside models, however when I come to use models further down the app I'm unable to loop through it's contents to pull out data.
Every loop method I've tried so far containing a single console.log() message just prints out the message once message when models will contain two objects so I think the problem actually lies with the creation of models. If I create a promise and print out the value of models.devices when it's finished processing an empty array is returned.
Any ideas?
var d = devices.split(','),
count = 0,
models = {devices:[]};
angular.forEach(d, function (device, i) {
var index = i;
if (index <= 1) {
var deviceName = device.replace(/ /g,"+").toLowerCase(),
req = '?__url_path_param=' + deviceName;
$http
.get('/api/cheapest_by_name' + req)
.success(function (obj) {
models.devices.push(obj.device);
count++;
});
}
});
$q.all(models).then(function (data) {
apiDeal.multi(data, 3, 2);
});
Then... (in api-deal.factory.js)
function apiDeal($http, $rootScope) {
return {
multi: function (devices, limit, type) {
console.log(devices); // equal to below image
console.log(devices.devices); // equal to '[]'
}
}
}
I then need to loop through devices in apiDeal.multi
You need to keep an array of promises, which should replace the models you're using the $q.all on. It has to be an array of promises.
So change your code to this:
var d = devices.split(','),
count = 0,
models = {devices:[]},
promises = [];
var promise = $http
.get('/api/cheapest_by_name' + req)
.success(function (obj) {
models.devices.push(obj.device);
count++;
});
promises.push(promise);
And then, do:
$q.all(promises).then(function (data) {
apiDeal.multi(data, 3, 2);
});
Simple Fiddle demonstration

Promise : execute after loop

I want to create several entries in a loop using waterline ORM.
Each created object is pushed in an array.
After all objects are created, I need to return the full Array.
Here is my code :
share: function(req, res, next) {
var params = {},
id = req.param('id'),
img = {},
targets;
if (!req.param('name')) {
res.status(400);
return res.json('Error.No.Name')
}
var promiseLoop = function(Tree) { // Function called after creating or finding Tree to attach to
var array = [];
if(!req.param('targets')) return Tree; // No targets provided => exit
targets = JSON.parse(req.param('targets'));
_.each(targets, function(target) {
params = {
target : target,
image: Tree.id,
message : req.param('message')
};
Leaf
.create(params)
.then(function(Leaf) {
array.push(Leaf);
});
});
Tree.array = array;
return Tree;
}
if (!id) {
params.owner = req.session.user ;
params.name = req.param('name');
Tree
.create(params)
.then(function(Tree) {
res.status(201); // Status for image creation
return Tree;
})
.then(promiseLoop)
.then(function(data) { res.status(201); res.json(data);})
.catch(function(err) { res.status(400); res.json(err);});
}
}
};
I want the Tree return to have a array member equal to an array of created Leaves.
But of course the part
Tree.array = array;
return Tree;
is executed befure array is populated.
and what I get in response is my Tree object :
{
...,
"array": []
}
What wan I do to be sure to execute this part only after all objects are created ?
Thank you by advance.
Promises know when a previous promise is done through then chaining. You can and should utilize that.
Return a promise from promiseLoop:
var promiseLoop = function(Tree) { // are you sure about that naming?
if(!req.param('targets')) return Tree;
targets = JSON.parse(req.param('targets'));
var leaves = targets.map(function(target){
return Leaf.create({ // map each target to a leaf
target : target,
image: Tree.id,
message : req.param('message')
});
});
return Promise.all(leaves).then(function(leaves){
Tree.array = leaves; // assign to tree
}).return(Tree); // finally resolve the promise with it
}

Ember Data model find by query deferred?

If I have a query that defines the model for my route:
App.MyRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('MyModel', {myProperty: myValue}).then(function(results) {
return results.get('firstObject');
});
}
});
How do I handle the case where the store has not yet been populated with the results of the above query?
I want the query to update the model with the results of the above query whenever a value is inserted into the store that satisfies the query.
Is this possible?
NOTE: I'm using the FixtureAdapter as I'm not using a REST back end and I don't need the persistence layer. As such, I have implemented the findQuery method myself as follows:
App.ApplicationAdapter = DS.FixtureAdapter.extend({
queryFixtures: function(records, query, type) {
return records.filter(function(record) {
for(var key in query) {
if (!query.hasOwnProperty(key)) { continue; }
var value = query[key];
if (record[key] != value) { return false; }
}
return true;
});
}
});
I am absolutely open to changing this up if it will help me reach this end.
Use a filter, it is a live record array that updates as new records are injected into the store. As a side note, filter doesn't make a call to the server for records, so if you still want that to happen you'll want to still make the find call, and then just return the filter.
App.MyRoute = Ember.Route.extend({
model: function (params) {
// trigger a find
this.store.find('MyModel', {myProperty: myValue});
// return a live filter (updates when the store updates)
return this.store.filter('MyModel', function(record){
return record.get('myProperty') == myValue;
});
}
});

BreezeJS - Chaining Queries

Say we have a Customer Object, that has a "Foo" collection. I'd like my "getCustomer" function to add all Foos it does not have already, and then return itself, as a promise...
So I'd like a promise to: Get a Customer, then add all missing Foos to this Customer, so that when the promise is resolved, the customer has all missing foos.
Example:
// dataservice.js
// returns Q.Promise<breeze.QueryResult>
function getCustomer(custId) {
var query = breeze.EntityQuery().from("Customers").where("CustomerId", "==", custId);
return this.em.executeQuery(query);
}
// returns Q.Promise<breeze.QueryResult>
function getFoosNotOnCustomer(customer) {
var query = breeze.EntityQuery().from("Foo").where("CustomerId", "!=", customer.Id());
return this.em.executeQuery(query);
}
I am struggling with how to "chain" these together properly, what do do if no customer is found, etc. How can I modify "getCustomer" to do this? I am basically trying to user breeze synchronously. Here is my attempt, but it turns into ugly nested code in a hurry.
// want to return Q.Promise<Customer> that has Foos loaded
// I think this is actually returning something like Q.Promise<Q.Promise<Customer>>
function getCustomer(custId) {
var query = breeze.EntityQuery().from("Customers")
.where("CustomerId", "==", custId);
return this.em.executeQuery(query) // return here?
.then(function(data) {
// what about conditionals?
if(data.results.length == 1) {
getFoosNotOnCustomer(data.results[0]).
then(function (foosArray) {
$.each(foosArray, function(i,el) {
// push foos onto customer instance
}
return custWithFoos; // return here?
}
// return something?
}
}
}
Here's what I ended up doing:
function getCustomer(custId) {
var query = breeze.EntityQuery().from("Customers").where("CustomerId", "==", custId);
return manager.executeQuery(query) // return here?
.then(addFoos)
.then(doSomethingElse);
}
function addFoos(data) {
var myDefer = Q.Defer();
if (data && data.result.length == 1) {
var customer = data.results[0];
var query = // get FOOS Customer doesn't have;
manager.executeQuery(query).then(function (fooData) {
$.each(fooData.results function (i, el) {
customer.Foos.push(el);
});
myDefer.reslove(customer);
});
} else {
myDefer.resolve(undefined);
}
return myDefer.promise;
}
function doSomethingElse(customer) {
var myDefer = Q.Defer();
customer.SomePropert("test");
return myDefer.resovlve(customer);
}
// ----- MVVM
var custPromise = getCustomer(1).then(function (customer) {
// do something
});
I will take your example on face value despite my inability to understand the semantics ... in particular my inability to understand why getting all Foos not belonging to the customer is going to be helpful.
I'll just focus on "chaining" and I'll assume you want the caller to take possession of the selected customer when you're done.
Sequential chaining
In this example, we wait for the customer before getting the Foos
function getCustomer(custId) {
var cust;
var em = this.em;
var query = breeze.EntityQuery().from("Customers")
.where("CustomerId", "==", custId);
// On success calls `gotCustomer` which itself returns a promise
return em.executeQuery(query)
.then(gotCustomer)
.fail(handleFail); // you should handleFail
// Called after retrieving the customer.
// returns a new promise that the original caller will wait for.
// Defined as a nested success function
// so it can have access to the captured `cust` variable
function gotCustomer(data) {
cust = data.results[0];
if (!cust) {
return null; // no customer with that id; bail out now
}
// got a customer so look for non-customer foos
// returning another promise so caller will wait
return breeze.EntityQuery().from("Foos")
.where("CustomerId", "!=", custId)
.using(em).execute()
.then(gotFoos);
}
// Now you have both the customer and the other Foos;
// bring them together and return the customer.
function gotFoos(data) {
var foos = data.results;
// assume `notMyFoos` is an unmapped property that
// should hold every Foo that doesn't belong to this Customer
foos.forEach(function(f) { cust.notMyFoos.push(f); }
return cust; // return the customer to the caller after Foos arrive.
}
}
Parallel async queries
In your scenario you really don't have to wait for the customer query before getting the foos. You know the selection criterion for both the customer and the foos from the start. Assuming you think that there is a high probability that the customer query will return a customer, you might fire off both queries in parallel and then mash up the data when both queries complete. Consider the Q.all for this.
function getCustomer(custId) {
var em = this.em;
var custPromise = breeze.EntityQuery().from("Customers")
.where("CustomerId", "==", custId)
.using(em).execute();
var fooPromise = breeze.EntityQuery().from("Foos")
.where("CustomerId", "!=", custId)
.using(em).execute();
Q.all([custPromise, fooPromise])
.then(success)
.fail(handleFail); // you should handleFail
// Now you have both the customer and the "other" Foos;
// bring them together and return the customer.
// `data` is an array of the results from each promise in the order requested.
function success(data) {
var cust = data[0].results[0];
if (!cust) return null;
var foos = data[1].results;
// assume `notMyFoos` is an unmapped property that
// should hold every Foo that doesn't belong to this Customer
foos.forEach(function(f) { cust.notMyFoos.push(f); }
return cust; // return the customer to the caller after Foos arrive.
}
}
Notice that I don't have to do so much null checking in the success paths. I'm guaranteed to have data.results when the success callback is called. I do have to account for the possibility that there is no Customer with custId.

Categories

Resources