Skip to content

Commit f70de5d

Browse files
committed
feat: model associations as graph implemented
1 parent 92a2cc8 commit f70de5d

File tree

3 files changed

+329
-1
lines changed

3 files changed

+329
-1
lines changed

src/BaseModel.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
* simply cast options to the same type name as error states, but import
7676
* this type from this module.
7777
*/
78+
import { Graph } from './Graph';
79+
7880
export * from 'sequelize-typescript';
7981

8082
import Promise = require('bluebird');
@@ -709,8 +711,8 @@ export class Sequelize extends SequelizeOrigin {
709711
*/
710712
export abstract class BaseModel<T> extends Model<BaseModel<T>> {
711713

712-
// noinspection JSUnusedGlobalSymbols
713714
/**
715+
// noinspection JSUnusedGlobalSymbols
714716
* Override native drop method to add support of view drops
715717
*
716718
* @param {DropOptions} options
@@ -1093,4 +1095,55 @@ export abstract class BaseModel<T> extends Model<BaseModel<T>> {
10931095
: JSON.parse(JSON.stringify(val));
10941096
}
10951097
}
1098+
1099+
/**
1100+
* Returns graph representation of the model associations.
1101+
* This would allow to traverse model association paths and detect
1102+
* cycles.
1103+
*
1104+
* @param {Graph<typeof BaseModel>} [graph]
1105+
* @return {Graph<typeof BaseModel>}
1106+
*/
1107+
public static toGraph(
1108+
graph = new Graph<typeof BaseModel>(),
1109+
): Graph<typeof BaseModel> {
1110+
if (!graph.hasVertex(this)) {
1111+
graph.addVertex(this);
1112+
}
1113+
1114+
for (const field of Object.keys(this.associations)) {
1115+
const relation = this.associations[field] as any;
1116+
const { target, options } = relation;
1117+
const through = options && options.through && options.through.model;
1118+
1119+
if (through && graph.hasEdge(this, through)) {
1120+
continue;
1121+
}
1122+
1123+
if (through) {
1124+
graph.addEdge(this, through);
1125+
through.toGraph(graph);
1126+
1127+
if (target && graph.hasEdge(through, target)) {
1128+
continue;
1129+
}
1130+
1131+
if (target && !graph.hasVertex(target)) {
1132+
graph.addEdge(through, target);
1133+
target.toGraph(graph);
1134+
}
1135+
} else {
1136+
if (target && graph.hasEdge(this, target)) {
1137+
continue;
1138+
}
1139+
1140+
if (target) {
1141+
graph.addEdge(this, target);
1142+
target.toGraph(graph);
1143+
}
1144+
}
1145+
}
1146+
1147+
return graph;
1148+
}
10961149
}

src/Graph.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*!
2+
* @imqueue/sequelize - Sequelize ORM refines for @imqueue
3+
*
4+
* Copyright (c) 2019, imqueue.com <[email protected]>
5+
*
6+
* Permission to use, copy, modify, and/or distribute this software for any
7+
* purpose with or without fee is hereby granted, provided that the above
8+
* copyright notice and this permission notice appear in all copies.
9+
*
10+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
12+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
15+
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16+
* PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
/**
19+
* Graph internal storage data type
20+
*
21+
* @type {Map<any, any>}
22+
*/
23+
export type GraphMap<T> = Map<T, T[]>;
24+
25+
/**
26+
* Callback type used on graph traversal iterations steps. It will obtain
27+
* vertex as a first argument - is a vertex on iteration visit, and a map
28+
* of visited vertices as a second argument.
29+
* If this callback returns false value it will break iteration cycle.
30+
*
31+
* @type {(vertex: T, visited: Map<T, boolean>): false | void}
32+
*/
33+
export type GraphForeachCallback<T> = (
34+
vertex: T,
35+
visited: Map<T, boolean>
36+
) => false | void;
37+
38+
/**
39+
* Class Graph
40+
* Simple undirected, unweighted graph data structure implementation
41+
* with DFS (depth-first search traversal implementation)
42+
*/
43+
export class Graph<T> {
44+
/**
45+
* Internal graph data storage
46+
*
47+
* @access private
48+
* @type {GraphMap<any>}
49+
*/
50+
private list: GraphMap<T> = new Map<T, T[]>();
51+
52+
/**
53+
* Adds vertices to graph
54+
*
55+
* @param {...any[]} vertex - vertices to add
56+
* @return {Graph<any>}
57+
*/
58+
public addVertex(...vertex: T[]): Graph<T> {
59+
for (const v of vertex) {
60+
this.list.set(v, []);
61+
}
62+
63+
return this;
64+
}
65+
66+
// noinspection JSUnusedGlobalSymbols
67+
/**
68+
* Removes vertices from a graph with all their edges
69+
*
70+
* @param {...any[]} vertex
71+
* @return {Graph<any>}
72+
*/
73+
public delVertex(...vertex: T[]): Graph<T> {
74+
for (const v of vertex) {
75+
this.list.delete(v);
76+
}
77+
78+
return this;
79+
}
80+
81+
/**
82+
* Adds an edges to a given vertex
83+
*
84+
* @param {any} fromVertex
85+
* @param {...any[]} toVertex
86+
* @return {Graph<any>}
87+
*/
88+
public addEdge(fromVertex: T, ...toVertex: T[]): Graph<T> {
89+
let edges = this.list.get(fromVertex);
90+
91+
if (!edges) {
92+
this.addVertex(fromVertex);
93+
edges = this.list.get(fromVertex) as T[];
94+
}
95+
96+
edges.push(...toVertex);
97+
98+
return this;
99+
}
100+
101+
// noinspection JSUnusedGlobalSymbols
102+
/**
103+
* Removes given edges from a given vertex
104+
*
105+
* @param {any} fromVertex - target vertex to remove edges from
106+
* @param {...any[]} toVertex - edges to remove
107+
* @return {Graph<any>}
108+
*/
109+
public delEdge(fromVertex: T, ...toVertex: T[]): Graph<T> {
110+
const edges = this.list.get(fromVertex);
111+
112+
if (!(edges && edges.length)) {
113+
return this;
114+
}
115+
116+
for (const vertex of toVertex) {
117+
while (~edges.indexOf(vertex)) {
118+
edges.splice(edges.indexOf(vertex), 1);
119+
}
120+
}
121+
122+
return this;
123+
}
124+
125+
/**
126+
* Checks if a given vertex has given edge, returns true if has, false -
127+
* otherwise
128+
*
129+
* @param {any} vertex
130+
* @param {any} edge
131+
* @return {boolean}
132+
*/
133+
public hasEdge(vertex: T, edge: T): boolean {
134+
return !!~(this.list.get(vertex) || []).indexOf(edge);
135+
}
136+
137+
/**
138+
* Checks if this graph contains given vertex, returns true if contains,
139+
* false - otherwise
140+
*
141+
* @param {any} vertex
142+
* @return {boolean}
143+
*/
144+
public hasVertex(vertex: T): boolean {
145+
return this.list.has(vertex);
146+
}
147+
148+
// noinspection JSUnusedGlobalSymbols
149+
/**
150+
* Performs DFS traversal over graph, executing on each step passed callback
151+
* function. If callback returns false - will stop traversal at that
152+
* step.
153+
*
154+
* @param {GraphForeachCallback<any>} callback
155+
* @return {Graph<any>}
156+
*/
157+
public forEach(callback: GraphForeachCallback<T>): Graph<T> {
158+
const visited = new Map<T, boolean>();
159+
160+
for (const node of this.list.keys()) {
161+
this.walk(node, callback, visited);
162+
}
163+
164+
return this;
165+
}
166+
167+
/**
168+
* Performs DFS walk over graph staring from given vertex, unless
169+
* graph path is end for that vertex. So, literally, it performs
170+
* walking through a possible path down the staring vertex in a graph.
171+
*
172+
* @param {any} vertex
173+
* @param {GraphForeachCallback<any>} callback
174+
* @param {Map<any, boolean>()} visited
175+
* @return {Graph<any>}
176+
*/
177+
public walk(
178+
vertex: T,
179+
callback?: GraphForeachCallback<T>,
180+
visited = new Map<T, boolean>(),
181+
): Graph<T> {
182+
if (!visited.get(vertex)){
183+
visited.set(vertex, true);
184+
185+
if (callback && callback(vertex, visited) === false) {
186+
return this;
187+
}
188+
189+
for (const neighbor of this.list.get(vertex) || []) {
190+
this.walk(neighbor, callback, visited);
191+
}
192+
}
193+
194+
return this;
195+
}
196+
197+
// noinspection JSUnusedGlobalSymbols
198+
/**
199+
* Returns max possible path down the graph for a given vertex,
200+
* using DFS traversal over the path
201+
*
202+
* @param {any} vertex
203+
* @return {IterableIterator<any>}
204+
*/
205+
public path(vertex: T): IterableIterator<T> {
206+
const visited = new Map<T, boolean>();
207+
208+
this.walk(vertex, undefined, visited);
209+
210+
return visited.keys();
211+
}
212+
213+
// noinspection JSUnusedGlobalSymbols
214+
/**
215+
* Returns true if graph has al least one cycled path in it,
216+
* false - otherwise
217+
*
218+
* @return {boolean}
219+
*/
220+
public isCycled(): boolean {
221+
const visited = new Map<T, boolean>();
222+
const stack = new Map<T, boolean>();
223+
224+
for (const node of this.list.keys()) {
225+
if (this.detectCycle(node, visited, stack)) {
226+
return true;
227+
}
228+
}
229+
230+
return false;
231+
}
232+
233+
// noinspection JSUnusedGlobalSymbols
234+
/**
235+
* Returns list of vertices in this graph
236+
*
237+
* @return {IterableIterator<any>}
238+
*/
239+
public vertices(): IterableIterator<T> {
240+
return this.list.keys();
241+
}
242+
243+
/**
244+
* Performs recursive cycles detection on a graph.
245+
* Private method. If you need to detect cycles, use isCycled() instead.
246+
*
247+
* @access private
248+
* @param {any} vertex
249+
* @param {Map<any, boolean>} visited
250+
* @param {Map<any, boolean>} stack
251+
*/
252+
private detectCycle(
253+
vertex: T,
254+
visited: Map<T, boolean>,
255+
stack: Map<T, boolean>,
256+
): boolean {
257+
if (!visited.get(vertex)) {
258+
visited.set(vertex, true);
259+
stack.set(vertex, true);
260+
261+
for (const currentNode of this.list.get(vertex) || []) {
262+
if ((!visited.get(currentNode) && this.detectCycle(
263+
currentNode, visited, stack,
264+
)) || stack.get(currentNode)) {
265+
return true;
266+
}
267+
}
268+
}
269+
270+
stack.set(vertex, false);
271+
272+
return false;
273+
}
274+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import isDefined = js.isDefined;
2626
import isOk = js.isOk;
2727

2828
/* models exports! */
29+
export * from './Graph';
2930
export * from './BaseModel';
3031
export * from './helpers';
3132
export * from './decorators';

0 commit comments

Comments
 (0)