question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

4.0 Model definition syntax

See original GitHub issue

Because the dicussion is spread across many issues and PRs, I wanted to open a new issue to discuss the future syntax for model definition.

Currently, besides the old define(), this syntax is possible in v4.0.0-1:

export class User extends Model {
 
    get fullName() {
        return this.firstName + ' ' + this.lastName;
    }
 
    set fullName(fullName) {
        const names = fullName.split(' ');
        this.lastName = names.pop();
        this.firstName = names.join(' ');
    }
}
User.init(({
    username: {
        type: DataTypes.STRING,
        primaryKey: true
    },
    lastName: DataTypes.STRING,
    firstName: DataTypes.STRING,
}, { sequelize })

If you prefer to not do the initialization (which also attaches the connection to the model) inside the model file, but rather from a model index file, this is also possible:

export class User extends Model {
 
    get fullName() {
        return this.firstName + ' ' + this.lastName;
    }
 
    set fullName(fullName) {
        const names = fullName.split(' ');
        this.lastName = names.pop();
        this.firstName = names.join(' ');
    }

    static init(sequelize) {
        super.init({
            username: {
                type: DataTypes.STRING,
                primaryKey: true
            },
            lastName: DataTypes.STRING,
            firstName: DataTypes.STRING,
        }, { sequelize })
    }
}

With the help of a decorator library and Babel transpilation, this is also possible:

@Options({ sequelize })
@Attributes({
    username: {
        type: DataTypes.STRING,
        primaryKey: true
    },
    lastName: DataTypes.STRING,
    firstName: DataTypes.STRING,
})
export class User extends Model {
 
    get fullName() {
        return this.firstName + ' ' + this.lastName;
    }
 
    set fullName(fullName) {
        const names = fullName.split(' ');
        this.lastName = names.pop();
        this.firstName = names.join(' ');
    }
}

And with TypeScript you can go even further:

@Options({ sequelize })
export class User extends Model {
 
    @Attribute({
        type: DataTypes.STRING,
        primaryKey: true
    })
    public username: string;
 
    @Attribute(DataTypes.STRING)
    public firstName: string;
 
    @Attribute() // Type is inferred as DataTypes.STRING 
    public lastName: string;
 
    get fullName(): string {
        return this.firstName + ' ' + this.lastName;
    }
 
    set fullName(fullName: string) {
        const names = fullName.split(' ');
        this.lastName = names.pop();
        this.firstName = names.join(' ');
    }
}

BUT we definitely do not want to require the usage of any transpiler. The Model.init() API feels a bit weird, especially how the sequelize connection is passed. Some thoughts:

  • If we want to make models independent one day from connections, this is has the benefit of easily omitting the sequelize option
  • But we would probably still need to require that models are registered in a model manager, so we could inverse the syntax (something like sequelize.add(Model) instead of passing the connection. That is not implemented atm though, currently the model needs to know about the connection.

Please chime in and share your thoughts and ideas!

Issues/PRs for reference: https://github.com/sequelize/sequelize/issues/5898 https://github.com/sequelize/sequelize/pull/6439 https://github.com/sequelize/sequelize/pull/5877 https://github.com/sequelize/sequelize/pull/5924 https://github.com/sequelize/sequelize/issues/5205 https://github.com/sequelize/sequelize/issues/4728

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:17
  • Comments:83 (30 by maintainers)

github_iconTop GitHub Comments

77reactions
zhanwenchencommented, Sep 15, 2017

After all night of tweaking, I finally have a working example, based on @leebenson and @felixfbecker’s posts on this thread. @snewell92 take a look at this.

@felixfbecker one needs to explicitly return super.init({...}, {sequelize}); in static init(...). The child init function should also take a sequelize instance.

To use, put all 4 files in an empty directory and do node index.js.

@mickhansen In my example, using Node 6.11.3 LTS, I did not need a transpiler for static.

index.js
// models/index.js
/**
  index.js is an import utility that grabs all models in the same folder,
  and instantiate a Sequelize object once for all models (instead of for each model).
  This is done by passing the single Sequelize object to each
  model as a reference, which each model then piggy-backs (sequelize.define())
  for creating a single db class model.
*/

"use strict"; // typical JS thing to enforce strict syntax

const fs = require("fs"); // file system for grabbing files
const path = require("path"); // better than '\/..\/' for portability
const Sequelize = require("sequelize"); // Sequelize is a constructor
const env = process.env.NODE_ENV || "development"; // use process environment
const config = require(path.join(__dirname, '..', 'config.js'))[env] // Use the .config.json file in the parent folder
const sequelize = new Sequelize(config.database, config.username, config.password, {
  dialect: config.dialect,
});

// Load each model file
const models = Object.assign({}, ...fs.readdirSync(__dirname)
  .filter(file =>
    (file.indexOf(".") !== 0) && (file !== "index.js")
  )
  .map(function (file) {
    const model = require(path.join(__dirname, file));
    // console.log(model.init(sequelize).tableName)
    return {
      [model.name]: model.init(sequelize),
    };
  })
);

// Load model associations
for (const model of Object.keys(models)) {
  typeof models[model].associate === 'function' && models[model].associate(models);
}

module.exports = models;

Post.js
// models/Post.js
/**
  Post.js
  Class model for Post
*/

'use strict';

const Sequelize = require('sequelize');

module.exports =
  class Post extends Sequelize.Model {
    static init(sequelize) {
      return super.init({
        title: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        body: {
          type: Sequelize.TEXT,
          allowNull: false,
        },
        assets: {
          type: Sequelize.JSON,
          allowNull: true
        },
      }, { sequelize })
    };

    static associate(models) {
      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.Post);
      this.hasMany(models.Comment, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });

      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.Post);
      this.belongsTo(models.User, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });
    }
  }
