Passed
Push — master ( 3dc51d...479647 )
by Kolja
01:13
created

jgfGraph.js   C

Complexity

Total Complexity 53
Complexity/F 1.56

Size

Lines of Code 335
Function Count 34

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 2 Features 2
Metric Value
cc 0
eloc 129
nc 1
dl 0
loc 335
rs 6.96
c 11
b 2
f 2
wmc 53
mnd 2
bc 49
fnc 34
bpm 1.4411
cpm 1.5588
noi 0

26 Functions

Rating   Name   Duplication   Size   Complexity  
A ➔ loadFromJSON 0 12 1
A ➔ _isEdgeEqual 0 5 1
A ➔ graphDimensions 0 11 1
A ➔ _getJsonEdges 0 8 3
A ➔ _findNode 0 3 1
A ➔ _findNodeById 0 7 2
A ➔ type 0 3 1
A ➔ edges 0 3 1
A ➔ removeNode 0 7 2
A ➔ _getJsonMetadata 0 8 2
A ➔ addNode 0 7 2
A ➔ addEdge 0 22 5
A ➔ _nodeExistsById 0 4 1
A ➔ _guardAgainstEmptyNodeParams 0 9 3
A ➔ getNodeById 0 3 1
A ➔ constructor 0 9 1
A ➔ nodes 0 3 1
A ➔ json 0 16 2
A ➔ addNodes 0 5 2
A ➔ _nodeExists 0 3 1
A ➔ label 0 3 1
A ➔ getEdges 0 5 1
A ➔ removeEdges 0 3 1
A ➔ _guardAgainstNonExistentNodes 0 9 3
A ➔ metadata 0 3 1
A ➔ addEdges 0 7 3

How to fix   Complexity   

Complexity

Complex classes like jgfGraph.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const check = require('check-types');
2
const _ = require('lodash');
3
const { cloneObject } = require('./common');
4
const { JGFNode } = require('./jgfNode');
5
6
/**
7
 * A single JGF graph instance, always contained in a parent JGFContainer
8
 */
9
class JGFGraph {
10
11
    /**
12
     * Constructor
13
     * @param {*} type graph classification
14
     * @param {*} label a text display for the graph
15
     * @param {*} directed true for a directed graph, false for an undirected graph
16
     * @param {*} metadata about the graph
17
     */
18
    constructor(type = '', label = '', directed = true, metadata = null) {
19
        this._nodes = [];
20
        this._edges = [];
21
22
        this._type = type;
23
        this._label = label;
24
        this._directed = directed;
25
        this._metadata = metadata;
26
    }
27
28
29
    /**
30
     * Loads the graph from a JGF JSON object
31
     * @param {*} graphJson JGF JSON object
32
     */
33
    loadFromJSON(graphJson) {
34
        this._type = graphJson.type;
35
        this._label = graphJson.label;
36
        // todo: this makes the graph always directed (even if false is passed), I doubt that this was the intention here
37
        this._directed = graphJson.directed || true;
38
        this._metadata = graphJson.metadata;
39
40
        this._nodes = [];
41
        this._edges = [];
42
        this.addNodes(graphJson.nodes);
43
        this.addEdges(graphJson.edges);
44
    }
45
46
    /**
47
     * @param {JGFNode} node Node to be found.
48
     * @private
49
     */
50
    _findNode(node) {
51
        return this._findNodeById(node.id);
52
    }
53
54
    /**
55
     * @param {string} nodeId Node to be found.
56
     * @private
57
     */
58
    _findNodeById(nodeId) {
59
        let foundNode = _.find(this._nodes, (existingNode) => { return existingNode.id === nodeId } );
60
        if (!foundNode) {
61
            throw new Error(`A node does not exist with id = ${nodeId}`);
62
        }
63
        return foundNode;
64
    }
65
66
    /**
67
     * @param {JGFNode} node Node to be found.
68
     * @private
69
     */
70
    _nodeExists(node) {
71
        return this._nodeExistsById(node.id);
72
    }
73
74
    /**
75
     * @param {string} nodeId Node to be found.
76
     * @private
77
     */
78
    _nodeExistsById(nodeId) {
79
        let foundNode = _.find(this._nodes, (existingNode) => { return existingNode.id === nodeId } );
80
        return !!foundNode;
81
    }
82
83
    /**
84
     * Returns the graph type
85
     */
86
    get type() {
87
        return this._type;
88
    }
89
90
    /**
91
     * Set the graph type
92
     */
93
    set type(value) {
94
        this._type = value;
95
    }
96
97
    /**
98
     * Returns the graph label
99
     */
100
    get label() {
101
        return this._label;
102
    }
103
104
    /**
105
     * Set the graph label
106
     */
107
    set label(value) {
108
        this._label = value;
109
    }
110
111
    /**
112
     * Returns the graph meta data
113
     */
114
    get metadata() {
115
        return this._metadata;
116
    }
117
118
    /**
119
     * Set the graph meta data
120
     */
121
    set metadata(value) {
122
        this._metadata = value;
123
    }
124
125
    /**
126
     * Returns all nodes
127
     */
128
    get nodes() {
129
        return this._nodes;
130
    }
131
132
    /**
133
     * Returns all edges
134
     */
135
    get edges() {
136
        return cloneObject(this._edges);
137
    }
138
139
140
    /**
141
     * Returns the graph as JGF Json
142
     */
143
    get json() {
144
        let json = {
145
            type: this._type,
146
            label: this._label,
147
            directed: this._directed,
148
            nodes: this._nodes,
149
            edges: this._getJsonEdges(),
150
        };
151
152
        let metadata = this._getJsonMetadata();
153
        if (metadata) {
154
            json.metadata = metadata;
155
        }
156
157
        return cloneObject(json);
158
    }
159
160
    _getJsonMetadata() {
161
        let metadata = null;
162
        if (check.assigned(this._metadata)) {
163
            metadata = this._metadata;
164
        }
165
166
        return metadata;
167
    }
168
169
    _getJsonEdges() {
170
        let edges = [];
171
        if (check.assigned(this._edges) && this._edges.length > 0) {
172
            edges = this._edges;
173
        }
174
175
        return edges;
176
    }
177
178
    /**
179
     * Adds a new node
180
     * @param {JGFNode} node Node to be added.
181
     */
182
    addNode(node) {
183
        if (this._nodeExists(node)) {
184
            throw new Error(`A node already exists with id = ${node.id}`);
185
        }
186
187
        this._nodes.push(node);
188
    }
189
190
191
    /**
192
     * Adds multiple nodes
193
     * @param {JGFNode[]} nodes A collection of JGF node objects.
194
     */
195
    addNodes(nodes) {
196
        for (let node of nodes) {
197
            this.addNode(node);
198
        }
199
    }
200
201
    /**
202
     * Removes an existing graph node
203
     * @param {JGFNode} node Node to be removed.
204
     */
205
    removeNode(node) {
206
        if (!this._nodeExists(node)) {
207
            throw new Error(`A node does not exist with id = ${node.id}`);
208
        }
209
210
        _.remove(this._nodes, (existingNode) => { return existingNode.id === node.id });
211
    }
212
213
    /**
214
     * Get a node by a node id.
215
     * @param {string} nodeId Unique node id
216
     */
217
    getNodeById(nodeId) {
218
        return this._findNodeById(nodeId);
219
    }
220
221
    /**
222
     * Adds an edge between a source node and a target node
223
     * @param {*} source Source node id
224
     * @param {*} target Target node id
225
     * @param {*} relation Edge relation (AKA 'relationship type')
226
     * @param {*} label Edge label (the display name of the edge)
227
     * @param {*} metadata Custom edge meta data
228
     * @param {*} directed true for a directed edge, false for undirected
229
     */
230
    addEdge(source, target, relation = null, label = null, metadata = null, directed = null) {
231
        JGFGraph._guardAgainstEmptyNodeParams(source, target);
232
233
        let edge = {
234
            source,
235
            target,
236
        };
237
        if (check.assigned(relation)) {
238
            edge.relation = relation;
239
        }
240
        if (check.assigned(label)) {
241
            edge.label = label;
242
        }
243
        if (check.assigned(metadata)) {
244
            edge.metadata = metadata;
245
        }
246
        if (check.assigned(directed)) {
247
            edge.directed = directed;
248
        }
249
250
        this._edges.push(edge);
251
    }
252
253
    _guardAgainstNonExistentNodes(source, target) {
254
        if (!(source in this._nodes)) {
255
            throw new Error(`addEdge failed: source node isn't found in nodes. source = ${source}`);
256
        }
257
258
        if (!(target in this._nodes)) {
259
            throw new Error(`addEdge failed: target node isn't found in nodes. target = ${target}`);
260
        }
261
    }
262
263
    static _guardAgainstEmptyNodeParams(source, target) {
264
        if (!source) {
265
            throw new Error('addEdge failed: source parameter is not valid');
266
        }
267
268
        if (!target) {
269
            throw new Error('addEdge failed: target parameter is not valid');
270
        }
271
    }
272
273
    /**
274
     * Adds multiple edges
275
     * @param {*} edges A collection of JGF edge obejcts
276
     */
277
    addEdges(edges) {
278
        if (edges) {
279
            for (let edge of edges) {
280
                this.addEdge(edge.source, edge.target, edge.relation, edge.label, edge.metadata, edge.directed);
281
            }
282
        }
283
    }
284
285
    /**
286
     * Checks whether the passed edge is equal to an edge with all other three passed params.
287
     * @param {*} edge
288
     * @param {*} source Source node id
289
     * @param {*} target Target node id
290
     * @param {*} relation Specific edge relation type to remove. If empty then all edges will be removed, regardless of their relation
291
     */
292
    static _isEdgeEqual(edge, source, target, relation) {
293
        return edge.source === source &&
294
            edge.target === target &&
295
            (relation === '' || edge.relation === relation);
296
    }
297
298
    /**
299
     * Removes existing graph edges
300
     * @param {*} source Source node id
301
     * @param {*} target Target node id
302
     * @param {*} relation Specific edge relation type to remove. If empty then all edges will be removed, regardless of their relation
303
     */
304
    removeEdges(source, target, relation = '') {
305
        _.remove(this._edges, (edge) => JGFGraph._isEdgeEqual(edge, source, target, relation));
306
    }
307
308
    /**
309
     * Get edges between source node and target node, with an optional edge relation
310
     * @param {*} source
311
     * @param {*} target
312
     * @param {*} relation
313
     */
314
    getEdges(source, target, relation = '') {
315
        let edges = _.filter(this._edges, (edge) => JGFGraph._isEdgeEqual(edge, source, target, relation));
316
317
        return cloneObject(edges);
318
    }
319
320
    get graphDimensions() {
321
        let dimensions = {
322
            nodes: 0,
323
            edges: 0,
324
        };
325
326
        dimensions.nodes = Object.keys(this._nodes).length;
327
        dimensions.edges = this._edges.length;
328
329
        return dimensions;
330
    }
331
}
332
333
module.exports = {
334
    JGFGraph,
335
};