A Sequelize column that cannot be updated - mysql

Is it possible to create a column on a MySQL table using Sequelize that can be initialized when creating a new row, but never updated?
For example, a REST service allows a user to update his profile. He can change any field except his id. I can strip the id from the request on the API route, but that's a little redundant because there are a number of different models that behave similarly. Ideally, I'd like to be able to define a constraint in Sequelize that prevents the id column from being set to anything other than DEFAULT.
Currently, I'm using a setterMethod for the id to manually throw a ValidationError, but this seems hackish, so I was wondering if there's a cleaner way of doing this. Even worse is that this implementation still allows the id to be set when creating a new record, but I don't know a way around this as when Sequelize generates the query it calls setterMethods.id to set the value to DEFAULT.
return sequelize.define('Foo',
{
title: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
notEmpty: true
}
}
},
{
setterMethods: {
id: function (value) {
if (!this.isNewRecord) {
throw new sequelize.ValidationError(null, [
new sequelize.ValidationErrorItem('readonly', 'id may not be set', 'id', value)
]);
}
}
}
}
);

Look at this Sequelize plugin:
https://www.npmjs.com/package/sequelize-noupdate-attributes
It adds support for no update and read-only attributes in Sequelize models.
In your specific case, you could configure the attribute with the following flags:
{
title: {
type: DataTypes.STRING,
allowNull: false,
unique : true,
noUpdate : true
}
}
That will allow the initial set of the title attribute if is null, and then prevent any further modifications once is already set.
Disclaimer: I'm the plugin author.

Related

Sequelize - Enable paranoid: true on query level while its false on the defined Model

So lets say I have a Sequelize model defined with paranoid defaulting to "false":
const Country = sequelize.define('Country', {
name: {
type: DataTypes.STRING,
defaultValue: '',
},
code: {
type: DataTypes.STRING,
defaultValue: '',
},
currency: {
type: DataTypes.STRING,
defaultValue: '',
},
languages: {
type: DataTypes.STRING,
defaultValue: '',
},
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
deletedAt: DataTypes.DATE
});
Now when I invoke Model.destroy() on any records of Country table, the records would be hard deleted. Enabling paranoid: true on the Model definition would result in soft deletes.
I wanted to achieve the opposite of this. Where, the paranoid flag on model definition is set to false and we need to explicitly define a flag on the Model.destroy() method to soft-delete an entry and by default all records would be hard deleted.
I tried to sift through the documentation in order to find something but couldn't. Would appreciate any help I can get in case I missed something or if there's a workaround.
Why I need to do this? Some background
I joined a project with about 100+ defined models (even more) on which the paranoid flag is not defined and is false by default. Thankfully, the createdAt, updatedAt and deletedAt timestamps are defined explicitly. But any call to the Model.destroy() function results in a hard delete.
I need to introduce the functionality of a soft delete without changing any model definitions (because that would result in unintended consequences). Again, thankfully, the Model.destroy() method is wrapped in a function which is used in the entire codebase.
I was thinking of introducing an optional flag on this wrapper function which would indicate whether the delete needs to be soft or hard. So the default functionality would be hard delete unless explicitly specified to be a soft delete.
Worst case solution I can think of is that in case soft delete is required, then replace the destroy method with a raw query where I update the deletedAt timestamp manually. But hoping to find cleaner solutions than this :)
The simplest solution would be to use force: false option in case of soft-delete and force: true in case of hard-delete:
async function wrappedDestroy(item, isSoftDelete) {
await item.destroy({ force: !isSoftDelete })
}
Of course, you need to turn on paranoid: true in the model because it also affects all findAll/findOne queries as well (I suppose you wish to hide all soft-deleted records from findAll/findOne by default).

SailsJS update and mySQL custom ID column not working

