Paginate Result of related models - node.js

This is another follow up to a previous question. There are only two models involved: category and game which share a hasMany relation. Example: The endpoint /Categories/1001/games/mature list all games of fighting category that have mature flag set to true. However, I am unable to paginate the response. What is the proper way to paginate based on the code shown below? I would like to only display 10 results at a time.
common/models/category.js
Category.mature = function(id, callback) {
var app = this.app;
var Game = app.models.game;
Category.findById(id, {}, function(err, category) {
if (err) return callback(err);
//Now call the Game find method
Game.find({
"where": {
categoryId: id,
mature: true
}
}, function(err, gameArr) {
if (err) return callback(err);
callback(null, gameArr);
});
});
}
Category.remoteMethod(
'mature', {
accepts: [{
arg: 'id',
type: 'number',
required: true
}],
// mixing ':id' into the rest url allows $owner to be determined and used for access control
http: {
path: '/:id/games/mature',
verb: 'get'
},
returns: {
arg: 'games',
type: 'array'
}
}
);
Tables/Models
catgories
category_name category_id
------------- -----------
fighting 1001
racing 1002
sports 1003
games
game_id game_name category_id mature description published_date
----------- ------------ ----------- ------- ----------- --------------
13KXZ74XL8M Tekken 10001 true Published by Namco. 1994
138XZ5LPJgM Forza 10002 false Published by Microsoft 2005
API Result:
games [
{
gameName: 'Tekken',
gameInfo :
[
{
description : 'Published by Namco.',
published_date : '1994'
}
],
categorName: 'fighting',
categoryId: 1001,
mature: true
}
.....
]

If you are stuck with the code above, you will need to download the full set of Games and then paginate on the frontend. Without being able to send in limit and skip values to your queries, there is no other way.
If you can change this code and add arguments to the remote method, the underlying mysql connector format with the Node API would look like this:
Game.find({
"where": {
categoryId: id,
mature: true
},
"limit": 10, // 10 per page
"skip": 10 // hard coded for page 2, this needs to be passed in
}, function(err, gameArr) {
if (err) return callback(err);
callback(null, gameArr);
});
The values for limit and skip should be added to your remote method definition and registration, and then the UI can send in dynamic values based on the page displayed.
The page on the skip filter has an example for pagination as well:
https://docs.strongloop.com/display/public/LB/Skip+filter
If this will be using some sort of UI library like the Angular SDK, you can make the same query at the Controller level using the lb-ng generator script and the models created there. You could also add the pagination values there, no need for a custom remote method.
Update:
To add the skip and limit numbers to your remote method, you need to update your remote method signature, the accepts array would change to
accepts: [
{
arg: 'id',
type: 'number',
required: true
},
{
arg: 'skip',
type: 'number',
required: true
},
{
arg: 'limit',
type: 'number',
required: true
}
]
And then add the same 2 new arguments to the method itself:
Category.mature = function(id, skip, limit, callback) {
// ...your code...
});
You can call it using query parameters at this point, like ?skip=10&limit=10 just appended to the existing path. Or, you can change the http.path to something like
path: '/:id/games/mature/:skip/:limit'
and then call the resource with /1/games/mature/10/10 to achieve the same result with a parameterized URL.

Related

When sails default model attributes get their values?

