Loopback: detect a change in the model - loopbackjs

My overall goal in the hook I'm trying to use here in my /models/LocatableUser.js is to figure out if there's an actual change I need to update for, and if there is, do something (make another api call).
I have a structure of custom models that inherit from this custom model, and so it works for both sub models when defining the before save hook in the parent model. Here's a sample of the method I defined in the parent model, LocatableUser:
LocatableUser.observe('before save', function (ctx, next) {
if (ctx.instance){ // new record
ctx.instance._address.getGeopoint(function (error, location) {
setLocation(error, location, ctx.instance, next);
});
} else if (ctx.currentInstance) { // this is an update, currentInstance is treated as immutable
LocatableUser.findById(ctx.currentInstance.id, function(err, data) {
console.log('Locatable User: current data is: ', err, data)
})
console.log('Locatable User: ctx is:', ctx);
ctx.currentInstance._address.getGeopoint(function (error, location) {
setLocation(error, location, ctx.data, next);
});
} else {
console.warn('no context instance');
}
});
The problem with this code is, since there are no concrete classes of a LocatableUser, calling LocatableUser.findById() won't find anything, because the actual class will be some child class of LocatableUser. The only thing I've found that works is defining this method in both child classes, but that duplicates code.
Is there a way to have the derived classes' findById method called from the LocatableUser class?
Loopback version 2.22.0

So it turns out I was going about this the wrong way:
In the PUT call, ctx.currentInstance comes in as the currently stored instance, removing the need for me to query for that same instance by ID. The ctx.data object is the actual payload coming from the call to the rest API, so I could just compare the data coming in from that to the currentInstance, in order to determine if I need to run some update logic.

Related

How to pass a #tracked object from an Ember route model hook

My question is two-fold:
Where is the best place to put some kind of polling logic - in the route file right?
How do I pass this constantly updating value from the Route to some child component? Labeling some variable as "#tracked" and then passing the tracked variable via the model hook?
Let's say I have something like this:
routes/index.js
export default class IndexRoute extends Route {
#tracked
recent: {
A: 0,
...
},
constructor() {
super(...arguments);
this.getRecent();
}
getRecent() {
// poll data / fetch latest
const {A, ...partialObject} = this.recent;
this.recent = { ...partialObject, A: <some new value fetched>};;
later(this, this.getRecent, 2000);
}
model() {
return this.recent;
}
}
application.hbs
<p>constantly updating "this.recent": {{ this.model.A }} </p>
I thought if I use the model hook like this, it would be tracked and therefore auto-update but that was not the case. I have this sample Ember Twiddle that emulates what I'm trying to do. I tried to force a re-compute by reassigning the entire variable but it didn't work.
This question is a deeper dive from my initial question here.
You are returning a reference to object stored in this.recent in your model hook. But the getRecent method does not change that object but overrides this.recent. After the first execution of getRecent method the model of the route and this.recent aren't the same object anymore. The model of the route, which you can access through this.modelFor(this.routeName) is the initial value and this.recent is the new value.
You want to mutate the object returned from model hook instead.
The object given in your example has a fixed schema. This allows you to mark the property A as tracked:
recent: {
#tracked A: 0,
...
}
As currently you return the value of this.recent in your model hook. But instead of overwriting it in getRecent method, you are only changing the value of it's property A:
getRecent() {
this.recent.A = <some new value fetched>;
later(this, this.getRecent, 2000);
}
If you don't know the schema of the object returned in model hook or if you are dealing with an array, it's a little bit more complicated. You wouldn't have a property to decorate with #tracked. I would recommend to use the tracked-built-ins package in that case.
For arrays you can also fallback to legacy MutableArray from #ember/array/mutable package. But you must make sure in that case that you use it's custom methods to manipulate the array (e.g. pushObject instead of push).

Baqend onUpdate Handler

