I've been making a simple blog site using Meteor.js. In the index, the page shows recent posts and the list can be rearranged by their tags and category. It also (for sure) has separate pages for each post.
When the index is refreshed, the list is ordered well, sorted by publishedAt. When I try to arrange posts by tags or category, or even when I go to see an individual post, it's good. But when I go back to the index, the last fetched posts (or a post in the latter case) are always on the top of the list, not properly sorted by publishedAt.
For example, let's say we have the list of 4-3-2-1. By sorting them, I have 3-2. When I go back to the index, now the list goes like 3-2-4-1.
When I console.log() from client.js, those last fetched posts are logged first, and then the other posts including the logged ones are logged. How can I fetch posts without being affected by previous fetched data?
client.js
Posts = new Mongo.Collection("posts");
Router.route('/', {
name: 'index',
template: 'index',
layoutTemplate: 'ApplicationLayout',
waitOn: function() {
return Meteor.subscribe('recentPosts');
},
data: function() {
return {
posts: Posts.find()
};
}
});
Router.route('/category/:_category', {
name: 'category',
template: 'category',
layoutTemplate: 'ApplicationLayout',
waitOn: function() {
return Meteor.subscribe('categorizedPosts', this.params._category);
},
data: function() {
return {
posts: Posts.find(),
category: this.params._category
};
}
});
server.js
Posts = new Mongo.Collection("posts");
Meteor.publish('recentPosts', function() {
return Posts.find({}, {
sort: {publishedAt: -1},
fields: postListFields
});
});
Meteor.publish('categorizedPosts', function(category) {
return Posts.find({
$or: [{category: {$regex: category}}]}, {
sort: {publishedAt: -1},
fields: postListFields
});
});
The order of the locally stored data has no relation to the order of the documents that the server gives you (as you've discovered). Consider also the case where multiple publications with different sort orders are sent to one local collection - how could that be handled?
In your client code's data context you need to specify a sort order to the find() method. This will sort the locally stored posts using MiniMongo. It will not cause a new server request, only the subscribe method does that.
You may be wondering what the point is sorting on the server? As an example, if you're limiting the number of posts that you're sending to the client then it's very important that the server sorts them first so that it sends the right ones (e.g. the first 10 of posts sorted by date) over the DDP link.
A result of this is that you very often need to replicate the sort order for the find() method on both the server and client. You may wish to try to user some common code or variable to ensure that they're in sync.
On your "Posts" collection try with a method submitted: new Date(), then sort with it in your helper : return Posts.find({}, {sort: {submitted: -1}});
Related
I am having a customer model and a product model
customer model
import DS from 'ember-data';
export default DS.Model.extend({
type: DS.attr('string'),
name: DS.attr('string'),
product: DS.hasMany('product')
});
product model
import DS from 'ember-data';
export default DS.Model.extend({
type: DS.attr('string'),
name: DS.attr('string'),
customer: DS.belongsTo('customer')
});
I load all the customer in model() hook of route.js as below
model() {
customers: this.store.findAll('customer'),
}
so as of now I have all the records of customer.
then using findRecord method I load perticular product as
this.store.findRecord('product', productId);
I call this on click of a button say Find Products, this is written in controller.js
now when I write in template as
{{product.customer.name}}
it send a get request to load the customer which is already loaded. I want to stop that request how I can do this ?
As per my knowledge
When we already have the record, store.findRecord will resolve immediately with cached data, but will send a request in the background to update the record and once the record is updated your template will show the new changes.
I want to stop that request.
You problem is probably that ember-data does not know that the requested customer is already in the store. In your own answer you use the url to peek the customer, so I assume your id is your url, and thats probably your problem.
There is to mention that ember-data should work as you expect, so you are doing something wrong. Since you haven't shown your adapter, serializer and response I can not tell you exactly what's wrong, but I can show you where to look to it.
Also to mention is that you have an attribute type. This is probably not what you want, because the type is, as the id, an ember internal and you should not declare it yourself.
There seems also to be an syntax error in your model hook, because you use the : like in a object definition:
model() {
customers: this.store.findAll('customer'),
}
You have to understand that model(){} is a function, and identical to model: function() {}!
What you should do is to load all the customers with findAll, but its essential to wait for the promise to be resolved until you ask for the customer on the product. So you should do something like this:
model() {
return Ember.RSVP.hash({
customers: this.store.findAll('customer'),
product: this.store.find('product', 1),
}).then(h => h.product);
}
I have built a little twiddle with your situation, and as you see there is no request made for the customer, but also the customer is not shown. This is because of how I mock the server response in the adapter:
export default DS.JSONAPIAdapter.extend({
findRecord (store, type, id, snapshot) {
console.log('find product ', id);
return Ember.RSVP.resolve({
data: {
id: '2',
type: 'product',
attributes: { name: 'product-one'},
}
});
}
});
Because the customer relationship is not declared ember-data assumes it has none. Lets fix this by including the customer in the product like this:
return Ember.RSVP.resolve({
data: {
id: '2',
type: 'product',
attributes: { name: 'product-one'},
relationships: {
customer: {
links: {
related: '/customers/1'
}
}
}
}
});
Now I have exact the same situation as you have! Ember ties to fetch the customer it already has. Is is because ember-data does not know that it has the customer, because it only knows his url, not his id. This is also the reason why the background reload hooks don't work for you, because its not a reload for ember-data, its an initial load!
Its pretty easy to fix this by just giving ember-data the information:
customer: {
data: { type: 'customer', id: '1' },
links: {
related: '/customers/1'
}
}
And woha, everything works! Now you could even remove the related link because ember-data could just build it by its own.
So in conclusion the right place to fix this is either to adjust the server response itself, or use the serializer to fix it so ember-data can know if it already has the record.
You can use this.store.peekRecord('product', productId) instead of findRecord. Peek will return results without doing a network request. It will completely rely on what is already in store.
DS.Adapter classes have methods for this specific need (and therefore all other adapters):
shouldBackgroundReloadAll for background reload in store.findAll calls.
shouldBackgroundReloadRecord for background reload in store.findRecord calls.
shouldReloadAll for blocking reload in store.findAll calls.
shouldReloadRecord for blocking reload in store.findRecord calls.
If any of these methods returns false, the store will not reload the record/s/ from the backend.
However I would recommend to have some care if you override them, otherwise I suspect some things may stop working as expected, read the documentation about these methods and do some research about its implications.
I got the solution.
If we return false from shouldBackgroundReloadAll and shouldBackgroundReloadRecord, then also ember will send background request if the model we are looking for is in belongsTo relationship.
In template I am calling customer name
{{product.customer.name}}
It is sending ajax request as it has belongsTo relationship.
I overriding the method findBelongsTo in the adapters/application.js. I am checking whether record is loaded or not if loaded then do not send request
findBelongsTo: function(store, snapshot, url, relationship) {
var record = store.peekRecord(relationship.type, url);
if(record){
return new Ember.RSVP.Promise(function(resolve, reject) {
// on success
var r = Ember.copy(record._internalModel._data, true);
r._id = record.id;
var d ={ meta: {} };
d[Ember.String.pluralize(relationship.type)] = [r];
resolve(d);
// on failure
reject(null);
});
} else{
url = this.buildURL(relationship.type, url, snapshot, 'findRecord');
return this.ajax(url, 'GET');
}
}
I have the following method:
function getRelevantArticles(amount,
userSuggestions,
suggestionPage,
relevantArticles,
continueFlag,
success,
error)
{
if(continueFlag)
{
getSuggestedArticles(suggestionPage, userSuggestions, function (articles)
{
if(articles.length == 0)
getRelevantArticles(amount, userSuggestions, suggestionPage, relevantArticles, false, success, error); // continueFlag= false
getUnvisitedArticles(articles, function (unvisited)
{
for(var i = 0; i < unvisited.length; i++)
relevantArticles.push(unvisited[i]);
relevantArticles= filterRelevant(amount, userSuggestions, relevantArticles);
if(relevantArticles.length < amount)
getRelevantArticles(amount, userSuggestions, suggestionPage + 1, relevantArticles, true, success, error); // continueFlag= true
else
getRelevantArticles(amount, userSuggestions, suggestionPage, relevantArticles, false, success, error); // continueFlag= false
}, error);
}, error);
}
else if(success)
{
fillWithContent(relevantArticles, success, error); //Should be last method to execute
}
}
The context
I know it may be hard to understand and can be optimized a lot, I'll try my best to explain what it's doing (or trying to):
The method is first called with a flag of continueFlag= true, so it starts by calling getSuggestedArticles, which is an async method that makes an AJAX request. I pass in a callback function with the result of the request.
getSuggestedArticles gets me the article Ids related to the user suggestions. (The user suggestions is a list of topics that may be of interest to the user).
I pass in a suggestionPage because the suggestions can be a lot, and I should be able to get the relevant articles with just a few (the first page).
If no articles are retrieved, then it means we are out of suggestions (every suggestion has at least one article), i. e., we reached the last page, so we set the continueFlag flag to false, to call the finalizer method.
If there is at least one article, I call getUnvisitedArticles, which is another asynchronous method that makes an AJAX request. This method gives me the articles that were not visited or read by the user, which are the ones I care about.
I have a relevantArticles variable which keeps track of the articles that I found out to be relevant and will be presented to the user. Once I get the relevant ones from my current page of unvisited articles, and have appended them to the previous page's relevant ones, I check if I have the minimum amount of articles to show.
If I don't yet satisfy the minimum amount, then I go on with the next page (suggestionPage + 1);
If I reached the minimum threshold, then I go on with the finalizer method (continueFlag= false)
fillWithContent is the method which will be called when I finished identifying the relevant articles. It is an asynchronous method that will make an AJAX request, and will fill my article objects with additional information.
getSuggestedArticles(Number: suggestionPage, Array: userSuggestions, Function: success, Function: error)
Receives an array of user suggestions, and takes the nth page of this array (page size 100).
Let's suppose we call the method like this:
getSuggestedArticles(0, [ 745, 4567, 1500 ], function (data) {
var articles = data;
}, error);
The method makes request to Database Web API, and pass an array of suggested articles to the success function. In the preceding example, the articles variable would have an array like this (Note that all the returned suggested articles have at least one user suggestion among their topics):
[
{
id: 12345,
topics: [ 998, 1500, 323 ] //has user suggestion 1500
},
{
id: 45778,
topics: [ 009, 1500, 745] //Has user suggestion 745 and 1500
},
...
]
getUnvisitedArticles(Array: articles, Function: success, Function: error)
Receives an array of articles and returns all the ones who weren't visited by the user.
Let's suppose we call this method like this:
//We are using the same "articles" variable from the previous example
getUnvisitedArticles(articles, function (data) {
var unvisited = data;
}, error);
The function makes request to the Database Web Api and pass an array with the unvisited articles to the success function. In the preceding example, the variable unvisited would have an array like this:
[
{
id: 45778,
topics: [ 009, 1500, 745]
}
]
Notice that article with id 12345 is gone. This is because it has been visited by the user.
fillWithContent(Array: relevantArticles, Function: success, Function: error)
Receives an array of articles and fill these objects with additional information.
Let's suppose we call this method like this:
//We are using the same "unvisited" variable from the previous example
fillWithContent(unvisited, function (data) {
filledArticles = data;
}, error);
The function makes request to the Database Web Api and pass an array with the filled articles to the success function. In the preceding example, the variable filledArticles would have an array like this:
[
{
id: 45778,
topics: [009, 1500, 745],
title: 'Article title',
publicationDate: 'Some date',
author: 'Some author',
...
}
]
This is the array my caller is expeting, by caller I mean the one calling my getRelevantArticles function.
The problem
The problem with this method is that fillWithContent is called infinitely, therefore causing a great deal of requests to be made, the browser to crash, and a recursion overflow to arise.
I'm not calling this method from another place, so it has to be a problem with this function.
I writed a console.log(suggestionPage), and it seems it keeps on incrementing the variable infinitely too. It should have stopped at page 3, because articles.length == 0. But it's not stopping.
What is going on here?
I think you should split your workload into independent parts that are easy reason about and easy to combine for more complex results.
As I understand it, the work consists three basic parts, all done through Ajax:
[topic IDs] to [article stubs for those topics] (getSuggestedArticles)
[article stubs] to [relevant (unread) article stubs] (getUnvisitedArticles)
[article stubs] to [full articles] (fillWithContent)
All of those Ajax requests should be made in a paged manner, e.g. 1000 items are handled by 10 requests with 100 items each.
For that we define a utility function that takes a list of items, pages them, makes an Ajax request for each page (through a worker function we pass as an argument) and returns the combined result of all requests.
// utility: runs an ajax request and handles errors on the low level
function ajax(options) {
return $.ajax(options).fail(function(jqXHR, textStatus, errorThrown) {
console.log(textStatus, errorThrown, jqXHR);
});
}
// utility: makes paged Ajax requests through a worker function
function pagedAjax(ajaxFunc, items, pageSize) {
var temp = [].slice.call(items), page,
requests = [];
// start as many parallel Ajax requests as we have pages
while (temp.length) {
page = temp.splice(0, pageSize);
requests.push( ajaxFunc(page) );
}
// wait until all requests have finished, return combined result
return $.when.apply($, requests).then(function (results) {
var combined = [];
$.each(results, function (i, result) {
// result is an array [data, textStatus, jqXhr]
// push all contained items onto combined
// (the following assumes that data is an array of objects)
[].push.apply(combined, result[0]);
});
return combined;
});
}
Now we can set up our three worker functions. They take arbitrary amounts of input, because all paging is done by the utility function above:
// worker: retrieves a list of article IDs from topic IDs
function getSuggestedArticles(topics) {
return ajax({method: 'post', url: '/articlesByTopic', data: topics});
// or whatever API request returns a list of articles IDs from topic IDs
}
// worker: takes a list of article IDs, returns a list of _unread_ article IDs
function getUnvisitedArticles(articles) {
return ajax({method: 'post', url: '/unvisitedArticles', data: articles});
// or whatever API request returns a list of unvisited articles from IDs
}
// worker: takes a list of article IDs, returns a list of articles
function fillWithContent(articles) {
return ajax({method: 'post', url: '/articles', data: articles});
// or whatever API request fills articles with content
}
After that, combining the functions is not difficult anymore:
// takes a list of topic IDs, requests article IDs, filters them, returns actual articles
function getUnvisitedArticlesByTopic(topicIds) {
var pageSize = 100;
return pagedAjax(getSuggestedArticles, topicIds, pageSize)
.then(function (allArticles) {
return pagedAjax(getUnvisitedArticles, allArticles, pageSize);
})
.then(function (unvisitedArticles) {
return pagedAjax(fillWithContent, unvisitedArticles, pageSize);
});
}
And we can use it all through a very simple call:
// renders unvisited articles
function renderUnvisitedArticles() {
var topicIds = [9, 1500, 745];
getUnvisitedArticlesByTopic(topicIds).done(function (articles) {
$.each(articles, function (i, article) {
// show article on page
});
});
}
Benefits of this promise-based approach:
No callback hell.
Short functions that do exactly one thing.
No self-calling functions.
Good re-usability and testability of the individual parts.
Recommended reading is of course jQuery's documentation on Deferred objects.
Disclaimer: The code is indeed untested. If you find mistakes, tell me.
I'm requesting Users from the server. There is only one user, and that's what comes over HTTP. However, when I loop over the rows returned from the .findAll() call, I see multiple values.
My Route:
U.UsersIndexRoute = Ember.Route.extend({
model: function() {
return {
users: this.store.findAll('user')
};
},
setupController: function(controller, model) {
controller.set('model', model.users);
}
});
My Controller (abridged):
U.UsersIndexController = U.ApplicationArrayController.extend({
_modelChanged: function() {
this.get('model').then(_.bind(function(users) {
_.each(users.get('content'), function(user) {
// I should only see one name logged
console.log(user.get('name'), user.get('id'));
});
}, this));
}.observes('model', 'orgNameForId')
})
Server response to GET /users
{
"Results": [{
"id": 16,
"Name": "Chris"
}]
}
When this runs, console shows 2 users, one that's Chris 16 and the other that's Chris null.
My questions
Does the null ID mean something else in my code is creating the record?
Should I just always filter out models without an ID?
Help!
When you create a user in your application, it expects a success call back from the server with an id, which will automatically update for you if configured right. If not, it's left as null. When you do your GET request, it will add what the server sends it to ember-data, which will be another model that it won't recognize as the same one since the id won't match.
If you don't use it already, I'd check out the Ember Inspector Chrome plugin, which allows you to see routes, data, etc.
As mentioned in the comment, ember-data expects the following format, but you mentioned taking care of that in your adapter:
{ 'users' : [{
'id': 16,
'Name": "Chris" }]
}
As for your last question, you shouldn't filter out models whose id is null. You should create your app so that there are no ids that are null.
I'm building a relatively big NodeJS application, and I'm currently trying to figure out how to fetch the data I need from the DB. Here is a part of my models :
One user has one role, which has access to many modules (where there's a table role_modules to link roles and modules).
In Rails, I would do something like user.role.modules to retrieve the list of the modules he has access to. In NodeJS it's a bit more complicated. I'm using node-orm2 along with PostgreSQL. Here is what I have so far:
req.models.user.find({email: req.body.user}, function(err, user) {
user[0].getRole(function(err, role) {
role.getModules(function(err, modules) {
var list_modules = Array();
modules.forEach(function(item) {
console.log(item);
list_modules.push(item.name);
})
But I can't do this, because item only contains role_id and module_id. If I want to have the name, I would have to do item.getModule(function() {...}), but the results would be asynchronous ... so I don't really see how I could end up with an array containing the names of the modules a user has access to ... have any idea?
Also, isn't that much slower than actually running a single SQL query with many JOIN? Because as I see it, the ORM makes multiple queries to get the data I want here...
Thank you!
I wrote an ORM called bookshelf.js that aims to simplify associations and eager loading relations between models in SQL. This is what your query would probably look like to load the role & modules on a user given your description:
var Module = Bookshelf.Model.extend({
tableName: 'modules'
});
var Role = Bookshelf.Model.extend({
tableName: 'roles',
modules: function() {
return this.belongsToMany(Module);
}
});
var User = Bookshelf.Model.extend({
tableName: 'users'
role: function() {
return this.hasOne(Role);
}
});
User.forge({email: req.body.user})
.fetch({
require: true,
withRelated: ['role.modules']
})
.then(function(user) {
// user, now has the role and associated modules eager loaded
console.log(user.related('role'));
console.log(user.related('role').related('modules'))
}, function(err) {
// gets here if no user was found.
});
Might be worth taking a look at.
I am attempting to retrieve the information for a Model and an associated model that contains a hasOne association. I was referencing the following Sencha documentation page (http://docs.sencha.com/ext-js/4-1/#!/guide/data). I currently have the following sample code working:
var Mtd = Ext.ModelMgr.getModel('Mtd');
Mtd.load(4, {
success: function(mtd){
console.log("Loaded! " + mtd.get('id'));
mtd.getTreatmentdesign(function(treatment,operation){
console.log(treatment.get('id'));
}, this);
}
});
Now, when I call mtd.getTreatmentdesign(), I notice that two requests are made to retrieve information. The first one is to retrieve the Mtd information which I am expecting but then it's also making a request to retrieve the Treatmentdesign information. The response for the Mtd contains the Mtd information as well as the Treatmentdesign information. So I want to process the Mtd and Treatmentdesign information with one request. It puzzled me that the documentation stated the following:
You may be wondering why we passed a success function to the User.load call but didn't have to do so when accessing the User's posts and comments. This is because the above example assumes that when we make a request to get a user the server returns the user data in addition to all of its nested Posts and Comments. By setting up associations as we did above, the framework can automatically parse out nested data in a single request.
So how can I retrieve associated information without having to make another request? I simply just want to use all the json from a single request as opposed to having to make multiple requests.
Be sure to set the associationKey config on the HasOne association to the property that contains the data for the associated model. By default, this is the name of the associated model class in all lowercase letters.
For instance, if the data for an Mtd record is returned by the server in the form
{
...
treatmentDesign: {
...
}
}
set the associationKey to 'treatmentDesign'.
Here's an example in action: http://jsfiddle.net/HP6fq/3/
Yes, associationKey works
Ext.define('User', {
extend:'Ext.data.Model',
fields: ['id', 'name', 'status'],
associations: [{ type: 'hasOne', model: 'Status', associationKey: 'status' }]
});
Ext.define('Status', {
extend:'Ext.data.Model',
fields: ['id', 'title'],
});
Demo here