In SailsJS, I created a model Profiles including a custom primary key as follows:
module.exports = {
tableName: 'tbl_profiles',
autoPK: false,
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
user_id: {
type: 'integer',
size: 11,
columnName: 'user_id',
unique: true,
primaryKey: true,
},
...
Now, when calling the blueprint route to update a user profile, I get the following error:
ER_BAD_FIELD_ERROR: Unknown column 'tbl_profiles.id' in 'where clause'
Debugging this down (and seeing question SailsJS and mySQL custom ID name not working with blue prints not helping) I found out that the update is carried through all right in the db and that the record is changed but in the controller callback function an error and status 400 is raised nevertheless:
Profiles.update({user_id: req.param('id')}, req.body).exec(function(err, profile) {
if (err) {
return res.status(400).json(err);
} else {
return res.status(200).json(profile);
}
});
Tracing down the SQL involved in /node_modules/sails-mysql/node_modules/mysql/lib/protocol/sequences/Sequence.js:48:14, it seems the following statement is executed just after the update is finished (note the final WHERE clause):
SELECT `tbl_profiles`.`user_id`,
`tbl_profiles`.`lastName`,
`tbl_profiles`.`firstName`,
`tbl_profiles`.`date_of_birth`,
`tbl_profiles`.`address_line1`,
`tbl_profiles`.`address_line2`,
`tbl_profiles`.`zip_code`,
`tbl_profiles`.`city`,
`tbl_profiles`.`gender`,
`tbl_profiles`.`country_id`,
`tbl_profiles`.`phone`,
`tbl_profiles`.`user_id`
FROM `tbl_profiles` AS `tbl_profiles`
WHERE `tbl_profiles`.`id` = undefined
Where could I set SailsJS/Waterline to use the custom column ID? Setting autoPK true either in the beginning or the end of the model wouldn't do the trick..

How to get create() return auto-increment primary key after creation in sails.js?

I have a model, Case.js:
...
attributes: {
id: {
type: 'integer',
unique: true,
primaryKey: true,
columnName: 'pid' //an auto-increment primary key, generated by MySQL
},
...
}
And I want to get this id after creation:
Case.create({...}).then(function(aCase){
console.log(aCase.id);
})
The creation succeeded, but the output I got is undefined.
I tried setting autoPK to false, and deleting "unique" and "primaryKey" entry, but the result didn't change.
Please tell me how to make create() return this id.
I've worked it out myself. The problem lies in my model Case.js.
In sails.js, if you want a primary key (usually id) created by MySQL with auto-increment to be returned after create(), your model should look like this:
module.exports = {
...
autoPK: false,
attributes: {
id: {
type: 'integer',
unique: true,
primaryKey: true,
autoIncrement: true,
},
...
}
}
Pay attention to "autoIncrement" attribute, it is necessary in my case, and probably in every auto-increment primary key.
I'm looking at the Sails.js documentation for creating a new entry in a database. The method is indicated as
Something.create(values).exec(function (err, records) {
});
In your case, you should have
Case.create({...}).exec(function(err, aCase){
console.log(aCase.id);
})

Sequelize Change Unique Composites

I'm using the sequelize syntax to generate a unique composite with two columns in a table like so:
sequelize.define('Group', {
code: {
type: DataTypes.STRING,
unique: 'GroupCompositeIndex'
},
active: {
type: DataTypes.BOOLEAN,
unique: 'GroupCompositeIndex'
}
});
The goal is to have many inactive versions of this group code and change which is active; only one at a time. I will be changing the active value to null on one record, and to 1 on another. I have never constructed a table to work like this. Will it behave the way I expect by precluding there from ever being 2 groups active with the same code, while permitting which group is active to change?

Unique doesn't work on Node.js Sails.js "sails-mysql"

I've just started to get into the framework of Sails for Node. But it seems like I can't get the unique- requirements to work when adding for example users to the sails-mysql database. I can atm add unlimited number of new users with the same username and email.
From what I have read it should work, I did also try with sails-memory and there this exact code did work. Is it something I have missed out?
module.exports = {
attributes: {
username: {
type: 'string',
required: true,
unique: true
},
firstname: {
type: 'string',
required: true
},
lastname: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true
},
birthdate: {
type: 'date',
required: true
},
email: {
type: 'email',
required: true,
unique: true
},
phonenumber: 'string',
// Create users full name automaticly
fullname: function(){
return this.firstname + ' ' + this.lastname;
}
}
};
As I mentioned above, this does work with the memory-storage. And now I have also tried with mongodb where it does work fins as well.
Got support from Sails.js on twitter: "it uses the db layer- suspect it's an issue with automigrations. Would you try in a new MySQL db?"
This answer did work, a new db and everything was just working :)
Just to add to this, since sails uses auto-migrations, if you initially start the server and your model does not have an attribute as unique, the table is built without the unique (index) switch. If you then change an existing attribute in the model to unique, the table will not be rebuilt the subsequent times you start the server.
One remedy during development is to set migrations in your model to drop like this:
module.exports = {
migrate: 'drop' // drops all your tables and then re-create them Note: You loose underlying.
attributes: {
...
}
};
That way, the db would be rebuilt each time you start the server. This would of course drop any existing data as well.