Will doing partialupdate() cause code in a data class' onUpdate Handler to run?
I have this setup in the data class:
exports.onUpdate = function(db, obj) {
DB.log.info(obj.ShiftID);
db.Shifts.load(obj.ShiftID)
.then((Shift) => {
DB.log.info(Shift);
if (Shift.User == db.User.me) {
Shift.User = null;
Shift.status = 0;
return Shift.update();
}
})
};
(yes, role 2 for node has permissions to query and update the Shifts data class)
But I am getting zero logs when I make a partialupdate(). Do I need to do a real update query...load the object, modify the data, update()?
Also it seems that this code causes the partialupdate() to not run at all, but when I delete the handler, it starts working again.
Yes, that is currently an unimplemented feature since a partial update can't execute an onUpdate handler since there is no object which can be passed to the update handler.
On the other hand, a partial update can't be executed directly since that will result in a security issue (since your onUpdate handler can contain validation code etc.)
So we currently reject any partial update on a class which has an onUpdate handler because there doesn't exist a way how we can actually validate the partial update against your onUpdate code.
We have planned that you can define an extra onPartial handler where you can take some extra steps before the partialUpdate is executed. But that handler will only get the partial update and not the object itself.
I'm pretty sure that partialupdate() will not cause the onUpdate Handler to run.
When I put the log line in and edit the records using website data manager it does log as expected. Not a big deal, I can just rewrite the query to be a full update.
BUT having any code in there does break partialupdate() which is not good.
Here is the code I'm using that works as long as there is nothing in the onUpdateHandler:
requestShift(shiftID) {
db.ready().then((db) => {
db.Applicants.find()
.where({
"shiftID": { "$in": [shiftID] },
})
.singleResult((applicants) => {
return applicants.partialUpdate()
.add("applicants", db.User.me.id)
.add("photos", this.props.UserData.photo)
.execute()
})
Alert.alert(
'Confirmation',
'Shift has been requested.',
)
this.props.navigation.dispatch(goToFindShifts)
})
}

Loopback include unrelated lists

