Using UUIDs in Id Column With Sequelize and PostgreSQL

Using UUIDs in Id Column With Sequelize and PostgreSQL

I've recently been working on firming my Node JS skills, and to do this, I decided to rewrite a project I originally wrote in Laravel as a full stack, server rendered app. The idea is to rebuild it using Node JS as a modern app with an API as the backend and a separate frontend, with a mobile app probably, further down the line.

The stack i chose for this project is PEVN, which is PostgreSQL as the database, Express and Node JS for the backend API and Vue JS for the frontend. I also decided to use Sequelize as the ORM to simplify database interactions.

As I was using an SQL database, and knew this was a project whose requirements could change over time, requiring revisions to the database schema, probably even after deployment, I had to use Sequelize migrations. This decision meant I had to use the Sequelize CLI.

Generating models and migrations using the Sequelize CLI is really simple, but with a few gotchas I found.

Firstly, if you're not using the auto generated and auto incrementing integer Ids as your primary key, it means that you would have to define the primary key in both the migration and the model. See examples below:

// Role Migration
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Roles', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        type: Sequelize.STRING
      },
      display_name: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Roles');
  }
};
// Role Model
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Role extends Model {
    static associate(models) {
      Role.belongsToMany(models.User, { through: 'RoleUser' })
    }
  };
  Role.init({
    name: DataTypes.STRING,
    display_name: DataTypes.STRING
  }, {
    sequelize,
    modelName: 'Role',
  });

  return Role;
};

Notice how the id field is only defined in the migration file above. However, when changing the column type to UUID as I did with the user model, for security and privacy reasons, the field would have to be defined in both the migration and the model, as below:

// User Migration
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Users', {
      id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        allowNull: false,
        primaryKey: true,
        autoIncrement: false,
      },
      name: {
        type: Sequelize.STRING,
        allowNull: false
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false
      },
      avatar: {
        type: Sequelize.STRING,
        allowNull: false
      },
     email_verified_at: {
        allowNull: true,
        type: Sequelize.DATE
      },
      remember_token: {
        allowNull: true,
        type: Sequelize.STRING
      },
     createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
    });
  }
down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Users');
  }
};
// User Model
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class User extends Model {
    static associate(models) {
      User.belongsToMany(models.Role, { through: 'RoleUser' })
    }
  };
  User.init({
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    name: DataTypes.STRING,
    email: {
      type: DataTypes.STRING,
      unique: true,
    },
    password: DataTypes.STRING,
    avatar: DataTypes.STRING,
    email_verified_at: DataTypes.STRING,
    remember_token: DataTypes.STRING,
    }, {
    sequelize,
    modelName: 'User',
  });

  return User;
};

Without defining the id field explicitly in the User model, I kept getting a not null constraint violation from the database. After searching through github issues and discussions, everybody seemed to have a different solution, but the general gist of it seemed to be that passing "DEFAULT" to the database was not enough to have it generate the UUID using the defined defaultValue.