Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Model Loader for Sequelize v5 & Typescript

Have used Sequelize before for projects ( v4 ), but attempting to start a new project with Sequelize v5 & Typescript

I have followed Sequelize's documentation for how to define Models at: https://sequelize.org/master/manual/typescript.html#usage-of--code-sequelize-define--code-

I have a working ORM now, but only when importing the actual model for use, not through importing the db from the model loader.
i.e. import { User } from "../db/models/user";

importing the db, just returns undefined when trying to access db.User.

Trying to figure out how to get the model loader to place nice with the Sequelize V5 and Typescript, but currently its coming up empty.

Now, I can tell that its searching for .js files. So obviously it won't be picking up the user.ts file. Changing this to .ts then gives me the error....

    at Sequelize.import (/node_modules/sequelize/lib/sequelize.js:486:38)
    at fs_1.default.readdirSync.filter.forEach.file (/src/db/models/index.ts:26:35)
    at Array.forEach (<anonymous>)
    at Object.<anonymous> (/src/db/models/index.ts:25:4)

I have been trying to get a clear answer from web searches, but seem to come up empty. It was a headache enough with trying to get everything to play nice.... and at this point I am running migrations/seeders as js files because I don't want to deal with the sequelize-typescript-cli or sequelize-typescript

src/db/models/user.ts User Model

import { Sequelize, Model, DataTypes, BuildOptions } from 'sequelize';
import { HasManyGetAssociationsMixin, HasManyAddAssociationMixin, HasManyHasAssociationMixin, Association, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin } from 'sequelize';
const db = require('./index')
import * as bcrypt from "bcryptjs";

export interface UserAttributes extends Model {
  id: string;
  email: string;
  username: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
  validatePassword(password: string): boolean;
  generateHash(password: string): string;
}

export type UserModel = typeof Model & {
  new (): UserAttributes;
};

export const User = <UserModel>db.sequelize.define("User", {
  id: {
    type: DataTypes.UUID,
    allowNull: false,
    primaryKey: true
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  },
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
  }
},
{
  tableName: "User",
  freezeTableName: true,
 });

 User.prototype.validatePassword = function (password: string) {

  return bcrypt.compareSync(password, this.password)
 }

 User.prototype.generateHash = function (password: string) {
    return bcrypt.hashSync(password, bcrypt.genSaltSync(10))
  }

src/db/models/index.ts Model Loader

'use strict';

import fs from "fs";
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(module.filename);

const env = process.env.NODE_ENV || 'development';
const config = require(`${__dirname}/../config/config.json`)[env];

interface DB {
  [key: string]: any;
}

var db: DB = {};

const sequelize = new Sequelize(config.database, config.username, config.password, config);

fs.readdirSync(__dirname)
  .filter(file => {
    return (
      file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
    );
  })
  .forEach(file => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model;
  });
// Important: creates associations based on associations defined in associate function in the model files
Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

Furthermore reading https://sequelize.org/master/manual/typescript.html#usage

There seems to be a little clearer ( but somewhat more redundant way ) of defining Models, but then how is this init method called when initializing Sequelize from index.js?

like image 200
ndyr Avatar asked Oct 16 '19 19:10

ndyr


1 Answers

So I have got it working, but in a non looping Model Loader. I ignored the define documentation, https://sequelize.org/master/manual/typescript.html#usage-of--code-sequelize-define--code-

for the long winded class approach here : https://sequelize.org/master/manual/typescript.html#usage

I will go through the process of setting up 2 models and their associations to hopefully help those that are trying to integrate Typescript with Sequelize v5.

WOULD DEFINITELY LOVE FEEDBACK ON THIS APPROACH.

Let's start with the classes for a User and associated Identities ( for accessing the API )

/src/db/models/user.ts

import { Sequelize, Model, DataTypes, BuildOptions } from 'sequelize';
import { Association, HasManyGetAssociationsMixin, HasManyAddAssociationMixin, HasManyHasAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin } from 'sequelize';
import { Identity } from './identity';
export class User extends Model {
  public id!: string; // Note that the `null assertion` `!` is required in strict mode.
  public active!: boolean;

  // timestamps!
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public getIdentities!: HasManyGetAssociationsMixin<Identity>; // Note the null assertions!
  public addIdentity!: HasManyAddAssociationMixin<Identity, number>;
  public hasIdentity!: HasManyHasAssociationMixin<Identity, number>;
  public countIdentities!: HasManyCountAssociationsMixin;
  public createIdentity!: HasManyCreateAssociationMixin<Identity>;

  // You can also pre-declare possible inclusions, these will only be populated if you
  // actively include a relation.
  public readonly identities?: Identity[]; // Note this is optional since it's only populated when explicitly requested in code

  public static associations: {
    identities: Association<User, Identity>;
  };

}

export function initUser(sequelize: Sequelize): void {
  User.init({
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
    },
    active: {
      type:DataTypes.BOOLEAN,
      defaultValue: true,
      allowNull: false
    }
  }, {
    tableName: 'User', 
    sequelize: sequelize, // this bit is important
  });


}

export function associateUser(): void {
  // Here we associate which actually populates out pre-declared `association` static and other methods.
  User.hasMany(Identity, {
    sourceKey: 'id',
    foreignKey: 'UserId',
    as: 'identities' // this determines the name in `associations`!
  });
}

/src/db/models/identity.ts

import { Sequelize, Model, DataTypes, BuildOptions } from 'sequelize';
import { Association, HasOneGetAssociationMixin, HasOneCreateAssociationMixin } from 'sequelize';
import { User } from './user'

import * as bcrypt from "bcryptjs";

