Fetch Backbone collection with search parameters - search

I'd like to implement a search page using Backbone.js. The search parameters are taken from a simple form, and the server knows to parse the query parameters and return a json array of the results. My model looks like this, more or less:
App.Models.SearchResult = Backbone.Model.extend({
urlRoot: '/search'
});
App.Collections.SearchResults = Backbone.Collection.extend({
model: App.Models.SearchResult
});
var results = new App.Collections.SearchResults();
I'd like that every time I perform results.fetch(), the contents of the search form will also be serialized with the GET request. Is there a simple way to add this, or am I doing it the wrong way and should probably be handcoding the request and creating the collection from the returned results:
$.getJSON('/search', { /* search params */ }, function(resp){
// resp is a list of JSON data [ { id: .., name: .. }, { id: .., name: .. }, .... ]
var results = new App.Collections.SearchResults(resp);
// update views, etc.
});
Thoughts?

Backbone.js fetch with parameters answers most of your questions, but I put some here as well.
Add the data parameter to your fetch call, example:
var search_params = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
...
'keyN': 'valueN',
};
App.Collections.SearchResults.fetch({data: $.param(search_params)});
Now your call url has added parameters which you can parse on the server side.

Attention: code simplified and not tested
I think you should split the functionality:
The Search Model
It is a proper resource in your server side. The only action allowed is CREATE.
var Search = Backbone.Model.extend({
url: "/search",
initialize: function(){
this.results = new Results( this.get( "results" ) );
this.trigger( "search:ready", this );
}
});
The Results Collection
It is just in charge of collecting the list of Result models
var Results = Backbone.Collection.extend({
model: Result
});
The Search Form
You see that this View is making the intelligent job, listening to the form.submit, creating a new Search object and sending it to the server to be created. This created mission doesn't mean the Search has to be stored in database, this is the normal creation behavior, but it does not always need to be this way. In our case create a Search means to search the DB looking for the concrete registers.
var SearchView = Backbone.View.extend({
events: {
"submit form" : "createSearch"
},
createSearch: function(){
// You can use things like this
// http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery
// to authomat this process
var search = new Search({
field_1: this.$el.find( "input.field_1" ).val(),
field_2: this.$el.find( "input.field_2" ).val(),
});
// You can listen to the "search:ready" event
search.on( "search:ready", this.renderResults, this )
// this is when a POST request is sent to the server
// to the URL `/search` with all the search information packaged
search.save();
},
renderResults: function( search ){
// use search.results to render the results on your own way
}
});
I think this kind of solution is very clean, elegant, intuitive and very extensible.

Found a very simple solution - override the url() function in the collection:
App.Collections.SearchResults = Backbone.Collection.extend({
urlRoot: '/search',
url: function() {
// send the url along with the serialized query params
return this.urlRoot + "?" + $("#search-form").formSerialize();
}
});
Hopefully this doesn't horrify anyone who has a bit more Backbone / Javascript skills than myself.

It seems the current version of Backbone (or maybe jQuery) automatically stringifies the data value, so there is no need to call $.param anymore.
The following lines produce the same result:
collection.fetch({data: {filter:'abc', page:1}});
collection.fetch({data: $.param({filter:'abc', page:1})});
The querystring will be filter=abc&page=1.
EDIT: This should have been a comment, rather than answer.

Related

how to passing few variables from mongodb query to jade tpl

//posts
var docs, cats;
var db = req.db;
var catcollection = db.get('catcollection');
var postcollection = db.get('postcollection');
// find all post
postcollection.find({},{},function(e,docs){
console.log('posts ---> '+util.inspect(docs));
}); // end find all post
catcollection.find({},{},function(e,catss){
cats=catss;
console.log('cats --> '+util.inspect(cats)); //<<<---- write objects from mongo
}); // end find all cats for select
res.render('newpost', {
posts : docs, cats:cats, title: 'Add New post'});
}); **//<<<---it didn't passing the cats:cats and post vars to jade **
jade template
extends layout
block content
h1= title
form#formAddPost(name="addpost",method="post",action="/addpost")
input#inputPostTitle(type="text", placeholder="posttitle", name="posttitle")
textarea#inputPostTitle(placeholder="postdesc", name="postdesc")
textarea#inputPostTitle(placeholder="posttext", name="posttext")
select#selectPostCats(placeholder="postdesc", name="posttext")
each cat, i in cats
option(value="#{cat._id}") #{cat.titlecat}
button#btnSubmit(type="submit") submit
ul
each post, i in posts
li= i+" "
a(href="/editpst/#{post._id}")=#{post.title}
I get this error message in jade tpl
Cannot read property 'length' of undefined
but if I wrote
catcollection.find({},{},function(e,catss){
cats=catss;
console.log('cats --> '+util.inspect(cats));
**res.render('newpost', {
cats:cats, title: 'Add New post'});**
}); // end find all cats for select
It passing category list to jade , but i can't pass post list to jade.
How to passing few variables (posts and cats ) to jade tpl?
Both of the .finds execute asynchronously so you don't know when (or if) either one will complete. That is to say you need to wait until both of the callbacks are called before you attempt to render the template.
The simplest way in your current implementation would be to nest everything:
postcollection.find({},{},function(e,docs){
// handle errors
catcollection.find({},{},function(e,cats){
res.render('newpost', {
posts : docs, cats:cats, title: 'Add New post'});
});
});
});
However you can do these queries simultaneously because they do not depend on each other. The best way to do that is to probably use promises.
Promise.all([postcollection.find(), catcollection.find()])
.then(function (docs, cats) {
res.render('newpost', {
posts : docs, cats:cats, title: 'Add New post'});
});
});
This assumes that .find returns a promise. It should for the current Mongo driver.

