How to update a record inside model's method like 'node orm-2' In 'Sequelize'
In orm-2, Just use this.save()
var users = db.define('users',
{
id : { type: 'serial', key: true },
username : { type: 'text', size: 25, unique: true, required: true },
balance : { type: 'integer', defaultValue: 0 },
}, {
timestamp: true,
methods: {
addBalance: function (howMany) {
howMany = Math.abs(howMany);
this.balance += howMany;
this.save(function (err) {
console.log("added money to "+this.username+" : "+howMany);
});
}
}
});
But in Sequelize, I don't know yet
var wallet = sequelize.define('wallet', {
balance : { type: Sequelize.INTEGER, defaultValue: 0, validate: { min: 0 } }
}, {
timestamps: true,
classMethods: {
addBalance: function (howMany) {
howMany = Math.abs(howMany);
this.balance += howMany;
//UPDATE Or SAVE HERE...
}
}
});
Is it have simple command or prefer another methods?
You should place the addBalance method inside instanceMethods, not in classMethods, because you want to operate on a single instance of specified model
instanceMethods: {
addBalance: function(howMany) {
howMany = Math.abs(howMany);
return this.set('balance', this.get('balance') + howMany).save();
}
}
This method would return Promise resolving to current instance of model.
EDIT
Even better solution would be to use instance.increment method
addBalance: function(howMany) {
howMany = Math.abs(howMany);
return this.increment('balance', { by: howMany });
}
It would return the same as the option above.
Related
I have CommenentSchema:
const CommentSchema: Schema = new Schema(
{
article: {
type: Schema.Types.ObjectId,
ref: "Article",
},
comment: {
type: String,
required: [true, "Cannot post an empty comment"],
},
commentBy: {
type: Schema.Types.ObjectId,
ref: "User",
},
},
{
toJSON: {
virtuals: true,
transform: (_doc, ret) => {
delete ret.id;
delete ret.__v;
},
},
toObject: { virtuals: true },
timestamps: true,
}
I'm trying to get the totalComments of an article (in a separate ArticleSchema) which I was able to achieve with aggregate and pre save middleware like so:
// Comment Count
// 1 - Calc. number of comments of an article when a comment is submitted
CommentSchema.statics.calcTotalComments = async function (articleId) {
const stats = await this.aggregate([
{
$match: { article: articleId },
},
{
$group: {
_id: "article",
nComment: { $sum: 1 },
},
},
]);
if (stats.length > 0) {
// update article
await Article.findByIdAndUpdate(articleId, {
totalComments: stats[0].nComment,
});
} else {
// set to default
await Article.findByIdAndUpdate(articleId, {
totalComments: 0,
});
}
};
CommentSchema.post("save", function () {
// points to current comment
this.constructor.calcTotalComments(this.article);
});
I also want the number of totalComments to change when a comment is deleted but it doesn't fire at pre /^findOneAnd/
// 2 - Calc. number of comments of an article when a comment is updated/deleted
CommentSchema.pre<Query<ICommentDocument, ICommentDocument>>(
/^findOneAndUpdate/,
async function (next: Function) {
// points to query of coment
// #ts-ignore: igore property doc does not exist on type Query...
this.doc = await this.model.findOne(this.getFilter());
// #ts-ignore: igore property doc does not exist on type Query...
console.log(this.doc);
next();
}
);
CommentSchema.post<Query<ICommentDocument, ICommentDocument>>(
/^findOneAnd/,
async function () {
// #ts-ignore: igore property doc does not exist on type Query...
await this.doc.constructor.calcTotalComments(this.doc.article);
}
);
I have tried all previous solution but none did work. Even the pre hook doesn't fire a simple console.log('hello');
What am I doing wrong?
General issue:
I've been having a hard time trying to build a simple CRUD app using JavaScript + node JS + express + sequelize (MySQL).
Contextualization about the project
My CRUD app is being developed to manage students from a particular English teacher.
There are some table models created: Alunos - English: Students, Boletos - English: Pay Order, Aulas - English: Classes, and others that are not mandatory to explain for this issue for now.
Specific issue:
There is a post route that takes some of my body contents and inserts a new student to table "Alunos". After the row containing student data is created, I need to create registries for "Boletos", it would be 12 month of Pay Orders registered into this table. There are two problems with this part: the first I register a student, it works fine, but I could not get the auto-increment id generated by the model to insert in the foreign key "AlunoId", so the foreign key for "Boletos" table is set to null. The other problem is that the both entries (1 into "Alunos" and 12 into "Boletos") works fine at first, to registry the first student, but after the page is refreshed and I try to registry another student, node JS throws an error:
(node:5720) UnhandledPromiseRejectionWarning: SequelizeUniqueConstraintError: Validation error
at Query.formatError (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\mysql\query.js:242:16)
at Query.run (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\mysql\query.js:77:18)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at async D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\sequelize.js:619:16
at async MySQLQueryInterface.insert (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\dialects\abstract\query-interface.js:749:21)
at async model.save (D:\ProgramacaoEstudos\ProjetoCRUD2\node_modules\sequelize\lib\model.js:3954:35)
at async D:\ProgramacaoEstudos\ProjetoCRUD2\routes\admin.js:101:30
(node:5720) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
Code:
Models - Alunos:
// Create Aluno table model and export to be called to other file
module.exports = (sequelize, DataTypes) => {
const Aluno = sequelize.define('Aluno', {
name: {
type: DataTypes.STRING,
allowNull: false,
},
surname: {
type: DataTypes.STRING,
allowNull: false,
},
birth: {
type: DataTypes.DATEONLY,
allowNull: false,
},
phone_number: {
type: DataTypes.STRING,
allowNull: true,
unique: true,
},
mobile_number: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
residential_address: {
type: DataTypes.STRING,
allowNull: false,
},
profession: {
type: DataTypes.STRING,
allowNull: false,
},
company: {
type: DataTypes.STRING,
allowNull: false,
},
num_classes_week: {
type: DataTypes.INTEGER,
allowNull: false,
},
classes_day: {
type: DataTypes.STRING,
allowNull: false,
},
classes_time: {
type: DataTypes.TIME,
allowNull: false,
},
starts_at: {
type: DataTypes.DATEONLY,
allowNull: false,
},
value: {
type: DataTypes.FLOAT,
allowNull: false,
},
discount: {
type: DataTypes.FLOAT,
defaultValue: 0,
},
due_date: {
type: DataTypes.DATEONLY,
allowNull: false,
},
});
Aluno.associate = (models) => {
Aluno.hasMany(models.Aula, {
onDelete: 'CASCADE',
});
};
Aluno.associate = (models) => {
Aluno.hasMany(models.Boleto, {
onDelete: 'NO ACTION',
});
};
Aluno.associate = (models) => {
Aluno.hasMany(models.Assiste_Aula, {
onDelete: 'NO ACTION',
});
};
return Aluno;
};
Models - Boletos:
module.exports = (sequelize, DataTypes) => {
const Boleto = sequelize.define('Boleto', {
value: {
type: DataTypes.FLOAT,
allowNull: false
},
due_date: {
type: DataTypes.DATEONLY,
allowNull: false
},
status: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false
}
})
Boleto.associate = (models) => {
Boleto.belongsTo(models.Aluno, {
foreignKey: 'AlunoId'
})
}
return Boleto
}
Routes - Post - registry:
// Post Routes
router.post('/realizado', async (req, res) => {
// Create Aluno
// Compute values of some attributes
var cls_day = req.body.classes_day;
var num_cls = cls_day.length;
var dias;
for (let i = 0; i < num_cls; i++) {
if (i + 1 < num_cls) {
dias += cls_day[i] + '-';
} else {
dias += cls_day[i];
}
}
// Instantiate Aluno model
const aluno = db.Aluno.build({
name: req.body.name,
surname: req.body.surname,
birth: req.body.birth,
phone_number: req.body.phone_number,
mobile_number: req.body.mobile_number,
email: req.body.email,
residential_address: req.body.residential_address,
profession: req.body.profession,
company: req.body.company,
num_classes_week: num_cls,
classes_day: dias,
classes_time: req.body.classes_time,
starts_at: req.body.starts_at,
value: req.body.value,
discount: req.body.discount,
due_date: req.body.due_date,
});
// Insert into database
const newAluno = await aluno.save();
// Create boleto
var objList = [];
for (var i = 0; i < 12; i++) {
var firstDt = new Date(req.body.due_date);
// Compute current date
var dt = firstDt.setMonth(firstDt.getMonth() + i);
//dt.setDate(dt.getMonth() + i)
// Build boleto object
var boleto;
boleto = db.Boleto.build({
value: req.body.value,
due_date: dt,
alunoId: newAluno.id,
});
objList.push(boleto);
}
for (var i = 0; i < objList.length; i++) {
try {
await objList[i].save();
} catch (err) {
console.log(err);
}
}
res.render('realizado');
});
Final Considerations:
Since I'm new with node JS and JavaScript I didn't know about the Promises and the syntactic sugar await/async. I've already studied with a bunch of videos and I think I got the basics concepts about it, but I'm not being able to apply it to the project.
You need to either use db.Aluno.create() or set db.Aluno.build({...}, { isNewRecord: true }) to let Sequelize know it's an insert and not a record with a primary key value of 0. Your DB likely sees the ID of 0 and either inserts it or sets it to 1, either way you will get a conflict on the second insert.
It's also a good idea to wrap your router/controller code in a try/catch to handle any errors. Use a transaction that is passed to all your queries/inserts so that you can roll them all back if there is an error at any stage - const transaction = await sequelize.transaction(); const aluno = await db.Aluno.create({...}, { transaction });. Commit at the end with await transaction.commit() or in the catch block roll back with await transaction.rollback().
Don't use await in a for loop - its the same as blocking for each call which is inefficient and slow. Instead you can pass an array of promises to Promise.all() and resolve them concurrently.... objList.push(boleto.save()); (don't await here) and then await Promise.all(objList);.
Some last notes - it's good to use const when a variable won't change and let when it might to make your results more consistent. You should also try to have an explicit return from arrow functions.
Here is your code with the changes applied.
// Post Routes
router.post('/realizado', async (req, res) => {
// we may or may not be able to create a transaction, so use let
let transaction;
try {
// start a new transaction an pass to all the create() calls
transaction = await sequelize.transaction();
// Compute values of some attributes
var cls_day = req.body.classes_day;
var num_cls = cls_day.length;
var dias;
for (let i = 0; i < num_cls; i++) {
if (i + 1 < num_cls) {
dias += cls_day[i] + '-';
} else {
dias += cls_day[i];
}
}
// Create Aluno model, use const since it won't change
const aluno = await db.Aluno.create(
{
name: req.body.name,
surname: req.body.surname,
birth: req.body.birth,
phone_number: req.body.phone_number,
mobile_number: req.body.mobile_number,
email: req.body.email,
residential_address: req.body.residential_address,
profession: req.body.profession,
company: req.body.company,
num_classes_week: num_cls,
classes_day: dias,
classes_time: req.body.classes_time,
starts_at: req.body.starts_at,
value: req.body.value,
discount: req.body.discount,
due_date: req.body.due_date,
},
{
// use the transaction
transaction,
}
);
// Create boleto insert promise array
// Use const because we will be adding items, but into the same const array
const promises = [];
for (let i = 0; i < 12; i++) {
const firstDt = new Date(req.body.due_date);
// put the promise into the promises array
promises.push(
// the create call here will start the insert but not wait for it to complete
db.Boleto.create(
{
value: req.body.value,
due_date: firstDt.setMonth(firstDt.getMonth() + i),
alunoId: aluno.id,
},
{
// use the transaction so we can rollback if there are errors
transaction,
}
)
);
}
// await the result of all the boleto inserts
await Promise.all(promises);
// no errors, we can commit the transaction
await transaction.commit();
return res.render('realizado');
} catch (err) {
console.log(err);
if (transaction) {
await transaction.rollback;
}
return res.status(500).send(err.message);
}
});
I have a mongoose schema for stories that looks like this:
{
id: {
type: Number,
default: 0
},
title: {
type: String,
maxLength: 60
},
author: {
userid: {
type: Number
},
username: {
type: String
}
}
chapters: [chapter],
numchapters: {
type: Number,
default: 1
},
favs: {
type: Number,
default: 0
},
completed: {
type: Boolean,
default: false
}
}
What I'm trying to do is reference a document in a separate collection (users), and use the values of its userid and username fields in the author field.
how do I do this?
current code:
storyobj.populate('author', {path: 'author', model: 'users', select: 'userid username'}, (err) => {
if (err) {
console.log(err)
}
})
just in case it's relevant, the structure of the users collection looks like this:
{
username: {
type: String,
},
email: {
type: String,
},
password: {
type: String,
},
userid: {
type: Number
},
isAdmin: {
type: Boolean,
default: false
},
banned: {
type: Boolean,
default: false
}
}
EDIT:
I've changed the author field in the Stories model to look like this:
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
This is so I tell Mongoose, "Hey, I want this field to reference a user in the User collection".
Here are some more details that I hope will be of help.
Full code:
var storydb = require('../models/stories/story');
var chapterdb = require('../models/stories/chapter');
var userdb = require('../models/user');
const file = JSON.parse(fs.readFileSync('test.json')); // this is a file with the data for the stories I am trying to insert into my database
for (const d in file) {
var storyobj = new storydb({
id: d,
chapters: []
});
for (let e = 0; e < file[d].length; e++) {
var abc = file[d][e];
var updatey = {
chaptertitle: abc.chapter,
chapterid: parseInt(abc.recid),
words: abc.wordcount,
notes: abc.notes,
genre: abc.gid.split(', '),
summary: abc.summary,
hidden: undefined,
loggedinOnly: undefined,
posted: new Date(Date.parse(abc.date)),
bands: abc.bandnames.split(', ')
};
var kbv = getKeyByValue(userlookup, abc.uid);
storyobj.title = abc.title;
storyobj.numchapters = file[d].length;
storyobj.favs = file[d][0].numfavs;
updatey.characters = abc.charid.split(/, |,/g);
storyobj.chapters.push(updatey)
}
storyobj.save();
}
In file, there's a unique ID representing the author of each story. kbv returns the userid associated with that unique ID (note that they're NOT the same).
Now, here's where I'm having trouble:
What I want to do is find a user matching the userid in kbv, and make that the author property in the story model.
The code I'm currently using to try and achieve that:
storydb.findOne({storyobj}, 'author').populate("author", (f) => console.log(f));
const Stories = require("./path/to/model");
Stories
.find({ /* query */ }, { /* projection */ })
.populate("author.username", ["userid", "username"])
.then(/* handle resolve */)
.catch(/* handle rejection */)
For this to work, you have to add a ref key to the userid key in your model, where the ref value is the name of the model it's referencing.
Story.model.js
const StorySchema = new Schema({
author: {
userid: { type: Schema.Types.ObjectId, ref: "users", required: true },
/* other props */
}
/* other props */
});
I am developing app using Nodejs and Mongodb and mongoose. user and Subscriptions are 2 mongoose schemas. I want to get each members expire date from subscriptions collection and include it with each of members object array. But it is not working.
var UserSchema = new Schema({
title: {
type: String
},
firstName: {
type: String
},
lastName: {
type: String
},
displayName: {
type: String
},
});
var SubscriptionSchema = new Schema({
member_id: {
type: Schema.ObjectId,
ref: 'User'
},
renewal_date: {
type: Date
},
expire_date: {
type: Date
},
amount: {
type: String
},
paid_mode: {
type: String
},
});
exports.memberlist = function(req, res) {
var expire='';
user.find({}).lean().exec(function(err, collection) {
var i;
for(i=0;i<collection.length; i++)
{
Subscriptions.find({'member_id':collection[i]._id}).lean().exec(function(err, subs){
if(subs.length > 0)
{
expire = subs[0].expire_date || '';
collection[i].expire_date = 'expire';
}
});
}
res.send(collection);
});
};
It's control flow issue. You should use something like this
var async = require('async');
// ...
exports.memberlist = function(req, res) {
var expire='';
user.find({}).lean().exec(function(err, collection) {
async.eachSeries(collection, function(item, cb){
Subscriptions.find({'member_id':item._id}).lean().exec(function(err, subs){
if(subs.length > 0)
{
expire = subs[0].expire_date || '';
collection[i].expire_date = 'expire';
cb()
}
});
}, function(){
res.send(collection);
});
});
};
Read here about node control flow, and here about async module.
First I'm doing this in NoSQL & node.js. I assume that shouldn't effect the question but it will help understand my example.
I have a model for a page and a tag that look something like this:
var Page = app.ormDb.define('pages', {
uuid : { type: String, index: true, length: 40, default: function () { return uuid.v4(); } },
title : { type: String, index: true, length: 255 },
url : { type: String, index: true, length: 255 },
summary_mu : { type: String, length: 1024 },
summary_html : { type: String },
summary_hist : { type: JSON, default: function() { rerutn { items : [] }; } },
sections : { type: JSON, default: function() { rerutn { items : [] }; } },
tags : { type: JSON, default: function() { rerutn { items : [] }; } },
date : { type: Date, default: function() { return new Date(); } },
updated : { type: Date, default: function() { return new Date(); } }
});
var Tag = app.ormDb.define('tags', {
uuid : { type: String, index: true, length: 40, default: function () { return uuid.v4(); } },
name : { type: String, index: true, length: 255 },
url : { type: String, index: true, length: 255 },
desc : { type: String, length: 1024 },
date : { type: Date, default: function() { return new Date(); } },
updated : { type: Date, default: function() { return new Date(); } }
});
So now I have some data maintenance issues, for example when I add a tag to a page I need to make sure there is a tag entry. To this end I've created a method on the model.
// Add a tag to a page
// tag .. The tag to add
Page.prototype.addTag(tag, done) {
var _this = this;
if (_this.tags == null) {
_this.tags = { items:[] };
}
var index = _this.tags.items.indexOf(tag);
if (index == -1) {
_this.tags.items.push(tag);
}
async.waterfall([
function (cb) {
app.models.tag.count({'name': tag}, cb);
},
function (count, cb) {
if (count == 0) {
app.models.tag.create({
name : tag,
}, function (err, newTag) {
return cb(err, tag);
});
} else {
return cb(null, tag);
}
}
], function (err, items) {
done(err, items);
});
}
In the controller I have code that verifies the user input, loads the current page, calls the Page method above to add the tag, and finally saving the updated page. Note, in the method above I'm checking the Tag collection for the existence of the tag, and creating it if needed.
Is this correct, or should the Page method's logic be moved to the controller and the model only deal with the one model and not others?
My reasoning is that I would never add a tag to a page without checking/creating the tag in the Tag collection.