Defining foreign key associations correctly in Sequelize - node.js

I have been working on my models for my ERD in node / postgres / sequelize and have been loosely defining my associations as I have been going along. In the case of the below am I correct in believing that I only have to define the foreign keys on the one table? In the example below I have two fields that I want to pull the associated data from other tables when I query them. Am I going about this the right way.
I have previously defined foreign keys in PhpMyAdmin and only done them on the one table. I have yet to pull the correct data in my Postman queries and have only returned the int values of the primary table. Is this dependent on me having my foreign keys set-up correctly? Or do I need to specify something in my controller API?
articleID: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
articleTitle: {
type: Sequelize.TEXT
},
articleContent: {
type: Sequelize.TEXT
},
articlePhotos: {
data: Sequelize.Buffer,
type: Sequelize.TEXT
},
articleAuthor: {
type: Sequelize.INTEGER
},
articleType: {
type: Sequelize.INTEGER
}},
{
timestamps: false
}, {});
article.associate = function(models){
article.articleType.belongsTo(models.article, {
foreignKey: {name: 'articleTypeID', as: 'articleType'}})
article.articleAuthor.belongsTo(models.userLogin, {
foreignKey: {name: 'userID', as: 'userAuthor'}})
};

Related

Creating a foreignKey in sequelize, that is not a integer association

I want to translate this psql table creation query to sequelize:
PSQL:
CREATE TABLE categories
(
id INTEGER NOT NULL PRIMARY KEY,
name CHARACTER VARYING(50) NOT NULL UNIQUE,
description CHARACTER VARYING(100) NOT NULL
);
CREATE TABLE posts
(
id INTEGER NOT NULL PRIMARY KEY,
title CHARACTER VARYING(50) NOT NULL,
content CHARACTER VARYING(100) NOT NULL,
from_category CHARACTER VARYING(50) NOT NULL,
CONSTRAINT fk_from_category
FOREIGN KEY(from_category)
REFERENCES categories(name)
)
Its a simple fk association, with varchar type.
I have read sequelize docs, but i still don't know how to change the relation from primary keys to varchar.
From what i read, this is what you can do with associations:
Post.belongsTo(Category, {
foreignKey: {
onDelete: ...,
onUpdate: ...,
validate: {...},
},
});
and thats all i could find about on youtube too..
I would be really happy if you can help me. I have spent too much time on this already, but i want it to work!
Follow this example using belongsTo:
class Category extends Model { }
Category.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
name: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
description: {
type: DataTypes.STRING,
allowNull: false
}
}, { sequelize, modelName: 'categories' });
class Post extends Model { }
Post.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.STRING,
allowNull: false
},
from_category: {
type: DataTypes.STRING,
allowNull: false
}
}, { sequelize, modelName: 'posts' });
Post.belongsTo(Category, {
foreignKey: 'from_category',
targetKey: 'name'
});
Read more about to understand with more details in the official docs.

Sequelize CLI how to create migrations from models?

