Can't insert records when using Typescript.
See original GitHub issueI’m struggling for more than 4 hours now in one of the most basic operations with Sequelize using the Typescript
example from the doc. I’m simply trying to store a simple record in SQL Server. I came to the conclusion that it’s either a gigantic bug sitting somewhere OR the doc is omitting something crucial. Here is the code.
What are you doing?
// connection.ts
export const sequelize = new Sequelize('mssql://sa:test@test:1433/Test', {
dialectOptions: {
instanceName: "SQLEXPRESS",
}
});
// model.ts
class MemberInfo extends Model {
public memb_guid!: number;
public memb___id!: string;
public memb__pwd!: string;
public memb_name!: string;
public sno__numb!: string;
public bloc_code!: string;
public ctl1_code!: string;
public post_code!: string | null;
public addr_info!: string | null;
public addr_deta!: string | null;
public mail_addr!: string | null;
public fpas_ques!: string | null;
public fpas_answ!: string | null;
public mail_chek!: boolean | null;
}
MemberInfo.init({
memb_guid: {
type: DataTypes.INTEGER,
autoIncrement: true,
allowNull: false
},
memb___id: {
type: DataTypes.STRING(10),
primaryKey: true,
allowNull: false
},
memb__pwd: {
type: DataTypes.STRING,
allowNull: false
},
memb_name: {
type: DataTypes.STRING,
allowNull: false
},
sno__numb: {
type: DataTypes.STRING,
allowNull: false
},
post_code: {
type: DataTypes.STRING,
allowNull: true
},
addr_info: {
type: DataTypes.STRING,
allowNull: true
},
addr_deta: {
type: DataTypes.STRING,
allowNull: true
},
tel__numb: {
type: DataTypes.STRING,
allowNull: true
},
phon_numb: {
type: DataTypes.STRING,
allowNull: true
},
mail_addr: {
type: DataTypes.STRING,
allowNull: true
},
fpas_ques: {
type: DataTypes.STRING,
allowNull: true
},
fpas_answ: {
type: DataTypes.STRING,
allowNull: true
},
job__code: {
type: DataTypes.STRING,
allowNull: true
},
appl_days: {
type: 'SMALLDATETIME',
allowNull: true
},
modi_days: {
type: 'SMALLDATETIME',
allowNull: true
},
out__days: {
type: 'SMALLDATETIME',
allowNull: true
},
true_days: {
type: 'SMALLDATETIME',
allowNull: true
},
mail_chek: {
type: DataTypes.STRING,
allowNull: true
},
bloc_code: {
type: DataTypes.STRING,
allowNull: false
},
ctl1_code: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
tableName: 'MEMB_INFO',
timestamps: false,
freezeTableName: true,
});
export { MemberInfo };
// Using
const account = await MemberInfo.create({
memb___id: input.username,
memb__pwd: input.password,
memb_name: input.username,
mail_addr: input.email,
fpas_ques: input.secretQuestion,
fpas_answ: input.secretAnswer,
bloc_code: '0',
ctl1_code: '0',
sno__numb: "2019-06-19",
});
The create statement throws the following error (even though the values are all passed in valid):
notNull Violation: MemberInfo.memb_name cannot be null,
notNull Violation: MemberInfo.sno__numb cannot be null,
notNull Violation: MemberInfo.bloc_code cannot be null,
notNull Violation: MemberInfo.ctl1_code cannot be null
The fun fact is that when I console.log
in the beforeValidate
hook you can see a very awkward behaviour of the dataValue
and _previousDataValues
MemberInfo {
dataValues:
{ memb___id: 'ddddd',
memb__pwd: undefined,
memb_name: undefined,
mail_addr: undefined,
fpas_ques: undefined,
fpas_answ: undefined,
bloc_code: undefined,
ctl1_code: undefined,
sno__numb: undefined,
memb_guid: undefined,
post_code: undefined,
addr_info: undefined,
addr_deta: undefined,
mail_chek: undefined },
_previousDataValues:
{ memb___id: undefined,
memb__pwd: 'ddddd',
memb_name: 'ddddd',
mail_addr: 'ddd@sss.xom',
fpas_ques: 'ssssss',
fpas_answ: 'ssssss',
bloc_code: '0',
ctl1_code: '0',
sno__numb: '2019-06-19',
memb_guid: undefined,
post_code: undefined,
addr_info: undefined,
addr_deta: undefined,
mail_chek: undefined },
_changed:
{ memb___id: true,
memb__pwd: true,
memb_name: true,
mail_addr: true,
fpas_ques: true,
fpas_answ: true,
bloc_code: true,
ctl1_code: true,
sno__numb: true,
memb_guid: true,
post_code: true,
addr_info: true,
addr_deta: true,
mail_chek: true },
During those hours of debugging I found out that if i remove (any or all) properties from the class, then the value gets populated correctly in the dataValues
.
If I remove them all it actually saves! But I just lost typescript type safety here.
One another thing I was also suspecting is that I’m using webpack + babel + typescript setup (for other reasons), but I couldn’t find anything which could compromise the properties of the Model class definition.
I’ll post my babelrc just in case:
{
"presets": [
["@babel/env", {
"modules": false,
"targets": {
"node": "current"
}
}], "@babel/preset-react", "@babel/typescript"
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true } ],
["@babel/plugin-proposal-class-properties", { "loose" : true }],
["module-resolver", {
"root": ["./"],
"alias": {
"modules": "./src/client/modules"
}
}],
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
"universal-import",
"react-hot-loader/babel"
]
}
If i use the define
way of creating my models all works fine. It’s just wayyyy uglier 😄
What do you expect to happen?
That save just fine!
Environment
Dialect:
- mysql
- postgres
- sqlite
- mssql
- any Dialect library version: 6.1.2 Database version: 2008r2 Sequelize version: 5.8.7 Node Version: 10 OS: MacOS If TypeScript related: TypeScript version: 3.4.1 Tested with latest release:
- No
- Yes, specify that version:
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:11 (6 by maintainers)
I encountered this issue yesterday afternoon and I did some debugging with the sequelize source code. Read below for (1) solution(s) to the problem (2) my findings from debugging on the problem (3) and my proposed long-term solution to the problem.
TL;DR - If using Babel (which probably means you’re using the
@babel/plugin-proposal-class-properties
plugin), create a custom constructor in your class:…or…
Remove the
@babel/plugin-proposal-class-properties
plugin. I was originally using it for static properties of classes in 1 module and refactored my code to remove it.If you want to figure out why I chose this solution and my findings from debugging this issue, read on.
Env
Type of application: Nodejs backend app Dialect: Postgres Dialect library version: (pg) 6.4.2 Database version: 2.6 Sequelize version: 5.19.0 Node Version: 10 OS: MacOS If TypeScript related: TypeScript version: 3.4.2
Babel versions:
Babel config:
Code
Then, to use this API:
Error
When I run my Typescript application, I am getting the same error as everyone else:
My findings
tsc
, no error.Here is the key output differences with the babel output and the tsc output that I believe is causing the issue:
babel
tsc
Full outputs if you find it interesting…
babel produces this output for my class:
tsc produces this output:
Through my debugging of the Sequelize source, this is the behavior that I am seeing (I understand maintainers already know all of this, but I wanted to include this here for inclusion of everyone into the conversation of how to resolve this problem).
SequelizeMode.init()
when you first setup your code base, sequelize calls the static function:Model.refreshAttributes()
which sets up property setters on all of therawAttributes
for your Model so each time that you set properties on your custom Model class instance, a sequelize function runs (refs 1, 2, 3)Now when you call:
modelInstance.propertyHere = valueHere
, Sequelize code will run which in result callsSequelize.Model.set()
for that property with the value.SequelizeModel.create()
, sequelize internally callsSequelizeModel.build()
and then calls.save()
on that built instance.The static
SequelizeModel.build()
function has the following behavior:SequelizeModel
constructor to create an instance. Because babel produces a constructor, the overriden constructor gets called.SequelizeModel.create()
into the constructor to get the values set on the new instance (see this and this).Model.set()
wherevalues
parameter is an object (the same object being passed to.create()
), is iterated and callingModel.set()
on each of the values which is where each of the properties of the Model instance is being set. When each of the properties of the Model instance get set, the sequelize function setup inSequelizeModel.init()
gets called.Model.set()
is important to note here because it will get called twice for each property whenSequelizeModel.build()
is being called. One time whensuper()
is being called in theOAuthAppSequelizeModel
constructor, another time when each of the properties is being set aftersuper()
is done:this.clientId = void 0;
.The problem
This is where I believe the problem lays.
Sequelize.create()
creates an instance of the Model where it sets the properties of the Model instance in the constructor and then callssave()
on the newly built instance. However, it does not account for manipulion of the object’s properties in between thebuild()
andsave()
steps. This is understandable as Sequelize can assume that if the properties are ever manipulated in the constructor, that’s your doing.The solution(s)
Don’t use babel. The project I am working on is pretty small and doesn’t need to be maintained in a long-term fashion, so I did try out the idea of switching to
tsc
over babel. I quickly realized that the babel development tooling withbabel-node
, nodemon and others work really nicely and the tsc alterntives are not quite up to par. So, I personally felt staying with babel would be better if I could get that to work.Create custom implementation for
SequelizeModel.create()
that performs the operations: (1)Model.build()
, (2) set the properties for a 2nd time from values object, (3)Model.save()
.Stop using the
@babel/plugin-proposal-class-properties
plugin. It’s not required and it only added value to my code when I wanted to add static properties to my classes which I refactored to avoid using static properties.I did do this:
…but what sucks is that you also need to create alternatives for
findCreateFind()
,findOrCreate()
, and others because they all callcreate()
internally.SequelizeModel
subclass, do this:Now, babel will produce the following:
This is good! Babel still gets to cause the original problem with
this.clientId = void 0;
, but you reverse the action again withthis.clientId = values.clientId;
.You lose a small bit of typescript safety with the
any
type, but because this is just the constructor for 1 simple class, I find this the easiest, most future-proof method to solving this problem.Conclusion
I believe I have shown that a babel plugin is the culprit of this issue. The dialect does not have to do with this issue. I do not believe this is anything that sequelize needs to handle.
However, I do believe that sequelize needs to include a note in the documentation about this. From reading some other github issues after searching “babel”, it seems that babel and sequelize doesn’t get along sometimes. Luckily, I have only had this issue thus far. A lot of typescript users could be using babel, I propose we create a babel section in the typescript documentation explaining some of the pitfalls of using babel and how to avoid them.
Hi @levibostian thank you very much for the detailed analysis! I agree with your suggestion and I have just created an issue about it: #11494 - Thanks!