Currently i am working on a single-page-application with backbone.js.
My server is providing a REST api.
http://server.com/article is returning the latest articles from all categories.
http://server.com/article/categoryname is returning the latest articles from specified category only.
My current collection looks like this:
define(['backbone', 'models/article'], function(Backbone, Article) {
return Backbone.Collection.extend({
model: Article,
url: "http://server.com/article",
});
});
So with fetch i can receive all the latest articles from all categories. Where should i implement my specific category requests, so i do not have to create a collection for every category by myself and a possible caching won't be so difficult at all.
Would it be better to change my rest api and receive all articles and filter them in the backbone app? Or should i implement a fetchFromCategory method in my collection and build the caching myself?
I am thankful for every hint or idea!
If fetching all the Articles in a single request is not an issue, you can do that. To get articles by categoryName, you can then add a method in the collection as below :
define(['backbone', 'models/article'], function(Backbone, Article) {
return Backbone.Collection.extend({
model: Article,
url: "http://server.com/article",
getArticlesByCategory : function(category) {
return this.filter(function(article){
//Assuming 'Article' has a property named 'categoryName'
return article.get('categoryName') == category;
});
}
});
});
Now, if articles is your collection you can do :
var categoryXYZArticles = articles.getArticlesByCategory('XYZ');
Related
How do you guys handle the case:
Query return list of post
Then edit the post, so I just need to fetch detail of that post again
It might somehow be related to normalization but I think we can have some best-practice to handle this.
Maybe we can have a keys
List: ['post']
Post item: ['post', {id: number}]
Then when we fetch the list, we actually set data for the post items, and the ['list'] only save the id.
Does that make sense? or anyone have a better solution for this
You can either prefetch your posts using QueryClient.prefetchQuery and then display them by retrieving individual posts from cache by id or you can perform optimistic update using single query key like this:
const queryClient = useQueryClient();
const { mutate } = useMutation(someUpdateFn, {
onMutate: async post => {
await queryClient.cancelQueries(['posts']);
queryClient.setQueryData(['posts'], posts => {
const previousPosts = posts.filter(({ id }) => post.id !== id);
return [...previousPosts, post];
});
},
});
You can read more about optimistic updates here
At a certain point, you should just manage your own state, global or local, via your own array and such. Do fetches with axios and add, delete, modify your own cache.
I have tried some complicated stuff and sooner or later, doing react query and making it work isn't worth it in my opinion. However, for most cases, react query is just brilliant.
Given the data structure below in firebase, i want to run a query to retrieve the blog 'efg'. I don't know the user id at this point.
{Users :
"1234567": {
name: 'Bob',
blogs: {
'abc':{..},
'zyx':{..}
}
},
"7654321": {
name: 'Frank',
blogs: {
'efg':{..},
'hij':{..}
}
}
}
The Firebase API only allows you to filter children one level deep (or with a known path) with its orderByChild and equalTo methods.
So without modifying/expanding your current data structure that just leaves the option to retrieve all data and filter it client-side:
var ref = firebase.database().ref('Users');
ref.once('value', function(snapshot) {
snapshot.forEach(function(userSnapshot) {
var blogs = userSnapshot.val().blogs;
var daBlog = blogs['efg'];
});
});
This is of course highly inefficient and won't scale when you have a non-trivial number of users/blogs.
So the common solution to that is to a so-called index to your tree that maps the key that you are looking for to the path where it resides:
{Blogs:
"abc": "1234567",
"zyx": "1234567",
"efg": "7654321",
"hij": "7654321"
}
Then you can quickly access the blog using:
var ref = firebase.database().ref();
ref.child('Blogs/efg').once('value', function(snapshot) {
var user = snapshot.val();
ref.child('Blogs/'+user+'/blogs').once('value', function(blogSnapshot) {
var daBlog = blogSnapshot.val();
});
});
You might also want to reconsider if you can restructure your data to better fit your use-case and Firebase's limitations. They have some good documentation on structuring your data, but the most important one for people new to NoSQL/hierarchical databases seems to be "avoid building nests".
Also see my answer on Firebase query if child of child contains a value for a good example. I'd also recommend reading about many-to-many relationships in Firebase, and this article on general NoSQL data modeling.
Given your current data structure you can retrieve the User that contains the blog post you are looking for.
const db = firebase.database()
const usersRef = db.ref('users')
const query = usersRef.orderByChild('blogs/efg').limitToLast(1)
query.once('value').then((ss) => {
console.log(ss.val()) //=> { '7654321': { blogs: {...}}}
})
You need to use limitToLast since Objects are sorted last when using orderByChild docs.
It's actually super easy - just use foreslash:
db.ref('Users').child("userid/name")
db.ref('Users').child("userid/blogs")
db.ref('Users').child("userid/blogs/abc")
No need of loops or anything more.
In Mongoose, I can use a query populate to populate additional fields after a query. I can also populate multiple paths, such as
Person.find({})
.populate('books movie', 'title pages director')
.exec()
However, this would generate a lookup on book gathering the fields for title, pages and director - and also a lookup on movie gathering the fields for title, pages and director as well. What I want is to get title and pages from books only, and director from movie. I could do something like this:
Person.find({})
.populate('books', 'title pages')
.populate('movie', 'director')
.exec()
which gives me the expected result and queries.
But is there any way to have the behavior of the second snippet using a similar "single line" syntax like the first snippet? The reason for that, is that I want to programmatically determine the arguments for the populate function and feed it in. I cannot do that for multiple populate calls.
After looking into the sourcecode of mongoose, I solved this with:
var populateQuery = [{path:'books', select:'title pages'}, {path:'movie', select:'director'}];
Person.find({})
.populate(populateQuery)
.execPopulate()
you can also do something like below:
{path:'user',select:['key1','key2']}
You achieve that by simply passing object or array of objects to populate() method.
const query = [
{
path:'books',
select:'title pages'
},
{
path:'movie',
select:'director'
}
];
const result = await Person.find().populate(query).lean();
Consider that lean() method is optional, it just returns raw json rather than mongoose object and makes code execution a little bit faster! Don't forget to make your function (callback) async!
This is how it's done based on the Mongoose JS documentation http://mongoosejs.com/docs/populate.html
Let's say you have a BookCollection schema which contains users and books
In order to perform a query and get all the BookCollections with its related users and books you would do this
models.BookCollection
.find({})
.populate('user')
.populate('books')
.lean()
.exec(function (err, bookcollection) {
if (err) return console.error(err);
try {
mongoose.connection.close();
res.render('viewbookcollection', { content: bookcollection});
} catch (e) {
console.log("errror getting bookcollection"+e);
}
//Your Schema must include path
let createdData =Person.create(dataYouWant)
await createdData.populate([{path:'books', select:'title pages'},{path:'movie', select:'director'}])
So, I'm trying to show how Restivus API works and Meteor is nice to my colleagues. :)
I've made a simple blog app at http://askar-blog.meteor.com/ (thanks to DiscoverMeteor book).
My repo https://github.com/tenzan/blog
(I'm reading https://github.com/kahmali/meteor-restivus#restivus)
I have three collections:
users
posts
comments
So, post has many comments. Usually, we used to have comments as a nested documents inside of a post, but from the Meteor's nature these two attributes are split up into different collections.
I want to implement a REST API, so that I can access (including CRUD operations) posts and collections in the way:
http://example.com/api/posts - all posts
http://example.com/api/posts/post_id - a specific post
http://example.com/api/posts/post_id/comments - all comments that belongs to a given post
http://example.com/api/posts/post_id/comments/comment_id - a specific comment that belongs to a given post
If you have a look at my repo, you will see there're posts.js and comments.js under lib/collections.
As I understood, to enable REST API, I will need the following snippet in the posts.js:
if (Meteor.isServer) {
// Global API configuration
var Api = new Restivus({
useDefaultAuth: true,
prettyJson: true
});
// Generates: GET, POST on /api/post and GET, PUT, DELETE on
// /api/items/:id for the Posts collection
Api.addCollection(Posts);
// Generates: POST on /api/users and GET, DELETE /api/users/:id for
// Meteor.users collection
Api.addCollection(Meteor.users, {
excludedEndpoints: ['getAll', 'put'],
routeOptions: {
authRequired: true
},
endpoints: {
post: {
authRequired: false
},
delete: {
roleRequired: 'admin'
}
}
});
As you see, I've added Api.addCollection(Posts); and I've confirmed I can access all posts or a specific one.
My questions:
1- How can I setup API to access comments for their parent post?
2 - Will I have to have to following code to access posts ? I'm asking because, I'm already able to access them as I have Api.addCollection(Posts); :
Maps to: /api/posts/:id
Api.addRoute('posts/:id', {authRequired: true}, {
get: function () {
return Posts.findOne(this.urlParams.id);
},
delete: {
roleRequired: ['author', 'admin'],
action: function () {
if (Articles.remove(this.urlParams.id)) {
return {status: 'success', data: {message: 'Post removed'}};
}
return {
statusCode: 404,
body: {status: 'fail', message: 'Post not found'}
};
}
}
});
I apologise, I got confused myself trying to figure out the correct way of making a REST API.
Please feel free to add anything important on this regard I have missed here.
I've discussed with the author of the package
https://github.com/kahmali/meteor-restivus/issues/128
The feature is being developed
https://github.com/kahmali/meteor-restivus/issues/70
I'm designing an API and also consuming it with Backbone.js. Part of the API will include search operations. For example when searching for cars, I might have something like:
http://api.mysite.com/search/cars?q=volvo
With backbone, I can see two options for consuming the results.
Option 1: A search is a Collection
var CarSearch = Backbone.Collection.extend({
model: Car,
initialize : function(models, options){
this.query = options.query;
},
url: function(){
return "http://api.mysite.com/search/cars?q="+this.query;
}
});
var volvos = new CarSearch([], {query:'volvo'});
volvos.fetch();
Option 2: A search is a Model, and the results are a Collection
var CarSearchResults = Backbone.Collection.extend({
model: Car
});
var CarSearch = Backbone.Model.extend({
defaults: {
"query":"",
"carSearchResults":null
},
url: function(){
return "http://api.mysite.com/search/cars?q="+this.get('query');
},
parse: function(resp,xhr){
resp.carSearchResults = new CarSearchResults(resp.carSearchResults);
return resp;
}
});
var volvoSearch = new CarSearch();
volvoSearch.set({query:'volvo'});
volvoSearch.save();
What are the advantages / disadvantages of these options? Is there a backbone-y way of designing this?
I'm leaning towards option 2 because it seems easier to add things to the response like pagination details, or a next url. But option 2 seems messier in a couple of ways. For example, would I generate an ID on the server for the search model when it is saved? Don't think I need to get that model by ID, deleting or updating it doesn't really make sense either cause I'm not persisting it.
i dont know if its a good practice,
but i use for my search the "data" option in the "fetch" method.
https://stackoverflow.com/a/6659501/1067061
Maybe it helps.
Good Luck!
EDIT
This is the right way to pass query parameters in your collections url,
The reference to the Docs shows how to pass the data attribute in fetch options, the data attribute is actually an object with key value pairs referring to query params and their values
I would go with option one. At least imo a model should correspond to a single search result and the collection to the entire set of search results. so if you search for volvo and there are 6 items returned, each item should be a model contained within your collection.
Now this will largely depend on how you are representing a result on your server. If say for instance you have car instances then you just do the search server side using the query and return the resulting objects as json. Then you can have the returned list be the collection of car models that match the criteria. but if you are planning on returning the query results some other way then you will have to think about how the model should represent the data
I would recommend using a collection, like in option 1, but without the need to define a new collection just for the search.
Take a look at my blog post about this here: http://willdemaine.ghost.io/restful-search-with-backbone/
var SearchableCollection = Backbone.Collection.extend({},{
search: function(query, options){
var search = $.Deferred();
options = options || {};
var collection = new this([], options);
collection.url = _.result(collection, 'url') + 'search?q=' + query;
var fetch = collection.fetch();
fetch.done(_.bind(function(){
Backbone.Events.trigger('search:done');
search.resolveWith(this, [collection]);
}, this));
fetch.fail(function(){
Backbone.Events.trigger('search:fail');
search.reject();
});
return search.promise();
}
});
Then you can do:
var Cars = SearchableCollection.extend({});
var findCars = Cars.search('volvo');
findCars.done(function(cars){
var carsView = new CarsView({
collection: cars
});
carsView.render();
});