Sequelize 'where' on parent and child when parent hasMany childs - mysql

I have 2 models:
class User extends Model {
static associate(models) {
User.hasMany(models.Role, {
foreignKey: 'userId'
});
}
};
User.init({
firstname: {
type: DataTypes.STRING,
},
lastname: {
type: DataTypes.STRING,
},
allowedApps: {
type: DataTypes.ENUM({
values: Object.keys(PORTALS)
}),
allowNull: false
}
}, {
sequelize,
paranoid: true,
modelName: 'User',
});
class Role extends Model {
static associate(models) {
Role.BelongsTo(models.User, {
foreignKey: 'userId'
});
}
};
Role.init({
type: {
type: DataTypes.STRING,
unique: true
},
name: DataTypes.STRING,
userId: {
type: DataTypes.INTEGER,
allowNull: false,
}
}, {
sequelize,
paranoid: true,
modelName: 'Role',
});
I would like to get all users where the firstname OR the role type matches a certain condition. Something like:
User
.findAndCountAll({
where: {
$or: [
{
firstname: "John Doe"
},
{
"$Role.type$": "Admin"
}
]
},
include: [{
model: Role,
}],
}).limit=10,offset=0
.then(users => res.status(200).send(users))
.catch(error => {
return res.sendStatus(500);
});
above query giving me error: "SequelizeDatabaseError: Unknown column 'Role.type' in 'field list'"
I want to search through child model when it has one to many relationship having my limit and offset intact.Same query would give me success if user would have HasOne relation with role.
This is just an example code of what I try to achieve so please ignore any typos and silly mistakes.

After some digging:
await User.findAll({
where: {
$or: [
{
firstname: "John Doe"
},
{
"$Role.type$": "Admin"
}
]
},
include: {
model: Role,
as: 'Role',
required: false
}
});
However, it doesn't make logical sense to select Users that have no associated Role (required: false), while querying such Users with a property that exists on Role ($or: $Role.type$). If we set Required = true, then we violate your initial condition
firstname OR the role type matches a certain condition.
The following addresses this problem:
await User.findAll({
include: {
model: Role,
required: false
}
})
.then(
users => users
.filter(user => user?.firstName === "John Doe" || user.role?.type ===
"Admin");
);

Related

Sequelize M:N association not creating through table record

When I create a recipe with associated tags using the through option, no record is created in the joining table in the mysql database I'm connected to.
Here are my models definitions:
export const Recipe = sequelize.define('Recipe', {
// Model attributes are defined here
title: {
type: DataTypes.STRING,
allowNull: false
},
image: {
type: DataTypes.STRING,
allowNull: true
},
prepTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
cookTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
totalTime: {
type: DataTypes.DOUBLE,
allowNull: false
},
servings: {
type: DataTypes.INTEGER,
allowNull: false
},
rating: {
type: DataTypes.INTEGER,
allowNull: false
},
notes: {
type: DataTypes.STRING, allowNull: true
},
}, {
// Other model options go here
tableName: 'Recipes'
});
export const Tag = sequelize.define('Tag', {
// Model attributes are defined here
name: {
type: DataTypes.STRING,
allowNull: false
},
}, {
// Other model options go here
tableName: 'Tags'
});
export const RecipeTag = sequelize.define('RecipeTag', {
// Model attributes are defined here
}, {
// Other model options go here
timestamps: false,
tableName: 'RecipeTags'
});
Here are my associations:
Recipe.belongsToMany(Tag, {
through: RecipeTag,
foreignKey: 'recipeId',
as: 'tags'
})
Tag.belongsToMany(Recipe, {
through: RecipeTag,
foreignKey: 'tagId',
as: 'recipes'
})
Here is the create call:
Recipe.create(args, {
model: Tag,
through: RecipeTag,
as: 'tags'
});
And here is the data:
{
"title": "Test Recipe",
"image": "test",
"prepTime": 20,
"cookTime": 40,
"totalTime": 60,
"servings": 2,
"rating": 5,
"categoryId": 1,
"tags": [
{
"name": "New tag",
"id": 1
}
],
}
With this set up the create method only creates a new recipe. How can I use the create method to add a record to the joining RecipeTags table at the same time as creating a new recipe? I've managed to get it working by doing something like this:
args.tags.map(async (tag: { tagId: number }) => {
await RecipeTag.create({tagId: tag.tagId, recipeId: recipe.id})
});
But I'd rather have it done in the create if it's possible.
You need to wrap the association options with include.
Recipe.create(args, {
include: {
model: Tag,
through: RecipeTag,
as: 'tags'
}
});
UPDATE:
In order to prevent the duplicates, you can add ignoreDuplicates option and data must include the primary key value.
{
"title": "Test Recipe",
...
"tags": [
{
"name": "New tag",
"id": 1 # this is important
}
]
}
Then
Recipe.create(args, {
include: {
model: Tag,
through: RecipeTag,
as: 'tags',
ignoreDuplicates: true // Add this
}
});
There were some bugs for this option, I suggest you to use the newer version of Sequelize, if you haven't updated lately.

