How do you query data with Bookshelf with a lot of relations? - node.js

So, I have the following:
var Device = services.bookshelf.Model.extend({
tableName: 'device',
calendar: function() {
return this.belongsToMany(Calendar, 'calendar_device', 'deviceId', 'calendarId');
}
});
var Calendar = module.exports = services.bookshelf.Model.extend({
tableName: 'calendar',
device: function() {
return this.belongsToMany(Device, 'calendar_device', 'calendarId', 'deviceId');
},
schedule: function() {
return this.hasMany(Schedule);
}
});
var Schedule = module.exports = services.bookshelf.Model.extend({
tableName: 'schedule',
calendar: function() {
return this.belongsTo(Calendar);
},
channel: function() {
return this.hasMany(Channel);
}
});
var Channel = module.exports = services.bookshelf.Model.extend({
tableName: 'channel',
schedule: function() {
return this.belongsTo(Schedule);
},
screenItem: function() {
return this.hasMany(ScreenItem);
}
});
And the relations keep going...
How am I supposed to make a query to get, for example, the channels of a device? I don't know if I'm missing something but y I haven't found much information on this...

Device.where('id', id).fetch({withRelated: ['calendar.schedule.channel']}).then(function(devices) {
...
})
What I do is to fetch the Device with 'id' = id specifying the relations of the model that I want to return to (that are declared as it can be seen in the models declaration in my question).
So, saying:
{withRelated: ['calendar']} returns a Device with its Calendars
{withRelated: ['calendar.schedule']} returns a Device with its Calendars with its Schedules
{withRelated: ['calendar.schedule.channel']} returns a Device with its Calendars with its Schedules with its Channels
and so on if there are more related models you want to get.
The response is something like:
{
id: 1,
...,
calendars: [
id: 1,
...,
schedules: [
...
]
]
}

Related

How to handle concurrent request with socket io while joining room

I'm trying to create/join a room with limit of 2 user per room everything is working fine
but when 2 users make a concurrent request to create/join a room all 3 users are added to same room. i have tried using
simple mongodb insert/update
using transaction
now storing in memory
here is the code
const rooms = {};
module.exports = function ({ io, socket }) {
socket.on("/createJoinPublicRequest", async ({ user }) => {
let title = `${user._id}.${Date.now()}`;
let foundRoom = Object.keys(rooms).find((room) => {
return rooms[room].roomLimit > rooms[room].users.length;
});
if (!foundRoom) {
//create a room if not found
rooms[title] = {
title,
users: [
{
_id: user._id,
},
],
roomLimit: 2,
roomType: "public",
status: "open",
};
socket.join(title);
} else {
// join existing room
room = rooms[foundRoom];
room.users.push({ _id: user._id });
if (room.users.length == room.roomLimit) {
room.status = "full";
if (await storeToMongoDB(room)) {
delete rooms[foundRoom];
}
}
socket.join(room.title);
}
await User.findByIdAndUpdate(user, {
joinedRoom: title,
});
return;
});
you should use mutex lock to prevent adding more Users to Room.
For Single Instance : https://www.npmjs.com/package/async-mutex
For Multi Instance : Redis Mutex Lock

SUM column in bookshelfjs relationship

I want to sum a column in a Bookshelfjs relationship. I have my query set up as
return this.hasMany('MutualFundPortfolio').query().sum('balance');
But I am having this error TypeError: Cannot read property 'parentFk' of undefined any body has any clue how solve this? It seems Bookshelf doesn't support sum
const moment = require('moment');
const Bookshelf = require('../bookshelf');
require('./wishlist');
require('./kyc');
require('./wallet');
const User = Bookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true,
hidden: ['code', 'password'],
toJSON(...args) {
const attrs = Bookshelf.Model.prototype.toJSON.apply(this, args);
attrs.created_at = moment(this.get('created_at')).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss');
attrs.updated_at = moment(this.get('updated_at')).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss');
return attrs;
},
local_wallet() {
return this.hasMany('LocalWallet').query((qb) => {
qb.orderBy('id', 'DESC').limit(1);
});
},
mutual_fund_portfolio() {
return this.hasMany('MutualFundPortfolio').query().sum('balance');
},
global_wallet() {
return this.hasMany('GlobalWallet').query((qb) => {
qb.orderBy('id', 'DESC').limit(1);
});
},
local_gift_card_wallet() {
return this.hasMany('LocalGiftCardWallet').query((qb) => {
qb.orderBy('id', 'DESC').limit(1);
});
},
global_gift_card_wallet() {
return this.hasMany('GlobalGiftCardWallet').query((qb) => {
qb.orderBy('id', 'DESC').limit(1);
});
}
});
module.exports = Bookshelf.model('User', User);
Above is the full user model. I am then getting the value as
return User.where({ id })
.orderBy('id', 'DESC')
.fetch({
withRelated: [
'mutual_fund_portfolio',
'local_wallet',
'global_wallet',
'local_gift_card_wallet',
'global_gift_card_wallet'
]
})
The mutual_fund_portfolio comes out as an empty array.
hasMany performs a simple SQL join on a key. I believe the TypeError: Cannot read property 'parentFk' of undefined error refers to the fact that the table you are referencing here MutualFundPortfolio does not share a key with the table in the model you are using here.
It's not visible above sample but I'm assuming it's something like:
const User = bookshelf.model('User', {
tableName: 'users',
books() {
return this.hasMany('MutualFundPortfolio').query().sum('balance');
}
})
In my hypothetical example the users table has a primary key id column userId that is also in MutualFundPortfolio as a foreign key. My guess is that the error is because MutualFundPortfolio does not have that column/foreign key.

