/
AncestryCollection.ts
160 lines (136 loc) · 5.16 KB
/
AncestryCollection.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import ModelCollection from './ModelCollection';
import type Model from './Model';
import type { MaybeArray } from '../Support/type';
type Ancestralised<
T extends Model,
CT extends string = 'children'
> = T & { [key in CT]: ModelCollection<Ancestralised<T, CT>> };
export default class AncestryCollection<
T extends Model,
CT extends string = 'children'
> extends ModelCollection<Ancestralised<T, CT>> {
/**
* The name of the key that will be set when arranging the items in a tree structure.
*/
public static depthName = 'depth';
/**
* The name of the attribute that includes the related models.
*
* @protected
*/
protected childrenRelation: CT;
/**
* @param models - The models already arranged in an ancestry tree format.
* @param childrenRelation - The key that will include descendants.
*
*/
protected constructor(
models?: MaybeArray<Ancestralised<T, CT>>,
childrenRelation: CT = 'children' as CT
) {
super(models);
this.childrenRelation = childrenRelation;
}
/**
* Arrange the items in an ancestry tree format.
*
* @param models - The ModelCollection to sort.
* @param parentKey - The key that identifies the parent's id.
* @param childrenRelation - The key that will include descendants.
*
* @return {AncestryCollection}
*/
public static treeOf<ST extends Model, CT extends string = 'children'>(
models: ModelCollection<ST> | ST[],
parentKey = 'parentId',
childrenRelation: CT = 'children' as CT
): AncestryCollection<ST, CT> {
const buildModelsRecursive = (
modelItems: ST[],
parent?: ST,
depth = 0
): Ancestralised<ST, CT>[] => {
const modelArray: Ancestralised<ST, CT>[] = [];
modelItems.forEach(model => {
// if this is a child, but we are looking for a top level
if (!parent && model.getAttribute(parentKey)) {
return;
}
// if this is a child, but this child doesn't belong to this parent
if (parent && model.getAttribute(parentKey) !== parent.getKey()) {
return;
}
model.setAttribute(this.depthName, depth)
.syncOriginal(this.depthName)
.setAttribute(
childrenRelation,
// by this filter we eventually will run out of items on the individual branches
buildModelsRecursive(modelItems.filter(m => m.getKey() !== model.getKey()), model, depth + 1)
);
modelArray.push(model as Ancestralised<ST, CT>);
});
return modelArray;
};
return new AncestryCollection(
buildModelsRecursive(Array.isArray(models) ? models : models.toArray()),
childrenRelation
);
}
/**
* Return all the models in a single level with no children set.
*
* @return {ModelCollection}
*/
public flatten(): ModelCollection<T> {
const getModelsRecursive = (models: T[]): T[] => {
const modelArray: T[] = [];
models.forEach(model => {
const children = (model.getAttribute(this.childrenRelation) ?? []) as ModelCollection<T> | T[];
model.setAttribute(this.childrenRelation, []);
modelArray.push(model);
if (children.length) {
modelArray.push(...getModelsRecursive(
ModelCollection.isModelCollection<T>(children) ? children.toArray() : children
));
}
});
return modelArray.map(
m => m.deleteAttribute((this.constructor as typeof AncestryCollection).depthName)
.syncOriginal((this.constructor as typeof AncestryCollection).depthName)
);
};
return new ModelCollection(getModelsRecursive(this.toArray()));
}
/**
* All the models that do not have any children.
*
* @return {ModelCollection}
*/
public leaves(): ModelCollection<T> {
const getLeaves = (models: T[]): T[] => {
const leaves: T[] = [];
models.forEach(model => {
const children = model.getAttribute(this.childrenRelation) as ModelCollection<T> | T[] | undefined;
if (!children?.length) {
leaves.push(model);
}
leaves.push(
...getLeaves(ModelCollection.isModelCollection<T>(children) ? children.toArray() : children!)
);
});
return leaves;
};
return new ModelCollection(getLeaves(this.toArray()));
}
/**
* Asserts whether the given value
* is an instance of AncestryCollection.
*
* @param value
*
* @return {boolean}
*/
public static isAncestryCollection<M extends Model>(value: any): value is AncestryCollection<M> {
return this.isModelCollection(value) && value instanceof AncestryCollection;
}
}