• Stars
    star
    295
  • Rank 140,902 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 10 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Nested hierarchies for Sequelize

sequelize-hierarchy.js

Nested hierarchies for Sequelize

NPM version Build Status Dependency Status Dev dependency Status Greenkeeper badge Coverage Status

What's it for?

Relational databases aren't very good at dealing with nested hierarchies.

Examples of hierarchies are:

  • Nested folders where each folder has many subfolders, those subfolders themselves have subfolders, and so on
  • Categories and sub-categories e.g. for a newspaper with sections for different sports, Sports category splits into Track Sports and Water Sports, Water Sports into Swimming and Diving, Diving into High Board, Middle Board and Low Board etc
  • Tree structures

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.

Current status

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.

Usage

Loading module

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);

Initializing hierarchy

Model#isHierarchy( [options] )

const sequelize = new Sequelize('database', 'user', 'password');

const Folder = sequelize.define('folder', { name: Sequelize.STRING });
Folder.isHierarchy();

Folder.isHierarchy() does the following:

  • Adds a column parentId to Folder model
  • Adds a column hierarchyLevel to Folder model (which should not be updated directly)
  • Creates a new model FolderAncestor which contains the ancestry information (columns folderId and ancestorId)
  • Creates the following associations (with foreign key constraints):
    • 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})
  • Creates hooks into standard Sequelize methods (create, update, destroy, bulkCreate etc) to automatically update the ancestry table and hierarchyLevel field as details in the folder table change
  • Creates hooks into Sequelize's Model#find() and Model#findAll() methods so that hierarchies can be returned as javascript object tree structures

The column and table names etc can be modified by passing options to .isHierarchy(). See below for details.

via Sequelize#define() options

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.

Creating database tables

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();

Retrieving hierarchies

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

Accessors are also supported:

folder.getParent()
folder.getChildren()
folder.getAncestors()
folder.getDescendents()

Setters work as usual e.g. folder.setParent(), folder.addChild().

Options

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' }
});

Aliases for relations

  • 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()

Fields

  • 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'.

Hierarchy (through) table

  • 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.

Cascading deletions

  • onDelete: Set to 'CASCADE' if you want deleting a node to delete all its children.

Misc

  • 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.

Rebuilding the hierarchy

Model#rebuildHierarchy( [options] )

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.

Bulk creation

You can use .bulkCreate() method in the usual way. Ensure that parents are created before their children.

Errors

Errors thrown by the plugin are of type HierarchyError. The error class can be accessed at Sequelize.HierarchyError.

Tests

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.

Changelog

See changelog.md

Issues

If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/sequelize-hierarchy/issues

Contribution

Pull requests are very welcome. Please:

  • ensure all tests pass before submitting PR
  • do not add an entry to changelog - changelog will be created when cutting releases
  • add tests for new features
  • document new functionality/API additions in README

More Repositories

1

react-async-ssr

Render React Suspense on server
JavaScript
159
star
2

fs-extra-promise

Node file system library and fs-extra module promisified with bluebird
JavaScript
44
star
3

react-lazy-ssr

React.lazy substitute which works with server-side rendering
JavaScript
41
star
4

livepack

Serialize live running code to Javascript
JavaScript
22
star
5

sequelize-virtual-fields

Sequelize virtual fields magic
JavaScript
18
star
6

google-drive-uploader

Upload large files to Google Drive
JavaScript
17
star
7

promisify-any

Promisify any of: callback function, sync function, generator function, promise-returning function
JavaScript
11
star
8

got-resume

Fetch via HTTP/HTTPS using got with automatic resume after network failures
JavaScript
10
star
9

sequelize-definer

Sequelize plugin to help easily define a set of models
JavaScript
10
star
10

sequelize-values

Easily get raw data from Sequelize instances
JavaScript
7
star
11

react-lazy-data

Lazy-load data with React Suspense
JavaScript
7
star
12

sequelize-queue

A worker queue persisted to a Sequelize model
JavaScript
7
star
13

swc-parse-test

SWC parse experiments
JavaScript
7
star
14

co-series

Run in series with co
JavaScript
6
star
15

yauzl-promise

yauzl unzipping with Promises
JavaScript
6
star
16

walk-folder-tree

Recursively walk file system tree and callback on every file
JavaScript
5
star
17

bluebird-extra

Extra methods for bluebird promises library
JavaScript
5
star
18

srt-cut

Cut up SRT subtitle file into parts
JavaScript
3
star
19

lock-queue

Simple locking mechanism to serialize (queue) access to a resource
JavaScript
3
star
20

cls-bluebird-test

Testing CLS context passing with Bluebird promises
JavaScript
3
star
21

require-folder-tree

Utility to require multiple files in a folder tree with flexible options
JavaScript
3
star
22

sort-route-paths

Sort route paths
JavaScript
2
star
23

sequelize-extra

Collection of extensions to Sequelize
JavaScript
2
star
24

co-bluebird

co with bluebird promises
Makefile
2
star
25

eslint-plugin-ejs-js

EJS plugin for ESLint
JavaScript
2
star
26

pluggi

Base for building modularised apps with plugins
JavaScript
1
star
27

class-extension

Class extensions
JavaScript
1
star
28

toposort-extended

toposort with objects
JavaScript
1
star
29

srt

SRT subtitling utilities
JavaScript
1
star
30

worker-server

Worker server to run jobs instructed by central server
JavaScript
1
star
31

got-headers

Hit URL and get HTTP headers only (using got module)
JavaScript
1
star
32

stream-gen

Create streams from generators and test them
JavaScript
1
star
33

semver-select

Select an attribute of an object based on semver versioning
JavaScript
1
star
34

jest-expect-arguments

Jest expect matcher for arguments objects
JavaScript
1
star
35

overlook-framework

Yet another node web framework
JavaScript
1
star
36

shimming

A very small library of functions to shim other libraries
JavaScript
1
star
37

shimstack

Middleware for functions
JavaScript
1
star
38

terser-sync

Execute Terser minify synchronously
JavaScript
1
star
39

yauzl-clone

Clone yauzl for patching
JavaScript
1
star
40

promise-methods

Useful Promise helpers
JavaScript
1
star
41

co-use

co using your choice of promise implementation
JavaScript
1
star
42

cls-bluebird2

Patch Bluebird promise library to support continuation-local-storage
JavaScript
1
star