Mariusz Rajczakowski
Software Engineer
60 min read | 5 months ago

TDD a RESTful API with node, express, typescript and jest. PART 1

What we are going to build?

We will build a RESTful API in node.js aiming the following requirements:

  • Handle CRUD (create, read, update, delete) on resources (posts, comments, users, roles, permissions)
  • Access to the API will be secured with JWT auth + RBAC (Role-based access control)
  • Data will be returned in JSON format
  • All requests will be logged to the console

Our app dependencies

We are going to use the following tools/packages:

RESTful API dictionary:

We will start with a few important terms, to make sure we speak the same language when we will be coding later on:

  • Endpoint - an API URL which represents eiter resource or collection of resources (i.e. https://example.com/api/v1/users)
  • Resource - a single instance of an object (i.e. user or comment)
  • Collection of resources - a group of same types objects (i.e. users or comments)
  • Client - computer program or a device that accesses a service (i.e. our API) provided by server
  • Server - computer program or a device that provides functionality for other programs or devices, called clients
  • Idempotent operations - operations that produce the same results without side effects, regardless whether happen once or many times (i.e. GET request should not produce any side effects if you send it once or thousand times)

Getting started

Let’s first specify the collection of resources we are going to include in our API:

  • users
  • roles
  • permissions
  • posts
  • comments

Let's draw the relationoships between the models

relationships between models

We will be using the following relationships:

1. Users - Posts

  • One user has many posts
  • One post belongs to one user

2. Posts - Comments:

  • One post has many comments
  • One comment belongs to one post

3. Users - Reset Password Tokens:

  • One user has many reset password tokens
  • One token belongs to one user

4. Users - Roles:

  • One user belongs to many roles (through role_user pivot table)
  • One role belongs to many users (through role_user pivot table)

5. Roles - Permissions:

  • One role belongs to many permissions (through role_permission pivot table)
  • One permission belongs to many roles (through role_permission pivot table)

6. User - Comments:

  • One user has many comments
  • One comment belongs to one user

Schema planing

We will come back to this section later, after we set up a project, and we would talk about db structure in the context of migrations which basically mirror db structure.

API root URL

Chosing the root location is important part of designing an api.

The most commons ones are:

  1. https://your-website.com/api/v1/*
  2. https://api.your-website.com/v1/*

Which one to choose? If your application is just simple API, which presumably not going to be another facebook API than go with option 1. However when you think that your API might grow fast and to make it more scalable, putting it on subdomain might be a good choice (option 2).

API versioning

Why you should version your API? The reason is simple, your code is going to change in the future, stuff like models attributes and relationships might be added/removed or changed.

It is important to keep in mind that, API when published is contract between client and server.

If you make breaking changes, and clients would not work with your changed API, then you would eventually lose your customers.

Therefore, we would introduce some versioning to maintain the code which is currently released and when necessary introduce some new changes in new versions of our API.

There are two main schools of API versioning:

  1. Version specification in the headers, with other metadata, using the Accept header with a custom content type, i.e.
    Accept: application/json; version=2
  2. Version specification as part of the root API URL segement i.e. https://your-website/api/v1/*

For the simplicity, we are going to stick to the second option in this tutorial.

Https everywhere

As you might notice all the previous url we mentioned already where proceed with SSL.

It is important for the production environment to secure the connections and data transmissions with SSL.

When you would be enabling ssl, make sure that when request would come from unsecure port 80 connection to throw 403 Forbidden error, to avoid any insecure data exchange.

For the simplitcity in this tutorial we won't be enabling SSL, however you must do it for your production environment.

REST verbs

We are going to perform basic CRUD operations on our resources (users, posts, comments and tokens)

It is important to understand the difference between the HTTP verbs:

HTTP verb CRUD operation Description
POST CREATE HTTP POST method is most often used to create new resources. On successful creation it should return status code 201 CREATED. POST requests are neither safe nor idempotent, means when you send to same POST requests it might create two resources contained same attributes.
GET READ HTTP GET method is used to retrieve a reprentation of resource or collection of resources. If successful should return response 200. When resource cannot be found should return 404 or 400 when the the server cannot process request due to client error. According to HTTP spec, GET simiarily to HEAD requests should be only used to read data and not modify it (should be idempotent).
PUT UPDATE HTTP PUT method is known for updating the entire resource resource (true PUT doesn't happen that often as you might be i.e. living updating the timestamps to the server - then is not a truly PUT request, but PATCH request). Even if it's possible to create a brand new request with specifying new resource ID, it is recommended to use POST instead. When request succeeded it should return response code 200 (or 204 when, not returning content in the body). Put is not safe operation, but is idempotent (regardless how many times you send it, resource state going to be the same as first call).
PATCH UPDATE HTTP PATCH method is used for partially update the resource. Patch is neither safe nor idempotent. It should return 200 when sucessful, 404 when resource not found and 400 when request cannot be processed due to client error.
DELETE DELETE HTTP DELETE method removes the resource identified by a URI. When request succeeded it should return 200 resonse code or 204 (when no content). Issuing again same request should return 404 NOT FOUND - because resource is gone. If you want to know more about delete here is some good article about HTTP DELETE in the context of soft-deleting.

API endpoints

Auth routes

We are going to use AuthController for those routes:

Route Permission required Controller method Description
POST /api/v1/auth/register N/A register Allows unauthenticated user to register
POST /api/v1/auth/login N/A login Allows unauthenticated user to log in and receive JWT token for signing API requests
POST /api/v1/auth/forgot-password N/A forgotPassword Allows unauthenticated user to request an email with reset password token used for password reset
POST /api/v1/auth/reset-password N/A resetPassword Allows unauthenticated user to reset own password using token received via email

Users routes

We are going to use UsersController for those routes:

Route Permission required Controller method Description
POST /users user.store store Allows to create a new user
GET /users users.index index Allows to get all users
GET /users/:user_id user.show show Allows to get user for a given id
PUT /users/:user_id user.update update Allows to update/replace entire user resource, contains complete resource
PATCH /users/:user_id user.update update Allows to modify particular user resource, only contains changes to the part of resource/td>
DELETE /users/:user_id user.delete destroy Soft deletes user by a given id

UsersRoles routes

We are going to use UsersRolesController for those routes:

Route Permission required Controller method Description
POST /users/:user_id/roles role.assign assign Assign user's role
DELETE /users/:user_id/roles/:role_id role.remove remove Remove user's role

Posts routes

We are going to use PostsController for those routes:

Route Permission required Controller method Description
POST /posts post.store store Allows to create a new post
GET /posts posts.index index Allows to get all posts
GET /posts/:post_id post.show show Allows to get post for a given id
PUT /posts/:post_id post.update update Allows to update/replace entire post resource, contains complete resource
PATCH /posts/:post_id post.update update Allows to modify particular post resource, only contains changes to the part of resource
DELETE /posts/:post_id post.delete destroy Soft deletes post by a given id

Comments routes

We are going to use CommentsController for those routes:

Route Permission required Controller method Description
POST /comments comment.store store Allows to create a new comment
GET /comments comments.index index Allows to get all comments
GET /comments/:comment_id comment.show show Allows to get comment for a given id
PUT /comments/:comment_id comment.update update Allows to update/replace entire comment resource, contains complete resource
PATCH /comments/:comment_id comment.update update Allows to modify particular comment resource, only contains changes to the part of resource
DELETE /comments/:comment_id comment.delete destroy Soft deletes comment by a given id

Roles routes

We are going to use RolesController for those routes:

Route Permission required Controller method Description
POST /roles role.store store Allows to create new role
GET /roles roles.index index Allows to get all roles
GET /roles/:role_id role.show show Allows to get role for a given id
PUT /roles/:role_id role.update update Allows to update/replace entire role resource, contains complete resource
PATCH /roles/:role_id role.update update Allows to modify particular role resource, only contains changes to the part of resource
DELETE /roles/:role_id role.delete destroy Deletes role

Permissions routes

We are going to use PermissionsController for those routes:

Route Permission required Controller method Description
POST /permissions permission.store store Allows to assign new permission to the role
GET /permissions permissions.index index Allows to get all permissions
GET /permissions/:permission_id permission.show show Allows to get permission for a given id
PUT /permissions/:permission_id permission.update update Allows to update/replace entire permission resource, contains complete resource
PATCH /permissions/:permission_id permission.update update Allows to modify particular permission resource, only contains changes to the part of resource
DELETE /permissions/:permission_id permission.delete destroy Removes role's permission

RolesPermissions routes

We are going to use RolesPermissionsController for those routes:

Route Permission required Controller method Description
POST /roles/:role_id/permissions permission.assign assign Assign role's permission
DELETE /roles/:role_id/permissions/:permission_id permission.remove remove Remove role's permission

Why mainly flat routes?

There are following reasons behind it (based on this stackoverflow answer):

  • Nested routes require redundant endpoints
  • Nested endpoints are not future-proof, when you have those and i.e. add some search you will have to add another endpoint, with flat structure however, you will add more query params instead
  • Nested endpoints kind of lock yourself with current resource descendants tree structure, however if you change something and decide i.e. that some child resource could have multiple parents, then you would have redundant endpoints for multiple parents resulting in returning the same resource
  • Redundant points makes api harder to learn and docs harder to right

Hands on code

1. Create your github repo

adding new repository

2. Choose name and description, then tick add readme checkbox, choose .gitignore for node and MIT license (most popular)

repo_details

3. Then clone the repo on your local machine

git clone https://github.com/mariocoski/rest-api-node-typescript.git

4. Then you are ready for app initialization

//cd into your cloned repo
cd rest-api-node-typescript  
//initialize 
yarn init
//or
npm init

//then choose package name (default is fine)
package name: (rest-api-node-typescript)
//choose version 0.1.0 (feature according to semantic versioning)
version: (1.0.0) 0.1.0
//choose package description
description: restful api build in node and typescript
//choose entry point to your program (default is fine for now)
entry point: (index.js) src/index.js
//set test command 
test command: jest --coverage 
//set git repo (default is fine)
git repository: (https://github.com/mariocoski/rest-api-node-typescript.git)
//choose keywords describing your package:
keywords: rest, api, node, express, typescript, sequelize, jwt
//specify license (MIT will do)
license: (ISC) MIT
Is this ok? (yes) 

After that you should see newly created package.json in root dir

cat package.json

//it should look like this
{
  "name": "rest-api-node-typescript",
  "version": "0.1.0",
  "description": "restful api build in node and typescript",
  "main": "index.js",
  "repository": "https://github.com/mariocoski/rest-api-node-typescript.git",
  "keywords": [
    "rest",
    "api",
    "express",
    "typescript",
    "sequelize",
    "jwt"
  ],
  "author": "Mariusz Rajczakowski <mariuszrajczakowski@gmail.com> (http://mariuszrajczakowski.me)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/mariocoski/rest-api-node-typescript/issues"
  },
  "homepage": "https://github.com/mariocoski/rest-api-node-typescript#readme"
}

5. Let's add engines property to package.json.

Npm treats engines.node as advisory. We will use version equal or higher than 4.8.2.

//package.json
{
   //rest of config
   "engines" : {
      "node": ">=4.8.2"
   }
}

Installing dependencies

To save up some time we will install most of our dependencies need upfront.

//installing dependencies (or using npm with npm install ... --save)
yarn add  babel-polyfill bcrypt bluebird body-parser compression cors  express express-validator iconv-lite express-jwt express-jwt-permissions ramda mailgun-js morgan mysql2 sqlite3 passport passport-jwt passport-local sequelize codecov --save

//installing devDependencies (or using npm with npm install ... --save-dev)
yarn add  @types/node @types/bcrypt @types/body-parser @types/compression @types/ramda @types/cors @types/dotenv @types/es6-shim @types/express @types/express-serve-static-core @types/iconv-lite @types/jest @types/express-jwt @types/mime @types/morgan @types/passport @types/passport-jwt @types/passport-local @types/sequelize @types/supertest @types/superagent babel-cli babel-jest babel-plugin-transform-runtime babel-preset-env babel-preset-stage-0 dotenv typescript jest ts-jest  supertest superagent --dev

//then we create a config file for typescript
tsc --init

//that will create a file with predefined settings (most of them are commented out)
//uncomment those which are necessary to match the following config:

//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",                         
    "module": "commonjs",                    
    "moduleResolution": "node",           
    "baseUrl": ".",   
    "allowJs": true,                   
    "outDir": "build",
    "sourceMap": true,
    "strict" : true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

//then we will create src dir
mkdir src

//and create few files app.ts, server.ts, router.ts and two env files
// (one for version control: .env.example and one for dev usage .env)
// we will be using dotenv package to load env variables from .env file
touch src/server.ts src/app.ts src/router.ts .env .env.example

Edit your app.ts file:

//src/app.ts
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as logger from 'morgan';
import * as passport from 'passport';
import * as cors from 'cors';
import * as compression from 'compression';
import * as fs from 'fs';
//we will import the module which will handle routing for our app
//we will populate this file in as sec
import httpRouter from './router';
//that will create an express app which we will
//exports and pass to http.createServer() function
const app: express.Application = express();

//body parser parses request bodies. Those could contain
//like json or url encoded form data. 
//The form data will then appear in req.body
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

//in the meantime (as we don't have any gzip module on nginx yet - 
//we will compress response bodies for all requests) using 
//compression middleware
app.use(compression());

//we would you morgan for logging requests
//flags: 'a' opens the file in append mode.
app.use(logger('common', {
    stream: fs.createWriteStream('./access.log', {flags: 'a'})
}));
//doing console.log
app.use(logger('dev'));

//we will use cors middleware for enabling cores and for all requests
//you can read more about cors here:
//https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
const corsMiddleware = cors({ origin: '*', preflightContinue: true });
app.use(corsMiddleware);
app.options('*', corsMiddleware);

httpRouter(app);

const myApp: express.Application = app;

export default myApp;

Then go to your src/router.ts and these lines:

import {Application, Router, Request, Response, NextFunction} from 'express';

const router =  (app: Application) => {
  //init your main express router
  const apiRouter: Router = Router();

  //handle GET request to /api/v1
  apiRouter.get('/', (req: Request, res: Response) => {
      res.status(200).json({message: "This is where the awesomeness happen..."});
  });

  app.use('/api/v1', apiRouter);
};

export default router;

Edit also your server.ts file

// src/server.ts
//this will emulate a full ES2015+ environment 
//and is intended to be used in an application rather than a library/tool. 
require('babel-polyfill');

//this will load all env variables for dev and test mode
if(process.env.NODE_ENV !== 'production'){
  require('dotenv').config();
}

//load http module
import * as http from 'http';
import app from './app';

import * as iconvLite from 'iconv-lite';
//used for characted encoding conversion
iconvLite.encodingExists('foo');

//signal events are emitted when the Node.js process receives a signal
//SIGINT signal is with -C in most terminal programs
process.on('SIGINT', () => {
  process.exit(0);
});

//this is when testing with jest - its set up
//process.env.NODE_ENV to be test
//in this case we will choose test port accordingly
const IS_TEST: boolean = process.env.NODE_ENV === 'test';

//we will replace those port number later on with env vars
const port: number = IS_TEST ? 3001 : 3000;

//create a server
const server: http.Server = new http.Server(app);

//listen on the provided port
server.listen(port, () => {
  if(! IS_TEST){
    console.log(`Listening at http://localhost:${port}/api/v1`);
  }
});

//server error handler
server.on('error', (error: any, port: number) => {
  if (error.syscall !== "listen") {
    throw error;
  }
  switch (error.code) {
    case 'EACCES':
      if(process.env.NODE_ENV !== 'test'){
        console.log(`${port} requires elevated privileges`);
      }
      process.exit(1);
    case 'EADDRINUSE':
      if(process.env.NODE_ENV !== 'test'){
        console.log(`${port} is already in use`);
      }
      process.exit(1);
    default:
      throw error;
  }
});

export default server;

Modify your package.json by adding additional scripts section:

//package.json
{
  //...other config
  "scripts" : {
    "build": "rm -rf ./build && tsc",
    "build:watch": "rm -rf ./build && tsc --watch",
    "start": "NODE_ENV=development && node ./build/server.js"
  }
}

//then run
yarn build 
//or 
npm build

//after that:
yarn start 
//or npm start

//the you should see in your terminal
$ NODE_ENV=development && node ./build/server.js
Listening at http://localhost:3000

//go to your browser and type in the url bar:
http://localhost:3000/api/v1

You should see this:

http://localhost:3000/api/v1

Let's now populate our env files:

//env.example and .env (same content for now)
NODE_ENV=development
DEBUG=true
PORT=3000
TEST_PORT=3001
JWT_SECRET=your_jwt_secret
JWT_EXPIRATION_TIME=3600000

DEV_DB_USERNAME=root
DEV_DB_PASSWORD=root
DEV_DB_NAME=database_dev
DEV_DB_HOSTNAME=localhost

TEST_DB_USERNAME=root
TEST_DB_PASSWORD=root
TEST_DB_NAME=database_test
TEST_DB_HOSTNAME=localhost

PROD_DB_USERNAME=root
PROD_DB_PASSWORD=password
PROD_DB_NAME=database_prod
PROD_DB_HOSTNAME=localhost

MAILGUN_DOMAIN=
MAILGUN_API_KEY=
MAILGUN_TEST_RECIPIENT=

Then create new folder config and few more files

mkdir src/config src/database src/database/migrations src/database/seeds src/models
touch .sequelizerc src/config/database.js

//edit .sequelizerc and add:
const path = require('path');

module.exports = {
  'config' : path.resolve(__dirname, 'src/config/database.js'),
  'migrations-path' : path.resolve(__dirname, 'src/database/migrations'),
  'seeders-path' : path.resolve(__dirname, 'src/database/seeds'),
  'models-path' : path.resolve(__dirname, 'src/models'),
}

Edit config/database.js

//config/database.js
const path = require('path');
require('dotenv').config();
module.exports = {
  development: {
    username: process.env.DEV_DB_USERNAME,
    password: process.env.DEV_DB_PASSWORD,
    database: process.env.DEV_DB_NAME,
    host:  process.env.DEV_DB_HOSTNAME,
    dialect: 'mysql',
    operatorsAliases: false
  },
  test: {
    username: process.env.TEST_DB_USERNAME,
    password: process.env.TEST_DB_PASSWORD,
    database: process.env.TEST_DB_NAME,
    host: process.env.TEST_DB_HOSTNAME,
    dialect: 'sqlite',
    storage: ':memory:',
    operatorsAliases: false
  },
  production: {
    username: process.env.PROD_DB_USERNAME,
    password: process.env.PROD_DB_PASSWORD,
    database: process.env.PROD_DB_NAME,
    host: process.env.PROD_DB_HOSTNAME,
    operatorsAliases: false,
    dialect: 'mysql'
  }
}

Database structure

We will design our db structure, create migrations and models.

Prior to this I would recommend this post on how to set up db structure with Sequelize (the ORM we are using in this tutorial).

//let's start with installing sequelize-cli 
//you can do it locally
yarn add sequelize-cli --save
//or globally
yarn global add sequelize-cli

//if you install it locally you would use it like so (unless you add it to the PATH)
node_modules/.bin/sequelize
 
//if installed globally then just:
sequelize

We wil start index.ts file which will load our models and connect to db.

We will then create a models and their interfaces manually, after that we will use sequelize-cli to create a missing migrations

//create an index.ts
touch src/models/index.ts

//edit it and paste those lines:
//src/models/index.ts
import * as fs from 'fs';
import * as path from 'path';
import * as SequelizeStatic from 'sequelize';
import {UserAttributes, UserInstance} from './interfaces/user';
import {RoleAttributes, RoleInstance} from './interfaces/role';
import {PermissionAttributes, PermissionInstance} from './interfaces/permission';
import {PostAttributes, PostInstance} from './interfaces/post';
import {CommentAttributes, CommentInstance} from './interfaces/comment';
import {ResetPasswordTokenAttributes, ResetPasswordTokenInstance} from './interfaces/reset_password_token';
import {RolePermissionAttributes, RolePermissionInstance} from './interfaces/role_permission';
import {UserRoleAttributes, UserRoleInstance} from './interfaces/user_role';
import {Sequelize} from 'sequelize';

export interface SequelizeModels {
  User: SequelizeStatic.Model<UserInstance, UserAttributes>;
  Role: SequelizeStatic.Model<RoleInstance, RoleAttributes>;
  UserRole:  SequelizeStatic.Model<UserRoleInstance, UserRoleAttributes>;
  Permission: SequelizeStatic.Model<PermissionInstance, PermissionAttributes>;
  RolePermission: SequelizeStatic.Model<RolePermissionInstance, RolePermissionAttributes>;
  Post: SequelizeStatic.Model<PostInstance, PostInstance>;
  Comment: SequelizeStatic.Model<UserInstance, CommentAttributes>;
  ResetPasswordToken: SequelizeStatic.Model<ResetPasswordTokenInstance, ResetPasswordTokenAttributes>;
}

export interface DbEnvConfig {
  database: string,
  username: string,
  password: string,
  host: string,
  operatorsAliases: boolean,
  storage?: string
}

export interface DbConfig {
  [key: string]: DbEnvConfig;
}

const dbConfig: DbConfig = require('../config/database');
const env: string = process.env.NODE_ENV || 'development';
const config: DbEnvConfig = dbConfig[env];
const basename: string = path.basename(module.filename);

const _sequelize: Sequelize = new SequelizeStatic(
  config.database, config.username, config.password, 
  {...config, operatorsAliases: false, logging: false}
);

let _models: any = {};

//we dynamically load all the models for a given directory
const files: Array = fs.readdirSync(__dirname);
files.filter(file => {
  return (file.indexOf('.') !== 0) && (file !== basename) 
         && (file.slice(-3) === '.js' || file.slice(-3) === '.ts')
         && (file !== 'interfaces');
  }).forEach(file  => {
    let model: any = _sequelize.import(path.join(__dirname, file));
    _models[model.name] = model;
  });

//we create the relationships for a models we will create shortly after
_models.Comment.belongsTo(_models.Post);
_models.Post.hasMany(_models.Comment, {as: 'comments', onDelete: 'CASCADE'});
_models.Post.belongsTo(_models.User);
_models.User.hasMany(_models.Post, {as: 'posts', onDelete: 'CASCADE'});
_models.User.hasMany(_models.ResetPasswordToken, { as: 'reset_password_tokens', onDelete: 'CASCADE'});
_models.Role.belongsToMany(_models.User, { through: _models.UserRole, as: 'users', onDelete: 'CASCADE',individualHooks: true});
_models.User.belongsToMany(_models.Role, { through: _models.UserRole, as: 'roles', onDelete: 'CASCADE',individualHooks: true});
_models.Role.belongsToMany(_models.Permission, { through: _models.RolePermission, as: 'permissions', onDelete: 'CASCADE',individualHooks: true});
_models.Permission.belongsToMany(_models.Role, { through: _models.RolePermission, as: 'roles', onDelete: 'CASCADE',individualHooks: true});


//we will export models and sequelize instance
export const models: SequelizeModels = _models;
export const sequelize: Sequelize = _sequelize;

Then edit your server.ts

//src/server.ts
//add additional import at the top:
import {sequelize} from './models';

//then before this line of code: server.listen(port, () => {
//add async function for intializing db and additional check

async function dbInit(){
  await sequelize.sync();
}
//this db init will only run for dev and production as we will have our own init in tests
if(process.env.NODE_ENV !== 'test'){
  dbInit();
}

We will manually create the models and interfaces and then one by one we are going to populate them.

//we create the models first
touch src/models/user.ts  src/models/user_role.ts  src/models/role.ts src/models/role_permission.ts src/models/permission.ts src/models/reset_password_token.ts src/models/post.ts   src/models/comment.ts

//then we create interfaces folder and relevant files
mkdir src/models/interfaces

touch src/models/interfaces/user.ts  src/models/interfaces/user_role.ts src/models/interfaces/role.ts  src/models/interfaces/permission.ts src/models/interfaces/role_permission.ts  src/models/interfaces/reset_password_token.ts src/models/interfaces/post.ts  src/models/interfaces/comment.ts 

Then we will edit models and corresponding with them interfaces.

User Model

//src/models/user.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize, Instance} from "sequelize";
import {UserAttributes, UserInstance} from "./interfaces/user";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<UserInstance, UserAttributes> => {
  
  const User = sequelize.define<UserInstance, UserAttributes>("User", {
    firstname: dataTypes.STRING,
    lastname: dataTypes.STRING,
    bio: dataTypes.TEXT,
    email: {
      type: dataTypes.STRING,
      validate: {
        isEmail: true
      }
    },
    password: dataTypes.STRING,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'users',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true,
  });
  
  User.beforeCreate(user: UserInstance, options: Object) => {
    //@todo implement bcrypt
    user.dataValues.password = 'hash';
  });

  User.afterDestroy((user: UserInstance, options: Object) => {
    sequelize.models.Post.destroy({where: {user_id: user.dataValues.id},individualHooks: true}); 
    sequelize.models.UserRole.destroy({where: {user_id: user.dataValues.id}, individualHooks: true}); 
    sequelize.models.ResetPasswordToken.destroy({where: {user_id: user.dataValues.id}, individualHooks: true}); 
  });

  return User;
}

User Interface

//src/models/interfaces/user.ts
import {Instance} from "sequelize";
import {RoleInstance} from './role';
import {SequelizeModels} from '../index';

export interface UserAttributes {
  id: number,
  firstname: string,
  lastname: string,
  bio: string,
  email: string,
  password: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface UserInstance extends Instance {
  dataValues: UserAttributes;
}

Role Model

//src/models/role.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {RoleAttributes, RoleInstance} from "./interfaces/role";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<RoleInstance, RoleAttributes> => {
  const Role = sequelize.define<RoleInstance, RoleAttributes>("Role", {
    name: dataTypes.STRING,
    description: dataTypes.TEXT,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'roles',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true
  });

  Role.afterDestroy((role: RoleInstance, options: Object) => {
    sequelize.models.UserRole.destroy({where: {role_id: role.dataValues.id}, individualHooks: true}); 
    sequelize.models.RolePermission.destroy({where: {role_id: role.dataValues.id}, individualHooks: true}); 
  });

  return Role;
}

Role Interface

//src/models/interfaces/role.ts
import {Instance} from "sequelize";

export interface RoleAttributes {
  id: number,
  name: string,
  description: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface RoleInstance extends Instance {
  dataValues: RoleAttributes;
}

UserRole Model

//src/models/user_role.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {UserRoleAttributes, UserRoleInstance} from "./interfaces/user_role";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<UserRoleInstance, UserRoleAttributes> => {
  const UserRole = sequelize.define<UserRoleInstance, UserRoleAttributes>("UserRole", {
    user_id: dataTypes.INTEGER,
    role_id: dataTypes.INTEGER,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'user_role',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    paranoid: true,
    underscored: true
  });

  return UserRole;
}

UserRole Interface

//src/models/interfaces/user_role.ts
import {Instance} from "sequelize";

export interface UserRoleAttributes {
  id: number,
  user_id: number,
  role_id: number,
  deleted_at: string
}

export interface UserRoleInstance extends Instance {
  dataValues: UserRoleAttributes;
}

Permission Model

//src/models/permission.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {PermissionAttributes, PermissionInstance} from "./interfaces/permission";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<PermissionInstance, PermissionAttributes> => {
  const Permission = sequelize.define<PermissionInstance, PermissionAttributes>("Permission", {
    name: dataTypes.STRING,
    label: dataTypes.STRING,
    description: dataTypes.TEXT,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'permissions',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true
  });

  return Permission;
}

Permission Interface

//src/models/interfaces/permission.ts
import {Instance} from "sequelize";

export interface PermissionAttributes {
  id: number,
  name: string,
  label: string,
  description: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface PermissionInstance extends Instance {
  dataValues: PermissionAttributes;
}

RolePermission Model

//src/models/role_permission.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {RolePermissionAttributes, RolePermissionInstance} from "./interfaces/role_permission";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<RolePermissionInstance, RolePermissionAttributes> => {
  const RolePermission = sequelize.define<RolePermissionInstance, RolePermissionAttributes>("RolePermission", {
    role_id: dataTypes.INTEGER,
    permission_id: dataTypes.INTEGER,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'role_permission',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    paranoid: true,
    underscored: true
  });

  return RolePermission;
}

Role Interface

//src/models/interfaces/role_permission.ts
import {Instance} from "sequelize";

export interface RolePermissionAttributes {
  id: number,
  role_id: number,
  permission_id: number,
  deleted_at: string
}

export interface RolePermissionInstance extends Instance {
  dataValues: RolePermissionAttributes;
}

ResetPasswordToken Model

//src/models/reset_password_token.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {ResetPasswordTokenAttributes, ResetPasswordTokenInstance} from "./interfaces/reset_password_token";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<ResetPasswordTokenInstance, ResetPasswordTokenAttributes> => {
  const ResetPasswordToken = sequelize.define<ResetPasswordTokenInstance, ResetPasswordTokenAttributes>("ResetPasswordToken", {
    user_id:dataTypes.INTEGER,
    token: dataTypes.STRING,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'reset_password_tokens',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true
  });

  return ResetPasswordToken;
}

ResetPasswordToken Interface

//src/models/interfaces/reset_password_token.ts
import {Instance} from "sequelize";

export interface ResetPasswordTokenAttributes {
  id: number,
  user_id: number,
  token: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface ResetPasswordTokenInstance extends Instance {
  dataValues: ResetPasswordTokenAttributes;
}

Post Model

//src/models/post.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {PostAttributes, PostInstance} from "./interfaces/post";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<PostInstance, PostAttributes> => {
  const Post = sequelize.define<PostInstance, PostAttributes>("Post", {
    post_id: dataTypes.INTEGER,
    title: dataTypes.STRING,
    body: dataTypes.TEXT,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'posts',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true
  });

  Post.afterDestroy((post: PostInstance, options: Object) => {
    sequelize.models.Comment.destroy({where: {post_id: post.dataValues.id}, individualHooks: true}); 
  });

  return Post;
}

Post Interface

//src/models/interfaces/post.ts
import {Instance} from "sequelize";

export interface PostAttributes {
  id: number,
  user_id: number,
  title: string,
  body: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface PostInstance extends Instance {
  dataValues: PostAttributes;
}

Comment Model

//src/models/comment.ts
import * as SequelizeStatic from "sequelize";
import {DataTypes, Sequelize} from "sequelize";
import {CommentAttributes, CommentInstance} from "./interfaces/comment";
import {SequelizeModels} from './index';

export default (sequelize: Sequelize, dataTypes: DataTypes):
  SequelizeStatic.Model<CommentInstance, CommentAttributes> => {
  const Comment = sequelize.define<CommentInstance, CommentAttributes>("Comment", {
    post_id: dataTypes.INTEGER,
    user_id: dataTypes.INTEGER,
    body: dataTypes.STRING,
    deleted_at: dataTypes.DATE
  }, {
    tableName: 'comments',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    indexes: [],
    paranoid: true,
    underscored: true
  });

  return Comment;
}

Comment Interface

//src/models/interfaces/comment.ts
import {Instance} from "sequelize";

export interface CommentAttributes {
  id: number,
  post_id: number,
  user_id: number,
  body: string,
  created_at: string,
  updated_at: string,
  deleted_at: string
}

export interface CommentInstance extends Instance {
  dataValues: CommentAttributes;
}

Migrations

We will start with migrations for each model. We will be using sequelize-cli installed globally (if you have installed it locally use it as: node_modules/.bin/sequelize)

sequelize migration:generate --name create_users_table
sequelize migration:generate --name create_roles_table
sequelize migration:generate --name create_user_role_table
sequelize migration:generate --name create_permissions_table
sequelize migration:generate --name create_role_permission_table
sequelize migration:generate --name create_reset_password_tokens_table
sequelize migration:generate --name create_posts_table
sequelize migration:generate --name create_comments_table

Users Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('users', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      firstname: Sequelize.STRING,
      lastname:  Sequelize.STRING,
      email: {
        type: Sequelize.STRING,
        unique: true
      },
      password:  Sequelize.STRING,
      bio: Sequelize.TEXT,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('users');
  }
};

Roles Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('roles', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: Sequelize.STRING,
      description: Sequelize.TEXT,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('roles');
  }
};

UserRole Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('user_role', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      user_id: Sequelize.INTEGER,
      role_id: Sequelize.INTEGER,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('user_role');
  }
};

Permissions Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('permissions', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: Sequelize.STRING,
      label: Sequelize.STRING,
      description: Sequelize.TEXT,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('permissions');
  }
};

RolePermission Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('role_permission', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      role_id: Sequelize.INTEGER,
      permission_id: Sequelize.INTEGER,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('role_permission');
  }
};

ResetPasswordTokens Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('reset_password_tokens', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      user_id: Sequelize.INTEGER,
      token: Sequelize.STRING,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('reset_password_tokens');
  }
};

Posts Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('posts', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      user_id: Sequelize.INTEGER,
      title: Sequelize.STRING,
      body: Sequelize.TEXT,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('posts');
  }
};

Comments Migration

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('comments', { 
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      post_id: Sequelize.INTEGER,
      user_id: Sequelize.INTEGER,
      body: Sequelize.TEXT,
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updated_at: {
        allowNull: true,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      deleted_at: {
        allowNull: true,
        type: Sequelize.DATE
      }
     });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('comments');
  }
};

Seeding our database

Sequelize-cli is equiped with commands to generate and execute seeds.

We are going to create a basic data, to play around. Then we switch to writing tests.

Users seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name user-seeder
sequelize seed:generate --name user-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-user-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('users', [
      {
        firstname: 'Joe',
        lastname: 'Admin',
        bio: 'I have been admins for years...',
        email: 'joe@test.com',
        password: 'password',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        firstname: 'Jane',
        lastname: 'Editor',
        bio: 'I have been editor for years...',
        email: 'jane@test.com',
        password: 'password',
        created_at: new Date(),
        updated_at: new Date()
      }
    ],{ individualHooks: true });
  },

  down: (queryInterface, Sequelize) => {
   
    return queryInterface.bulkDelete('users', null, {});
  }
};

Roles seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name role-seeder
sequelize seed:generate --name role-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-role-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('roles', [{
      name: 'admin',
      description: 'Has all possible permissions across the app',
      created_at: new Date(),
      updated_at: new Date()
    }],{ individualHooks: true });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('roles', null, {});
  }
};

UserRole seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name user-role-seeder
sequelize seed:generate --name user-role-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-user-role-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('user_role', [{
      user_id: 1,
      role_id: 1,
      created_at: new Date(),
      updated_at: new Date()
    }],{ individualHooks: true });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('user_role', null, {});
  }
};

Permissions seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name permissions-seeder
sequelize seed:generate --name permissions-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-permissions-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('permissions', [
      {
        name: 'user.store',
        label: 'Create user',
        description: 'Allows to create a new user',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        name: 'users.index',
        label: 'Get all users',
        description: 'Allows to get all users',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        name: 'user.show',
        label: 'Get user',
        description: 'Allows to get user for a given id',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        name: 'user.update',
        label: 'Update user',
        description: 'Allows to update/replace entire user resource, contains complete resource',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        name: 'user.delete',
        label: 'Delete user',
        description: 'Soft deletes user by a given id',
        created_at: new Date(),
        updated_at: new Date()
      }
  ],{ individualHooks: true });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('permissions', null, {});
  }
};

