Sequelize. Right way to populate data and remove keys from output? - node.js

How to remove some fields of model from find* (Like password, token)?
I think overriding toJSON() function (Like here https://stackoverflow.com/a/27979695/6119618) is not a good way, because i sometimes need this field for password validation or token for checking etc..
Is there something like as .select('+token') as mongoose has?
And another question i think it's fit this topic.
How to remove generated by through fields from find* output?
When i call User.find() it responds { id: 0, name: 'somename', UserProjectsTie: { /* complex object of many-to-many relation table */ } }

To exclude an attribute from find*:
Model.findAll({
attributes: { exclude: ['baz'] }
});
To make this the default behavior, use a scope:
const Model = sequelize.define('Model', {
// Attributes
}, {
defaultScope: {
attributes: { exclude: ['baz'] }
}
});
Unless I'm mistaken the 'through' should only show up when using 'include'. To get rid of through in that case:
Model.findAll({
include: [{association: 'OtherModels', through: {attributes: []}}]
});

Related

Get specific attributes from database using sequelize model query

I have red the documentation of sequelize but could not get the idea/concept i wanted ,I don't want just one attribute to be shown.
We have the following syntax to get the attributes we need
Model.findAll({
attributes:['foo','bar]
})
In my case I have many attributes in single table , I just want to hide one attribute.Is there any way in sequelize that we define the attribute which we don't want to see and get all other by default..
For Example....
Model.findAll({
attributes:hide foo , show all other attributes
})
AnyOne Can help..
You can use below syntax.
Model.findAll({
attributes: {
exclude: ['foo'] // hide this
}
});
You can also exclude fields on model level by adding in their default scope like below.
const Model = sequelize.define('model',{
secretColumn: Sequelize.STRING,
//... and other columns
}, {
defaultScope: {
attributes: { exclude: ['secretColumn'] }
}
});

Sequelize: BelongsToMany with Polymorphic Relationships fails on scope after first (successful) try

We are running:
NodeJS v13.11
Sequelize v5.22.3
Postgres v11.7-2
I have 3 models, GameVersion, Tag, and TagTaggable (representing the associative entity). A GV can have many Tags, and a Tag can be associated with many GVs (or any other model via TagTaggable).
Models are built with this (not showing the hooks to handle and clean-up after the polymorphism):
GameVersion:
this.belongsToMany(models.Tag, {
through: {
model: models.TagTaggable,
unique: false,
scope: {
taggableType: 'game_version',
},
},
as: 'tags',
foreignKey: 'taggable_id',
constraints: false,
});
this.hasMany(models.TagTaggable, {
foreignKey: 'taggable_id',
scope: {
taggableType: 'game_version',
},
});
Tag:
this.belongsToMany(models.GameVersion, {
through: {
model: models.TagTaggable,
unique: false,
scope: {
taggableType: 'game_version',
},
},
as: 'gameVersions',
foreignKey: 'tag_id',
constraints: false,
});
this.hasMany(models.TagTaggable, {
scope: {
taggableType: 'game_version',
},
});
TagTaggable:
this.belongsTo(models.Tag, {
as: 'tag',
foreignKey: 'tag_id',
});
// Polymorphic relationships
this.belongsTo(models.GameVersion, {
foreignKey: 'taggable_id',
constraints: false,
as: 'gameVersion',
});
The Tags are applied to a GV by:
await gv.setTags(metadata.tags);
where metadata.tags is a collection of Tag models.
This works perfectly, the first time I run it, and I can see that, debugging into the bowels of the Sequelize BelongsToMany.updataAssociations method, the scope is correct on that first time, by looking at this.through.scope on the BelongsToMany object.
I get:
{taggableType: 'game_version'}
Notice the camel-cased key, which is what it should be.
This results in the following query:
INSERT INTO "tag_taggable" ("taggable_type","created_at","updated_at","taggable_id","tag_id") VALUES ('game_version','2020-11-27 18:49:34.767 +00:00','2020-11-27 18:49:34.767 +00:00',82,29)
The problem arises on any subsequent attempt (meaning it works the first time after starting the server, but any attempts prior to re-starting the server fail), and I can see that this.through.scope now results in:
{taggable_type: 'game_version'}
Notice the snake-cased key.
This results in the following query (notice the lack of the "taggable_type" column):
INSERT INTO "tag_taggable" ("created_at","updated_at","taggable_id","tag_id") VALUES ('2020-11-27 18:51:16.423 +00:00','2020-11-27 18:51:16.423 +00:00',83,29)
and throws a "not-null constraint violation."
This SEEMS to be down in the guts of Sequelize, but I cannot imagine it not being surfaced already if it were (unless polymorphic m:n is really uncommon).
Has anyone had this experience, and/or can anyone shed any light on what is going on here?
I would really like to use the setTags magic method, but I am at the point of just hand-building the TagTaggable objects and stuffing them in the DB myself.
TIA for any insights/assistance,
Chris

Automatically include referenced model in Sequelize on each GET

So I have user model and admin model and they're associated as user n:1 admin. The code defining the user model as follows:
// users.model.ts
const users = sequelize.define('users', {
...
adminId: {
field: 'admin_id',
type: DataTypes.BIGINT,
allowNull: true
},
...
});
(users as any).associate = function associate(models: any) {
models.users.belongsTo(models.admins);
};
return users;
and the admin model:
// admins.model.ts
const admins = sequelizeClient.define('admins', {
...
});
(admins as any).associate = function associate(models: any) {
models.admins.hasOne(models.users);
};
return admins;
Is it possible to implement some rule in the association, or some Sequelize hook f.e. afterGet that will automatically fetch the referenced record?
I would like to get the admin object as a property of the user object when I query just the User model, f.e. when I call User.findOne(123) it will have the object of the referenced admin record included. Basically telling Sequelize to always do the JOIN when getting the user record. Is that possible in Sequelize or I'll have to write logic separately?
Eventually I figured out that this is done through scopes (docs here and here).
I added this to the users.model.ts:
/*
* When called as `model.scope('<scope_name>').<method>` it will override the default behaviour of Sequelize and
* will add to the query whatever is requested in the scope definition.
*/
users.addScope('includeAdmin', {
include: [{
attributes: ['id', 'name'],
model: sequelize.models.admins,
as: 'admin'
}]
});
Eventually, I will make the following call: User.scope('includeAdmin').findOne(123), at which point Sequelize will automatically JOIN the admins model.
By default the admin entity's properties will be returned as such in the user object:
{
"admin.id": ...,
"admin.name": ...
}
So, if you want to have them as a nested admin object, then you must add nest: true property in the call, as follows: User.scope('includeAdmin').findOne(123, {nest: true})
If I want to make this behaviour default and not call .scope('...'), then when you declare the scope in the .addScope() function, call it 'defaultScope'.
That's why associations are used, to get things. To get the relation as attribute you can use eager loading:
const awesomeCaptain = await Captain.findOne({
where: {
name: "Jack Sparrow"
},
include: Ship
});
// Now the ship comes with it
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);
console.log('Ship Name:', awesomeCaptain.ship.name);
console.log('Amount of Sails:', awesomeCaptain.ship.amountOfSails);

Easy way to only allow one item per user in mongoose schema array

I'm trying to implement a rating system and I'm struggling to only allow one rating per user in a reasonable way.
Simply put, i have an array of ratings in my schema, containing the "rater" and the rating, as such:
var schema = new Schema({
//...
ratings: [{
by: {
type: Schema.Types.ObjectId
},
rating: {
type: Number,
min: 1,
max: 5,
validate: ratingValidator
}
}],
//...
});
var Model = mongoose.model('Model', schema);
When i get a request, i wish to add the users rating to the array if the user has not already voted this document, otherwise i wish to update the rating (you should not be able to give more than one rating)
One way to do this is to find the document, "loop through" the array of ratings and search for the user. If the user has got already a rating in the array, the rating is changed, otherwise a new rating is pushed. As such:
Model.findById(id)
.select('ratings')
.exec(function(err, doc) {
if(err) return next(err);
if(doc) {
var rated = false;
var ratings = doc.ratings;
for(var i = 0; i < ratings.length; i++) {
if(ratings[i].by === user.id) {
ratings[i].rating = rating;
rated = true;
break;
}
}
if(!rated) {
ratings.push({
by: user.id,
rating: rating
});
}
doc.markModified('ratings');
doc.save();
} else {
//Not found
}
});
Is there an easier way? A way to let mongodb do this automatically?
The mongodb $addToSet operator could be an alternative, however i have not managed to use it for this, since that could allow two ratings with different scores from the same user.
As you note the $addToSet operator will not work in this case as indeed a userId with a different vote value would be a different value and it's own unique member of the set.
So the best way to do this is to actually issue two update statements with complementary logic. Only one will actually be applied depending on the state of the document:
async.series(
[
// Try to update a matching element
function(callback) {
Model.update(
{ "_id": id, "ratings.by": user.id },
{ "$set": { "ratings.$.rating": rating } },
callback
);
},
// Add the element where it does not exist
function(callback) {
Model.update(
{ "_id": id, "ratings.by": { "$ne": user.id } },
{ "$push": { "ratings": { "by": user.id, "rating": rating } }},
callback
);
}
],
function(err,result) {
// all done
}
);
The principle is simple, try to match the userId present in the ratings array for the document and update the entry. If that condition is not met then no document is updated. In the same way, try to match the document where there is no userId present in the ratings array, if there is a match then add the element, otherwise there will be no update.
This does bypass the built in schema validation of mongoose, so you would have to apply your constraints manually ( or inspect the schema validation rules and apply manually ) but it is better than you current approach in one very important aspect.
When you .find() the document and call it back to your client application to modify using code as you are, then there is no guarantee that the document has not changed on the server from another process or request. So when you issue .save() the document on the server may no longer be in the state that it was when it was read and any modifications can overwrite the changes made there.
Hence while there are two operations to the server and not one ( and your current code is two operations anyway ), it is the lesser of two evils to manually validate than to possibly cause a data inconsistency. The two update approach will respect any other updates issued to the document possibly occurring at the same time.

Is there a simple way to make Sequelize return it's date/time fields in a particular format?

We need to have sequelize return dates in a particular format, not the default one. As far as I can tell, there is no way to set that up in options, or any other way. Short of manually updating the dates every time after they are retrieved, anyone been able to solve this easily? Or am I missing something?
You can, use the Sequelize fn method. From the API Reference, the fn function will help create an object representing a SQL function in your query.
For example:
model.findAll({
attributes: [
'id',
[sequelize.fn('date_format', sequelize.col('date_col'), '%Y-%m-%d'), 'date_col_formed']
]})
.then(function(result) {
console.log(result);
});
Will return data values:
[
{"id": 1, "date_col_formed": "2014-01-01"},
{"id": 2, "date_col_formed": "2014-01-02"}
// and so on...
]
21/06/2019
A little bit late but providing an update.
Sequelize is a powerful ORM (I am not saying is the best solution out there) but has a very bad documentation.
Anyway if you want to have this configured in your models one way apart from having to repeat this across your queries as other responses state you could be doing:
const Test = sequelize.define('test', {
// attributes
name: {
type: DataType.STRING,
allowNull: false
},
createdAt: {
type: DataType.DATE,
//note here this is the guy that you are looking for
get() {
return moment(this.getDataValue('createdAt')).format('DD/MM/YYYY h:mm:ss');
}
},
updatedAt: {
type: DataType.DATE,
get() {
return moment(this.getDataValue('updatedAt')).format('DD/MM/YYYY h:mm:ss');
}
}
If you are using dates to provide info as: last updated, first login, last login. This is the way to go.
The function get returns the new formatted element so it should not be restricted to dates either! Just bear in mind that this will slow your query so use cautiously. ;)
more info (i know i know but docs is the name of the game): https://sequelize.org/docs/v6/core-concepts/getters-setters-virtuals/
you can define custom instance methods getDate/setDate which would translate date between sequelize internal representation and desired format like so https://sequelize.org/master/manual/model-basics.html#taking-advantage-of-models-being-classes
If you are wondering to cast it as VARCHAR :
attributes:{include:[[sequelize.cast(sequelize.col('dob'), 'VARCHAR') , 'dob']],exclude:['dob']}
In case of Model that we create using Sequelize CLI
try something like this
var sequelize= require('../models');
model.findAll({
attributes: [
'id',
'title'
[sequelize.Sequelize.fn('date_format', sequelize.Sequelize.col('col_name'), '%d %b %y'), 'col_name']
]}.then(function(result))
{ // dateformate=04 Nov 2017
console.log(result)
}
visit this link for formate
To format Date, you can use Sequelize.fn method. I tried all mentioned methods but it won't work with date_formate. Try to create with bellow one.
model.findAll({
attributes: [
'id',
[sequelize.fn('FORMAT', sequelize.col('col_name'), 'yyyy-mm-dd'), 'col_name']
]})
.then(function(result) {
console.log(result);
});
here, i used 'FORMAT' instead of 'DATE_FORMAT', because it through error :
'date_format' is not a recognized built-in function name in api call.
For me, date_format and Format functions in above answers do not work.
I solved as below:
attributes: [
[sequelize.literal('date("dateTime")'), 'dateWithoutTime'],
],

Resources