Sequelize findAll and Join table - mysql

I want to do a simple chat app and my tables are like this (I don't know if it's the proper way to implement it):
var User = sequelize.define('User', {
userName: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING
}, {
classMethods: {
associate: function(models) {
User.hasMany(models.Friend);
}
}
Friends:
var Friend = sequelize.define('Friend', {
realId: DataTypes.INTEGER,
invitStatus: DataTypes.STRING
}, {
classMethods: {
associate: function(models) {
Friend.hasMany(models.Message);
}
Messages:
var Message = sequelize.define('Message', {
text: DataTypes.TEXT,
sentBy: DataTypes.INTEGER
}, {
classMethods: {
associate: function(models) {
}
This is my current query:
models.User.findAll({
where: {
id: req.payload.userId
},
include: [{
model: models.Friend,
include: [{
model: models.Message,
order: [['createdAt', 'DESC']],
limit: 1
}]
}]
}).then(function(result) {
reply(result).code(200);
})
}
Everything is working fine, except that I want to tell sequelize to get the Friend data from the User table with the realId from the Friend table.
Should I use the keyword "through"? I didn't get exactly how it works even though I read the docs multiple times. I'm a beginner in SQL.
Thanks for your help.
Edit: My current output is:
{
"id": 2,
"userName": "user2",
"email": "user2#example.com",
"password": "123456",
"createdAt": "2016-05-26T16:28:02.000Z",
"updatedAt": "2016-05-26T16:28:13.000Z",
"Friends": [
{
"id": 1,
"realId": 1,
"invitStatus": "accepted",
"createdAt": "2016-05-26T16:30:15.000Z",
"updatedAt": "2016-05-26T16:30:15.000Z",
"UserId": 2,
"Messages": [
{
"id": 18,
"text": "ok",
"sentBy": 1,
"createdAt": "2016-05-26T16:59:36.000Z",
"updatedAt": "2016-05-26T16:59:36.000Z",
"FriendId": 1
}
]
}
What I want is to add the value "userName", "email" and "password" to the Friends array by getting it from the User table with the "Friend.realId = User.id.

I'm not sure to clearly understand your problem. I assume that Friends is linked to User by realId so you shouldn't have a Friends model but only one User model.
If this answer can help anyone, in this case you should probably use self-association.
You can achive self-association between User and User (as friend) with something like
User.belongsToMany(models.User, { as: 'friends', foreignKey: 'userId', through: 'User_Friends' });
And if your User_Friends model need additional attributes you can specified them like below, else this join table will be automatically created.
var User_Friends = sequelize.define('User_Friends', {
// ...
invitStatus: DataTypes.STRING
// ...
}

Related

sequelize count associated table rows

Using sequelize and mySQL, I have two tables: User and Post.
Relation between two tables is M : N
db.User.belongsToMany(db.Post, { through: "Likes", as: "Liked" });
db.Post.belongsToMany(db.User, { through: "Likes", as: "Likers" });
What I want is getting post with whole likers id and count of whole likers.
I know how to get whole likers like this.
const post = await Post.findOne({
where: { id: postId },
attributes: ["id", "title", "imageUrl"],
include: [{
model: User,
as: "Likers",
attributes: ["id"],
through: { attributes: [] },
}]
})
// result
{
"id": 36,
"title": "test",
"imageUrl": "하늘이_1644886996449.jpg",
"Likers": [
{
"id": 13
},
{
"id": 16
}
]
}
And, I also know how to get count of whole likers.
const post = await Post.findOne({
where: { id: postId },
attributes: ["id", "title", "imageUrl"],
include: [{
model: User,
as: "Likers",
attributes: [[sequelize.fn("COUNT", "id"), "likersCount"]],
}]
})
// result
{
"id": 36,
"title": "test",
"imageUrl": "하늘이_1644886996449.jpg",
"Likers": [
{
"likersCount": 2
}
]
}
But, I don't know how to get both of them at once.
Check the result when I use both of them.
{
model: User,
as: "Likers",
attributes: ["id", [sequelize.fn("COUNT", "id"), "likersCount"]],
through: { attributes: [] },
}
// result
"Likers": [
{
"id": 13,
"likersCount": 2
}
]
It only shows 1 liker(id: 13)
It must show another liker(id: 16).
What is the problem?
It shows only one because COUNT is an aggregating function and it groups records to count them. So the only way to get both - use a subquery to count records in a junction table while getting records on the other end of M:N relationship.
const post = await Post.findOne({
where: { id: postId },
attributes: ["id", "title", "imageUrl",
// you probably need to correct the table and fields names
[Sequelize.literal('(SELECT COUNT(*) FROM Likes where Likes.postId=Post.id)'), 'LikeCount']],
include: [{
model: User,
as: "Likers",
attributes: ["id"],
through: { attributes: [] },
}]
})

Sequelize populate joined table attributes

I have a few tables and I want to do some includes on a joined table, but I can't seem to figure it out. Here are my models:
/* Staff model */
const model = {
fisrName: {
type: DataTypes.INTEGER,
references: { model: 'Roles', key: 'id' },
},
lastName: {
type: DataTypes.INTEGER,
references: { model: 'Profiles', key: 'id' },
}
};
const Staff = createModel('Staff', model, { paranoid: true });
export default Staff
/* Service model */
const model = {
name: {
type: DataTypes.STRING,
},
category: {
type: DataTypes.STRING,
},
description: {
type: DataTypes.STRING,
}
};
const Service = createModel('Service', model, {});
export default Service;
/* Appointment model */
const model = {
endDate: {
type: DataTypes.DATE,
},
startDate: {
type: DataTypes.DATE,
},
day: {
type: DataTypes.DATE,
},
};
const Appointment = createModel('Appointment', model, {})
Appointment.belongsToMany(Service, { through: 'Products', as: 'products' });
export default Appointment;
/* Products model */
const model = {
serviceId: {
type: DataTypes.INTEGER,
},
appointmentId: {
type: DataTypes.INTEGER,
},
staffId: {
type: DataTypes.INTEGER,
references: { model: 'Staff', key: 'id' },
}
};
const Product = createModel('Product', model, {});
Product.belongsTo(Staff, { foreignKey: 'staffId', as: 'staff' });
export default Product;
This is my appointment query, where I include the services array, and on this services array, I have a Products object, and in this object I have a staffId that I want to populate, and I'm not sure how. I have tried different ways, but nothing worked.
const appointment = await Appointment.findByPk(req.params.id, {
include: [
{
model: Service,
as: 'services',
through: { attributes: { include: ['id', 'staffId', 'serviceId', 'appointmentId'], exclude: ['createdAt', 'updatedAt', 'AppointmentId', 'ServiceId'] },
},
],
});
And this is my response:
{
"startDate": "date",
"endDate": "date",
"day": "date",
"services": [{
"id": 1,
"name": "Service name",
"category": "service category",
"description": "service description",
"Products": {
"id": 1,
"staffId": 2,
"serviceId": 1,
"appointmentId": 1
}
}]
}
What I want is to do is to populate the staffId from Products with the model from the collection, something like this:
{
"startDate": "date",
"endDate": "date",
"day": "date",
"services": [{
"id": 1,
"name": "Service name",
"category": "service category",
"description": "service description",
"Products": {
"id": 1,
"staffId": {
"firstName": "First Name",
"lastName": "Last Name"
},
"serviceId": 1,
"appointmentId": 1
}
}]
}
const appointment = await Appointment.findByPk(req.params.id, {
include: [
{
model: Service,
as: 'services',
through: {
attributes: { include: ['id', 'staffId', 'serviceId', 'appointmentId'], exclude: ['createdAt', 'updatedAt', 'AppointmentId', 'ServiceId'] },
},
include: [{
as: 'Products', model: Product,
include: 'staff'
}]
}
]
});

How to name the fields of related model in filter object?

In my project, two models "UserProfile" and "UserAccount" are with a relation that the former "has one" the later. The .json files look like:
userprofile.json:
{
"name": "Userprofile",
"base": "PersistedModel",
//...
"properties": {
"userid": {
"type": "Number"
},
"phoneno": {
"type": "String"
}
},
//...
"relations": {
"userAccounts": {
"type": "hasOne",
"model": "UserAccount",
"foreignKey": "id",
"options": {
"validate": true,
"forceId": false
}
}
}
}
useraccount.json:
{
"name": "UserAccount",
"base": "User",
"idInjection": true,
"restrictResetPasswordTokenScope": true,
"emailVerificationRequired": true,
"properties": {},
"relations": {}
//...
}
The models have corresponding tables in a MariaDB.
Now the quest is to "GET" UserProfile with a keyword that match any one field of UserProfile.phoneno or UserAccount.email (yes, the key point is or). In SQL terms, that is:
SELECT * FROM UserProfile INNER JOIN UserAccount
ON UserProfile.userid = UserAccount.id
WHERE UserProfile.phoneno LIKE '%keyword%'
OR UserAccount.email LIKE '%keyword%'
It should be a common and simple query in SQL but seems become difficult in LookBack. My implementation is:
userprofile.js:
'use strict';
module.exports = function (Userprofile) {
Userprofile.remoteMethod('profileByEmailOrPhoneno', {
description: '...',
http: {path:'/profileByEmailOrPhoneno', verb: 'get'},
accepts: {arg: 'keyword', type: 'string', required: true},
returns: {arg: 'profile', type: 'array' }
})
Userprofile.profileByEmailOrPhoneno = function (keyword, cb) {
let filter = {
fields: {userid: true, nickname: true, phoneno: true},
include: {
relation: 'userAccounts',
scope: {
fields: {username: true, email: true}
}
},
where: {or: [
{phoneno: {like: `%${keyword}%`}},
{'userAccount.email': {like: `%${keyword}%`}}
]}
}
Userprofile.find(
filter,
function (err, records) {
if (err) console.log(err)
else cb(null, records)
}
)
}
};
I tested it on StrongLoop API Explorer and it always returned the whole records in UserProfile no matter whatever keyword. If the criterium
{'userAccount.email': {like: `%${keyword}%`}}
was removed the codes worked correctly. I think this criterium is wrong so LookBack ignores it and evaluate the where section to be true. I modified it to:
{'email': {like: `%${keyword}%`}}
and it was still wrong.
So, I wonder how to correctly name the relation model's field (eg.'email'), or, how to write the correct filter. Anybody can give some help? I'll very appreciated for it. ^^
The include statement in Loopback is a left-outer-join, so the query will always return ALL the Userprofile records. Some will have userAccounts with an array of values, other's wont. You need to further filter the Userprofile records.
Also, you need to put he userAccoutns filter in the scope statement of your filter:
Userprofile.profileByEmailOrPhoneno = function (keyword, cb) {
let filter = {
fields: {userid: true, nickname: true, phoneno: true},
include: {
relation: 'userAccounts',
scope: {
fields: {username: true, email: true},
where: {'email':{'like': `%${keyword}%`}} // userAccounts filter goes here
}
},
where: {phoneno: {like: `%${keyword}%`}}
}
Userprofile.find(filter, function (err, records) {
if (err) console.log(err)
else {
// filter the records for those that have userAccounts
var filteredResults = records.filter(record =>
record.userAccounts &&
Array.isArray(record.userAccounts()) &&
record.userAccounts().length);
cb(null, filteredResults)
}
})
}

Sequelize findAll with association and foreign key

I'm using sequelize to model a mySql-database schema in my node-application. Let's say I have 3 table: Project, User and Role.
It's a "Many to Many" association between Project and User through "Project_User" where is defined the role of a user for a project.
Project Model :
var Project = sequelize.define('Project', {
name:{type: DataTypes.STRING, unique: true}
},
classMethods: {
associate: function(models) {
Project.belongsToMany(models.User, { through: 'Project_User', as: 'users'});
}
}
// Methods...
);
User Model :
var User= sequelize.define('User', {
name:{type: DataTypes.STRING, unique: true}
},
classMethods: {
associate: function(models) {
User.belongsToMany(models.Project, { through: 'Project_User', as: 'projects'});
}
}
// Methods...
);
And here is the association table Project_User Model :
var Project_User = sequelize.define('Project_User', {
role:
{
type: DataTypes.INTEGER,
allowNull: false,
references: 'Role',
referencesKey: 'id'
}
},{
classMethods: {
associate: function(models) {
Project_User.belongsTo(models.Role, {foreignKey: 'role'});
}
}
});
Now, I want to find all project, with their users and their role. So I've used findAll with the "include" parameters like below:
models.Projects.findAll({
include:[
{
model: models.User,
as:'users',
through: {attributes: ['role'], as: 'role'}
}]
}).then(function(result) {
// ...
});
This works great but I only have the roleId associated to the user. I wasn't be able to link this "roleId" with the role table to get the other attributes like role name...
Here is the JSON I've got :
[
{
"id": 1,
"name": "Project name",
"users": [
{
"id": 1,
"name": "User name",
"role": {
"role": 1
}
}
]
}
]
But I would like to have something like that :
[
{
"id": 1,
"name": "Project name",
"users": [
{
"id": 1,
"name": "User name",
"role": {
"id": 1,
"name": "Role name",
"description": "Some info...",
}
}
]
}
]
I've tried many things to realize this association, even successive includes but it was unsuccessful. What is needed in the findAll options to get this JSON result ?
Thanks
Assuming that your User model is linked to the Role model, something like this should work:
models.Projects.findAll({
include:[
{
model: models.User,
as:'users',
through: {attributes: []},
include: [models.Role]
}] }).then(function(result) {
// ... });

Derived attribute absent in JSON response in Sails.js

I'm writing an API for users in an example app. The api/models/User-file looks as follows:
module.exports = {
attributes: {
firstName: {
type: 'string',
required: true
},
lastName: {
type: 'string',
required: true
},
fullName: function () {
return this.firstName + ' ' + this.lastName;
}
}
};
However, when I find all my users, the derived attribute is nowhere to be found in the response:
[
{
"firstName": "Marlon",
"lastName": "Brando",
"createdAt": "2015-09-13T10:05:15.129Z",
"updatedAt": "2015-09-13T10:05:15.129Z",
"id": 8
},
{
"firstName": "Bjoern",
"lastName": "Gustavsson",
"createdAt": "2015-09-13T10:05:36.221Z",
"updatedAt": "2015-09-13T10:05:36.221Z",
"id": 10
},
{
"firstName": "Charlie",
"lastName": "Sheen",
"createdAt": "2015-09-13T10:06:59.999Z",
"updatedAt": "2015-09-13T10:06:59.999Z",
"id": 11
}
]
Am I missing something, or is it simply not possible to derive attributes like this?
When you are set attributes in Model with function it doesn't mean that it will be executed in resulting attribute. It means that you can call this function in your code. For instance, I have exactly your User model. I can make in my code smth like this:
// api/controllers/UserController.js
module.exports = {
index: function(req, res) {
User
.create({firstName: req.param('firstName'), lastName: req.param('lastName')})
.then(function(user) {
console.log(user.fullName());
return user;
})
.then(res.ok)
.catch(res.negotiate);
}
};
If you want to make it like a dynamic attribute, then you should take a look at toJSON method in your model. You can override it and implement your own logic. I think it will looks like this in your case:
// api/models/User.js
module.exports = {
attributes: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
fullName: function() {
return [this.firstName, this.lastName].join(' ');
},
toJSON: function() {
var obj = this.toObject();
obj.fullName = this.fullName();
return obj;
}
}
};
I didn't check this code but think that should work. You can play around with toJSON method and see what you got. Ping me in comments if code doesn't work.