Using an arbitrary number of query params to filter results in mongoose

I'm building an API using node express and mongodb, with mongoose.
I have a post resource that handles user posts, and would like to be able to perform various queries on the post resource.
For instance I have a functions as that returns all posts as follows:
// Gets a list of Posts
exports.index = function(req, res) {
console.log(req.query);
Post.findAsync()
.then(mUtil.responseWithResult(res))
.catch(mUtil.handleError(res));
};
I looking for a good way of processing any additional query params that might come with the request.
/posts will return all posts, but /posts?user=12 will return posts by user with id 12 and /posts?likes=12 will return posts with 12 or more likes.
How can I check for and apply the these query params to filter and return the results since they may or may not be present.
Thanks ;)
If user=12 means "users with id 12", how does likes=12 mean "likes greater than 12"? You need to be more descriptive with your queries. You can do that by passing an array of objects. Send your query in a way that can be interpreted like this:
var filters = [
{
param: "likes",
type: "greater"
value: 12
},
{
param: "user",
type: "equal",
value: "12"
}]
var query = Post.find();
filters.forEach(function(filter) {
if (filter.type === "equal") {
query.where(filter.param).equals(filter.value);
}
else if (filter.type === "greater") {
query.where(filter.param).gt(filter.value);
}
// etc,,,
})
query.exec(callback);

Securing access to collection from the client's side

I have a meteor app prototype that works well, but is very insecure as of now: I needed to display a list of matching users to the currently logged-in user. For starters, I decided to publish all users, limiting the fields to what I would need to filter the user list on the client side.
Meteor.publish('users', function () {
return Meteor.users.find({}, {
fields: {
'profile.picture': 1,
'profile.likes': 1,
'profile.friends': 1,
'profile.type': 1
}
});
});
Then in my router, I would do a request to only show what I wanted on the client side:
Router.map(function() {
this.route('usersList', {
path: '/users',
waitOn: function () {
return Meteor.subscribe('users');
},
data: function () {
var user = Meteor.user();
return {
users: Meteor.users.find({ $and: [
{_id: {$ne : user._id}},
{'profile.type': user.profile.interest}
]})
};
}
});
});
In the code above, I query all users who are not the current user and whose type correspond the current user's interest. I also display a certain border on the photos of users who have my user in their "profile.friends" array, using this client helper:
Template.userItem.helpers({
getClass: function(userId) {
var user = Meteor.user();
var lookedup = Meteor.users.findOne({_id: userId});
if ($.inArray(user._id, lookedup.profile.friends) !== -1)
return "yes";
return "no";
}
});
Now this all worked great, but with this setup, every client can query every users and get their type, picture, list of friends and number of likes. If I was in an MVC, this info would only be accessible on server side. So I decided my next iteration to be a security one. I would move my query from the router file to the publications file. That's where trouble began...
Meteor.publish('users', function () {
var user = Meteor.users.findOne({_id: this.userId});
var interest = user.profile.interest;
// retrieve all users, with their friends for now
allUsers = Meteor.users.find({ $and: [
{'_id': {$ne: user._id}},
{'profile.type':interest}
]},
{ fields: {'profile.picture': 1, 'profile.friends': 1}}
);
return allUsers;
});
And in the router:
Router.map(function() {
this.route('usersList', {
path: '/users',
waitOn: function () {
return Meteor.subscribe('users');
},
data: function () {
var user = Meteor.user();
return {users: Meteor.users.find({_id: {$ne : user._id}})};
}
});
});
(note that I still need to exclude the current user from the router query since the current user is always fully published)
This works, but:
the user list does not get updated when I change the user interest and then do a Router.go('usersList'). Only when I refresh the browser, my list is updated according to the user's new interest. No idea why.
this solution still publishes the users' friends in order to display my matching borders. I wish to add a temporary field in my publish query, setting it to "yes" if the user is in the user's friends and "no" otherwise, but... no success so far. I read I could use aggregate to maybe achieve that but haven't managed to so far. Also, aggregate doesn't return a cursor which is what is expected from a publication.
This problem makes me doubt about the praises about meteor being suitable for secure apps... This would be so easy to achieve in Rails or others!
EDIT: As requested, here is the code I have so far for the transition of my "matching" check to the server:
Meteor.publish('users', function () {
var user = Meteor.users.findOne({_id: this.userId});
var interest = user.profile.interest;
// retrieve all users, with their friends for now
allUsers = Meteor.users.find({ $and: [
{'_id': {$ne: user._id}},
{'profile.type':interest}
]},
{ fields: {'profile.picture': 1, 'profile.friends': 1}}
);
// ------------- ADDED ---------------
allUsers.forEach(function (lookedup) {
if (_.contains(lookedup.profile.friends, user._id))
lookedup.profile.relation = "yes";
else
lookedup.profile.relation = "no";
lookedup.profile.friends = undefined;
return lookedup;
});
// ------------- END ---------------
return allUsers;
});
Obviously this code doesn't work at all, since I cannot modify cursor values in a foreach loop. But it gives an idea of what I want to achieve: give a way to the client to know if a friend is matched or not, without giving access to the friend lists of all users to the client. (and also, avoiding having to do one request per each user during display to ask the server if this specific user matches this specific one)
You can add a transform function and modify a cursor docs on the fly
meteor Collection.find

