Nested hierarchies for Sequelize
MIT License
Relational databases aren't very good at dealing with nested hierarchies.
Examples of hierarchies are:
To store a hierarchy in a database, the usual method is to give each record a ParentID field which says which is the record one level above it.
Fetching the parent or children of any record is easy, but if you want to retrieve an entire tree/hierarchy structure from the database, it requires multiple queries, recursively getting each level of the hierarchy. For a big tree structure, this is a lengthy process, and annoying to code.
This plugin for Sequelize solves this problem.
API is stable. All features and options are fairly well tested. Works with all dialects of SQL supported by Sequelize (MySQL, Postgres, SQLite) except for Microsoft SQL Server.
Requires Sequelize v2.x.x, v3.x.x, v4.x.x or v5.x.x. Supports only Node v8 or higher.
To load module:
const Sequelize = require('sequelize-hierarchy')();
// NB Sequelize must also be present in `node_modules`
or, a more verbose form useful if chaining multiple Sequelize plugins:
const Sequelize = require('sequelize');
require('sequelize-hierarchy')(Sequelize);
const sequelize = new Sequelize('database', 'user', 'password');
const Folder = sequelize.define('folder', { name: Sequelize.STRING });
Folder.isHierarchy();
Folder.isHierarchy()
does the following:
parentId
to Folder modelhierarchyLevel
to Folder model (which should not be updated directly)FolderAncestor
which contains the ancestry information (columns folderId
and ancestorId
)Folder.belongsTo(Folder, {as: 'parent', foreignKey: 'parentId'})
Folder.hasMany(Folder, {as: 'children', foreignKey: 'parentId'})
Folder.belongsToMany(Folder, {as: 'descendents', foreignKey: 'ancestorId', through: FolderAncestor})
Folder.belongsToMany(Folder, {as: 'ancestors', foreignKey: 'folderId', through: FolderAncestor})
hierarchyLevel
field as details in the folder table changeModel#find()
and Model#findAll()
methods so that hierarchies can be returned as javascript object tree structuresThe column and table names etc can be modified by passing options to .isHierarchy()
. See below for details.
Hierarchies can also be created in define()
:
const Folder = sequelize.define('folder', {
name: Sequelize.STRING
}, {
hierarchy: true
});
or on an attribute in define()
:
const Folder = sequelize.define('folder', {
name: Sequelize.STRING,
parentId: {
type: Sequelize.INTEGER,
hierarchy: true
}
});
If defining the hierarchy via model options, do not also call .isHierarchy()
. The two methods are equivalent - only use one or the other.
Defining the hierarchy sets up the models in Sequelize, not the database tables. You will need to create or modify the tables in the database.
If table already exists, add the following columns:
parentId
(same type as id
)hierarchyLevel
(INTEGER
type)If the table does not already exist, you can ask Sequelize to create it:
await Folder.sync();
NB Call .sync()
after .isHierarchy()
.
The ancestry model (FolderAncestor
in the above example) also needs its database table created:
await sequelize.models.FolderAncestor.sync();
Examples of getting a hierarchy structure:
// Get entire hierarchy as a flat list
const folders = await Folder.findAll();
// [
// { id: 1, parentId: null, name: 'a' },
// { id: 2, parentId: 1, name: 'ab' },
// { id: 3, parentId: 2, name: 'abc' }
// ]
// Get entire hierarchy as a nested tree
const folders = await Folder.findAll({ hierarchy: true });
// [
// { id: 1, parentId: null, name: 'a', children: [
// { id: 2, parentId: 1, name: 'ab', children: [
// { id: 3, parentId: 2, name: 'abc' }
// ] }
// ] }
// ]
// Get all the descendents of a particular item
const folder = await Folder.findOne({
where: { name: 'a' },
include: {
model: Folder,
as: 'descendents',
hierarchy: true
}
});
// { id: 1, parentId: null, name: 'a', children: [
// { id: 2, parentId: 1, name: 'ab', children: [
// { id: 3, parentId: 2, name: 'abc' }
// ] }
// ] }
// Get all the ancestors (i.e. parent and parent's parent and so on)
const folder = await Folder.findOne({
where: { name: 'abc' },
include: [ { model: Folder, as: 'ancestors' } ],
order: [ [ { model: Folder, as: 'ancestors' }, 'hierarchyLevel' ] ]
});
// { id: 3, parentId: 2, name: 'abc', ancestors: [
// { id: 1, parentId: null, name: 'a' },
// { id: 2, parentId: 1, name: 'ab' }
// ] }
The forms with { hierarchy: true }
are equivalent to using Folder.findAll({ include: { model: Folder, as: 'children' } })
except that the include is recursed however deeply the tree structure goes.
Accessors are also supported:
folder.getParent()
folder.getChildren()
folder.getAncestors()
folder.getDescendents()
Setters work as usual e.g. folder.setParent()
, folder.addChild()
.
The following options can be passed to Model#isHierarchy( { /* options */ } )
or in a model definition:
const Folder = sequelize.define('folder', {
name: Sequelize.STRING
}, {
hierarchy: { /* options */ }
});
Defaults are inherited from sequelize.options.hierarchy
if defined in call to new Sequelize()
.
Examples:
Folder.isHierarchy( { as: 'above' } );
const Folder = sequelize.define('folder', {
name: Sequelize.STRING
}, {
hierarchy: { as: 'above' }
});
as
: Name of parent association. Defaults to 'parent'
.childrenAs
: Name of children association. Defaults to 'children'
.ancestorsAs
: Name of ancestors association. Defaults to 'ancestors'
.descendentsAs
: Name of descendents association. Defaults to 'descendents'
.These affect the naming of accessors e.g. instance.getParent()
levelFieldName
: Name of the hierarchy depth field. Defaults to 'hierarchyLevel'
.levelFieldType
: Type of the hierarchy depth field. Defaults to Sequelize.INTEGER.UNSIGNED
.levelFieldAttributes
: Attributes to add to the hierarchy depth field. Defaults to undefined
.primaryKey
: Name of the primary key. Defaults to model's primaryKeyAttribute
.foreignKey
: Name of the parent field. Defaults to 'parentId'
.foreignKeyAttributes
: Attributes to add to the parent field. Defaults to undefined
.throughKey
: Name of the instance field in hierarchy (through) table. Defaults to '<model name>Id'
.throughForeignKey
: Name of the ancestor field in hierarchy (through) table. Defaults to 'ancestorId'
.through
: Name of hierarchy (through) model. Defaults to '<model name>ancestor'
.throughTable
: Name of hierarchy (through) table. Defaults to '<model name plural>ancestors'
.throughSchema
: Schema of hierarchy (through) table. Defaults to model.options.schema
, and is optional.freezeTableName
: When true
, through table name is same as through model name. Inherits from sequelize define options.camelThrough
: When true
, through model name and table name are camelized (i.e. folderAncestor
not folderancestor
). Inherits from sequelize define options.All auto-created field names respect the setting of model.options.underscored
and the through table name respects sequelize.options.define.underscoredAll
.
onDelete
: Set to 'CASCADE'
if you want deleting a node to delete all its children.labels
: When true
, creates an attribute label
on the created parentId
and hierarchyLevel
fields which is a human-readable version of the field name. Inherits from sequelize define options or false
.To build the hierarchy data on an existing table, or if hierarchy data gets corrupted in some way (e.g. by changes to parentId being made directly in the database not through Sequelize), you can rebuild it with:
await Folder.rebuildHierarchy()
NB: In normal circumstances, you should never need to use this method. It is only intended for the above two use cases.
You can use .bulkCreate()
method in the usual way. Ensure that parents are created before their children.
Errors thrown by the plugin are of type HierarchyError
. The error class can be accessed at Sequelize.HierarchyError
.
Use npm test
to run the tests. Use npm run cover
to check coverage.
To run tests on a particular database, use npm run test-mysql
, npm run test-postgres
, npm run test-postgres-native
, npm run test-sqlite
or npm run test-mssql
.
Requires a database called 'sequelize_test' and a db user 'sequelize_test' with no password.
See changelog.md
If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/sequelize-hierarchy/issues
Pull requests are very welcome. Please: