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()withDataTypes; NormalJS uses a class with a staticfieldsobject. - Relations: Sequelize associations → NormalJS relation fields (
many-to-one,one-to-many,many-to-many). - Reads: NormalJS selects just
idby 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(orprimaryfor PK) - FLOAT/DECIMAL →
float - STRING →
string(withsize), 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
| Feature | Sequelize | NormalJS |
|---|---|---|
| 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; callModel.invalidateCache()or setstatic cacheInvalidation = trueto 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
- Create a
ConnectionandRepository; keep your database driver. - Translate each Sequelize model to a NormalJS class with
static _name,static table, andstatic fields. - Convert associations to relation fields (
many-to-one,one-to-many,many-to-many). - Move class/instance methods to static methods and record methods/getters.
- Map validators (use field
validate,required, or custom logic). - Replace
include/eager loading with.include(). - Replace raw transactions with
repo.transaction(async tx => { ... }). - Decide on schema: use
repo.sync()for prototyping; maintain migrations for production. - (Optional) Enable caching: set
static cacheon hot-read models and use.cache(ttl)on read queries; considerstatic cacheInvalidation = true. - 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.mdanddocs/cache.md - Custom fields: see
docs/custom-fields.md