User.js
// models/User.js
/**
  User.js
  Class model for User
*/

'use strict';

const Sequelize = require('sequelize');

module.exports =
  class User extends Sequelize.Model {
    static init(sequelize) {
      return super.init({
        firstName: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        lastName: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        username: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        password_hash: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        salt: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        email: {
          type: Sequelize.STRING,
          allowNull: false,
          validate: {
            isEmail: true
          }
        },
        isActive: {
          type: Sequelize.BOOLEAN
        }
      }, { sequelize })
    };

    static associate(models) {
      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.User);
      this.hasMany(models.Post, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });

      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.User);
      this.hasMany(models.Comment, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });
    }
  }
Comment.js
// models/Comment.js
/**
  Comment.js
  Class model for Comment
*/

'use strict';

const Sequelize = require('sequelize');

module.exports =
  class Comment extends Sequelize.Model {
    static init(sequelize) {
      return super.init({
        title: {
          type: Sequelize.STRING,
          allowNull: false,
        },
        body: {
          type: Sequelize.TEXT,
          allowNull: false,
        }
      }, { sequelize })
    };

    static associate(models) {
      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.Comment);
      this.belongsTo(models.User, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });

      // Using additional options like CASCADE etc for demonstration
      // Can also simply do Task.belongsTo(models.Comment);
      this.belongsTo(models.Post, {
        onDelete: "CASCADE",
        foreignKey: {
          allowNull: false
        }
      });
    }
  }
38reactions
leebensoncommented, Sep 1, 2016

I understand that your example above is the way it works now. I provided alternate syntax because it doesn’t seem like you’re settled a pattern yet, and IMO, my way was cleaner.

FWIW, here’s a more complete example of what my User model currently looks like (in production; cut for brevity)

import Sequelize, { Model } from 'sequelize';

export default class User extends Model {

  /* FIELDS */

  static fields() {
    return {
      id: {
        type: Sequelize.UUID,
        defaultValue: Sequelize.UUIDV4,
        primaryKey: true,
        allowNull: false,
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      firstName: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      lastName: {
        type: Sequelize.STRING,
        allowNull: false,
      },
    };
  }

  /* RELATIONSHIPS */

  static associate(models) {
    this.hasMany(models.Session);
  }

  /* CLASS-LEVEL FUNCTIONS */

  // Create a new user
  static create(args) {
    // logic to create a user
  }

  /* GRAPHQL MUTATIONS */
  static mutations(g, t) {
    return {

      // Create a new user
      createUser: {
        type: t.User,
        args: {
          email: {
            type: g.GraphQLString,
          },
          firstName: {
            type: g.GraphQLString,
          },
          lastName: {
            type: g.GraphQLString,
          },
          password: {
            type: g.GraphQLString,
          },
        },
        async resolve(root, args) {
          return create(args);
        },
      },
    };
  }
}

There are a few things here, IMO, that are improved over the syntax I’ve seen so far:

  1. Few dependencies. Importing Sequelize + Model is all I need.
  2. I don’t need to worry about an instantiated sequelize object or pass it around models. That’s defined in another file, where I also have a ‘model manager’ that pulls in the models. IMO, models should be loaded into the connection… not the other way around. Otherwise every model winds up importing the same object and repeating the boilerplate.
  3. The associate static (pulled from the CLI) is actually one of the best parts of the model loader, IMO. It ensures that Model A can always reference Model B, and it neatly avoids circular dependencies. In your example above, if User.belongsTo(UserGroup) and UserGroup.hasMany(User), you’re relying on both model files being fully instantiated before exporting and importing into sister models. It’s neater to define this behaviour in a model manager when you’re already working with loaded data.
  4. It fills well in wider ecosystems. My model classes aren’t ‘just’ for Sequelize; they group together related logic for other parts of my app (in the above example, GraphQL). So having a common DI pattern where variables are passed to static fields is an easy way to reason about the purpose of a model class. Having fields() and attributes() satisfies an interface-like pattern for Sequelize, the same way mutations() does for GraphQL.
  5. It works out the box. That’s not a big deal for me (my webpack build is 500+ lines long, and I’m transpiling pieces for both the server and the front-end from one universal repo - so I’m used to designing for polyfills), but for other users following along with your documentation, it would be ideal to show a really easy way to define and manage models that’s as simple as something like this…

user.js

import { Model } from 'sequelize';

export class User extends Model {
  static fields() {
    // return field def...
  }

  static options() {
    // return options...
  }

  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

db.js

import Sequelize from 'sequelize';
import UserModel from './user'

export const connection = new Sequelize(/* connection vars */);
connection.addModel(UserModel);

app.js

import { connection } from './db';

connection.models.User.findOne(/* search args */).then(user => {
  // user = found user
});

Instead, a user has to:

  1. Use TypeScript, or fiddle with .babelrc to get the right combination of plug-ins (if they’re following the decorate route).
  2. Write classes empty classes and then use a slightly weird class.init() initialisation, that takes two parameters defining field types and a sequelize object- both completely disconnected from the class that’s being initialised OR…
  3. Use decorators, which are actually exactly the same thing, but just re-shuffled to appear before the class rather than after it.
  4. Figure out a way for models to play well with one another, whilst avoiding circular dependencies and juggling multiple imports.
  5. Remember parameter order and do things like pass { sequelize } in the ‘options’ object, which a model manager could inject behind the scenes.

I’m just providing friendly feedback, as a user of Sequelize since the very early days. In v4 you have a chance to drastically reduce boilerplate. I think what you currently have just adds noise. Why is it any better than defining instanceMethods or classMethods on a giant object to define()?

Again, I’m brand new to v4, so maybe you’ve figured those kinds of things out and I’m way off the mark. Maybe you already have a model manager that wires stuff up. I’m still figuring out my way around v4.

Read more comments on GitHub >

github_iconTop Results From Across the Web

1.2 Syntax Model
an expand phase that processes a syntax object to produce one that is fully parsed.
Read more >
Models - Django documentation
A model is the single, definitive source of information about your data. It contains the essential fields and behaviors of the data you're...
Read more >
HTML 4 Document Type Definition - W3C
HTML has two basic content models: %inline; character level elements and text strings %block; block-like elements e.g. paragraphs and lists --> <!
Read more >
Model | LoopBack Documentation
A model describes business domain objects, for example, Customer , Address , and Order . It usually defines a list of properties with...
Read more >
IBIS MODELING COOKBOOK For IBIS Version 4.0
conditions that define the corners of the model for proper data extraction. ... Note that the information provided under IBIS version 4.0 and...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found