export class Identity extends Model {
  public id!: string; // Note that the `null assertion` `!` is required in strict mode.
  public username!: string;
  public password!: string;
  public UserId: string;
  public active!: boolean;

  // timestamps!
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public getUser!: HasOneGetAssociationMixin<User>; // Note the null assertions!

  // You can also pre-declare possible inclusions, these will only be populated if you
  // actively include a relation.
  public readonly user?: User; // Note this is optional since it's only populated when explicitly requested in code

  public static associations: {
    user: Association<Identity, User>;
  };

  public validatePassword(password: string) : boolean {
    return bcrypt.compareSync(password, this.password)
  }
}

export function initIdentity(sequelize: Sequelize): void {
  Identity.init({
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
    },
    username: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
    UserId: {
      type: DataTypes.UUID,
      allowNull: true
    },
    active: {
      type:DataTypes.BOOLEAN,
      defaultValue: true,
      allowNull: false
    }
  }, {
    tableName: 'Identity', 
    sequelize: sequelize, // this bit is important
  });

}

export function associateIdentity(): void {
  // Here we associate which actually populates out pre-declared `association` static and other methods.
  Identity.belongsTo(User, {targetKey: 'id'});
}


So following this, we have declared all the "virtual" members and functions that will relate to Sequelize and the Database. In addition are the init<model> && associate<model> functions that will be used to tie everything together.

Note You may notice that in identity.ts, UserId is being used instead of userId in the associations. For some reason it kept assuming the association was going to be through UserId, even though I used userId. When doing a query, it complained that there was no column UserId ( but userId ). So updating that to a capital 'U' solved it. I am unsure about why it is was doing this at this point.

Now to tie it all together

/src/db/index.ts

import { initUser, associateUser } from "./user";
import { initIdentity, associateIdentity } from "./identity";

const Sequelize = require('sequelize');

const env = process.env.NODE_ENV || 'development';
const config = require(`${__dirname}/../config/config.json`)[env];


interface DB {
  [key: string]: any;
}

const sequelize = new Sequelize(config.database, config.username, config.password, config);

initUser(sequelize);
initIdentity(sequelize)

associateUser();
associateIdentity();

const db = {
  sequelize,
  Sequelize,
  User: sequelize.models.User,
  Identity: sequelize.models.Identity
}

module.exports = db;

The usual model loading to be done, went to the directory, found all the models and then imported them into sequelize. Now like I said before, trying to use the define in the model class caused problems when trying to go through this model loader, because non Typescript versions always looked for *.js and not *.ts. Changing to *.ts made everything crash on the define call. ( Not to mention since all of this code will be transpiled to js files, wouldn't this cause issue the other way around? )

But as you can see I am doing everything by hand instead of in a loop. There is probably a better looping way to do this, but at the moment this will suffice.

The models are initialized in sequelize with calls to their init<model> functions. After they are initialized, their associations are created through the function call of associate<model>

Before starting my express server, I require the index file, and this all gets kicked off. Boom.

Other things to note about my approach I did not want to install anymore packages than I needed. So I steered clear from sequelize-typescript and sequelize-typescript-cli. This means that all my seeder files and migration files need to be made by hand with out the use of a cli ( it's really not that bad ) and are not *.ts but *.js.

example: 20191017135846-create-identity.js

'use strict'
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable({tableName:'Identity'}, {
      id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        allowNull: false,
        autoIncrement: false,
        primaryKey: true,
      },
      username: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true,
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      UserId: {
        type: Sequelize.UUID,
        references: {
          model: 'User', // name of Target model
          key: 'id', // key in Target model that we're referencing
        },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      },
      active: {
        type: Sequelize.BOOLEAN,
        defaultValue: true,
        allowNull: false,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
      },
    })
  },
  down: (queryInterface) => {
    return queryInterface.dropTable({tableName:'Identity', schema:'public'})
  }
}

20191015141822-seed-users.js

'use strict'
var moment = require('moment');
var uuidv4 = require('uuid/v4');
const bcrypt = require('bcryptjs');

module.exports = {
  up: async (queryInterface) => {   
      // User
      const user1Id = uuidv4();
      await queryInterface.bulkInsert('User', 
        [
          {
            id:user1Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )
      await queryInterface.bulkInsert('Identity', 
        [
          {
            id:uuidv4(),
            username: "user1",
            password: bcrypt.hashSync('password', bcrypt.genSaltSync(10)),
            UserId: user1Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )

      const user2Id = uuidv4();
      await queryInterface.bulkInsert('User', 
        [
          {
            id:user2Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )
      await queryInterface.bulkInsert('Identity', 
        [
          {
            id:uuidv4(),
            username: "user2",
            password: bcrypt.hashSync('password', bcrypt.genSaltSync(10)),
            UserId: user2Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )

      const user3Id = uuidv4();
      await queryInterface.bulkInsert('User', 
        [
          {
            id:user3Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )
      await queryInterface.bulkInsert('Identity', 
        [
          {
            id:uuidv4(),
            username: "user3",
            password: bcrypt.hashSync('password', bcrypt.genSaltSync(10)),
            UserId: user3Id,
            createdAt: new Date( moment.utc().format() ), 
            updatedAt: new Date( moment.utc().format() )
          }
        ], 
      )


  },
  down: async (queryInterface) => {
    await queryInterface.bulkDelete({ tableName: 'User'}, null, {})
  }
}

which at this point you can run

sequelize db:migrate
sequelize db:seed:all

and have everything working and can access the db.

Now using classes/typescript I noticed that adding the models to and exported db object is redundant....

I can access the models needed through imports

import { User } from '../db/models/user' or require('./db/models/index')

which I can then do User.findAll() or with the other import db.User.findAll()

like image 152
ndyr Avatar answered Sep 20 '22 07:09

ndyr