Can't find a easy way out of multiple async for each node js (sails)

So here's the deal :
I have an array of objects with a child array of objects
askedAdvices
askedAdvice.replayAdvices
I'm looping trough the parent and foreach looping trough the childs and need to populate() two obejcts (I'm using sails)
The child looks like :
askedAdvices = {
replayAdvices : [{
bookEnd : "<ID>",
user : "<ID>"
}]
}
So my goal is to cycle and populate bookEnd and user with two findOne query, but I'm going mad with the callback hell.
Here's the Models code :
AskedAdvices Model
module.exports = {
schema : false,
attributes: {
bookStart : {
model : 'book'
},
replayAdvices : {
collection: 'replybookend'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
ReplyBookEnd Model
module.exports = {
schema : false,
attributes: {
bookEnd : {
model : 'book'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
Here's the Method code :
getAskedAdvices : function(req, res) {
var queryAskedAdvices = AskedAdvices.find()
.populate("replayAdvices")
.populate("user")
.populate("bookStart")
queryAskedAdvices.exec(function callBack(err,askedAdvices){
if (!err) {
askedAdvices.forEach(function(askedAdvice, i){
askedAdvice.replayAdvices.forEach(function(reply, i){
async.parallel([
function(callback) {
var queryBook = Book.findOne(reply.bookEnd);
queryBook.exec(function callBack(err,bookEndFound) {
if (!err) {
reply.bookEnd = bookEndFound;
callback();
}
})
},
function(callback) {
var queryUser = User.findOne(reply.user)
queryUser.exec(function callBack(err,userFound){
if (!err) {
reply.user = userFound;
callback();
}
})
}
], function(err){
if (err) return next(err);
return res.json(200, reply);
})
})
})
} else {
return res.json(401, {err:err})
}
})
}
I can use the async library but need suggestions
Thanks folks!
As pointed out in the comments, Waterline doesn't have deep population yet, but you can use async.auto to get out of callback hell. The trick is to gather up the IDs of all the children you need to find, find them with single queries, and then map them back onto the parents. The code would look something like below.
async.auto({
// Get the askedAdvices
getAskedAdvices: function(cb) {
queryAskedAdvices.exec(cb);
},
// Get the IDs of all child records we need to query.
// Note the dependence on the `getAskedAdvices` task
getChildIds: ['getAskedAdvices', function(cb, results) {
// Set up an object to hold all the child IDs
var childIds = {bookEndIds: [], userIds: []};
// Loop through the retrieved askedAdvice objects
_.each(results.getAskedAdvices, function(askedAdvice) {
// Loop through the associated replayAdvice objects
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
childIds.bookEndIds.push(replayAdvice.bookEnd);
childIds.userIds.push(replayAdvice.user);
});
});
// Get rid of duplicate IDs
childIds.bookEndIds = _.uniq(childIds.bookEndIds);
childIds.userIds = _.uniq(childIds.userIds);
// Return the list of IDs
return cb(null, childIds);
}],
// Get the associated book records. Note that this task
// relies on `getChildIds`, but will run in parallel with
// the `getUsers` task
getBookEnds: ['getChildIds', function(cb, results) {
Book.find({id: results.getChildIds.bookEndIds}).exec(cb);
}],
getUsers: ['getChildIds', function(cb, results) {
User.find({id: results.getChildIds.userIds}).exec(cb);
}]
}, function allTasksDone(err, results) {
if (err) {return res.serverError(err);
// Index the books and users by ID for easier lookups
var books = _.indexBy(results.getBookEnds, 'id');
var users = _.indexBy(results.getUsers, 'id');
// Add the book and user objects back into the `replayAdvices` objects
_.each(results.getAskedAdvices, function(askedAdvice) {
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
replayAdvice.bookEnd = books[replayAdvice.bookEnd];
replayAdvice.user = users[replayAdvice.bookEnd];
});
});
});
Note that this is assuming Sails' built-in Lodash and Async instances; if you're using newer versions of those packages the usage of async.auto has changed slightly (the task function arguments are switched so that results comes before cb), and _.indexBy has been renamed to _.keyBy.

Bookshelf JS Relation - Getting Count

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");
})

sails js save many to many is only one way

i have two models:
user.js
module.exports = {
attributes: {
...
profile: {
model: 'Profile'
},
groups: {
collection: 'group',
via: 'users',
dominate: true
},
roles: {
collection: 'role',
via: 'users',
dominate: true
}
}};
and, group.js
module.exports = {
attributes: {
...
users: {
collection: 'user',
via: 'groups'
}
}};
when i try to add users to a group (when i select a group and add users to it), it works as it is supposed to,
var defer = q.defer();
baseDbContext.single(req, 'users')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.users = [];
_.each(req.users, function(item){
op.obj.users.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
but when i try to add groups to the user (when i select the user and add groups to it) it fails silently!!!
var defer = q.defer();
baseDbContext.single(req, 'groups')
.then(function(op){
if(!op.status || !op.obj) {
defer.resolve(notFound);
return;
}
op.obj.groups = [];
_.each(req.groups, function(item){
op.obj.groups.add(item);
});
op.obj.save(function(err, obj){
if(err) defer.reject(operationResult().throwException(err));
else defer.resolve(operationResult().succeed());
});
});
return defer.promise;
when i check it in sails console it shows :
throw new Error('Unknown rule: ' + ruleName);
Error: Unknown rule: dominate
this is a simple many to many insertion why would it fail?
(a note about code, the function baseDbContext.single finds a object based on its id and the second parameter is for populate)
Seems like you have a misprint, documentation says that the rule you need is writes as "dominant: true", not "dominate: true".

Resources