mindrally

sequelize

3
0
# Install this skill:
npx skills add Mindrally/skills --skill "sequelize"

Install specific skill from multi-skill repository

# Description

Guidelines for developing with Sequelize, a promise-based Node.js ORM supporting PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server

# SKILL.md


name: sequelize
description: Guidelines for developing with Sequelize, a promise-based Node.js ORM supporting PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server


Sequelize Development Guidelines

You are an expert in Sequelize ORM, Node.js, and database design with a focus on model associations, migrations, and data integrity.

Core Principles

  • Sequelize is a promise-based ORM for Node.js and TypeScript
  • Supports PostgreSQL, MySQL, MariaDB, SQLite, and Microsoft SQL Server
  • Uses model definitions with DataTypes for schema declaration
  • Provides comprehensive support for associations, transactions, and hooks
  • Migrations should be used for all schema changes in production

Database Connection

Basic Setup

import { Sequelize } from "sequelize";

// Option 1: Connection URI
const sequelize = new Sequelize(process.env.DATABASE_URL!, {
  dialect: "postgres",
  logging: process.env.NODE_ENV === "development" ? console.log : false,
  pool: {
    max: 10,
    min: 0,
    acquire: 30000,
    idle: 10000,
  },
});

// Option 2: Individual parameters
const sequelize = new Sequelize({
  dialect: "postgres",
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || "5432"),
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  logging: false,
});

// Test connection
async function testConnection() {
  try {
    await sequelize.authenticate();
    console.log("Connection established successfully.");
  } catch (error) {
    console.error("Unable to connect to the database:", error);
  }
}

Model Definition

Basic Model with TypeScript

import {
  Model,
  DataTypes,
  InferAttributes,
  InferCreationAttributes,
  CreationOptional,
} from "sequelize";
import { sequelize } from "./database";

class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  declare id: CreationOptional<number>;
  declare email: string;
  declare name: string | null;
  declare isActive: CreationOptional<boolean>;
  declare createdAt: CreationOptional<Date>;
  declare updatedAt: CreationOptional<Date>;
}

User.init(
  {
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    email: {
      type: DataTypes.STRING(255),
      allowNull: false,
      unique: true,
      validate: {
        isEmail: true,
      },
    },
    name: {
      type: DataTypes.STRING(255),
      allowNull: true,
    },
    isActive: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
    },
    createdAt: DataTypes.DATE,
    updatedAt: DataTypes.DATE,
  },
  {
    sequelize,
    tableName: "users",
    modelName: "User",
    underscored: true, // Use snake_case for column names
  }
);

export { User };

Data Types Reference

// String types
DataTypes.STRING(255)      // VARCHAR(255)
DataTypes.TEXT             // TEXT
DataTypes.TEXT("tiny")     // TINYTEXT (MySQL)

// Numeric types
DataTypes.INTEGER          // INTEGER
DataTypes.BIGINT           // BIGINT
DataTypes.FLOAT            // FLOAT
DataTypes.DOUBLE           // DOUBLE
DataTypes.DECIMAL(10, 2)   // DECIMAL(10,2)

// Boolean
DataTypes.BOOLEAN          // BOOLEAN / TINYINT(1)

// Date/Time
DataTypes.DATE             // DATETIME/TIMESTAMP
DataTypes.DATEONLY         // DATE
DataTypes.TIME             // TIME

// Binary
DataTypes.BLOB             // BLOB

// JSON
DataTypes.JSON             // JSON (if supported)
DataTypes.JSONB            // JSONB (PostgreSQL)

// UUID
DataTypes.UUID             // UUID
DataTypes.UUIDV4           // Auto-generate UUID v4

// Enum
DataTypes.ENUM("active", "inactive", "pending")

// Array (PostgreSQL only)
DataTypes.ARRAY(DataTypes.STRING)

Associations

One-to-One

class User extends Model {
  declare id: number;
  declare profile?: Profile;
}

class Profile extends Model {
  declare id: number;
  declare userId: number;
  declare bio: string;
  declare user?: User;
}

// Define associations
User.hasOne(Profile, {
  foreignKey: "userId",
  as: "profile",
});

Profile.belongsTo(User, {
  foreignKey: "userId",
  as: "user",
});

One-to-Many

class User extends Model {
  declare id: number;
  declare posts?: Post[];
}

class Post extends Model {
  declare id: number;
  declare authorId: number;
  declare title: string;
  declare author?: User;
}

// Define associations
User.hasMany(Post, {
  foreignKey: "authorId",
  as: "posts",
});

Post.belongsTo(User, {
  foreignKey: "authorId",
  as: "author",
});

Many-to-Many

class Post extends Model {
  declare id: number;
  declare tags?: Tag[];
}

class Tag extends Model {
  declare id: number;
  declare name: string;
  declare posts?: Post[];
}

