What is populate in SailsJs? - node.js

I recently began to develop on sailsJs and not understanding the subtleties
Please explain to me what is populate in SailsJs and who can please do simple example
Thanks in advance ?
whats is ?
User.find({ name: 'foo' })
.populate('pets', { name: 'fluffy' })
.exec(function(err, users) {
if(err) return res.serverError(err);
res.json(users);
});

populate is used for associations. When your model is something like this:
// User.js
module.exports = {
attributes: {
name: {
type: "string"
},
pet: {
model: "pet"
}
}
}
Here pet attribute of user collection is a reference to pet table. In user table it will store only the id column of pet. However, when you do a populate while find, then it will fetch the entire record of the pet entry and display it here. This is just for one to one association. You can have many to one associations as well as many to many. See this documentation for more details

sailsjs project is use the wateline ORM. you can see the document. if you want to use 'populate()', you need define Associations in the model.
.populate()
populate is used with associations to include any related values specified in a model definition. If a collection attribute is defined in a many-to-many, one-to-many or many-to-many-through association the populate option also accepts a full criteria object. This allows you to filter associations and run limit and skip on the results.
as you example, you need do like this:
User.js
// A user may have many pets
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pets
pets: {
collection: 'pet',
via: 'owner'
}
}
});
Pet.js
// A pet may only belong to a single user
var Pet = Waterline.Collection.extend({
identity: 'pet',
connection: 'local-postgresql',
attributes: {
breed: 'string',
type: 'string',
name: 'string',
// Add a reference to User
owner: {
model: 'user'
}
}
});
you can ready the doc, and you can use it very easy

To resume what is .populate() (used by waterline) is a little what join is used by SQL.
.populate() allows you to join the tables in your database.
The link identifier is to be defined in your model.
In other words, to associate a user (who is in the "User" table) with a dog (who is in the "Dog" table), you use populate.
To resume your example:
You are looking for the user => User.find ({name: my_user})
You are looking for the dog named "fluffy" => {name: 'fluffy'}
You are looking for the dog 'fluffy' which is associated with your user (belongs) => populate ('pets')
Which give:
User.find({ name: 'foo' })
.populate('pets', { name: 'fluffy' })
.exec(function(err, users) {
if(err) return res.serverError(err);
res.json(users);
}
This association ("pets"), you define it in your models "User" and "Pets" like the example above.

Populate is all about displaying the content of the (id) on which it was refreed
"abc": [{
type: ObjectId,
ref: "xyz"
}],

Related

How to model a collection in nodejs+mongodb

Hello I am new to nodejs and mongodb.
I have 3 models:
"user" with fields "name phone"
"Shop" with fields "name, address"
"Member" with fields "shop user status". (shop and user hold the "id" of respective collections).
Now when I create "shops" api to fetch all shop, then I need to add extra field "isShopJoined" which is not part of the model. This extra field will true if user who see that shop is joined it otherwise it will be false.
The problem happens when I share my model with frontend developers like Android/iOS and others, They will not aware of that extra field until they see the API response.
So is it ok if I add extra field in shops listing which is not part of the model? Or do I need to add that extra field in model?
Important note
All the code below has NOT been tested (yet, I'll do it when I can setup a minimal environment) and should be adapted to your project. Keep in mind that I'm no expert when it comes to aggregation with MongoDB, let alone with Mongoose, the code is only here to grasp the general idea and algorithm.
If I understood correctly, you don't have to do anything since the info is stored in the Member collection. But it forces the front-end to do an extra-request (or many extra-requests) to have both the list of Shops and to check (one by one) if the current logged user is a Member of the shop.
Keep in mind that the front-end in general is driven by the data (and so, the API/back-end), not the contrary. The front-end will have to adapt to what you give it.
If you're happy with what you have, you can just keep it that way and it will work, but that might not be very effective.
Assuming this:
import mongoose from "mongoose";
const MemberSchema = new mongoose.Schema({
shopId: {
type: ObjectId,
ref: 'ShopSchema',
required: true
},
userId: {
type: ObjectId,
ref: 'UserSchema',
required: true
},
status: {
type: String,
required: true
}
});
const ShopSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
address: {
//your address model
}
});
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
phone: {
type: String,
required: true,
},
// Add something like this
shopsJoined: {
type: Array,
default: [],
required: true
}
});
You could tackle this problem via 2 ways:
MongoDB Aggregates
When retrieving (back-end side) the list of shops, if you know the user that made the request, instead of simply returning the list of Shops, you could return an aggregate of Shops and Members resulting in an hybrid document containing both the info of Shops and Models. That way, the front-end have all the info it needs with one back-end request.
Important note
The following code might not work as-is and you'll have to adapt it, I currently have nothing to test it against. Keep in mind I'm not very familiar with aggregates, let alone with Mongoose, but you'll get the general idea by looking the code and comments.
const aggregateShops = async (req, res, next) => {
try {
// $lookup will merge the "Model" and "Shop" documents into one
// $match will return only the results matching the condition
const aggreg = await Model.aggregate({$lookup: {
from: 'members', //the name of the mongodb collection
localField: '_id', //the "Shop" field to match with foreign collection
foreignField: 'shopId', //the "Member" field to match with local collection
as: 'memberInfo' //the field name in which to store the "Member" fields;
}, {
$match: {memberInfo: {userId: myUserId}}
}});
// the result should be an array of object looking like this:
/*{
_id: SHOP_OBJECT_ID,
name: SHOP_NAME,
address: SHOP_ADDRESS,
memberInfo: {
shopId: SHOP_OBJECT_ID,
userId: USER_OBJECT_ID,
status: STATUS_JOINED_OR_NOT
}
}*/
// send back the aggregated result to front-end
} catch (e) {
return next(e);
}
}
Drop the Members collection and store the info elsewhere
Instinctively, I would've gone this way. The idea is to either store an array field shopsJoined in the User model, or a membersJoined array field in the Shops model. That way, the info is retrieved no matter what, since you still have to retrieve the Shops and you already have your User.
// Your PATCH route should look like this
const patchUser = async (req, res, next) => {
try {
// How you chose to proceed here is up to you
// I tend to facilitate front-end work, so get them to send you (via req.body) the shopId to join OR "un-join"
// They should already know what shops are joined or not as they have the User
// For example, req.body.shopId = "+ID" if it's a join, or req.body.shopId = "-ID" if it's an un-join
if (req.body.shopId.startsWith("+")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $push: { shopsJoined: req.body.shopId } }
);
} else if (req.body.shopId.startsWith("-")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $pull: { shopsJoined: req.body.shopId } }
);
} else {
// not formatted correctly, return error
}
// return OK here depending on the framework you use
} catch (e) {
return next(e);
}
};
Of course, the above code is for the User model, but you can do the same thing for the Shop model.
Useful links:
MongoDB aggregation pipelines
Mongoose aggregates
MongoDB $push operator
MongoDB $pull operator
Yes you have to add the field to the model because adding it to the response will be only be a temporary display of the key but what if you need that in the future or in some list filters, so its good to add it to the model.
If you are thinking that front-end will have to be informed so just go it, and also you can set some default values to the "isShopJoined" key let it be flase for the time.