When sails fill default global attributes which we added on config/models.js ,
default settings looks like :
attributes: {
id: { type: 'number', autoIncrement: true },
createdAt: { type: 'number', autoCreatedAt: true },
updatedAt: { type: 'number', autoUpdatedAt: true },
}
Now if we add sth like creatorId to this default attributes , how we should fill it once for all our models ?
attributes: {
id: { type: 'number', autoIncrement: true },
createdAt: { type: 'number', autoCreatedAt: true },
updatedAt: { type: 'number', autoUpdatedAt: true },
creatorId: { type: 'number'}
}
After this change , all models have creatorId with 0 value , how I can set userId to all of my models creatorId before save without repeating my self?
In the controller you are creating the entry in the database this should be quite straight forward. Let's assume that you have two models, User, which comes with Sails built-in authentication, and a Thing, something that someone can own.
In the Thing model, I'd change the ownerId to be owner and associate it with the User model like so:
attributes: {
id: { ... },
createdAt: { ... },
updatedAt: { ... },
owner: {
model: 'User',
required: yes // Enable this when all the stuff in the db has this set
},
}
This creates an association or one-to-many relationship if you know database terminology.
Now in the controller where you create your object to be inserted:
Thing.create({
someAttribute: inputs.someValue,
someOtherAttribute: inputs.someOtherValue,
owner: this.req.me.id
});
If you want to use the created object right away, append .fetch() to the chain after .create({...}) like so:
var thing = await Thing.create({ ... }).fetch();
Let me know if something is unclear.
I'd actually recommend you invest the $9 in buying the SailsJS course. It's an official course, taught by the creator of SailsJS, Mike McNeil. It takes you from npm i sails -g to pushing to production on the Heroku cloud platform. It teaches basic Vue (parasails flavour), using MailGun, Stripe payments, and more. They link to the course on the site here
Update
Did some further digging, and was inspired by a couple of similar cases.
What you can do is expand your model with a custom method that wraps the .create() method. This method can receive the request object from your controllers, but doing this, rather than the previous suggestion, will probably be more work than just adding ownerId: this.req.me.id, to existing calls. I1ll demonstrate anyway.
// Your model
module.exports = {
attributes: { ... },
proxyCreate(req, callback) {
if(!req.body.ownerId){
req.body.ownerId = req.me.id // or req.user.id, cant remember
// which works here
}
Thing.create(request.body, callback);
}
}
And in your controller:
...
// Change from:
Thing.create(req.body);
// To:
Thing.proxyCreate(req);
...
Update #2
Another idea I had was adding the middleware on a per-route basis. I don't know the complexity of your routes, but you can create a custom middleware for only those routes.
In router.js you edit your routes (I'll show one for brevity):
....
'POST /api/v1/things/upload-thing': [
{ action: 'helpers/add-userid-to-ownerid' },
{ action: 'new-thing' }
],
....
In helpers/add-userid-to-ownerid:
module.exports: {
fn: function(req, res) {
if(!req.body.ownerId){
req.body.ownerId = req.me.id;
}
}
}

Conditional update, depending on field matched

Say I have a collection of documents, each one managing a discussion between a teacher and a student:
{
_id,
teacherId,
studentId,
teacherLastMessage,
studentLastMessage
}
I will get queries with 3 parameters: an _id, a userId and a message.
I'm looking for a way to update the teacherLastMessage field or studentLastMessage field depending on which one the user is.
At the moment, I have this:
return Promise.all([
// if user is teacher, set teacherLastMessage
db.collection('discussions').findOneAndUpdate({
teacherId: userId,
_id
}, {
$set: {
teacherLastMessage: message
}
}, {
returnOriginal: false
}),
// if user is student, set studentLastMessage
db.collection('discussions').findOneAndUpdate({
studentId: userId,
_id
}, {
$set: {
studentLastMessage: message
}
}, {
returnOriginal: false
})
]).then((results) => {
results = results.filter((result) => result.value);
if (!results.length) {
throw new Error('No matching document');
}
return results[0].value;
});
Is there a way to tell mongo to make a conditional update, based on the field matched? Something like this:
db.collection('discussions').findOneAndUpdate({
$or: [{
teacherId: userId
}, {
studentId: userId
}],
_id
}, {
$set: {
// if field matched was studentId, set studentLastMessage
// if field matched was teacherId, set teacherLastMessage
}
});
Surely it must be possible with mongo 3.2?
What you want would require referencing other fields inside of $set. This is currently impossible. Refer to this ticket as an example.
First of all, your current approach with two update queries looks just fine to me. You can continue using that, just make sure that you have the right indexes in place. Namely, to get the best performance for these updates, you should have two compound indexes:
{ _id: 1, teacherId: 1 }
{ _id: 1, studentId: 1 }.
To look at this from another perspective, you should probably restructure your data. For example:
{
_id: '...',
users: [
{
userId: '...',
userType: 'student',
lastMessage: 'lorem ipsum'
},
{
userId: '...',
userType: 'teacher',
lastMessage: 'dolor sit amet'
}
]
}
This would allow you to perform your update with a single query.
Your data structure is a bit weird, unless you have a specific business case which requires the data the be molded that way i would suggest creating a usertype unless a user can both be a teacher and a student then keep your structure.
The $set{} param can take a object, my suggestion is to do your business logic prior. You should already know prior to your update if the update is going to be for a teacher or student - some sort of variable should be set / authentication level to distinguish teachers from students. Perhaps on a successful login in the callback you could set a cookie/local storage. Regardless - if you have the current type of user, then you could build your object earlier, so make an object literal with the properties you need based on the user type.
So
if(student)
{
var updateObj = { studentLastMsg: msg }
}
else
{
var updateObj = { teacherLastMsg: msg }
}
Then pass in your update for the $set{updateObj} I'll make this a snippet - on mobile

Querying from models with HasManyThrough relations - strongloop api

This is a follow up to a previous question. Currently, the api can query from the category and game model which share a relation. For example, through this endpoint /Categories/1001/games/mature I can list all games of fighting category that have mature set to true. However, I have included a third model gameInfo from db table game_info. Since, I want to fetch the information from those three tables, i have included a through model named gamesCategoriesBridge from db table games_categories_bridge. I followed the guidelines to set HasManyThrough relations. The issue is that the additional information such as description and publishedDate doesnt show in the final result. How could I properly set the remoteMethod to accomplish the below?
common/models/category.js
module.exports = function(Category) {
Category.mature = function(id, callback) {
var app = this.app;
var Game = app.models.Game;
Game.find({
"where": {
categoryId: id,
mature: true
}
}, function(err, gameArr) {
if (err) return callback(err);
console.log(gameArr);
callback(null, gameArr);
});
}
Category.remoteMethod(
'mature', {
accepts: [{
arg: 'id',
type: 'number',
required: true
}],
// mixing ':id' into the rest url allows $owner to be determined and used for access control
http: {
path: '/:id/games/mature',
verb: 'get'
},
returns: {
arg: 'games',
type: 'array'
}
}
);
};
Table schema:
catgories
category_name category_id
------------- -----------
fighting 1001
racing 1002
sports 1003
games
game_id game_name category_id mature
----------- ------------ ----------- --------------
13KXZ74XL8M Tekken 10001 true
138XZ5LPJgM Forza 10002 false
game_info
game_id description published_date
----------- ----------- --------------
13KXZ74XL8M Published by Namco. 1994
138XZ5LPJgM Published by Microsoft Studios. 2005
games_categories_bridge
game_id category_id
----------- -----------
13KXZ74XL8M 10001
138XZ5LPJgM 10002
Endpoint: /categories/{id}/games/mature
Desired Format for API Response:
games [
{
gameName: 'Tekken',
gameInfo :
[
{
description : 'Published by Namco.',
published_date : '1994'
}
],
categorName: 'fighting',
categoryId: 1001,
mature: true
}
.....
]
First create a hasMany relation between game and game_info model
//Now inside remote_method.
Category.mature = function(id, callback) {
var app = this.app;
var Game = app.models.game;
Category.findById(id, {}, function(err, category) {
if (err) return callback(err);
//Now call the Game find method
Game.find({
"where": {
categoryId: id,
mature: true
},
include:'game_info'
}, function(err, gameArr) {
if (err) return callback(err);
gameArr.forEach(function(gameObj, index){
gameObj.categoryName = category.category_name;
});
callback(null, gameArr);
});
});
}

mongoose subdocument sorting

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.
What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.
the article schema:
var articleSchema = new Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
author: { type: Schema.ObjectId, ref: 'User' },
comments: [{
body: { type: String, default: '' },
author: { type: Schema.ObjectId, ref: 'User' },
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
}],
tags: { type: [], get: getTags, set: setTags },
image: {
cdnUri: String,
files: []
},
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});
static method on article schema: (i would love to sort the comments here, can i do that?)
load: function (id, cb) {
this.findOne({ _id: id })
.populate('author', 'email profile')
.populate('comments.author')
.exec(cb);
},
I have to sort it elsewhere:
exports.load = function (req, res, next, id) {
var User = require('../models/User');
Article.load(id, function (err, article) {
var sorted = article.toObject({ getters: true });
sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();
req.article = sorted;
next();
});
};
I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??
Anyways, i do the sorting logic on the plain object and done.
I am quite sure there is a lot better way of doing this, please let me know.
I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.
So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.
Fix the data in order as you create it
If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:
It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": { "comments": { "$each": [newComment], "$position": 0 } }
},
function(err,result) {
// other work in here
}
);
This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.
Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": {
"comments": {
"$each": [newComment],
"$sort": { "$created_at": -1 }
}
}
},
function(err,result) {
// other work in here
}
);
And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:
Article.update(
{ },
{
"$push": {
"comments": {
"$each": [],
"$sort": { "$created_at": -1 }
}
}
},
{ "multi": true },
function(err,result) {
// other work in here
}
);
And that will sort every "comments" array in your entire collection by the specified field in one hit.
Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.
Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.
The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.
The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.
You could also use JavaScript's native Array sort method after you've retrieved and populated the results:
// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();
articles.comments.sort((a, b) => {
const aDate = new Date(a.updated_at);
const bDate = new Date(b.updated_at);
if (aDate < bDate) return -1;
if (aDate > bDate) return 1;
return 0;
});
As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.
https://lodash.com/docs/4.17.15#sortBy
comments = _.sortBy(sorted.comments, 'created_at').reverse();

Sorting by virtual field in mongoDB (mongoose)

Let's say I have some Schema which has a virtual field like this
var schema = new mongoose.Schema(
{
name: { type: String }
},
{
toObject: { virtuals: true },
toJSON: { virtuals: true }
});
schema.virtual("name_length").get(function(){
return this.name.length;
});
In a query is it possible to sort the results by the virtual field? Something like
schema.find().sort("name_length").limit(5).exec(function(docs){ ... });
When I try this, the results are simple not sorted...
You won't be able to sort by a virtual field because they are not stored to the database.
Virtual attributes are attributes that are convenient to have around
but that do not get persisted to mongodb.
http://mongoosejs.com/docs/2.7.x/docs/virtuals.html
Virtuals defined in the Schema are not injected into the generated MongoDB queries. The functions defined are simply run for each document at the appropriate moments, once they have already been retrieved from the database.
In order to reach what you're trying to achieve, you'll also need to define the virtual field within the MongoDB query. For example, in the $project stage of an aggregation.
There are, however, a few things to keep in mind when sorting by virtual fields:
projected documents are only available in memory, so it would come with a huge performance cost if we just add a field and have the entire documents of the search results in memory before sorting
because of the above, indexes will not be used at all when sorting
Here's a general example on how to sort by virtual fields while keeping a relatively good performance:
Imagine you have a collection of teams and each team contains an array of players directly stored into the document. Now, the requirement asks for us to sort those teams by the ranking of the favoredPlayer where the favoredPlayer is basically a virtual property containing the most relevant player of the team under certain criteria (in this example we only want to consider offense and defense players). Also, the aforementioned criteria depend on the users' choices and can, therefore, not be persisted into the document.
To top it off, our "team" document is pretty large, so in order to mitigate the performance hit of sorting in-memory, we project only the fields we need for sorting and then restore the original document after limiting the results.
The query:
[
// find all teams from germany
{ '$match': { country: 'de' } },
// project only the sort-relevant fields
// and add the virtual favoredPlayer field to each team
{ '$project': {
rank: 1,
'favoredPlayer': {
'$arrayElemAt': [
{
// keep only players that match our criteria
$filter: {
input: '$players',
as: 'p',
cond: { $in: ['$$p.position', ['offense', 'defense']] },
},
},
// take first of the filtered players since players are already sorted by relevance in our db
0,
],
},
}},
// sort teams by the ranking of the favoredPlayer
{ '$sort': { 'favoredPlayer.ranking': -1, rank: -1 } },
{ '$limit': 10 },
// $lookup, $unwind, and $replaceRoot are in order to restore the original database document
{ '$lookup': { from: 'teams', localField: '_id', foreignField: '_id', as: 'subdoc' } },
{ '$unwind': { path: '$subdoc' } },
{ '$replaceRoot': { newRoot: '$subdoc' } },
];
For the example you gave above, the code could look something like the following:
var schema = new mongoose.Schema(
{ name: { type: String } },
{
toObject: { virtuals: true },
toJSON: { virtuals: true },
});
schema.virtual('name_length').get(function () {
return this.name.length;
});
const MyModel = mongoose.model('Thing', schema);
MyModel
.aggregate()
.project({
'name_length': {
'$strLenCP': '$name',
},
})
.sort({ 'name_length': -1 })
.exec(function(err, docs) {
console.log(docs);
});

Resources