I have two models with relations one to many.
I don't understand how to create migration files. Does each model have its own migration file or one migration file can create several tables from models and relations between them (for example as in rails migrations)?
I had a look at many examples including Sequelize docs, and there are primitive examples of models creating and its migration.
//User model
module.exports = function (sequelize, Sequelize) {
var User = sequelize.define('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
});
return User;
}
//Order model
module.exports = function (sequelize, Sequelize) {
var Order = sequelize.define('orders', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
price: {
type: Sequelize.INTEGER,
allowNull: false,
},
totalPrice: {
type: Sequelize.INTEGER,
allowNull: false,
},
});
return Order;
}
//db.js
//Relations
db.orders.belongsTo(db.users);
db.users.hasMany(db.orders);
Addition
I create migration for two models:
module.exports = {
up: function (queryInterface, Sequelize, done) {
return [
queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
}),
queryInterface.createTable('orders', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
price: {
type: Sequelize.INTEGER,
allowNull: false,
},
totalPrice: {
type: Sequelize.INTEGER,
allowNull: false,
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
}),
done()
]
},
down: function (queryInterface, Sequelize, done) {
return [
queryInterface.dropTable('users'),
queryInterface.dropTable('orders'),
done()
]
}
};
Do I need to add into my migration file class methods for my models?
//for Order
classMethods: {
associate: function(models) {
Model.belongsTo(models.users, (as: 'users'));
}
}
//for User
classMethods: {
associate: function(models) {
Model.hasMany(models.orders, (as: 'orders'));
}
}
//Addition 2
In order to create new migration file you need to call sequelize migration:create, which creates new file in /migrations directory (that is default migrations directory, can be different). In the migration file you can use bunch of functions in order to create tables, update them or specified table columns etc. If you want you can create all your database tables within single migration file. There is no straight connection between your models and migration files - they are independent on each other. The same concerns relations between models/table. You need to specify that given column in given table references other table.
// example column definition inside migration file
// creates a foreign key referencing table 'users'
userId: {
type: Sequelize.INTEGER,
references: {
model: 'users',
key: 'id'
},
onDelete: 'CASCADE'
}
You just need to remember about consistency between fields definition in Model and field/column definitions in the migration file corresponding to specified model/table.
You can also use command sequelize model:create, which, at the same time, creates a file used for defining a Sequelize model, as well as migration file responsible for creating a table corresponding to this model.
In order to show all possible sequelize-cli commands simply run sequelize help.
EDIT
The class methods like associate must be present only in the Model definition files, not in the migration files.
EDIT 2
The functions used in migration files like createTable are asynchronous, so you cannot simply run them in order just like you did it in your migration file. You can chain them via .then() method or return them as an array like
return [queryInterface.createTable(...), queryInterface.createTable(...)];

Sequelize create through association

I'm working on a create method for an association between two classes. The sequelize documentation indicates that this can be done in one step using includes
IntramuralAthlete.create(intramuralAthlete,{
include: [Person]
}).then((data,err)=>{
if(data)res.json(data);
else res.status(422).json(err);
}).catch(function(error) {
res.status(422).json({message: "failed to create athlete", error: error.message});
});
My model association looks like this
var Person = require('../models').person;
var IntramuralAthlete = require('../models').intramuralAthlete;
IntramuralAthlete.belongsTo(Person);
And the value of intramural athlete when I log it is
{
person:
{ firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
But I get the error notNull Violation: personId cannot be null. This error makes it sound like something is wrong with the way I'm indicating to Sequelize that I'm intending to create the personId in that same call.
Is there something wrong in the way I indicate to the create statement what associated tables to create with the IntramuralAthlete?
Thanks!
EDIT:
I have also tried with the following structure with the same result
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
My model is as follows:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
autoIncrement: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
},
guardianId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'contact',
key: 'id'
}
},
personId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'person',
key: 'id'
}
},
mobileAthleteId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'mobileAthlete',
key: 'id'
}
},
organizationId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'organization',
key: 'id'
}
}
}, {
tableName: 'intramuralAthlete'
});
};
I suppose that your models are named Person and IntramuralAthlete (first arguments of sequelize.define method). In this case, when you create an association like yours, and do not define the as attribute, your create data object should look as follows
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
If you want to use person instead (just as in your code), you should define the association a little bit differently
IntramuralAthlete.belongsTo(Person, { as: 'person' });
Then, you would have to perform some changes in the create query in the include attribute of the options like this
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
]
}).then((result) => {
// both instances should be created now
});
EDIT: Trick the save() method with empty value of personId
You can maintain the allowNull: false if you do something like that
{
person: {
// person data
},
personId: '', // whatever value - empty string, empty object etc.
grade: '12th',
organizationId: 1
}
EDIT 2: Disable validation when creating.
This case assumes that the validation is turned off. It seems like a bad idea to omit model validation, however there still maintains the database table level validation - defined in migrations, where it can still check if personId value was set
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
],
validate: false
}).then((result) => {
// both instances should be created now
});
In this case the data object can be as in your example - without the personId attribute. We omit the model level validation which allows to pass null value, however if during the save() method it would still be null value - database level validation would throw an error.
first of all, when you associatea a model with belongsTo, sequelize will add automatically the target model primary key as a foreign key in the source model. in most of cases you don't need to define it by yourself, so in your case when you define IntramuralAthlete.belongsTo(Person) sequelize adds PersonId as a foreign key in IntramuralAthlete. your IntramuralAthlete model should looks like:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
}
});
};
now you can create an intramuralAthlete like your code above. for example:
let data = {
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
notes: 'test notes'
}
IntramuralAthlete.create(data, {include: [Person]}).then((result) => {
// both instances should be created now
});
be carefull with the model name.
second I suppose that your IntramuralAthlete model has more than one belongsTo association. just you need to define them as the previous one association and sequelize will add their primary keys as foreign keys in the IntramuralAthlete model.
third, when you define a model, sequelize adds automatically an id datafield as a primary key and autoincrement and also adds createdAt and updatedAt datafields with a default CURRENT_TIMESTAMP value, so you don't need to define them in your model

Sequelize: Querying where I use foreignKey instead of as

So I'm having some trouble getting the query results I'm looking for. In terms of models I have
models.UserRole = sequelize.define('userrole', {
name: {
type: Sequelize.STRING,
primaryKey: true
},
permissions: {
type: Sequelize.ARRAY(Sequelize.STRING),
defaultValue: []
}
});
models.User = sequelize.define('user', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
...
});
models.User.belongsTo(models.UserRole, {foreignKey: 'role'});
And I used foreignKey instead of as because I wanted the field to be called role exactly and not what Sequelize was changing it to (roleName).
Anyway, I'm now trying to query and include the permissions along with the selected user, so I use
models.User.findById(id, {
attributes: ['id','role'],
include: [{
model: models.UserRole,
attributes: ['name', 'permissions']
}]
})
And it works, but it retrieves them on the field userrole and looks like this
{"id":"id-here","role":"admin","userrole":{"name":"admin", "permissions":["p1","p2",..]}}
So finally, my question is how do I make it so that the stuff it retrieves from the UserRole model is retrieved under the key "role" instead? So it looks like
{"id":"id-here","role":{"name":"admin", "permissions":["p1","p2",..]}}

Use Model collection as data-type in sequelize

Background:
This is for a product database prototype, each product can have multiple related products, and each relationship has a type, e.g. 'Accessory', 'Spare Part', 'Related to', 'Similar to', etc.
Technology
We are using sequelize js on node to define the model.
Model Snippet:
sequelize.define('Product', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
name: { type: DataTypes.STRING, allowNull: false, comment: 'product name'}
...
});
sequelize.define('ProductRelationType', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
name: { type: DataTypes.STRING, allowNull: false, comment: 'relationship type description' }
});
sequelize.sync({force: false}).then( function() {
...
});
Question
Is it possible to use models themselves as datatypes in Sequelize, to establish a collection in another table, for example:
sequelize.define('ProductRelation', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
relatedProduct: { type: Product } //reference to product model
});
and followed by:
Product.hasMany(ProductRelation, { as: 'relatedProducts' });
ProductRelation.hasOne(ProductRelationType, { as: 'RelationType' } );
alternatively, exclude the ProductRelation table definition, and use:
db.Product.hasMany(db.Product, { through: 'RelatedProduct' } );
db.RelatedProduct.hasMany(db.Product, { through: 'RelatedProduct' } );
Note: These are concept examples, they do not work.
Any suggestions, or alternative modeling approaches are appreciated.
Thank you
It appears, what you want is simply establishing an n:m relationship from Product to Product.
The only way to get there is by establishing a link- (or "through-") table. You can either do it manually or let Sequelize do it automatically using belongsToMany:
var RelatedProducts = sequelize.define('RelatedProducts', {
// other columns here
});
Product.belongsToMany(Product, { through: RelatedProducts, foreignKey: 'relatedProductId' });
Product.belongsToMany(Product, { through: RelatedProducts });

Resources