// Define associations with junction table
Post.belongsToMany(Tag, {
  through: "PostTags",
  foreignKey: "postId",
  otherKey: "tagId",
  as: "tags",
});

Tag.belongsToMany(Post, {
  through: "PostTags",
  foreignKey: "tagId",
  otherKey: "postId",
  as: "posts",
});

Querying

Basic Queries

// Find all
const users = await User.findAll();

// Find with conditions
const activeUsers = await User.findAll({
  where: {
    isActive: true,
  },
});

// Find one
const user = await User.findOne({
  where: { email: "[email protected]" },
});

// Find by primary key
const user = await User.findByPk(1);

// Find or create
const [user, created] = await User.findOrCreate({
  where: { email: "[email protected]" },
  defaults: {
    name: "New User",
  },
});

Advanced Queries with Operators

import { Op } from "sequelize";

// Multiple conditions
const users = await User.findAll({
  where: {
    [Op.and]: [
      { isActive: true },
      { createdAt: { [Op.gte]: new Date("2024-01-01") } },
    ],
  },
});

// OR condition
const users = await User.findAll({
  where: {
    [Op.or]: [{ name: "John" }, { name: "Jane" }],
  },
});

// LIKE
const users = await User.findAll({
  where: {
    email: { [Op.like]: "%@example.com" },
  },
});

// IN
const users = await User.findAll({
  where: {
    id: { [Op.in]: [1, 2, 3] },
  },
});

// Comparison operators
const users = await User.findAll({
  where: {
    id: { [Op.gt]: 10 },      // Greater than
    age: { [Op.gte]: 18 },    // Greater than or equal
    score: { [Op.lt]: 100 },  // Less than
    rank: { [Op.lte]: 5 },    // Less than or equal
    status: { [Op.ne]: "inactive" }, // Not equal
  },
});

Eager Loading (Include)

// Load user with posts
const user = await User.findOne({
  where: { id: 1 },
  include: [
    {
      model: Post,
      as: "posts",
    },
  ],
});

// Nested includes
const user = await User.findOne({
  where: { id: 1 },
  include: [
    {
      model: Post,
      as: "posts",
      include: [
        {
          model: Tag,
          as: "tags",
        },
      ],
    },
  ],
});

// Include with conditions
const users = await User.findAll({
  include: [
    {
      model: Post,
      as: "posts",
      where: {
        publishedAt: { [Op.ne]: null },
      },
      required: false, // LEFT JOIN (include users without posts)
    },
  ],
});

Pagination and Ordering

const page = 1;
const pageSize = 20;

const { count, rows: users } = await User.findAndCountAll({
  where: { isActive: true },
  order: [
    ["createdAt", "DESC"],
    ["name", "ASC"],
  ],
  limit: pageSize,
  offset: (page - 1) * pageSize,
});

const totalPages = Math.ceil(count / pageSize);

Aggregations

// Count
const count = await User.count({
  where: { isActive: true },
});

// Sum
const total = await Order.sum("amount", {
  where: { status: "completed" },
});

// Max/Min
const maxPrice = await Product.max("price");
const minPrice = await Product.min("price");

// Group by
const stats = await Order.findAll({
  attributes: [
    "status",
    [sequelize.fn("COUNT", sequelize.col("id")), "count"],
    [sequelize.fn("SUM", sequelize.col("amount")), "total"],
  ],
  group: ["status"],
});

CRUD Operations

Create

// Create single record
const user = await User.create({
  email: "[email protected]",
  name: "John Doe",
});

// Bulk create
const users = await User.bulkCreate(
  [
    { email: "[email protected]", name: "User 1" },
    { email: "[email protected]", name: "User 2" },
  ],
  {
    validate: true, // Run validations on each record
  }
);

// Create with associations
const user = await User.create(
  {
    email: "[email protected]",
    name: "John",
    profile: {
      bio: "Hello world",
    },
  },
  {
    include: [{ model: Profile, as: "profile" }],
  }
);

Update

// Update single record
const user = await User.findByPk(1);
if (user) {
  user.name = "Jane Doe";
  await user.save();
}

// Update with new data
await user.update({
  name: "Jane Doe",
  isActive: false,
});

// Bulk update
await User.update(
  { isActive: false },
  {
    where: {
      lastLoginAt: { [Op.lt]: new Date("2024-01-01") },
    },
  }
);

Delete

// Delete single record
const user = await User.findByPk(1);
if (user) {
  await user.destroy();
}

// Bulk delete
await User.destroy({
  where: {
    isActive: false,
  },
});

// Soft delete (requires paranoid: true in model options)
await user.destroy(); // Sets deletedAt instead of deleting

// Restore soft-deleted record
await user.restore();

Transactions

// Managed transaction (recommended)
const result = await sequelize.transaction(async (t) => {
  const user = await User.create(
    {
      email: "[email protected]",
      name: "User",
    },
    { transaction: t }
  );

  const post = await Post.create(
    {
      title: "First Post",
      authorId: user.id,
    },
    { transaction: t }
  );

  return { user, post };
});