RolePermission seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name role-permission-seeder
sequelize seed:generate --name role-permission-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-role-permission-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('role_permission', [
      {
        role_id: 1,
        permission_id: 1,
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        role_id: 1,
        permission_id: 2,
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        role_id: 1,
        permission_id: 3,
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        role_id: 1,
        permission_id: 4,
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        role_id: 1,
        permission_id: 5,
        created_at: new Date(),
        updated_at: new Date()
      },
    ],{ individualHooks: true });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('role_permission', null, {});
  }
};

Posts seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name posts-seeder
sequelize seed:generate --name posts-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-posts-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('posts', [{
      user_id: 1,
      title: 'First article',
      body: 'This is my first article.. Tell me your thoughts in comments..',
      created_at: new Date(),
      updated_at: new Date()
    }],{ individualHooks: true });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('posts', null, {});
  }
};

Comments seeder

//when you use sequelize-cli locally you would use:
// node_modules/.bin/sequelize seed:generate --name comments-seeder
sequelize seed:generate --name comments-seeder 

//then edit src/database/seeds/YOUR_TIMESTAMP-comments-seeder.js
'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('comments', [
      {
        post_id: 1,
        user_id: 2,
        body: 'Your article is really long.. can you shorten it?',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        post_id: 1,
        user_id: 2,
        body: 'Actually, can you split it into two articles?',
        created_at: new Date(),
        updated_at: new Date()
      }
    ],{ individualHooks: true });
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('comments', null, {});
  }
};

