You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
2.9 KiB
89 lines
2.9 KiB
2 years ago
|
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()
|
||
|
}
|
||
|
}
|