Skip to main content

Adoption guide: Sequelize → NormalJS

This guide helps you migrate an existing Sequelize codebase to NormalJS. It maps the core concepts and shows equivalent code for models, associations, queries, transactions, hooks, and more.

Key differences at a glance

  • Model definition: Sequelize uses sequelize.define() with DataTypes; NormalJS uses a class with a static fields object.
  • Relations: Sequelize associations → NormalJS relation fields (many-to-one, one-to-many, many-to-many).
  • Reads: NormalJS selects just id by default for speed, and lazily hydrates fields; you can enable request caching with .cache(ttl).
  • Extension and inheritance: NormalJS lets you extend models by re-registering them and supports inheritance with discriminators.
  • Cache: NormalJS has a built-in in-memory cache (per-connection) with optional UDP clustering.
  • Schema: NormalJS can sync tables from fields (good for prototyping); keep migrations for production.

1) Model definitions

Sequelize:

const User = sequelize.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING, unique: true, allowNull: false },
active: { type: DataTypes.BOOLEAN, defaultValue: true },
});

NormalJS:

class Users {
static _name = 'Users';
static table = 'users';
static fields = {
id: 'primary',
email: { type: 'string', unique: true, required: true },
active: { type: 'boolean', default: true },
};
}
repo.register(Users);

DataTypes mapping (typical):

  • INTEGER → integer (or primary for PK)
  • FLOAT/DECIMAL → float
  • STRING → string (with size), TEXT → text
  • BOOLEAN → boolean
  • DATE/DATEONLY → datetime/date
  • JSON → json

2) Associations → relation fields

Sequelize:

User.hasMany(Post, { foreignKey: 'author_id' });
Post.belongsTo(User, { as: 'author', foreignKey: 'author_id' });
Post.belongsToMany(Tag, { through: 'rel_posts_tags' });
Tag.belongsToMany(Post, { through: 'rel_posts_tags' });

NormalJS:

class Users {
static _name = 'Users';
static fields = {
id: 'primary',
posts: { type: 'one-to-many', foreign: 'Posts.author_id' },
};
}

class Posts {
static _name = 'Posts';
static fields = {
id: 'primary',
author_id: { type: 'many-to-one', model: 'Users' },
tags: { type: 'many-to-many', model: 'Tags', joinTable: 'rel_posts_tags' },
};
}

class Tags {
static _name = 'Tags';
static fields = { id: 'primary' };
}

On instances:

const post = await repo.get('Posts').findById(1);
await post.tags.load();
await post.tags.add(tagOrId);
await post.tags.remove(tagOrId);

3) Queries and eager loading

Sequelize:

const u = await User.findOne({ where: { email }, include: [{ model: Post, as: 'posts' }] });

NormalJS:

const Users = repo.get('Users');
const u = await Users.where({ email }).include('posts').first();

Notes:

  • include() accepts a string or array of relation names declared on the model.
  • NormalJS requests can be cached: .cache(60).

4) Transactions

Sequelize:

await sequelize.transaction(async (t) => {
await User.create({ email }, { transaction: t });
});

NormalJS:

await repo.transaction(async (tx) => {
await tx.get('Users').create({ email });
});

Inside a transaction, tx is an isolated repository. After commit, flushed records are pushed into the entry cache.

5) Hooks

Sequelize has model hooks like beforeCreate, afterUpdate, etc. In NormalJS, you typically attach behavior with field hooks and record lifecycle methods; and you can also extend models.

On fields (per value):

class Users {
static _name = 'Users';
static fields = {
id: 'primary',
email: { type: 'string' },
};
}

// In a custom field class, you can override:
// pre_create, post_create, pre_update, post_update, pre_unlink, post_unlink

On records (per instance):

// In your mixin/extension class
class Users {
static _name = 'Users';
async post_create() {
/* ... */
}
async pre_update() {
/* ... */
}
}

6) Class vs instance methods

Sequelize classMethods → NormalJS static methods on the model; Sequelize instance methods → instance methods/getters on the active record.

class Users {
static _name = 'Users';
static fields = { id: 'primary', email: 'string' };

// class (model) API
static byEmail(email) {
return this.where({ email }).first();
}

// instance API
get domain() {
return this.email?.split('@')[1] || null;
}
}

7) Validation

Sequelize uses validators in definitions. NormalJS includes basic validation (required/unique) and StringField validators. For richer validation, implement it in field validate() or in record hooks.

static fields = {
email: {
type: 'string', required: true,
validate: { isEmail: true }
}
}