Let's start TDD

We will start with a bit of scaffolding, then we will return to @todo on user model - we will create generateHash function.


//we create __tests__ (for storing tests) 
//and __mocks__ (for stroring mocks) 
//which is the name convention for jest framework
//if you want to know more about jest check the docs:
//https://facebook.github.io/jest
mkdir __tests__  __mocks__ __tests__/unit  __tests__/feature
//we create our first unit test file for all the utils stuff
//including generateHash function
touch __tests__/unit/utils.test.ts 

//then change your package.json and add:
{
  //...other settings
 "jest": {
    "coverageDirectory": "./coverage/",
    "collectCoverage": true,
    "transform": {
      ".(ts|tsx|js|jsx)": "/node_modules/ts-jest/preprocessor.js"
    },
    "testEnvironment": "node",
    "bail": true,
    "moduleFileExtensions": [
      "js",
      "jsx",
      "json",
      "ts",
      "tsx"
    ],
    "mapCoverage": true,
    "transformIgnorePatterns": [
      "node_modules/(?!(express-validator)/)"
    ],
    "testMatch": [
      "/__tests__/**/*.test.(ts|js)"
    ]
  }
}

//let's also add some test commands to package.json
//replace this
  "scripts" : {
    "build": "rm -rf ./build && tsc",
    "build:watch": "rm -rf ./build && tsc --watch",
    "start": "NODE_ENV=development && node ./build/server.js"
  }
