Sequelize: Force update for a JSON array - node.js

Sequelize won't update a JSON field under some circumstances.
For example, I have:
[[1]] (an array inside array)
And I'm trying to push something:
instance.arr[0].push(1); // [[1,1]]
instance.save();
// or:
instance.update({arr: instance.arr});
Now inside the instance I have changed the array and nothing changed inside the database. Not even a query is sent. :(
From the Sequelize website:
https://sequelize.org/master/manual/model-instances.html
The save method is optimized internally to only update fields that really
changed. This means that if you don't change anything and call save,
Sequelize will know that the save is superfluous and do nothing, i.e.,
no query will be generated (it will still return a Promise, but it
will resolve immediately).
That's good, but it seems like it doesn't work for JSON. Can I do a force update?
As of today, I have to do a deep copy of the array to save it.
I'm using MariaDB. I don't know if that matters.

It seems you have to specify that the field has changed
instance.changed( 'arr', true);
instance.save

Related

Mongoose pre hook middleware with typescript, how to set up types to access the query objects parameters?

I am using Mongoose 5+ and currently do not have the option of upgrading to Mongoose 6 (which seems to have fixed several issues concerning types and stuff)
I am refactoring from js to ts, and I keep hitting a wall when dealing with pre hooks. In this particular case, I want to understand how to pass generic types to the pre hook and not have typescript get mad that i am trying to access certain fields of this
So my prehook looks like this. It is using findOneAndUpdate and in this case the this is bound to the Query, which gives me some particular properties to access, such as this._update and this._conditions. I use this._update to access the information I am trying to update in this document, and I use that to modify another document in another collection before committing to the change in this document. I use this so the operation will be atomic and no changes will be committed to the DB if any of the other writes fails. However, typescript does not like me accessing values from this and outlined below are the errors i get
unitsSchema.pre('findOneAndUpdate', async function(next){
const update = this._update; //TSError: Property '_update' does not exist on type 'Query<any, any>'
const conditions = this._conditions; //Property '_conditions' does not exist on type 'Query<any, any>'
if(update.isDeleted === true){
//remove the unit from the condo model
await Condos.updateOne({_id:conditions.condoID},
{$pull:{
units:conditions._id
}}).catch(e=>next(e));
await UnitSttmt.updateMany({unitID:conditions._id},
{isDeleted:true})
.catch(e=>next(e));
}
//I even get an error here for some reason, i dont understand why here next is expecting a required argument, but not on other similar hooks
next(); // Expected 1 arguments, but got 0
}
I have tried passing it my document interface which extends mongoose.Document type and some other types too, but to no avail. Does anyone have any insight on how to get typescript to recognize the available Query paramters that exist?
Some examples I have tried
unitsSchema.pre<Query<any, UnitsDocument>>(...)
// this one obviously works but kind of defeats the purpose, but at least it gets rid of my error
unitsSchema.pre<any>(...)
also want ot mention the code works fine as javascript, it must be an error or limitation in the type declarations.. or maybe I'm just not supposed to be accessing those fields from the Query this ?

How to get an instance of the updated sequelize model from an afterUpsert hook?

Other sequelize hooks, like afterUpdate(instance, options) provide an instance of the modified model as a parameter. However, afterUpsert(created, options) provides a created boolean which indicates if the operation was a insert or update.
Is there a way to configure sequelize, or manipulate the afterUpsert parameters, to obtain an instance of the upserted model?
As far as I know you can not. As the result of upsert call to db via queryInterface is what is returned to the afterUpsert hook, which is boolean value whether there has been any newly created records or not in the upsert process.
While, you can use beforeUpsert with the values passed to upsert method. Not sure if that might be helpful ( may be to clear a cache )
I have faced the same issue recently, and I was able to manage it this way:
beforeUpsert: (values, options)=>{
/* afterUpsert will always get an array of type [Model, created] as first argument */
options.returning = true;
},
I decided to use the beforeUpsert hook to make sure that any call to Model.upsert is forced to return an instance and not only a created boolean. But, you can also call
upsert(values,{returning: true});
HTH

Mongoose compound index creation fields order

I have a question regarding compound index creation in mongodb:
Say I want to create this compound index:
cSchema.index({account:1, authorization: 1, c_type: 1});
Problem is, javascript doesn't guarantee dictionary order, so I won't be sure the compound index is in the order I want.
How can I make sure it's indeed {account:1, authorization:1, c_type:1} in that order ?
Thanks !
The simple answer is that most abuse the behaviour that simple String properties that don't parse to an integer on an Object will enumerate in creation order. Although not guaranteed in ES2015 for some enumeration methods, it does work in a confined environment like Node/V8. This has worked in most JS engines for a while now but had never been part of the ES spec before ES2015.
MongoDB
The underlying MongoDB driver createIndex function supports String, Array and Object index definitions. The parsing code is in a util function called parseIndexOptions.
If you specify an array of Strings, Array pairs, or Objects then the order will be fixed:
['location','type']
[['location', '2d'],['type', 1]]
[{location: '2d'},{type: 1}]
Also note that the createIndex code does use Object.keys to enumerate when it gets an Object of index properties.
Mongoose
The Schema#index function is documented as requiring an Object to pass to "MongoDB driver's createIndex() function" so looks to support passing through whatever options you provide.
There are a couple of places where indexes are further processed though. For example when you create a sub document with an index, the index needs to have the parent schema prefixed onto field names. On a quick glance I think this code still works with an Array but I can't see any tests for that in the Mongoose code so you might want to confirm it yourself.
Mongoose does have a compound index test that relies on Object property order.

Remove property from mongoose document instance

I need to remove a property from a mongoose document instance. I've found a lot of questions that show how to remove it from the database, but that's not what I'm looking for.
I need to pull the document down including a field to check security access, I then want to strip that field so that it doesn't get disclosed if downstream code decides to call toObject() and send the object back to the client.
Any thoughts?
I needed to remove password property from the document instance but I didn't find anything in the API documentation. Here is what I did:
doc.set('password', null); // doc.password is null
Then I found you can also do this:
delete doc._doc.password // doc.password is undefined
Since version 2.4 you can do:
doc.field = undefined;
await doc.save();
This will essentially $unset the field
Using the set function with a value of null will simply assign the value, not remove it. Best to first convert the document using toObject() (so that it is becomes a plain Object), make the changes and revive it back to a model document:
let tempDoc = doc.toObject();
delete tempDoc.password;
doc = new this(tempDoc);

Sequelize.js - how to properly use get methods from associations (no sql query on each call)?

I'm using Sequelize.js for ORM and have a few associations (which actually doesn't matter now). My models get get and set methods from those associations. Like this (from docs):
var User = sequelize.define('User', {/* ... */})
var Project = sequelize.define('Project', {/* ... */})
// One-way associations
Project.hasOne(User)
/*
...
Furthermore, Project.prototype will gain the methods getUser and setUser
according to the first parameter passed to define.
*/
So now, I have Project.getUser(), which returns a Promise. But if I call this twice on the very same object, I get SQL query executed twice.
My question is - am I missing something out, or this is an expected behavior? I actually don't want to make additional queries each time I call the same method on this object.
If this is expected - should I use custom getters with member variables which I manually populate and return if present? Or there is something more clever? :)
Update
As from DeBuGGeR's answer - I understand I can use includes when making a query in order to eager load everything, but I simply don't need it, and I can't do it all the time. It's waste of resources and a big overhead if I load my entire DB at the beginning, just to understand (by some criteria) that I won't need it. I want to make additional queries depending on situation. But I also can't afford to destroy all models (DAO objects) that I have and create new ones, with all the info inside them. I should be able to update parts of them, which are missing (from relations).
If you use getUser() it will make the query call, it dosent give you access to the user. You can manually save it to project.user or project.users depending on the association.
But you can try Eager Loading
Project.find({
include: [
{ model: User, as: 'user' } // here you HAVE to specify the same alias as you did in your association
]
}).success(function(project){
project.user // contains the user
});
Also e.g of getUser(). Dont expect it to automatically cache user and dont override this cleverly as it will create side effects. getUser is expected to get from database and it should!
Project.getUser().then(function(user){
// user is available and is a sequelize object
project.user = user; // save project.user and use it till u want to
})
The first part of things is clear - every call to get[Association] (for example Project.getUser()) WILL result in database query.
Sequelize does not maintain any kind of state nor cache for the results. You can get user in the Promisified result of the call, but if you want it again - you will have to make another query.
What #DeBuGGeR said - about using accessors is also not true - accessors are present only immediately after a query, and are not preserved.
As sometimes this is not ok, you have to implement some kind of caching system by yourself. Here comes the tricky part:
IF you want to use the same get method Project.getUser(), you won't be able to do it, as Sequelize overrides your instanceMethods. For example, if you have the association mentioned above, this won't work:
instanceMethods: {
getUser: function() {
// check if you have it, otherwise make a query
}
}
There are few possible ways to fix it - either change Sequelize core a little (to first check if the method exists), or use some kind of wrapper to those functions.
More details about this can be found here: https://github.com/sequelize/sequelize/issues/3707
Thanks to mickhansen for the cooperation on how to understand what to do :)

Resources