adonisjs-nested-set
v0.2.0
Published
Nested set model implementation for AdonisJS, similar to laravel-nestedset
Maintainers
Readme
AdonisJS Nested Set
Nested set model implementation for AdonisJS, similar to laravel-nestedset for Laravel.
This package provides efficient tree operations using the Nested Set Model algorithm.
Installation
npm install adonisjs-nested-setUsage
1. Create Migration
import { BaseSchema } from '@adonisjs/lucid/schema'
import { addNestedSetColumns } from 'adonisjs-nested-set'
export default class extends BaseSchema {
protected tableName = 'categories'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('name').notNullable()
// Add nested set columns (_lft, _rgt, parent_id)
addNestedSetColumns(table)
table.timestamps(true, true)
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}2. Create Model
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { applyNestedSet } from 'adonisjs-nested-set'
class Category extends BaseModel {
static table = 'categories'
@column({ isPrimary: true })
declare id: number
@column()
declare name: string
@column({ columnName: 'parent_id' })
declare parentId: number | null
@column({ columnName: '_lft' })
declare _lft: number
@column({ columnName: '_rgt' })
declare _rgt: number
}
// Apply nested set functionality to the model
// The function returns properly typed model with all nested set methods
const CategoryWithNestedSet = applyNestedSet(Category)
// Export the typed model
// TypeScript automatically infers all nested set methods with proper types
export default CategoryWithNestedSet3. Use in Your Code
Inserting Nodes
// Create root node
const root = await Category.create({ name: 'Root' })
await Category.fixTree()
// Create child node
const child = await Category.create({ name: 'Child', parentId: root.id })
await Category.fixTree()
// Or use appendTo method
const child2 = new Category()
child2.name = 'Child 2'
await child2.appendTo(root)
await Category.fixTree()Retrieving Nodes
// Get all roots
const roots = await Category.roots().exec()
// Get a node
const node = await Category.find(1)
if (node) {
// Static methods - called on Category class
const ancestors = await Category.ancestorsOf(node).exec()
const ancestorsAndSelf = await Category.ancestorsAndSelf(node).exec()
const descendants = await Category.descendantsOf(node).exec()
const descendantsAndSelf = await Category.descendantsAndSelf(node).exec()
const siblings = await Category.siblingsOf(node).exec()
const siblingsAndSelf = await Category.siblingsAndSelf(node).exec()
// Instance methods - called on node instance
const nodeAncestors = await node.ancestors().exec()
const nodeDescendants = await node.descendants().exec()
const nodeChildren = await node.children().exec()
const nodeParent = await node.parent()
}Building Tree
// Convert to tree structure
const nodes = await Category.all()
const tree = nodes.toTree()
// Convert to flat tree (children immediately after parent)
const flatTree = nodes.toFlatTree()
// Get subtree
const root = await Category.find(rootId)
const subtree = await Category.descendantsAndSelf(rootId).exec()
const treeStructure = subtree.toTree()Helper Methods
const node = await Category.find(1)
if (node) {
// Check node properties
const isRoot = node.isRoot() // Check if node is root
const isLeaf = node.isLeaf() // Check if node is leaf
const other = await Category.find(2)
if (other) {
const isDescendant = node.isDescendantOf(other) // Check if node is descendant
const isAncestor = node.isAncestorOf(other) // Check if node is ancestor
const isChild = node.isChildOf(other) // Check if node is child
const isSibling = node.isSiblingOf(other) // Check if node is sibling
}
const depth = await node.getDepth() // Get depth of node
}Checking Consistency
// Check if tree is broken
const isBroken = await Category.isBroken()
// Get error statistics
const errors = await Category.countErrors()
// Returns: { oddness, duplicates, wrong_parent, missing_parent }
// Fix tree structure
await Category.fixTree()Query Constraints
const node = await Category.find(1)
if (node) {
// Where ancestor of
const result = await Category.whereAncestorOf(node).exec()
const result2 = await Category.whereAncestorOrSelf(node).exec()
// Where descendant of
const result3 = await Category.whereDescendantOf(node).exec()
const result4 = await Category.whereDescendantOrSelf(node).exec()
// Get nodes with depth
const nodesWithDepth = await Category.withDepth().exec()
nodesWithDepth.forEach((node) => {
console.log(`Node ${node.name} is at depth ${node.$extras.depth}`)
})
}Deleting Nodes
// Delete a node - automatically deletes all descendants
const node = await Category.find(1)
await node.delete() // This will also delete all child nodes
// The delete method is automatically overridden to perform cascade deletionAPI Reference
Static Methods
Category.roots()- Get all root nodesCategory.ancestorsOf(node)- Get ancestors of a nodeCategory.ancestorsAndSelf(node)- Get ancestors including selfCategory.descendantsOf(node)- Get descendants of a nodeCategory.descendantsAndSelf(node)- Get descendants including selfCategory.siblingsOf(node)- Get siblings of a nodeCategory.siblingsAndSelf(node)- Get siblings including selfCategory.whereAncestorOf(node)- Where ancestor ofCategory.whereAncestorOrSelf(node)- Where ancestor or selfCategory.whereDescendantOf(node)- Where descendant ofCategory.whereDescendantOrSelf(node)- Where descendant or selfCategory.withDepth(as?)- Get nodes with depth information (depth stored in$extras.depth)Category.isBroken()- Check if tree is brokenCategory.countErrors()- Count errors in treeCategory.fixTree()- Fix tree structureCategory.getLftName()- Get left column name (default: '_lft')Category.getRgtName()- Get right column name (default: '_rgt')Category.getParentIdName()- Get parent ID column name (default: 'parent_id')
Instance Methods
node.isRoot()- Check if node is rootnode.isLeaf()- Check if node is leafnode.isDescendantOf(other)- Check if node is descendantnode.isAncestorOf(other)- Check if node is ancestornode.isChildOf(other)- Check if node is childnode.isSiblingOf(other)- Check if node is siblingnode.getDepth()- Get depth of nodenode.siblings()- Get siblings querynode.ancestors()- Get ancestors querynode.descendants()- Get descendants querynode.children()- Get children querynode.parent()- Get parent nodenode.makeRoot()- Make node a rootnode.appendTo(parent)- Append node to parentnode.delete()- Delete node and all its descendants (cascade delete)
Collection Methods
collection.toTree()- Convert collection to tree structurecollection.toFlatTree()- Convert collection to flat tree
Migration Helpers
addNestedSetColumns(table, lftColumn?, rgtColumn?, parentIdColumn?)- Add nested set columnsdropNestedSetColumns(table, lftColumn?, rgtColumn?, parentIdColumn?)- Drop nested set columns
Important Notes
Cascade Deletion
When you delete a node using node.delete(), all its descendants are automatically deleted as well. This is handled automatically by the package.
Column Names
The package uses the following default column names:
_lft- Left boundary_rgt- Right boundaryparent_id- Parent node ID (in database)parentId- Parent node ID (in model, camelCase)
Make sure to specify columnName in your model decorators if your database uses different naming:
@column({ columnName: 'parent_id' })
declare parentId: number | null
@column({ columnName: '_lft' })
declare _lft: number
@column({ columnName: '_rgt' })
declare _rgt: numberSQLite Compatibility
When using SQLite, use integer instead of unsignedInteger in migrations:
table.integer('_lft').nullable()
table.integer('_rgt').nullable()
table.integer('parent_id').nullable()TypeScript Support
Type Inference
The package provides full TypeScript support with automatic type inference - no configuration needed!
Static Methods: The
applyNestedSet()function returns a properly typed model. All static methods likeancestorsOf(),descendantsOf(), etc. automatically accept model instances without type assertions.Instance Methods: Instance methods are automatically typed via module augmentation for
LucidRow(included in the package). Methods likechildren(),isRoot(), etc. work without any type assertions.Query Results: Methods like
find(),create(),first(), etc. automatically return instances with nested set methods included in their types.
Example with Full Type Safety
import Category from '#models/category'
// Static methods work without type assertions - automatic type inference!
const node = await Category.find(1)
if (node) {
// ✅ All methods work without type assertions
const ancestors = await Category.ancestorsOf(node).exec()
const descendants = await Category.descendantsOf(node).exec()
const children = await node.children().exec()
const isRoot = node.isRoot()
const isLeaf = node.isLeaf()
}Key Benefits
- ✅ Zero Configuration: No need for declaration merging files or manual type assertions
- ✅ Full Type Safety: All methods are properly typed with correct return types
- ✅ IntelliSense Support: Full autocomplete for all nested set methods
- ✅ Type Inference: TypeScript automatically infers correct types for model instances
Testing
The package includes comprehensive unit tests. Run them with:
npm testFor integration tests with real database operations use AdonisJS application.
License
MIT
