Add TreeModel and HasSubtree implementation
This commit is contained in:
88
src/orm/model/TreeModel.ts
Normal file
88
src/orm/model/TreeModel.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {Model} from './Model'
|
||||
import {Collection, Maybe} from '../../util'
|
||||
import {HasSubtree} from './relation/HasSubtree'
|
||||
import {Related} from './relation/decorators'
|
||||
|
||||
/**
|
||||
* Model implementation with helpers for querying tree-structured data.
|
||||
*
|
||||
* This works by using a modified pre-order traversal to number the tree nodes
|
||||
* with a left- and right-side numbers. For example:
|
||||
*
|
||||
* ```txt
|
||||
* (1) A (14)
|
||||
* |
|
||||
* (2) B (9) (10) C (11) (12) D (14)
|
||||
* |
|
||||
* (3) E (6) (7) G (8)
|
||||
* |
|
||||
* (4) F (5)
|
||||
* ```
|
||||
*
|
||||
* These numbers are stored, by default, in `left_num` and `right_num` columns.
|
||||
* The `subtree()` method returns a `HasSubtree` relation which loads the subtree
|
||||
* of a model and recursively nests the nodes.
|
||||
*
|
||||
* You can use the `children()` helper method to get a collection of the immediate
|
||||
* children of this node, which also have the subtree set.
|
||||
*
|
||||
* To query the model without loading the entire subtree, use the `without()`
|
||||
* method on the `ModelBuilder`. For example:
|
||||
*
|
||||
* ```ts
|
||||
* MyModel.query<MyModel>().without('subtree')
|
||||
* ```
|
||||
*/
|
||||
export abstract class TreeModel<T extends TreeModel<T>> extends Model<T> {
|
||||
|
||||
/** The table column where the left tree number is stored. */
|
||||
public static readonly leftTreeField = 'left_num'
|
||||
|
||||
/** The table column where the right tree number is stored. */
|
||||
public static readonly rightTreeField = 'right_num'
|
||||
|
||||
/**
|
||||
* @override to eager-load the subtree by default
|
||||
* @protected
|
||||
*/
|
||||
protected with: (keyof T)[] = ['subtree']
|
||||
|
||||
/** Get the left tree number for this model. */
|
||||
public leftTreeNum(): Maybe<number> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.originalSourceRow?.[ctor.leftTreeField]
|
||||
}
|
||||
|
||||
/** Get the right tree number for this model. */
|
||||
public rightTreeNum(): Maybe<number> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.originalSourceRow?.[ctor.rightTreeField]
|
||||
}
|
||||
|
||||
/** Returns true if this node has no children. */
|
||||
public isLeaf(): boolean {
|
||||
const left = this.leftTreeNum()
|
||||
const right = this.rightTreeNum()
|
||||
return Boolean(left && right && (right - left === 1))
|
||||
}
|
||||
|
||||
/** Returns true if the given `node` exists within the subtree of this node. */
|
||||
public contains(node: this): boolean {
|
||||
const num = node.leftTreeNum()
|
||||
const left = this.leftTreeNum()
|
||||
const right = this.rightTreeNum()
|
||||
return Boolean(num && left && right && (left < num && right > num))
|
||||
}
|
||||
|
||||
/** The subtree nodes of this model, recursively nested. */
|
||||
@Related()
|
||||
public subtree(): HasSubtree<T> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.make<HasSubtree<T>>(HasSubtree, this, ctor.leftTreeField)
|
||||
}
|
||||
|
||||
/** Get the immediate children of this model. */
|
||||
public children(): Collection<T> {
|
||||
return this.subtree().getValue()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user