How to use findAll with associations in Sequelize

I'm having problems to use the findAll() method with associations from Sequelize.
I have two models: Posts and Authors (an author has many posts and one post has one author), that I have created with Sequelize-cli and then through the migration command npx sequelize db migrate:all i have created them in mysql. To keep things organized, I have the associations between the models in another migration file (created with npx sequelize init:migrations, after all the models already existent), so my code looks like this:
AUTHOR MODEL
'use strict';
module.exports = (sequelize, DataTypes) => {
const Author = sequelize.define('Author', {
authorName: {
type: DataTypes.STRING,
validate: {
is: ["^[a-z]+$",'i'],
}
},
biography: {
type: DataTypes.TEXT,
validate: {
notEmpty: true,
}
}
}, {});
Author.associate = function(models) {
Author.hasMany(models.Post);
};
return Author;
};
POST MODEL
'use strict';
module.exports = (sequelize, DataTypes) => {
const Post = sequelize.define('Post', {
title: {
type: DataTypes.STRING,
validate: {
is: ["^[a-z]+$",'i'],
notEmpty: true,
},
},
content: {
type: DataTypes.TEXT,
validate: {
notEmpty: true,
},
},
likes: {
type: DataTypes.INTEGER,
defaultValue: 0,
validate: {
isInt: true,
},
},
}, {});
Post.associate = function(models) {
// associations can be defined here
};
return Post;
};
ASSOCIATIONS FILE (MIGRATION) (showing only parts that matter)
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction(t => {
return Promise.all([
queryInterface.addColumn('Posts','AuthorId', {
type: Sequelize.INTEGER,
references: {
model: 'Authors',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
queryInterface.addColumn('Posts', 'ImagesId', {
type: Sequelize.INTEGER,
references: {
model: 'Images',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
queryInterface.addColumn('Posts', 'CategoryId', {
type: Sequelize.INTEGER,
references: {
model: 'Categories',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
]);
});
This is working fine apparently, since in Mysql-Workbench it shows me the following:
But, when I try to use the findAll() like this:
const { Post, Author } = require('../models/index');
function(response) {
Post.findAll({
attributes: ['id', 'title', 'content', 'likes'],
include: {
model: Author,
}
})
.then(result => response.json(result))
.catch(error => response.send(`Error getting data. Error: ${error}`));
It gives me the following error:
SequelizeEagerLoadingError: Author is not associated to Post!
So, I dont know anymore how to proceed. I've been trying many others approaches, but all of then unsuccessfully. I read already many other questions here in StackOverFlow about how to solve this sort of problem, but those were unsuccessfully too.
Thanks in advance.
You need to define the association for Post also as you are querying upon Post model
Post.associate = function(models) {
Post.belongsTo((models.Author);
};
You need to add an association from both ends, Post -> Author and Author -> Post , this way you will never stuck in this kind of error.
Summarizing this documentation we have the following:
If you have these models:
const User = sequelize.define('user', { name: DataTypes.STRING });
const Task = sequelize.define('task', { name: DataTypes.STRING });
And they are associated like this:
User.hasMany(Task);
Task.belongsTo(User);
You can fetch them with its associated elements in these ways:
const tasks = await Task.findAll({ include: User });
Output:
[{
"name": "A Task",
"id": 1,
"userId": 1,
"user": {
"name": "John Doe",
"id": 1
}
}]
And
const users = await User.findAll({ include: Task });
Output:
[{
"name": "John Doe",
"id": 1,
"tasks": [{
"name": "A Task",
"id": 1,
"userId": 1
}]
}]

Node.js Sequelize virtual column pulling value from other model

I'm working with Sequelize 5.7, trying to utilize virtual datatype,
to pull related information into a model.
Given simplified company and user models, how do I get company.name
into user.companyname ?
company
let Schema = sequelize.define(
"company",
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING(45)
}
}
);
user
let Schema = sequelize.define(
"user",
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
login: {
type: DataTypes.STRING(45),
unique: true
},
company: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: sequelize.model('company'),
key: 'id'
}
},
/* This companyname contruct is pure fantasy, and the target of my question */
companyname: {
type: new DataTypes.VIRTUAL(DataTypes.STRING,['company']),
references: {
model: 'company',
key: 'name'
}
}
}
);
In your case, I think it is a better idea to use a relationship (an association)
Sequelize Associations
const User = sequelize.define('user', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
login: {
type: DataTypes.STRING(45),
unique: true
},
company_id: {
type: DataTypes.INTEGER.UNSIGNED,
},
});
const Company = sequelize.define('company', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
},
});
User.belongsTo(Company, {
foreignKey: 'company_id', // you can use this to customize the fk, default would be like companyId
});
Company.hasMany(User);
Then when calling your model you do something like:
User.findAll({ include: Company }).then(users => console.log(users));
I solved the problem by using type: DataTypes.VIRTUAL in model
const { Model, DataTypes } = require('sequelize');
class User extends Model {
static init(sequelize) {
super.init({
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
login: {
type: DataTypes.STRING(45),
unique: true
},
company_id: {
type: DataTypes.INTEGER.UNSIGNED,
},
companyname:{
type: DataTypes.VIRTUAL,
get() {
return this.Company?.get().name;
},
set(/*value*/) {
throw new Error('Do not try to set the `companyname` value!');
}
},
}, {
sequelize
})
}
static associate(models) {
this.belongsTo(Company, {
foreignKey: 'company_id',
});
}
}
module.exports = User;
to search just include the association :
User.findAll({ include: Company })
I usually create each model using 'class' in different files, but if you need, just include the code below in the #jalex19 solution
companyname:{
type: DataTypes.VIRTUAL,
get() {
return this.Company?.get().name;
},
set(/*value*/) {
throw new Error('Do not try to set the `fullName` value!');
}
},

Sequelize: Using include for a joined table

I have 4 models: User, Skill, SkillToLearn, and SkillToTeach. The SkillToLearn and SkillToTeach contain two columns userId and skillId to keep track of a skill (given by skillId) that a user (given by userId) wants to learn or teach.
My goal is to use Sequelize's include statement such that I can return all users' data, including a list of skills they are learning and a list of skills they are teaching. I want a response similar to the following:
[
{
"id": 1,
"username": "janedoe",
"skillsLearning": [
{
"skillId": 1,
"name": "arts"
}
],
"skillsTeaching": [
{
"skillId": 2,
"name": "cooking"
}
]
}
]
However, instead, I'm getting the following response:
[
{
"id": 1,
"username": "janedoe",
"skillsLearning": [
{
"skillId": 1,
"Skill": {
"name": "arts"
}
}
],
"skillsTeaching": [
{
"skillId": 2,
"Skill": {
"name": "cooking"
}
}
]
}
]
I have tried to include Skill instead of SkillToLearn and SkillToTeach but I got an error saying that Skill is not associated to User. I am uncertain if my schemas and associations are incorrect.
User.js
const User = sequelize.define("User", {
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT
}
}, {
freezeTableName: true
});
User.associate = function (models) {
User.hasMany(models.SkillToLearn, {
as: "skillsLearning",
onDelete: "cascade",
foreignKey: "userId",
sourceKey: "id"
});
User.hasMany(models.SkillToTeach, {
as: "skillsTeaching",
onDelete: "cascade",
foreignKey: "userId",
sourceKey: "id"
});
};
Skill.js
const Skill = sequelize.define("Skill", {
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
}
}, {
freezeTableName: true
});
Skill.associate = function (models) {
Skill.hasMany(models.SkillToLearn, {
as: "usersLearning",
onDelete: "cascade",
foreignKey: "skillId",
sourceKey: "id"
});
Skill.hasMany(models.SkillToTeach, {
as: "usersTeaching",
onDelete: "cascade",
foreignKey: "skillId",
sourceKey: "id"
});
};
SkillToLearn.js
const SkillToLearn = sequelize.define("SkillToLearn", {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
skillId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
}
}, {
freezeTableName: true
});
SkillToLearn.associate = function (models) {
SkillToLearn.belongsTo(models.User, {
foreignKey: "userId",
targetKey: "id"
});
SkillToLearn.belongsTo(models.Skill, {
foreignKey: "skillId",
targetKey: "id"
});
};
SkillToTeach.js
const SkillToTeach = sequelize.define("SkillToTeach", {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
skillId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
}
}, {
freezeTableName: true
});
SkillToTeach.associate = function (models) {
SkillToTeach.belongsTo(models.User, {
foreignKey: "userId",
targetKey: "id"
});
SkillToTeach.belongsTo(models.Skill, {
foreignKey: "skillId",
targetKey: "id"
});
};
dataRoutes.js
db.User
.findAll({
attributes: ["id", "username"],
include: [
{
model: db.SkillToLearn,
as: "skillsLearning",
attributes: ["skillId"],
include: [
{
model: db.Skill,
attributes: ["name"]
}
]
},
{
model: db.SkillToTeach,
as: "skillsTeaching",
attributes: ["skillId"],
include: [
{
model: db.Skill,
attributes: ["name"]
}
]
}
]
})
.then(results => res.json(results));
});
Is there a way for me to get the skill's name without having it in an object? Should I just do multiple queries and construct the response using my own object literal? Thank you!

