/ah-sequelize-plugin

sequelize plugin for actionhero

Primary LanguageJavaScript

plugin

ah-sequelize-plugin

CircleCI

This plugin will use the sequelize orm to create api.models which contain your sequelize models.

Notes

Versions 1.0.0+ are only compatible with ActionHero versions 18.0.0+.

For versions compatible with ActionHero versions prior to 18.0.0, use version 0.9.0.

Setup

  • Install this plugin: npm install ah-sequelize-plugin --save
  • Add sequelize package: npm install sequelize --save
  • Add plugin to your project's ./config/plugins.js:
exports["default"] = {
  plugins: api => {
    return {
      "ah-sequelize-plugin": {
        path: __dirname + "/../node_modules/ah-sequelize-plugin"
      }
    };
  }
};

Add supported database packages

  • MySQL: npm install mysql2 --save
  • SQLite: npm install sqlite3 --save
  • Postgress: npm install --save pg pg-hstore
  • MSSQL: npm install --save tedious

For additional information on supported databases visit the Sequelize Docs.

Add optional dependencies

  • For automatic fixures: npm install sequelize-fixtures --save
  • For Sequelize CLI: npm install --save-dev sequelize-cli

Configuration

A ./config/sequelize.js file will be created which will store your database configuration. Read commented sections of configuration file for examples of multi-environment configurations.

To override the default location for models and/or migrations, use the modelsDir and migrationsDir configuration parameter with an array of paths relative to the project root.

const { URL } = require("url");

const databaseBaseName = "my-app";

exports.default = {
  sequelize: api => {
    let dialect = "postgres";
    let host = process.env.DB_HOST || "127.0.0.1";
    let port = process.env.DB_PORT || 5432;
    let database =
      process.env.DB_DATABASE ||
      `${databaseBaseName}_${api.env}${
        process.env.JEST_WORKER_ID ? "_" + process.env.JEST_WORKER_ID : ""
      }`;
    let username = process.env.DB_USER || undefined;
    let password = process.env.DB_PASS || undefined;

    // if your environment provides database information via a single JDBC-style URL like mysql://username:password@hostname:port/default_schema
    const connectionURL =
      process.env.DATABASE_URL ||
      process.env.MYSQL_URL ||
      process.env.PG_URL ||
      process.env.JAWSDB_URL;

    if (connectionURL) {
      const parsed = new URL(connectionURL);
      if (parsed.protocol) {
        dialect = parsed.protocol.slice(0, -1);
      }
      if (parsed.username) {
        username = parsed.username;
      }
      if (parsed.password) {
        password = parsed.password;
      }
      if (parsed.hostname) {
        host = parsed.hostname;
      }
      if (parsed.port) {
        port = parsed.port;
      }
      if (parsed.pathname) {
        database = parsed.pathname.substring(1);
      }
    }

    if (dialect === "postgresql") {
      dialect = "postgres";
    }

    return {
      autoMigrate: true,
      loadFixtures: false,
      logging: false,
      dialect: dialect,
      port: parseInt(port),
      database: database,
      host: host,
      username: username,
      password: password,
      modelsDir: [path.join(__dirname, "..", "models")],
      migrationsDir: [path.join(__dirname, "..", "migrations")],
      fixturesMatch: [
        path.join(__dirname, "..", "/__tests__/fixtures/*.{json,yml,js}")
      ]
    };
  }
};

Logging

The logging configuration parameter accepts either a false value, or a function which accepts a log value of type string and a event level value of type string (ex: console.log, api.log). If you are passing in a function for the logging parameter, you must also set the _toExpand config parameter to false in order for the logging function to not be called during ActionHero startup.

exports.test = {
  ...exports.default.sequelize(),
  sequelize: () => {
    return {
      _toExpand: false,
      database: "TEST_DB",
      logging: console.log
    };
  }
};

Models

Use the exports form of sequelize models in ./models with the file name matching that of the model, IE:

// from ./models/user.js

module.exports = function(sequelize, DataTypes, api) {
  // Define model structure and options
  const model = sequelize.define(
    "user",
    {
      id: {
        type: DataTypes.INTEGER.UNSIGNED,
        autoIncrement: true,
        primaryKey: true
      }
    },
    {
      paranoid: true
    }
  );

  // Attach Class methods
  model.rehydrate = function(user) {
    return this.build(user);
  };

  // Attach Instance methods
  model.prototype.apiData = function() {
    return {
      id: this.id
    };
  };

  return model;
};

Models are loaded into api.models, so the example above would be api.models.user. These module.exports allow for a third optional parameter "api" which is the ActionHero API object. This can be used to access configs and initializer functions, among other things.

Migrations

This plugin does not condone the use of Sequelize.sync() in favor of migrations. Keep you migrations in ./migrations and use the sequelize-cli to execute them.

An example migration to create a users table would look like:

// from ./migrations/20140101000001-create-users.js

module.exports = {
  up: async function(migration, DataTypes) {
    await migration.createTable("users", {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      name: DataTypes.STRING,
      email: DataTypes.STRING,
      phone: DataTypes.STRING,
      passwordHash: DataTypes.TEXT,
      passwordSalt: DataTypes.TEXT,
      createdAt: DataTypes.DATE,
      updatedAt: DataTypes.DATE
    });

    await migration.addIndex("users", ["email"], {
      indexName: "email_index",
      indicesType: "UNIQUE"
    });

    await migration.addIndex("users", ["name"], {
      indexName: "name_index",
      indicesType: "UNIQUE"
    });

    await migration.addIndex("users", ["phone"], {
      indexName: "phone_index",
      indicesType: "UNIQUE"
    });
  },

  down: async function(migration, DataTypes) {
    await migration.dropTable("users");
  }
};

You can use the sequelize-cli to create and execute migrations.

api.sequelize.migrate and api.sequelize.migrateUndo are now based on Umzug, and are maintained for legacy purposes. An Umzug instance is available at api.sequelize.umzug, and should be used to perform (and undo) migrations programatically using the official API.

If you want to sync, you can api.sequelize.sequelize.sync() or api.models.yourModel.sync();

By default, ah-sequelize-plugin will automatically execute any pending migrations when Actionhero starts up. You can disable this behaviour by adding autoMigrate: false to your sequelize config.

Associations

If you want to declare associations, best practice has you define an .associate() class method in your model such as:

module.exports = function(sequelize, DataTypes, api) {
  const model = sequelize.define("user", {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    }
  });

  model.associate = function(models) {
    this.hasMany(models.email);
  };

  return model;
};

Then you create an associations.js initializer within your project which might look like this:

const { Initializer, api } = require("actionhero");

module.exports = class AssociationsInitializer extends Initializer {
  constructor() {
    super();
    this.name = "AssociationsInitializer";
  }

  start() {
    Object.entries(api.models)
      .filter(([k, m]) => typeof m.associate === "function")
      .forEach(([k, m]) => m.associate(api.models));
  }
};

Fixtures

We use the sequelize-fixtures package to load in JSON-defined fixtures in the test NODE_ENV. Store your fixtures in ./test/fixtures/*.json or ./test/fixtures/*.yml.

By default, ah-sequelize-plugin will not automatically load your fixtures when Actionhero starts up. You can enable this behavior by adding loadFixtures: true to your sequelize config.