//with that:
  "scripts" : {
    "build": "rm -rf ./build && tsc",
    "build:watch": "rm -rf ./build && tsc --watch",
    "start": "NODE_ENV=development && node ./build/server.js",
    "test": "jest --coverage --runInBand",
    "test:watch": "jest --coverage --runInBand --watch"
  }

//edit your__tests__/unit/utils.test.ts
//and add just to see how easy is testing with jest:
describe('UNIT: utils', () => {
  it('can add two numbers',()=>{
    const sum: number = 2 + 2;
    expect(sum).toBe(4);
  });
});

//we will want to pass mock instead of real bcrypt module
touch __mocks__/bcrypt.js
//edit __mocks__bcrypt.js
module.exports = {
  hash: jest.fn((password)=>{
    return Promise.resolve(`hased${password}`);
  })
}

If you run your test with command: yarn test you should see:

first_fake_test

We will now cover the following lines:


//src/models/user.ts
User.beforeCreate((user: UserInstance, options: Object) => {
    //@todo implement bcrypt
    user.dataValues.password = 'hash';
});

//we will add new folder called utils:
mkdir src/utils 
//and make a new files
touch src/utils/index.ts

//then replace your dummy test in src/__tests__/unit/utils.test.ts
describe('UNIT: utils', () => {

});

Share:



Warning! This site uses cookies
By continuing to browse the site, you are agreeing to our use of cookies. Read our privacy policy