ProGrammer
ProGrammer

Reputation: 1031

Sequelize.js: Include unexpected. Element has to be either a Model, an Association or an object

I'm using Sequelize.js in my Node.js application and keep running into a very strange problem.

Background: I have two models, Account and AccountCategory as follows. My API endpoint calls the route /accounts which calls the accounts controller to do an Account.findAll() query.

Accounts model has a defaultScope to include the related category by default, without having to specify it each time inside the findAll({}) block.

Problem: When the Accounts model is attempting to access and return the data from the database, the defaultScope is trying to include the AccountCategory, Sequelize throws the error:

Include unexpected. Element has to be either a Model, an Association or an object.

I suspect it has to do with the fact that AccountCategory is placed after Account in my models folder when the models are being set up and thus not processed (associated). I base this on the fact that other associations like User and Role (ie. a user has a role) are fine using the same method (ie. no problem with path depth as this answer suggests).

I've spent the last 2 days trying to get the defaultScope working and stop producing this error without any luck. Similar questions do not provide an answer and I would greatly appreciate any help resolving this problem. Thanks.

Account:

module.exports = (sequelize, DataTypes) => {
    const Account = sequelize.define(
        "Account",
        {
            id: {
                type: DataTypes.INTEGER(11),
                allowNull: false,
                primaryKey: true,
                autoIncrement: true
            },
            name: {
                type: DataTypes.STRING(100)
            },
            category_id: {
                type: DataTypes.INTEGER(11),
                allowNull: false
            }
        },
        {
            timestamps: false,
            tableName: "Account",
            defaultScope: {
                include: [{
                    model: sequelize.models.AccountCategory,
                    as: "category"
                }]
            }
        }
    );

    Account.associate = models => {
        // Association: Account -> AccountCategory
        Account.belongsTo(models.AccountCategory, {
            onDelete: "CASCADE",
            foreignKey: {
                fieldName: "category_id",
                allowNull: false,
                require: true
            },
            targetKey: "id",
            as: "category"
        });
    };

    return Account;
};

Account Category:

module.exports = (sequelize, DataTypes) => {
    var AccountCategory = sequelize.define(
        "AccountCategory",
        {
            id: {
                type: DataTypes.INTEGER(11),
                allowNull: false,
                primaryKey: true,
                autoIncrement: true
            },
            name: {
                type: DataTypes.STRING(30),
                allowNull: false,
                unique: true
            }
        },
        {
            timestamps: false,
            tableName: "Account_Category"
        }
    );

    return AccountCategory;
};

Models Index:

const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || "development";
const db = {};

const sequelize = new Sequelize(
    process.env.DB_NAME,
    process.env.DB_USER,
    process.env.DB_PASS,
    {
        host: process.env.DB_HOST,
        dialect: "mysql",
        operatorAliases: false,

        pool: {
            max: 5,
            min: 0,
            acquire: 30000,
            idle: 10000
        }
    }
);

fs.readdirSync(__dirname)
    .filter(function(file) {
        return (
            file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
        );
    })
    .forEach(function(file) {
        var model = sequelize["import"](path.join(__dirname, file));
        db[model.name] = model;
    });

Object.keys(db).forEach(function(modelName) {
    if (db[modelName].associate) {
        db[modelName].associate(db);
    }
    db[modelName].associate(db);
});

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

module.exports = db;

Upvotes: 4

Views: 12421

Answers (1)

Dillon
Dillon

Reputation: 1554

You are correct when you say:

I suspect it has to do with the fact that AccountCategory is placed after Account in my models folder when the models are being set up and thus not processed (associated).

TLDR: Add a new function to your model class definition similar to the associate function, and use the addScope function to define any scopes that reference other models that may have not been initialized due to file tree order. Finally, call that new function the same way you call db[modelName].associate in your models.index.js file.

I had a similar problem and solved it by defining any scopes that reference any models, e.g. in an include, after all the models are initialized after running the following in your models/index.js file.

Here is an example:

models/agent.js

'use strict';
const { Model } = require('sequelize');
const camelCase = require('lodash/camelCase');
const { permissionNames } = require('../../api/constants/permissions');

module.exports = (sequelize, DataTypes) => {
  /**
   * @summary Agent model
   */
  class Agent extends Model {}

  Agent.init(
    {
      id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
      },
      firstName: {
        type: DataTypes.STRING,
        allowNull: false,
      },
      lastName: {
        type: DataTypes.STRING,
        allowNull: false,
      },
    },
    {
      sequelize,
      scopes: {
        // Works because the agent-role.js file / model comes before agent.js in the file tree
        [camelCase(permissionNames.readAgentRoles)]: {
          include: [
            {
              model: sequelize.models.AgentRole,
            },
          ],
        },
        // Doesn't work due to import order in models/index.js, i.e., agent.js is before role.js in the file tree
        // [camelCase(permissionNames.readRoles)]: {
        //   include: [
        //     {
        //       model: sequelize.models.Role,
        //     },
        //   ],
        // },
      },
    }
  );

  Agent.associate = function (models) {
    Agent.belongsToMany(models.Role, {
      through: 'AgentRole',
      onDelete: 'CASCADE', // default for belongsToMany
      onUpdate: 'CASCADE', // default for belongsToMany
      foreignKey: {
        name: 'agentId',
        type: DataTypes.INTEGER,
        allowNull: false,
      },
    });
    Agent.hasMany(models.AgentRole, {
      onDelete: 'CASCADE',
      onUpdate: 'CASCADE',
      foreignKey: {
        name: 'agentId',
        type: DataTypes.INTEGER,
        allowNull: false,
      },
    });
  };

  // Add a custom `addScopes` function to call after initializing all models in `index.js`
  Agent.addScopes = function (models) {
    Agent.addScope(camelCase(permissionNames.readRoles), {
      include: [
        {
          model: models.Role,
        },
      ],
    });
  };

  return Agent;
};

models/index.js

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const config = require('../database-config.js');
const db = {};

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

/**
 * Import and attach all of the model definitions within this 'models' directory to the sequelize instance.
 */
fs.readdirSync(__dirname)
  .filter((file) => {
    return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
  })
  .forEach((file) => {
    // Here is where file tree order matters... the sequelize const may not have the required model added to it yet
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
  // We need to add scopes that reference other tables once they have all been initialized
  if (db[modelName].addScopes) {
    db[modelName].addScopes(db);
  }
});

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

module.exports = db;

Goodluck!

Upvotes: 0

Related Questions