How can I add markers to Google Maps using Node.js/Express/MongoDB?

I decided this week that I'd like to learn Node.js (I'm not a programmer). I've been having a lot of fun with it so far but I'm stuck at the moment.
I've created a basic app using Express. Let's say I have a route /locations. I set my get request to render the related view and to find (using Mongoose's .find method) all docs in my location model. I know I can pass the docs to the view like this:
app.get('/locations', function(req, res) {
Location.find({}, function(err, docs) {
res.render('locations/index', {
title: 'Locations',
user: req.user,
docs: docs
});
});
});
I can then, for example, access the results in the (Jade) view and list them by doing something like:
if(docs.length)
each location in docs
p #{location.name} is at #{location.coordinates}
I want to add all these locations (using coordinates stored with each 'location') to a Google Map. I have an example map displaying on the view using the following script in the head, taken from the Google Maps API documentation.
function initialize() {
var myLatlng = new google.maps.LatLng(-25.363882,131.044922);
var mapOptions = {
zoom: 4,
center: myLatlng,
mapTypeId: google.maps.MapTypeId.ROADMAP,
}
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
var marker = new google.maps.Marker({
position: myLatlng,
title: "Hello World!"
});
marker.setMap(map);
}
google.maps.event.addDomListener(window, 'load', initialize);
I figured where the marker variable is created then set I could loop through my 'docs' and create and set a marker for each location stored in my database. That said, I'm too new to this and I can't seem to figure out how to do it as the script in the head can't access the 'docs' that I've passed to the view.
Any advice? Thanks in advance, it's much appreciated.
I JSON.stringify() any objects that my client scripts need and insert it as HTML5 data-whatever attributes.
For example:
//app.js
app.get('/map', function(req, res){
var data = {
id: '1234',
LL: {
lat: 42.1,
lng: 80.8,
};
res.locals.docsJSON = JSON.stringify([data]);
res.render('locations/index');
});
//jade
!!!
html
body(data-locations=locals.docsJSON)
script
var docs = JSON.parse($('body').attr('data-locations'));
console.log(docs[0].LL);

jQuery Autocomplete - Show Data Based on Selection

I have a standard jQuery autocomplete setup similar to the below:
$("input#autocomplete").autocomplete({
source: source,
minLength: 5 ,
select: function( event, ui ) {
alert(ui.item.value);
}
});
What I would like is, when the value is chosen, a data-table within the page appears and get populated with data from a database using the value as a search parameter.
So for instance if I select "RED", the table would then show and display data from a query such as SELECT * FROM TABLE WHERE COLUMN='RED'
The query is simplified but can anyone please point me in the right direction?
For this purpose you should request a kind of search page which will act as JSON endpoint for e.g.
$("input#autocomplete").autocomplete({
source: source,
minLength: 5 ,
select: function( event, ui ) {
var _value = ui.item.value;
$.post('services/populate_table.php', // endpoint URL
{ someParameterToTransmit: _value }, // some data to transmit
function(data) { // on complete handler
$('.result').html(data); // populate retrieved data in any form you need
} // on complete function
); // post
} // on select (autocomplete)
}); // autocomplete
Data from endpoint also can be retrieved as JSON.
You can read documentation for more information about request method.
If I understand you correctly, you're looking for $.post.
For example, your jQuery would be:
$("input#autocomplete").autocomplete({
source: source,
minLength: 5 ,
select: function( event, ui ) {
$.post("autocomplete.php", { option: ui.item.value }, function(data){
$("table").html( data[0] );
// sets the content of a table element to the first matched row
});
}
});
And in autocomplete.php, you would have something like this:
// DB connect
// use $_POST['option'] here for the selected option
$sth = mysql_query("SELECT ...");
$r = mysql_fetch_assoc($sth);
print $r;
What we do here is request the page autocomplete.php and POST the data, which in this case is the selected value. autocomplete.php grabs that POSTed value and searches the database (you can customize that query to fit your needs). The page then prints an array of the matched rows, which is the data received by the jQuery, and can be traversed as a Javascript array.

Resources