Sequelize is a promise-based ORM working for Node.js v4 and later. This ORM supports many dialects, such as:
PostgreSQL MySQLSQLite MSSQLThis provides a solid support for transactions. With Sequelize you have the possibility of using sequelize-typescript, which provides decorators to put in your entity and manages all the fields of your model, with types and constraints.
Also, Sequelize comes from many hooks providing you with the significant advantage of being able to check and manipulate your data at any level of the transaction.
In this chapter, we will see how to configure your database using postgresql and how to configure the connection to your database. After that we will see how to implement our first entity, which will be a simple User entity and then how to create a provider for this entity in order to inject the entity into a UserService. We will also see the migration
system through umzung, and how to create our first migration file.
You can have a look on the on the src/modules/database, src/modules/user, /src/shared/config, and /src/migrations
/migrate.ts of the repository.
In order to be able to use Sequelize, we have first to set up the connection between sequelize and our database.
In order to do that, we will create the DatabaseModule, which will contain the provider of the sequelize instance.
In order to set up this connection, we will define a configuration file, which will have as properties all you need to be connected to your database. This configuration will have to implement the IDatabaseConfig interface in order to void to forget some parameters.
exportinterfaceIDatabaseConfigAttributes{username:string;password:string;database:string;host:string;port:number;dialect:string;logging:boolean|(()=>void);force:boolean;timezone:string;}exportinterfaceIDatabaseConfig{development:IDatabaseConfigAttributes;}
This configuration should be set up as the following example, and set the parameters through the environment variable or the default value.
exportconstdatabaseConfig:IDatabaseConfig={development:{username:process.env.POSTGRES_USER||'postgres',password:process.env.POSTGRES_PASSWORD||null,database:process.env.POSTGRES_DB||'postgres',host:process.env.DB_HOST||'127.0.0.1',port:Number(process.env.POSTGRES_PORT)||5432,dialect:'postgres',logging:false,force:true,timezone:'+02:00',}};
After the configuration, you have to create the appropriate provider, which will have the purpose to create the instance of sequelize using the right configuration. In our case we just set up the environment configuration, but you can set up all the configuration with the same pattern, you just need to change the values.
This instance is for you to be aware about the different model that should be provided. In order to tell sequelize which model we need, we use the addModels method on the instance and pass an array of model. Of course, in the following
section we will see how to implement a new model.
exportconstdatabaseProvider={provide:'SequelizeInstance',useFactory:async()=>{letconfig;switch(process.env.NODE_ENV){case'prod':case'production':case'dev':case'development':default:config=databaseConfig.development;}constsequelize=newSequelize(config);sequelize.addModels([User]);returnsequelize;}};
This provider will return the instance of Sequelize. This instance will be useful to use the transaction provided by Sequelize. Also, in order to be able to inject it, we have provided in the provide parameter, the name of the
token SequelizeInstance, which will be used to inject it.
Sequelize also provides a way to immediately synchronize your model and your database using sequelize.sync(). This synchronisation should not be used in production mode, because it recreates a new database and removes all of the data each time.
We have now set up our Sequelize configuration, and we need to set up the DatabaseModule as shown in the following example:
@Global()@Module({providers:[databaseProvider],exports:[databaseProvider],})exportclassDatabaseModule{}
We defined the DatabaseModule as a Global in order to be added into all the modules as a related module, letting you inject the provider SequelizeInstance into any module as following:
@Inject('SequelizeInstance`)privatereadonlysequelizeInstance
We now have a complete working module to access our data in the database.
After having set up the sequelize connection, we have to implement our model.
As seen in the previous section, we tell Sequelize that we will have the User model using this method
sequelize.addModels([User]);.
You now see all of the required features to set up it.
This decorator will allow you to configure our representation of the data, and here are some parameters:
{timestamps:true,paranoid:true,underscored:false,freezeTableName:true,tableName:'my_very_custom_table_name'}
The timestamp parameter will tell you that you want to have an updatedAt and deletedAt columns. The paranoid parameter allows you to soft delete data instead of removing it to lose your data. If you pass true, Sequelize will expected a deletedAt column in oder to set the date of the remove action.
The underscored parameter will automatically transform all of the camelcase columns into underscored columns.
The freezTableName will provide a way to avoid Sequelize to pluralize the name of the table.
The tableName allows you to set the name of the table.
In our case we only use timestamp: true, tableName: 'users' in order to get the updatedAt and createdAt column and name the table as users.
This decorator will help define our column. You can also not pass any parameter, so in this case Sequelize will try
to infer the column type. The types that can be inferred are string, boolean, number, Date and Blob.
Some parameter allows us to define some constraints on the column. Let’s imagine the email column, where we would like this email as a string and that cannot be null, so the email has to be unique. Sequelize can recognize an email, but we have to tell it how to validate the email passing the validate#isUnique method.
Take a look at the following example.
@Column({type:DataType.STRING,allowNull:false,validate:{isEmail:true,isUnique:async(value:string,next:any):Promise<any>=>{constisExist=awaitUser.findOne({where:{value}});if(isExist){consterror=newError('The email is already used.');next(error);}next();},},})
In the previous example, we passed some options, but we could also use some decorator as @AllowNull(value: boolean),
@Unique or even @Default(value: any).
To set an id column, the @PrimaryKey and @AutoIncrement decorators are an easy way to set up the constraint.
Now that we have seen some useful decorator, let’s create our first model, the User.
In order to do that, we will create the class that has to extend from the base class Model<T>, and this class takes a template
value for the class itself.
exportclassUserextendsModel<User>{...}
We now add the @Table() decorator in order to configure our model.
This decorator takes options corresponding to the interface DefineOptions and as we described in the @Table section
we will pass as options the timestamp as true and the name of the table.
@Table({timestamp:true,tableName:'users'}asIDefineOptions)exportclassUserextendsModel<User>{...}
Now we have to define some columns for our model. To do this, sequelize-typescript provides the @Column() decorator.
This decorator allows us to provide some options to configure our field. You can pass the data type DataType.Type directly.
@Column(DataTypes.STRING)publicstring;
You can also use the options shown in the @Column section in order to validate and ensure the data of the email.
@Column({type:DataType.STRING,allowNull:false,validate:{isEmail:true,isUnique:async(value:string,next:any):Promise<any>=>{constisExist=awaitUser.findOne({where:{value}});if(isExist){consterror=newError('The email is already used.');next(error);}next();},},})publicstring;
You now know how to set up a column, so let’s set up the rest of the model with the column that we need for a simple user.
@Table(tableOptions)exportclassUserextendsModel<User>{@PrimaryKey@AutoIncrement@Column(DataType.BIGINT)publicid:number;@Column({type:DataType.STRING,allowNull:false,})publicfirstName:string;@Column({type:DataType.STRING,allowNull:false,})publiclastName:string;@Column({type:DataType.STRING,allowNull:false,validate:{isEmail:true,isUnique:async(value:string,next:any):Promise<any>=>{constisExist=awaitUser.findOne({where:{value}});if(isExist){consterror=newError('The email is already used.');next(error);}next();},},})publicstring;@Column({type:DataType.TEXT,allowNull:false,})publicpassword:string;@CreatedAtpubliccreatedAt:Date;@UpdatedAtpublicupdatedAt:Date;@DeletedAtpublicdeletedAt:Date;}
In all the added columns, you can see the password of type TEXT, but of course, you cannot store a password as a plain text, so we have to hash it in order to protect it. To do that, use the lifeCycle hooks provided by Sequelize.
Sequelize come with many lifeCycle hooks that allow you to manipulate and check the data along the process of creating, updating, or deleting a data.
Here are some interesting hooks from Sequelize.
beforeBulkCreate(instances,options)beforeBulkDestroy(options)beforeBulkUpdate(options)beforeValidate(instance,options)afterValidate(instance,options)beforeCreate(instance,options)beforeDestroy(instance,options)beforeUpdate(instance,options)beforeSave(instance,options)beforeUpsert(values,options)afterCreate(instance,options)afterDestroy(instance,options)afterUpdate(instance,options)afterSave(instance,options)afterUpsert(created,options)afterBulkCreate(instances,options)afterBulkDestroy(options)afterBulkUpdate(options)
In this case, we need to use the @BeforeCreate decorator in order to hash the password and replace the original value before storing it in the database.
@Table(tableOptions)exportclassUserextendsModel<User>{...@BeforeCreatepublicstaticasynchashPassword(user:User,options:any){if(!options.transaction)thrownewError('Missing transaction.');user.password=crypto.createHmac('sha256',user.password).digest('hex');}}
The BeforeCreate previously written allows you to override the password property value of the user in order to override it before the insertion of the object into the database, and to ensure a minimum of security.
Our first User model is now setup. Of course, we will have to inject it into a service or even a controller. To inject a model anywhere else, we must first create the appropriate provider in order to give it to the module.
This provider will define the key to use in order to inject it and take as a value the User model that we have implemented before.
exportconstuserProvider={provide:'UserRepository',useValue:User};
To inject it in into a service we will use the @Inject() decorator, which can take the string defined in the previous example UserRepository.
@Injectable()exportclassUserServiceimplementsIUserService{constructor(@Inject('UserRepository')privatereadonlyUserRepository:typeofUser){}...}
After injecting the model into the service, you will be able to use it to access and manipulate the data as you want. For example, you can execute this.UserRepository.findAll() to register the data in your database.
Finally, we have to set up the module to take as providers, the userProvider that provides access to the model and the UserService. The UserService can be exported to be used in another module by importing the UserModule.
@Module({imports:[],providers:[userProvider,UserService],exports:[UserService]})exportclassUserModule{}
You might remark this line, if (!options.transaction) throw new Error('Missing transaction.');, in the hashPassword method decorated with the @BeforeCreate. As said before, Sequelize provides a strong support of the transaction. So for each action or process of action, you can use a transaction. To use the Sequelize transaction, take a look at the
following example of a UserService.
@Injectable()exportclassUserServiceimplementsIUserService{constructor(@Inject('UserRepository')privatereadonlyUserRepository:typeofUser,@Inject('SequelizeInstance')privatereadonlysequelizeInstance){}...}
We have injected both the model and the Sequelize instance that we talked about earlier in this chapter.
To use a transaction to wrap some access to the database, you can do the following:
publicasynccreate(user:IUser):Promise<User>{returnawaitthis.sequelizeInstance.transaction(asynctransaction=>{returnawaitthis.UserRepository.create<User>(user,{returning:true,transaction,});});}
We use the sequelizeInstance to create a new transaction and pass it to the create method of the UserRepository.
With Sequelize you have a way to sync your model and your database. The thing is, this synchronization will remove all of your data in order to recreate all of the tables representing the model. So, this feature is useful in testing, but not in a production mode.
In order to manipulate your database, you have the possibility to use umzung, a framework agnostic library and migration tool for Nodejs. It is not related to any database, but provides an API in order to migrate or rollback the migration.
When you are using the command npm run migrate up, which executes ts-node migrate.ts, you can pass up/down as a parameter. In order to track all of the migration already applied, a new table will be created with the default
name SequelizeMeta, and all of the applied migrations will be stored in this table.
Our migration file can be found in the repository as the root with the name migrate.ts. Also, all of the migrations files will be stored in the migrations folder of the repository example.
In order to configure the umzung instance, you will be able to set some options:
storage, which correspond to the sequelize string key for usstorageOptions, which will take Sequelize, and it is in this option that you can change the default name of the table of the column used to store the name of the migrations applied throughout the modelName, tableName and columnName properties.Some other configurations are able, in order to set the up method name and the down method name, to pass a logging function. The migrations property will allow you to provide some params to pass to the method up/down and the path of the migrations to apply with the appropriate pattern.
constumzug=newUmzug({storage:'sequelize',storageOptions:{sequelize},migrations:{params:[sequelize,sequelize.constructor,// DataTypes],path:'./migrations',pattern:/\.ts$/},logging:function(){console.log.apply(null,arguments);}});
To execute the migration script, provide the migration that you want to apply. Imagine that you want to create the users table using migration. You must set an up and a down method.
exportasyncfunctionup(sequelize){// language=PostgreSQLsequelize.query(`CREATETABLE"users"("id"SERIALUNIQUEPRIMARYKEYNOTNULL,"firstName"VARCHAR(30)NOTNULL,"lastName"VARCHAR(30)NOTNULL,"email"VARCHAR(100)UNIQUENOTNULL,"password"TEXTNOTNULL,"birthday"TIMESTAMP,"createdAt"TIMESTAMPNOTNULL,"updatedAt"TIMESTAMPNOTNULL,"deletedAt"TIMESTAMP);`);console.log('*Table users created!*');}exportasyncfunctiondown(sequelize){// language=PostgreSQLsequelize.query(`DROPTABLEusers`);}
In each method, the parameter will be sequelize, which is the instance used in the configuration file. Throughout this instance you can use the que query method in order to write our SQL query. In the previous example, the function up will execute the query to create the users table. The down method has the purpose to drop this table in case of a rollback.
In this chapter you have seen how to set up the connection to the database by instanciating a Sequelize instance, using the factory in order to inject the instance directly in another place.
Also, you have seen decorators provided by sequelize-typescript in order to set up a new model. You have also seen how to add some constraints on the columns and how to use the lifeCycle hooks to hash a password before saving it. Of course, the hooks can be used to validate some data or check some information before doing anything else. But you also have seen how to use the @BeforeCreate hook. You are therefore ready to use a Sequelize transaction system.
Finally, you have seen how to configure umzung to execute migrations, and how to create your first migration in order to create the users table.
In the next chapter you will learn how to use Mongoose.