Welcome to this tutorial on scoping model attributes in Sequelize. By the end of this lesson, you should be able to:
Think of scopes like “smart filters” you can attach to your Sequelize models. Instead of writing
the same where, include, or attributes clauses over and over,
you can define them once in a scope and reuse them throughout your application.
A scope in Sequelize is a way to define commonly used queries (like filtering, including associations, or selecting certain attributes). Whenever you want those same constraints, you can simply invoke the scope instead of rewriting the query.
There are many real-world scenarios where scoping proves invaluable. Here are a few examples:
Scopes let you define these filters (and included fields) in one place rather than scattering the logic throughout your codebase.
Scopes in Sequelize are defined in the options object of the init function
when setting up your model. A simplified model might look like this:
class Book extends Model {}
Book.init({
// Define model attributes here
}, {
// Define options, including scopes, here
});
Scopes come in two flavors:
Both default and non-default scopes can use any of the usual Sequelize query options,
including where, limit, include, and attributes.
When defining your model’s options, you might see something like:
{
defaultScope: {
// details of the default scope
},
scopes: {
activeUsers: {
// details of the named scope
}
// more scopes...
}
}
You can tailor which model attributes appear in your query results with the
attributes option. For example, if you only want certain fields returned:
defaultScope: {
attributes: {
include: ["title", "author", "isCheckedOut", "location"]
}
}
This scope ensures that other attributes of Book (like createdAt,
updatedAt, etc.) do not show up.
If you only want to exclude certain attributes, you can use:
defaultScope: {
attributes: {
exclude: ["secretNotes"]
}
}
You can also filter which records a scope returns using a where clause.
For example, the default scope below includes only books that are currently available
(i.e., not checked out):
defaultScope: {
where: {
isCheckedOut: false
}
}
This means that simply calling Book.findAll() will return only those books
that meet the isCheckedOut: false condition.
A dynamic scope is any scope that can take parameters at runtime to build the query conditions. Instead of defining a static object, you return an object from a function, allowing for flexible or “dynamic” queries.
For instance, if you want to filter Book records by libraryId,
you can define:
scopes: {
atLibrary(libraryId) {
return {
where: { libraryId }
};
}
}
With this scope, you can request just the books belonging to a particular library
by passing libraryId as a parameter.
You might also want to return associated data. For example, if you have a Library model,
you can include it:
scopes: {
atLibrary(libraryId) {
const { Library } = require('../models');
return {
where: { libraryId },
include: [{ model: Library }]
};
}
}
Now, when you invoke Book.scope({ method: ["atLibrary", 19] }).findAll(),
you’ll get all books from library #19, plus each book’s associated library details.
In a real-world project, you’ll likely need a combination of default and named scopes to handle all your use cases. For example:
class Book extends Model {}
Book.init({
// Define attributes here
}, {
defaultScope: {
// Only show books that are NOT checked out
where: { isCheckedOut: false },
attributes: {
include: ["title", "author", "isCheckedOut", "location"]
}
},
scopes: {
atLibrary(libraryId) {
const { Library } = require('../models');
return {
where: { libraryId },
include: [{ model: Library }]
};
},
atOtherLibrary(libraryId) {
const { Library } = require('../models');
return {
where: {
libraryId: {
[Op.ne]: libraryId
}
},
include: [{ model: Library }]
};
},
checkedOut: {
where: {
isCheckedOut: true
}
}
}
});
Notice how you can define any number of scopes to handle multiple scenarios—like atOtherLibrary filtering out books from a certain library, or a checkedOut scope for books that are actively borrowed.
If you do nothing else, the defaultScope automatically applies whenever you query the model:
// applies defaultScope
await Book.findAll();
This code returns only books that are not checked out, based on our default scope definition.
However, if you explicitly invoke a non-default scope, the default scope does not apply (unless you specifically include it). For example:
// invokes the "checkedOut" scope
await Book.scope("checkedOut").findAll();
// returns books that are checked out, ignoring the default scope
For scopes that are defined as functions, you need to pass an object with a
method array to specify the scope name and any arguments:
await Book.scope({ method: ["atLibrary", 19] }).findAll();
// returns books where libraryId = 19
Since we’ve defined atLibrary(libraryId) to return a scope,
we pass 19 as the argument that dynamic scope expects.
Sometimes, you need more than one scope. You can pass an array to
scope() in the order you want them applied. If there’s a conflict,
scopes defined later in the array overwrite earlier ones.
await Book.scope(["defaultScope", { method: ["atLibrary", 19] }]).findAll();
// merges the default scope (books not checked out) with the atLibrary scope
// => returns only books that are NOT checked out, from library #19
This is powerful because it lets you compose scoping rules while avoiding
repeated where conditions throughout your code.
Imagine you manage a coffee shop. You want to see:
Scopes let you set these filtering rules in code once, so you don’t keep writing the same queries repeatedly.
Scopes in Sequelize help you write DRY (Don’t Repeat Yourself) code by letting you:
libraryId).By leveraging scopes, you ensure consistency and clarity in how data is accessed and presented, which leads to a more maintainable and secure codebase. Remember, the key advantage is reusability: once a scope is defined, you can invoke it across your entire application instead of duplicating logic.