how to have 1:m association in Sequelize?

I have two models as following:
const User = sequlize.define("user", {...})
const Project = sequelize.define("project". {...})
And I want a 1:M relation between them such that a Project can have multiple users but a User can have a single project.
I tried the following code:
User.hasMany(Project, { as: "project" })
but hasMany adds foreign key to Project table instead of User table. I know its possible to do it the other way around i.e:
Project.hasMany(User, { as: "project" })
but if I do that then when I use the following code it throws no association error.
User.findOne({ where: { id: "username", password: "password"},
include: [{ model: Project, as: 'project', required: true }]
})

Sails:How to join two different models using waterline

In MVC peoples are using join query to join the two different tables, but In sails.js what I have to use? There is any method in waterline?
The answer based on database you are using.
For instance, you need to populate values in Mongo not to join. Or you need to join tables if you are using MySQL or similar.
In a nutshell, all this stuff is covered via Waterline. So you can just declare model in api/models with associations. Joining and populating is executing under the Waterline adapter.
For instance, you have User and Comment.
// api/models/User.js
module.exports = {
attributes: {
name: {
type: 'string'
},
comments: {
collection: 'Comment',
via: 'user'
}
}
};
// api/models/Comment.js
module.exports = {
attributes: {
text: {
type: 'string'
},
user: {
model: 'User',
via: 'comments'
}
}
};
Then you are execute User.find() and get already joined\populated tables from database.
But, if you want to execute manual joining, you can use .populate() method on Model instance. For instance:
// api/controllers/AnyController.js
module.exports = {
action: function(req, res) {
User
.findOne('ID_HERE')
.populate('comments')
.then(function(result) {})
.catch(function(error) {});
}
};
You can read more about populate here - http://sailsjs.org/documentation/reference/waterline-orm/queries/populate

Using elemMatch for a nested condition query in Mongoose