8) Scopes

Sequelize scopes provide reusable query patterns. NormalJS now supports scopes natively with a similar API.

Sequelize Scopes

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

User.addScope('recent', {
order: [['createdAt', 'DESC']],
limit: 10
});

// Usage
User.scope('active').findAll();
User.scope('active', 'recent').findAll();

NormalJS Scopes

class Users {
static _name = 'Users';
static fields = {
id: 'primary',
email: 'string',
active: { type: 'boolean', default: true },
created_at: { type: 'datetime', default: () => new Date() },
};

// Define scopes
static scopes = {
active: {
where: { active: true },
},
recent: {
order: [['created_at', 'DESC']],
limit: 10,
},
};
}

// Usage
await repo.Users.scope('active');
await repo.Users.scope('active', 'recent');

Parameterized Scopes

Sequelize:

User.addScope('recentDays', (days) => ({
where: {
createdAt: { [Op.gte]: new Date(Date.now() - days * 24 * 60 * 60 * 1000) }
}
}));

User.scope({ method: ['recentDays', 7] }).findAll();

NormalJS:

class Users {
static scopes = {
recentDays: (qb, days = 7) => {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
return {
where: { created_at: { gte: cutoff } },
};
},
};
}

// Usage
await repo.Users.scope({ recentDays: [7] });

Default Scopes

Sequelize:

const User = sequelize.define('User', { /* ... */ }, {
defaultScope: {
where: { active: true }
},
scopes: {
all: {} // Remove default scope
}
});

User.findAll(); // Applies defaultScope
User.scope('all').findAll(); // No default scope

NormalJS:

class Users {
static _name = 'Users';
static fields = { /* ... */ };

static defaultScope = {
where: { active: true },
};

static scopes = {
inactive: {
where: { active: false },
},
};
}

// Usage
await repo.Users.query(); // Applies defaultScope
await repo.Users.unscoped(); // Bypass defaultScope
await repo.Users.scope('inactive'); // Merges with defaultScope

Scope Features Comparison

FeatureSequelizeNormalJS
Basic scopes
Parameterized scopes
Default scope
Multiple scopes
Scope merging✅ (AND for where)
Include in scopes✅ (basic support)
Cache in scopes

Scope with Caching (NormalJS Exclusive)

NormalJS scopes can include caching configuration:

class Users {
static scopes = {
popular: {
where: { followers: { gte: 1000 } },
cache: 300, // Cache for 5 minutes
},
};
}

// Cache is applied automatically
const popularUsers = await repo.Users.scope('popular');

For comprehensive scope documentation, see docs/scopes.md.

9) Migrations and schema sync

  • For greenfield or prototyping, await repo.sync({ force: true }) builds tables from fields.
  • For production, keep using migrations. You can compare generated metadata and write migration scripts accordingly. Fields expose helpers like replaceColumn() for careful changes.

10) Caching

  • Entry cache (Model:ID) is updated on create/update and expired on unlink.
  • Request cache is opt-in: call .cache(ttlSeconds).
  • Per-model invalidation markers ($Model) let you evict request-level cache without dropping entry cache; call Model.invalidateCache() or set static cacheInvalidation = true to auto-invalidate after writes/unlinks.

Enable cache on a model by setting static cache:

class Users {
static _name = 'Users';
static fields = {
id: 'primary',
email: 'string',
};
static cache = 300;
}

11) Raw SQL

Sequelize: sequelize.query(sql) → NormalJS: repo.cnx.raw(sql) or repo.cnx(table).where(...).

12) Migration checklist

  1. Create a Connection and Repository; keep your database driver.
  2. Translate each Sequelize model to a NormalJS class with static _name, static table, and static fields.
  3. Convert associations to relation fields (many-to-one, one-to-many, many-to-many).
  4. Move class/instance methods to static methods and record methods/getters.
  5. Map validators (use field validate, required, or custom logic).
  6. Replace include/eager loading with .include().
  7. Replace raw transactions with repo.transaction(async tx => { ... }).
  8. Decide on schema: use repo.sync() for prototyping; maintain migrations for production.
  9. (Optional) Enable caching: set static cache on hot-read models and use .cache(ttl) on read queries; consider static cacheInvalidation = true.
  10. Run tests and compare query semantics; leverage NormalJS’s lazy ID-first reads for performance.

References

  • Model definitions: see docs/models.md
  • Field types: see docs/fields.md
  • Requests and caching: see docs/requests.md and docs/cache.md
  • Custom fields: see docs/custom-fields.md