// Unmanaged transaction
const t = await sequelize.transaction();

try {
  const user = await User.create(
    { email: "[email protected]" },
    { transaction: t }
  );

  await Post.create(
    { title: "Post", authorId: user.id },
    { transaction: t }
  );

  await t.commit();
} catch (error) {
  await t.rollback();
  throw error;
}

Hooks

User.init(
  {
    // ... columns
  },
  {
    sequelize,
    hooks: {
      beforeValidate: (user) => {
        // Normalize email
        if (user.email) {
          user.email = user.email.toLowerCase().trim();
        }
      },
      beforeCreate: async (user) => {
        // Hash password
        if (user.password) {
          user.password = await bcrypt.hash(user.password, 10);
        }
      },
      afterCreate: async (user) => {
        // Send welcome email
        await sendWelcomeEmail(user.email);
      },
      beforeDestroy: async (user) => {
        // Clean up related data
        await Post.destroy({ where: { authorId: user.id } });
      },
    },
  }
);

// Or define hooks separately
User.addHook("beforeSave", "hashPassword", async (user) => {
  if (user.changed("password")) {
    user.password = await bcrypt.hash(user.password, 10);
  }
});

Hook with Transaction Access

User.addHook("beforeCreate", async (user, options) => {
  if (options.transaction) {
    // Use the same transaction for related operations
    await AuditLog.create(
      {
        action: "user_created",
        userId: user.id,
      },
      { transaction: options.transaction }
    );
  }
});

Validations

User.init(
  {
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        isEmail: {
          msg: "Must be a valid email address",
        },
        notEmpty: true,
      },
    },
    age: {
      type: DataTypes.INTEGER,
      validate: {
        min: {
          args: [0],
          msg: "Age must be non-negative",
        },
        max: {
          args: [150],
          msg: "Age must be realistic",
        },
      },
    },
    username: {
      type: DataTypes.STRING,
      validate: {
        len: {
          args: [3, 30],
          msg: "Username must be between 3 and 30 characters",
        },
        isAlphanumeric: {
          msg: "Username must contain only letters and numbers",
        },
        // Custom validator
        async isUnique(value: string) {
          const existing = await User.findOne({
            where: { username: value },
          });
          if (existing) {
            throw new Error("Username already taken");
          }
        },
      },
    },
  },
  { sequelize }
);

Migrations

Creating Migrations

# Generate migration
npx sequelize-cli migration:generate --name create-users

# Run migrations
npx sequelize-cli db:migrate

# Undo last migration
npx sequelize-cli db:migrate:undo

# Undo all migrations
npx sequelize-cli db:migrate:undo:all

Migration File Structure

// migrations/20240101000000-create-users.js
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable("users", {
      id: {
        type: Sequelize.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      email: {
        type: Sequelize.STRING(255),
        allowNull: false,
        unique: true,
      },
      name: {
        type: Sequelize.STRING(255),
        allowNull: true,
      },
      is_active: {
        type: Sequelize.BOOLEAN,
        defaultValue: true,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
      },
      updated_at: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
      },
    });

    await queryInterface.addIndex("users", ["email"]);
  },

  async down(queryInterface) {
    await queryInterface.dropTable("users");
  },
};

Best Practices

Use Eager Loading to Avoid N+1

// Bad: N+1 queries
const users = await User.findAll();
for (const user of users) {
  const posts = await user.getPosts(); // Query per user
}

// Good: Single query with include
const users = await User.findAll({
  include: [{ model: Post, as: "posts" }],
});

Always Use Migrations in Production

// sequelize config
{
  development: {
    // ...
  },
  production: {
    // ...
    migrationStorageTableName: "sequelize_migrations",
    seederStorageTableName: "sequelize_seeds",
  }
}

Use Aliases for Associations

// Good: Using aliases for clarity
User.hasMany(Post, { as: "posts", foreignKey: "authorId" });

// Query with alias
const user = await User.findOne({
  include: [{ model: Post, as: "posts" }],
});

Validate in Bulk Operations

// Always validate when using bulkCreate
await User.bulkCreate(users, { validate: true });

Use Transactions for Data Integrity

// Wrap related operations in transactions
await sequelize.transaction(async (t) => {
  // All operations use the same transaction
  const order = await Order.create({ ... }, { transaction: t });
  await OrderItem.bulkCreate(items, { transaction: t });
  await Inventory.decrement("quantity", { ... }, { transaction: t });
});

Scope Common Queries

User.addScope("active", {
  where: { isActive: true },
});

User.addScope("withPosts", {
  include: [{ model: Post, as: "posts" }],
});

// Use scopes
const activeUsers = await User.scope("active").findAll();
const usersWithPosts = await User.scope(["active", "withPosts"]).findAll();

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.