I have two schemas Student, User - where user can be class teacher or other designated persons.
Student schema
var StudentSchema = new Schema({
name : String,
.
.
.
owner: {
type: Schema.ObjectId,
ref: 'User'
}
})
Now I want to group the students for a particular owner.(classTeacher)
I have written a server api route -method for this.
get ==> localhost:3000/api/---/user/:userId/students
userId is the classTeacherid. = req.queryUserStudentsId
exports.getUserStudents=function(req,res){
Student
.find()
.select({
'owner': {
$elemMatch: {
_id: req.queryUserStudentsId
}
}
})
.populate('owner')
.exec(function(err, students) {
};
This results in all the students. Not only those which belong to the given owner id.
Please let me know where I am wrong.
Thanks.
select is used to select the fields to include in the returned docs. You need to provide the owner filter to the find itself to filter the student docs to just those owned by req.queryUserStudentsId:
Student.find({owner: req.queryUserStudentsId})
.populate('owner')
.exec(function(err, students) {
...
});
You can omit the .populate('owner') call if you don't need to the full details of the owner.

Hide embedded document in mongoose/node REST server

I'm trying to hide certain fields on my GET output for my REST server. I have 2 schema's, both have a field to embed related data from eachother into the GET, so getting /people would return a list of locations they work at and getting a list of locations returns who works there. Doing that, however, will add a person.locations.employees field and will then list out the employees again, which obviously I don't want. So how do I remove that field from the output before displaying it? Thanks all, let me know if you need any more information.
/********************
/ GET :endpoint
********************/
app.get('/:endpoint', function (req, res) {
var endpoint = req.params.endpoint;
// Select model based on endpoint, otherwise throw err
if( endpoint == 'people' ){
model = PeopleModel.find().populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().populate('employees');
} else {
return res.send(404, { erorr: "That resource doesn't exist" });
}
// Display the results
return model.exec(function (err, obj) {
if (!err) {
return res.send(obj);
} else {
return res.send(err);
}
});
});
Here is my GET logic. So I've been trying to use the query functions in mongoose after the populate function to try and filter out those references. Here are my two schema's.
peopleSchema.js
return new Schema({
first_name: String,
last_name: String,
address: {},
image: String,
job_title: String,
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null },
hourly_wage: Number,
locations: [{ type: Schema.ObjectId, ref: 'Locations' }],
employee_number: Number
}, { collection: 'people' });
locationsSchema.js
return new Schema({
title: String,
address: {},
current_manager: String, // Inherit person details
alternate_contact: String, // Inherit person details
hours: {},
employees: [{ type: Schema.ObjectId, ref: 'People' }], // mixin employees that work at this location
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null }
}, { collection: 'locations' });
You should specify the fields you want to fetch by using the select() method. You can do so by doing something like:
if( endpoint == 'people' ){
model = PeopleModel.find().select('locations').populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().select('employees').populate('employees');
} // ...
You can select more fields by separating them with spaces, for example:
PeopleModel.find().select('first_name last_name locations') ...
Select is the right answer but it also may help to specify it in your schema so that you maintain consistency in your API and I've found it helps me to not remember to do it everywhere I perform a query on the object.
You can set certain fields in your schema to never return by using the select: true|false attribute on the schema field.
More details can be found here: http://mongoosejs.com/docs/api.html#schematype_SchemaType-select
SOLUTION!
Because this was so hard for me to find i'm going to leave this here for anybody else. In order to "deselect" a populated item, just prefix the field with "-" in your select. Example:
PeopleModel.find().populate({path: 'locations', select: '-employees'});
And now locations.employee's will be hidden.
If you remember from you SQL days, SELECT does a restriction on the table(s) being queried. Restrict is one of the primitive operations from the relational model and continues to be a useful feature as the relational model has evolved. blah blah blah.
In mongoose, the Query.select() method allows you to perform this operation with some extra features. Particularly, not only can you specify what attributes (columns) to return, but you can also specify what attributes you want to exclude.
So here's the example:
function getPeople(req,res, next) {
var query = PeopleModel.find().populate({path: 'locations', select: '-employees'});
query.exec(function(err, people) {
// error handling stuff
// process and return response stuff
});
}
function getLocations(req,res, next) {
var query = LocationModel.find().populate({path: 'employees', select: '-locations'});
query.exec(function(err, people) {
// error handling stuff
// processing and returning response stuff
});
}
app.get('people', getPeople);
app.get('locations', getLocations);
Directly from the Mongoose Docs:
Go to http://mongoosejs.com/docs/populate.html and search for "Query conditions and other options"
Query conditions and other options
What if we wanted to populate our fans array based on their age,
select just their names, and return at most, any 5 of them?
Story
.find(...)
.populate({
path: 'fans',
match: { age: { $gte: 21 }},
select: 'name -_id',
options: { limit: 5 }
})
.exec()
I just wanted to remark, for the simplicity of the endpoint you may be able to get away with this way to define the endpoints. However, in general this kind of dispacher pattern is not necessary and may pose problems later in development when developing with Express.

Resources