sequelizejs eager fetch many to many

I am trying to fetch eager on a join table in Nodejs with Sequelizejs v3 .
So, 1 Tag can belong to Many Images, and Many Images can have multiple tags.
Tag 1 - > M ImageTag M < - 1 Image
I am getting Unhandled rejection Error: Tag is not associated to ImageDetails when i tried to excute a query.
function getImagesFromAlbum(albumid, callback, errCallback){
ImageDetails.findAll({where: { AlbumId: albumid }, include: [{model: Tag}]}).then((data) => {
return callback(data)
}).catch((err) => {
return errCallback(err)
})
}
The expected return result should be the data according to the albumid, with the assiociate tags for that image
Here are the relationship joining
ImageDetails.belongsToMany(Tag, { as: { singular: "tag", plural: "tags" }, through: { model: ImageTag, unique: false }, foreignKey: "ImageId"})
Tag.belongsToMany(ImageDetails, { as: { singular: "image", plural: "images" }, through: { model: ImageTag, unique: false }, foreignKey: "TagId"})
Here are the model designs
Tag Model
const model = {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING
}
}
const name = "Tag"
ImageTag model (Join Table)
const model = {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
}
}
const name = "ImageTag"
ImageDetails model
import { Sequelize, db } from "../config/MySqlConfiguration"
const model = {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
ImageLocation: {
type: Sequelize.STRING
},
originalName: {
type: Sequelize.STRING
}
}
const name = "ImageDetails"
*Note sequelize.define is purposely omitted.
When defining relation between models with use of alias (as) in the belongsToMany, you need to remember to include this alias when eager loading models, so your query would look as follows
ImageDetails.findAll({
where: { AlbumId: albumid },
include: [{ model: Tag, as: 'tags' }]
}).then((data) => {
return callback(data)
}).catch((err) => {
return errCallback(err)
});
And what is the AlbumId you want to perform query on? According to your models definition, the ImageDetails model does not have such a field.