In Loopback it is easy to include relational objects when querying for data. For example, one can include all the comments that belong to a blog post in a single call using the include filter.
But in my case I want to get data that doesn't have a relation.
I have a User Detail page. On that page a user can choose a username and there's also a dropdown list where a user can choose from what country he is.
So from the client side I do something like:
Country.find().$promise.then(function(countryData) {
$scope.countries = countryData;
});
Player.find().$promise.then(function(playerData) {
$scope.player = playerData;
}
But what if I get more lists that I want to fill? Like, city, state, colors etc.
Then I'd have to make a lot of separate calls.
Is there a way to include all this data in one call, eventhough they have no relation? Something like this:
Player.find({ filter: { include: ["countries", "colors"] } }).$promise.then(function(data) {
// some stuff
}
You may want to try using the Where filter as documented here
An example of this for querying two specific things would be:
Post.find({where: {and: [{title: 'My Post'}, {content: 'Hello'}]}},
function (err, posts) {
...
});
You could create a remote method on one of your models that makes the calls internally and packages them back up for you.
Use some promise library if not using ES6 to wait for all and then return
Model.getAll = function(next) {
var promises = [];
promises.push(Model.app.models.Country.find());
promises.push(Model.app.models.Player.find());
promises.push(Model.app.models.Color.find());
Promise.all(promises)
.then(function(results) {
next(results);
});
}
/**
Register your remote method here
*/
You could create a remote method on one of your models that makes the calls internally and packages them back up for you.
Use some promise library if not using ES6 to wait for all and then return
Model.getAll = function(next) {
var promises = [];
promises.push(Model.app.models.Country.find());
promises.push(Model.app.models.Player.find());
promises.push(Model.app.models.Color.find());
Promise.all(promises)
.then(function(results) {
next(results);
});
}
/**
Register your remote method here
*/
I have problem and try with this solution but i get error "Failed with multiple errors, see details for more information.". It seems like there is bug on Loopback while using promise.all

Force a controller to always act as a proxy to a model in Ember

I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.

Adding item to filtered result from ember-data

I have a DS.Store which uses the DS.RESTAdapter and a ChatMessage object defined as such:
App.ChatMessage = DS.Model.extend({
contents: DS.attr('string'),
roomId: DS.attr('string')
});
Note that a chat message exists in a room (not shown for simplicity), so in my chat messages controller (which extends Ember.ArrayController) I only want to load messages for the room the user is currently in:
loadMessages: function(){
var room_id = App.getPath("current_room.id");
this.set("content", App.store.find(App.ChatMessage, {room_id: room_id});
}
This sets the content to a DS.AdapterPopulatedModelArray and my view happily displays all the returned chat messages in an {{#each}} block.
Now it comes to adding a new message, I have the following in the same controller:
postMessage: function(contents) {
var room_id = App.getPath("current_room.id");
App.store.createRecord(App.ChatMessage, {
contents: contents,
room_id: room_id
});
App.store.commit();
}
This initiates an ajax request to save the message on the server, all good so far, but it doesn't update the view. This pretty much makes sense as it's a filtered result and if I remove the room_id filter on App.store.find then it updates as expected.
Trying this.pushObject(message) with the message record returned from App.store.createRecord raises an error.
How do I manually add the item to the results? There doesn't seem to be a way as far as I can tell as both DS.AdapterPopulatedModelArray and DS.FilteredModelArray are immutable.
so couple of thoughts:
(reference: https://github.com/emberjs/data/issues/190)
how to listen for new records in the datastore
a normal Model.find()/findQuery() will return you an AdapterPopulatedModelArray, but that array will stand on its own... it wont know that anything new has been loaded into the database
a Model.find() with no params (or store.findAll()) will return you ALL records a FilteredModelArray, and ember-data will "register" it into a list, and any new records loaded into the database will be added to this array.
calling Model.filter(func) will give you back a FilteredModelArray, which is also registered with the store... and any new records in the store will cause ember-data to "updateModelArrays", meaning it will call your filter function with the new record, and if you return true, then it will stick it into your existing array.
SO WHAT I ENDED UP DOING: was immediately after creating the store, I call store.findAll(), which gives me back an array of all models for a type... and I attach that to the store... then anywhere else in the code, I can addArrayObservers to those lists.. something like:
App.MyModel = DS.Model.extend()
App.store = DS.Store.create()
App.store.allMyModels = App.store.findAll(App.MyModel)
//some other place in the app... a list controller perhaps
App.store.allMyModels.addArrayObserver({
arrayWillChange: function(arr, start, removeCount, addCount) {}
arrayDidChange: function(arr, start, removeCount, addCount) {}
})
how to push a model into one of those "immutable" arrays:
First to note: all Ember-Data Model instances (records) have a clientId property... which is a unique integer that identifies the model in the datastore cache whether or not it has a real server-id yet (example: right after doing a Model.createRecord).
so the AdapterPopulatedModelArray itself has a "content" property... which is an array of these clientId's... and when you iterate over the AdapterPopulatedModelArray, the iterator loops over these clientId's and hands you back the full model instances (records) that map to each clientId.
SO WHAT I HAVE DONE
(this doesn't mean it's "right"!) is to watch those findAll arrays, and push new clientId's into the content property of the AdapterPopulatedModelArray... SOMETHING LIKE:
arrayDidChange:function(arr, start, removeCount, addCount){
if (addCount == 0) {return;} //only care about adds right now... not removes...
arr.slice(start, start+addCount).forEach(function(item) {
//push clientId of this item into AdapterPopulatedModelArray content list
self.getPath('list.content').pushObject(item.get('clientId'));
});
}
what I can say is: "its working for me" :) will it break on the next ember-data update? totally possible
For those still struggling with this, you can get yourself a dynamic DS.FilteredArray instead of a static DS.AdapterPopulatedRecordArray by using the store.filter method. It takes 3 parameters: type, query and finally a filter callback.
loadMessages: function() {
var self = this,
room_id = App.getPath('current_room.id');
this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
})
// set content only after promise has resolved
.then(function (messages) {
self.set('content', messages);
});
}
You could also do this in the model hook without the extra clutter, because the model hook will accept a promise directly:
model: function() {
var self = this,
room_id = App.getPath("current_room.id");
return this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
});
}
My reading of the source (DS.Store.find) shows that what you'd actually be receiving in this instance is an AdapterPopulatedModelArray. A FilteredModelArray would auto-update as you create records. There are passing tests for this behaviour.
As of ember.data 1.13 store.filter was marked for removal, see the following ember blog post.
The feature was made available as a mixin. The GitHub page contains the following note
We recommend that you refactor away from using this addon. Below is a short guide for the three filter use scenarios and how to best refactor each.
Why? Simply put, it's far more performant (and not a memory leak) for you to manage filtering yourself via a specialized computed property tailored specifically for your needs