I need to retrieve an object and also get the relations and nested relations.
So, I have the three models below:
User model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pets: {
collection: 'pet',
via: 'owner',
}
}
Pet model:
module.exports = {
attributes: {
name: {
type: 'string'
},
owner: {
model: 'user'
},
vaccines: {
collection: 'vaccine',
via: 'pet',
}
}
Vaccine model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pet: {
model: 'pet'
}
}
Calling User.findOne(name: 'everton').populate('pets').exec(....) I get the user and associated Pets. How can I also get the associated vaccines with each pet? I didn't find references about this in the official documentation.
I've ran into this issue as well, and as far as I know, nested association queries are not built into sails yet (as of this post).
You can use promises to handle the nested population for you, but this can get rather hairy if you are populating many levels.
Something like:
User.findOne(name: 'everton')
.populate('pets')
.then(function(user) {
user.pets.forEach(function (pet) {
//load pet's vaccines
});
});
This has been a widely discussed topic on sails.js and there's actually an open pull request that adds the majority of this feature. Check out https://github.com/balderdashy/waterline/pull/1052
While the answer of Kevin Le is correct it can get a little messy, because you're executing async functions inside a loop. Of course it works, but let's say you want to return the user with all pets and vaccines once it's finished - how do you do that?
There are several ways to solve this problem. One is to use the async library which offers a bunch of util functions to work with async code. The library is already included in sails and you can use it globally by default.
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets;
// async.each() will perform a for each loop and execute
// a fallback after the last iteration is finished
async.each(pets, function (pet, cb) {
Vaccine.find({pet: pet.id})
.then(function(vaccines){
// I didn't find a way to reuse the attribute name
pet.connectedVaccines = vaccines;
cb();
})
}, function(){
// this callback will be executed once all vaccines are received
return res.json(user);
});
});
There is an alternative approach solving this issue with bluebird promises, which are also part of sails. It's probably more performant than the previous one, because it fetches all vaccines with just one database request. On the other hand it's harder to read...
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets,
petsIds = [];
// to avoid looping over the async function
// all pet ids get collected...
pets.forEach(function(pet){
petsIds.push(pet.id);
});
// ... to get all vaccines with one db call
var vaccines = Vaccine.find({pet: petsIds})
.then(function(vaccines){
return vaccines;
});
// with bluebird this array...
return [user, vaccines];
})
//... will be passed here as soon as the vaccines are finished loading
.spread(function(user, vaccines){
// for the same output as before the vaccines get attached to
// the according pet object
user.pets.forEach(function(pet){
// as seen above the attribute name can't get used
// to store the data
pet.connectedVaccines = vaccines.filter(function(vaccine){
return vaccine.pet == pet.id;
});
});
// then the user with all nested data can get returned
return res.json(user);
});
Related
I want to create common crud operations in Express js with Sequelize.
I have created getAll function as below.
exports.getAll = (module, res,next) => {
module
.findAndCountAll({
where: {
CreatedBy: 1,
isDeleted: false,
isActive: true
},
offset: 0,
limit: 10,
}).then((result) => {
res.status(200).json({
message: "data Fetched from database",
statusCode: 200,
result: result,
});
}).catch((error) => {
console.log(error);
});
}
and I am calling this common function in Controller function as below by passing name of Model e.g. category
crudOperations.getAll(category, res);
It is working fine. but how do I create function for post data ?
For posting data, I want to use Sequelize's magic methods (because one user can associated with many category as below)
user.hasMany(category, {
foreignKey: 'CreatedBy'
});
Example, I want to add category with respect to user, so I want to use magic method as below.
req.user
.createCategory({
name: name,
})
How should I pass user and createCategory as parameter to common function?
How do I pass data to function?
Is it good practice to create common function for CRUD? or should go with writing function for each module?
I am building application architecture and design. When we are talking about creating common methods or services, it would completely depend on the use case or types of operation we are performing.
Sequelize has already basic methods which perform common operations. Following is my idea which might put more light on your way. Please refer to below pseudo-code.
Answer 1.
Following is the method that might help you, that how do you organize your functions. It is my basic thought, so there might room for an error.
BaseModel.helper.js
class BaseModelHelper{
static async find(params){
const {model, where, attributes, include=[] offset:0, limit: 10} = params;
objFind = {};
if(Object.keys(where).length > 0){
objFind = {...objFind, where}
}
if(attributes.length > 0){
objFind = {...objFind, attributes}
}
if(include.length > 0){
objFind = {...objFind, include}
}
model.find({
where: where,
attributes: attributes,
include,
offset,
limit
}).then((data)=>{
return data;
}).catch((err)=>{
throw new Error(err);
});
}
static async create(params){
const {model, properties} = params;
model.create(properties)
.then((data)=>{
return data;
}).catch((err)=>{
throw new Error(err);
});
}
static async update(params){
const {model, newData, where} = params;
model.update(newData, {
where
})
.then((data)=>{
return data;
}).catch((err)=>{
throw new Error(err);
});
}
static async delete(params){
const {model, where} = params;
model.destroy(where)
.then((data)=>{
return data > 0 ? true : false;
}).catch((err)=>{
throw new Error(err);
});
}
}
module.exports = BaseModelHelper;
Data.services.js
const BaseModelHelper = require("BaseModel.helper.js);
class DataServices{
static async add(){
// Owner table entry
const {id} = await BaseModelHelper.create({
model: "Owner'
properties:{
name: 'Loren',
role: 'admin',
},
});
// Cat table entry
const {id: petId} = await BaseModelHelper.create({
model: "Pet'
properties:{
owner_id: 'c0eebc45-9c0b',
type: 'cat',
}
});
}
static async findWithRelations(){
const arrData = await BaseModelHelper.find({
model: 'User',
where: {
role: 'admin'
},
attributes: ['id', 'username', 'age'],
include: [{
model: pet,
through: {
attributes: ['createdAt', 'startedAt', 'finishedAt'],
where: {completed: true}
}
}]
});
}
static async findBelongs(){
const arrData = await BaseModelHelper.find({
model: 'User',
where: {
role: 'admin'
},
attributes: ['id', 'username', 'age']
});
}
static async update(){
const arrData = await BaseModelHelper.update({
Model: 'Pet',
newData:{
name: 'lina',
}
where: {
name: 'suzi',
}
});
}
static async delete(){
const arrData = await BaseModelHelper.delete({
Model: 'Pet',
where: {
name: 'lina',
}
});
}
}
module.exports = DataServices;
The above way I have described has one benefit is to you don't go to do error handling every place, if you have to manage to centralize error handler. An when an error occurred it throws an error and catches by centralizing the error handler of Express.
The above class of common db operation is build based on my experience and the frequency of the operations we have performed. I know that there is room for more possibilities than we expect, but with the method, I have suggested you might not face many obstacles.
Answer 2. You should create a whole qualified query object at your service level. Once its build at the service level, then only you will pass it to our BaseModelHelper.
Answer 3. Same thing you should create your data object at the service level. If the data object builds from multiple tables, then you first encapsulate at the service level, then you should pass to the BaseModelHelper method.
Answer 4. Yes, I also favor the same. But one thing you should keep in mind that there is always room for improvement. If you wish to create a method for each module then you should copy this BaseModelHelper to everywhere else I suggest inheriting the file at the service level.
All the database operation objects build at the service level, not the controller level. Your database service and object preparation services might be different so it will give a more clear picture. The object preparation service might scope to include more different services.
Again above approach is my thought process and I suggested you based on my experience. Again there is more room for improvement, that you might take to create.
Below is my PurchaseOrder model defined in sequelize. I want to update the Supplier Model whenever there is an update to the PurchaseOrder. I thought of using the hooks to achieve this. But I couldn't able to access another model inside this model. I tried importing and all stuff, but no luck. Is this the right way to use the hooks or what should I use to achieve the same? Any help or direction is much appreciated!
module.exports = (sequelize, Sequelize) => {
const PurchaseOrder = sequelize.define("purchaseOrder", {
totalAmount: {
type: Sequelize.INTEGER
},
paid: {
type: Sequelize.BOOLEAN
},
paymentMode: {
type: Sequelize.ENUM('CASH', 'CHEQUE', 'BANK', 'CARD', 'NA')
}
}, {
freezeTableName: true,
hooks: {
beforeUpdate: (order, options) => {
// here I want to update the another model(Supplier).
// But I couldn't able to access another model inside the hook
Supplier.increment('balance'{
where: { id: order.supplierId }
});
}
}
});
return PurchaseOrder;
};
In my code I have a couple hooks that update other models (audit logging of changes for example). You need to make sure to pass along the options.transaction so that any changes are rolled back if there is an error later in the chain.
This example accesses another table keyed by other_model. When the hooks run the models should all already be registered with Sequelize.
module.exports = function Order(sequelize, DataTypes) {
const Order = sequelize.define(
'order',
{ /* columns */ },
{
hooks: {
beforeUpdate: async function(order, options) {
// get the transaction, if it is set
const { transaction } = options;
// use sequelize.models and make sure to pass the
// transaction so it is rolled back if there is an error
await sequelize.models.supplier.increment(balance, {
where: { id: order.supplierId },
transaction,
});
},
},
},
});
return Order;
}
You can try sequelize['Supplier'] because all models should be already registered in an Sequelize instance.
Nevertheless I suppose it's not a good idea to make modifications in a DB via other models in such hooks because in such cases you should take into account that all operations should be done in the same transaction i.e. should be executed as an atomic operation to avoid inconsistent state of data in a DB if some modifications fail.
Not a relatable answer, but if anyone wants to try querying a model to another model using validate custom functions. You can define your model like sequelize.models.ModelName sequelize shouldn't be imported like require('sequelize') but it should use the sequelize parameter defined in module.exports of your current model.
await sequelize.models.ModelName.findAll()
Trying to:
get a list of users
from the user details get the trips created by the users
and based on the output performing some actions
The following is the code I am trying to run.
models.user.findAll({})
.then(function (users) {
for (var i = 0; i < users.length; i++) {
var userName = users[i].name;
var userEmail = users[i].email;
models.trip.findOne({ attributes: [ [userName, 'name'], [userEmail, 'email'], 'id' ], where: { userId: users[i].id } })
.then(function (trip) {
if (trip == null) {
//Send Emails
}
});
}
})
.catch(function (error){ // enter code here
console.log(">>>>>",error);
});
Due to call back the second Sequelize does not run correctly.
Can you please advice on how to approach this issue? Is it using asyncawait/coyield?
You should debug this by using console.log for example. First you should try to print your first callback result, may be the database connection is not properly configured, may be the table is empty. there are many reasons. also it's more comfortably to use .forEach method instead of 'for' loop
array.forEach((item, i, arr)=>{
///....
})
Don't use the async method in for and any loop. It is better to use Promise.all or any async library.
The code will be like that.
var tasks = []
users.forEach(function (user) {
tasks.push(models.trip({/*some attrs*/}).then(function (trip){
if (trip) return Promise.resolve()
return sendEmailPromise(user)
}))
})
Promise.all(tasks).then(function() {
//done
}).catch(errorHandler)
If the models had associations, such that you could just include the other model and add where clause to it.
Sequelize Docs
User.hasMany(Trips, {foreignKey: 'userId'});
User.findAll({
include: {
model: Trips,
where: {userId: id}
}
});
I'm trying to get users count belongs to specific company.
Here is my model;
var Company = Bookshelf.Model.extend({
tableName: 'companies',
users: function () {
return this.hasMany(User.Model, "company_id");
},
users_count : function(){
return new User.Model().query(function(qb){
qb.where("company_id",9);
qb.count();
}).fetch();
},
organization: function () {
return this.belongsTo(Organization.Model, "organization_id");
}
});
method "users" works very well, no problem.
method "users_count" query works well, but cant get value to "company" model.
in routes, i'm using bookshelf models like this;
new Company.Model({id:req.params.id})
.fetch({withRelated:['users']})
.then(function(model){
res.send(model.toJSON())
})
.catch(function(error){
res.send(error);
});
How should i use users_count method, i'm kinda confused (probably because of promises)
Collection#count()
If you upgrade to 0.8.2 you can use the new Collection#count method.
Company.forge({id: req.params.id}).users().count().then(userCount =>
res.send('company has ' + userCount + ' users!');
);
Problem with your example
The problem with your users_count method is that it tries to make Bookshelf turn the result of your query into Models.
users_count : function(){
return new User.Model().query(function(qb){
qb.where("company_id",9);
qb.count();
}).fetch(); // Fetch wanted an array of `user` records.
},
This should work in this instance.
users_count : function(){
return new User.Model().query()
.where("company_id",9)
.count()
},
See relevant discussion here.
EDIT: How to get this in your attributes.
Maybe try something like this:
knex = bookshelf.knex;
var Company = bookshelf.Model.extend({
tableName: 'companies',
initialize: function() {
this.on('fetching', function(model, attributes, options) {
var userCountWrapped = knex.raw(this.getUsersCountQuery()).wrap('(', ') as user_count');
options.query.select('*', userCountWrapped);
}
}
users: function () {
return this.hasMany(User.Model, "company_id");
},
getUsersCountQuery: function() {
return User.Model.query()
.where("company_id",9)
.count();
}
organization: function () {
return this.belongsTo(Organization.Model, "organization_id");
}
});
Check out the bookshelf-eloquent extension. The withCount() function is probably what you are looking for. Your code would look something like this:
let company = await Company.where('id', req.params.id)
.withCount('users').first();
User.collection().query(function (qb) {
qb.join('courses', 'users.id', 'courses.user_id');
qb.groupBy('users.id');
qb.select("users.*");
qb.count('* as course_count');
qb.orderBy("course_count", "desc");
})
I've got myself a question regarding associations in Sails.js version 0.10-rc5. I've been building an app in which multiple models are associated to one another, and I've arrived at a point where I need to get to nest associations somehow.
There's three parts:
First there's something like a blog post, that's being written by a user. In the blog post I want to show the associated user's information like their username. Now, everything works fine here. Until the next step: I'm trying to show comments which are associated with the post.
The comments are a separate Model, called Comment. Each of which also has an author (user) associated with it. I can easily show a list of the Comments, although when I want to display the User's information associated with the comment, I can't figure out how to populate the Comment with the user's information.
In my controller i'm trying to do something like this:
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function(err, post) {
// Handle errors & render view etc.
});
In my Post's 'show' action i'm trying to retrieve the information like this (simplified):
<ul>
<%- _.each(post.comments, function(comment) { %>
<li>
<%= comment.user.name %>
<%= comment.description %>
</li>
<% }); %>
</ul>
The comment.user.name will be undefined though. If I try to just access the 'user' property, like comment.user, it'll show it's ID. Which tells me it's not automatically populating the user's information to the comment when I associate the comment with another model.
Anyone any ideals to solve this properly :)?
Thanks in advance!
P.S.
For clarification, this is how i've basically set up the associations in different models:
// User.js
posts: {
collection: 'post'
},
hours: {
collection: 'hour'
},
comments: {
collection: 'comment'
}
// Post.js
user: {
model: 'user'
},
comments: {
collection: 'comment',
via: 'post'
}
// Comment.js
user: {
model: 'user'
},
post: {
model: 'post'
}
Or you can use the built-in Blue Bird Promise feature to make it. (Working on Sails#v0.10.5)
See the codes below:
var _ = require('lodash');
...
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.then(function(post) {
var commentUsers = User.find({
id: _.pluck(post.comments, 'user')
//_.pluck: Retrieves the value of a 'user' property from all elements in the post.comments collection.
})
.then(function(commentUsers) {
return commentUsers;
});
return [post, commentUsers];
})
.spread(function(post, commentUsers) {
commentUsers = _.indexBy(commentUsers, 'id');
//_.indexBy: Creates an object composed of keys generated from the results of running each element of the collection through the given callback. The corresponding value of each key is the last element responsible for generating the key
post.comments = _.map(post.comments, function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
res.json(post);
})
.catch(function(err) {
return res.serverError(err);
});
Some explanation:
I'm using the Lo-Dash to deal with the arrays. For more details, please refer to the Official Doc
Notice the return values inside the first "then" function, those objects "[post, commentUsers]" inside the array are also "promise" objects. Which means that they didn't contain the value data when they first been executed, until they got the value. So that "spread" function will wait the acture value come and continue doing the rest stuffs.
At the moment, there's no built in way to populate nested associations. Your best bet is to use async to do a mapping:
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id: _.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = _.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
It's not as pretty as nested population (which is in the works, but probably not for v0.10), but on the bright side it's actually fairly efficient.
I created an NPM module for this called nested-pop. You can find it at the link below.
https://www.npmjs.com/package/nested-pop
Use it in the following way.
var nestedPop = require('nested-pop');
User.find()
.populate('dogs')
.then(function(users) {
return nestedPop(users, {
dogs: [
'breed'
]
}).then(function(users) {
return users
}).catch(function(err) {
throw err;
});
}).catch(function(err) {
throw err;
);
Worth saying there's a pull request to add nested population: https://github.com/balderdashy/waterline/pull/1052
Pull request isn't merged at the moment but you can use it installing one directly with
npm i Atlantis-Software/waterline#deepPopulate
With it you can do something like .populate('user.comments ...)'.
sails v0.11 doesn't support _.pluck and _.indexBy use sails.util.pluck and sails.util.indexBy instead.
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id:sails.util.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = sails.util.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
You could use async library which is very clean and simple to understand. For each comment related to a post you can populate many fields as you want with dedicated tasks, execute them in parallel and retrieve the results when all tasks are done. Finally, you only have to return the final result.
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function (err, post) {
// populate each post in parallel
async.each(post.comments, function (comment, callback) {
// you can populate many elements or only one...
var populateTasks = {
user: function (cb) {
User.findOne({ id: comment.user })
.exec(function (err, result) {
cb(err, result);
});
}
}
async.parallel(populateTasks, function (err, resultSet) {
if (err) { return next(err); }
post.comments = resultSet.user;
// finish
callback();
});
}, function (err) {// final callback
if (err) { return next(err); }
return res.json(post);
});
});
As of sailsjs 1.0 the "deep populate" pull request is still open, but the following async function solution looks elegant enough IMO:
const post = await Post
.findOne({ id: req.param('id') })
.populate('user')
.populate('comments');
if (post && post.comments.length > 0) {
const ids = post.comments.map(comment => comment.id);
post.comments = await Comment
.find({ id: commentId })
.populate('user');
}
Granted this is an old question, but a much simpler solution would be to loop over the comments,replacing each comment's 'user' property (which is an id) with the user's full detail using async await.
async function getPost(postId){
let post = await Post.findOne(postId).populate('user').populate('comments');
for(let comment of post.comments){
comment.user = await User.findOne({id:comment.user});
}
return post;
}
Hope this helps!
In case anyone is looking to do the same but for multiple posts, here's one
way of doing it:
find all user IDs in posts
query all users in 1 go from DB
update posts with those users
Given that same user can write multiple comments, we're making sure we're reusing those objects. Also we're only making 1 additional query (whereas if we'd do it for each post separately, that would be multiple queries).
await Post.find()
.populate('comments')
.then(async (posts) => {
// Collect all comment user IDs
const userIDs = posts.reduce((acc, curr) => {
for (const comment of post.comments) {
acc.add(comment.user);
}
return acc;
}, new Set());
// Get users
const users = await User.find({ id: Array.from(userIDs) });
const usersMap = users.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
// Assign users to comments
for (const post of posts) {
for (const comment of post.comments) {
if (comment.user) {
const userID = comment.user;
comment.user = usersMap[userID];
}
}
}
return posts;
});