• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.98
/graphty-element/src/data/GEXFDataSource.ts
1
import { XMLParser } from "fast-xml-parser";
15✔
2

3
import type { AdHocData } from "../config/common.js";
4
import { BaseDataSourceConfig, DataSource, DataSourceChunk } from "./DataSource.js";
15✔
5

6
// GEXF has no additional config currently, so just use the base config
7
export type GEXFDataSourceConfig = BaseDataSourceConfig;
8

9
interface GEXFAttribute {
10
    id: string;
11
    title: string;
12
    type: string;
13
}
14

15
/**
16
 * Data source for loading graph data from GEXF (Graph Exchange XML Format) files.
17
 * Supports node and edge attributes, attribute types, and dynamic graphs.
18
 */
19
export class GEXFDataSource extends DataSource {
15✔
20
    static readonly type = "gexf";
18✔
21

22
    private config: GEXFDataSourceConfig;
23

24
    /**
25
     * Creates a new GEXFDataSource instance.
26
     * @param config - Configuration options for GEXF parsing and data loading
27
     */
28
    constructor(config: GEXFDataSourceConfig) {
18✔
29
        super(config.errorLimit ?? 100, config.chunkSize);
22✔
30
        this.config = config;
22✔
31
    }
22✔
32

33
    protected getConfig(): BaseDataSourceConfig {
18✔
34
        return this.config;
22✔
35
    }
22✔
36

37
    /**
38
     * Fetches and parses GEXF format data into graph chunks.
39
     * @yields DataSourceChunk objects containing parsed nodes and edges
40
     */
41
    async *sourceFetchData(): AsyncGenerator<DataSourceChunk, void, unknown> {
18✔
42
        // Get XML content
43
        const xmlContent = await this.getContent();
22✔
44

45
        // Parse XML
46
        const parser = new XMLParser({
21✔
47
            ignoreAttributes: false,
21✔
48
            attributeNamePrefix: "@_",
21✔
49
            parseAttributeValue: false, // Keep as strings, we'll parse by type
21✔
50
            trimValues: true,
21✔
51
            isArray: (name) => {
21✔
52
                // These elements should always be treated as arrays
53
                return ["node", "edge", "attribute", "attvalue"].includes(name);
14,334✔
54
            },
14,334✔
55
        });
21✔
56

57
        let parsed;
21✔
58
        try {
21✔
59
            parsed = parser.parse(xmlContent);
21✔
60
        } catch (error) {
21!
61
            throw new Error(`Failed to parse GEXF XML: ${error instanceof Error ? error.message : String(error)}`);
×
62
        }
×
63

64
        const { gexf } = parsed;
21✔
65
        if (!gexf) {
21!
66
            throw new Error("Invalid GEXF: missing <gexf> root element");
2✔
67
        }
2✔
68

69
        // Get graph element
70
        const { graph } = gexf;
19✔
71
        if (!graph) {
21!
72
            throw new Error("Invalid GEXF: missing <graph> element");
1✔
73
        }
1✔
74

75
        // Parse attribute definitions
76
        const nodeAttributes = this.parseAttributeDefinitions(graph.attributes, "node");
18✔
77
        const edgeAttributes = this.parseAttributeDefinitions(graph.attributes, "edge");
18✔
78

79
        // Parse and yield nodes in chunks
80
        const nodes = this.parseNodes(graph.nodes?.node, nodeAttributes);
22✔
81
        const edges = this.parseEdges(graph.edges?.edge, edgeAttributes);
22✔
82

83
        // Use shared chunking helper
84
        yield* this.chunkData(nodes, edges);
22✔
85
    }
22✔
86

87
    private parseAttributeDefinitions(attributesData: unknown, forClass: "node" | "edge"): Map<string, GEXFAttribute> {
18✔
88
        const attributes = new Map<string, GEXFAttribute>();
36✔
89

90
        if (!attributesData) {
36!
91
            return attributes;
30✔
92
        }
30✔
93

94
        // Handle single or multiple <attributes> elements
95
        const attrGroups = Array.isArray(attributesData) ? attributesData : [attributesData];
36!
96

97
        for (const group of attrGroups) {
36✔
98
            const groupClass = (group as { "@_class"?: string })["@_class"];
10✔
99
            if (groupClass !== forClass) {
10✔
100
                continue;
5✔
101
            }
5✔
102

103
            const attrList = (group as { attribute?: unknown[] }).attribute;
5✔
104
            if (!attrList) {
10!
105
                continue;
×
106
            }
✔
107

108
            const attrArray = Array.isArray(attrList) ? attrList : [attrList];
10!
109

110
            for (const attr of attrArray) {
10✔
111
                const attrObj = attr as {
15✔
112
                    "@_id": string;
113
                    "@_title": string;
114
                    "@_type": string;
115
                };
116
                const id = attrObj["@_id"];
15✔
117
                const title = attrObj["@_title"] || id;
15!
118
                const type = attrObj["@_type"] || "string";
15!
119

120
                attributes.set(id, { id, title, type });
15✔
121
            }
15✔
122
        }
5✔
123

124
        return attributes;
6✔
125
    }
36✔
126

127
    private parseNodes(nodeData: unknown, attributes: Map<string, GEXFAttribute>): AdHocData[] {
18✔
128
        if (!nodeData) {
18!
129
            return [] as AdHocData[];
3✔
130
        }
3✔
131

132
        const nodeArray = Array.isArray(nodeData) ? nodeData : [nodeData];
18!
133
        const nodes: Record<string, unknown>[] = [];
18✔
134

135
        for (const node of nodeArray) {
18✔
136
            try {
5,339✔
137
                const nodeObj = node as {
5,339✔
138
                    "@_id": string;
139
                    "@_label"?: string;
140
                    attvalues?: { attvalue?: unknown[] };
141
                    "viz:position"?: {
142
                        "@_x"?: string;
143
                        "@_y"?: string;
144
                        "@_z"?: string;
145
                    };
146
                    "viz:color"?: {
147
                        "@_r"?: string;
148
                        "@_g"?: string;
149
                        "@_b"?: string;
150
                        "@_a"?: string;
151
                    };
152
                    "viz:size"?: {
153
                        "@_value"?: string;
154
                    };
155
                };
156

157
                const id = nodeObj["@_id"];
5,339✔
158
                if (!id) {
5,339!
159
                    this.errorAggregator.addError({
1✔
160
                        message: "Node missing id attribute",
1✔
161
                        category: "missing-value",
1✔
162
                        field: "id",
1✔
163
                    });
1✔
164
                    continue;
1✔
165
                }
1✔
166

167
                const nodeData: Record<string, unknown> = { id };
5,338✔
168

169
                // Add label if present
170
                if (nodeObj["@_label"]) {
5,338✔
171
                    nodeData.label = nodeObj["@_label"];
332✔
172
                }
332✔
173

174
                // Parse attribute values
175
                if (nodeObj.attvalues?.attvalue) {
5,339✔
176
                    const attvalues = Array.isArray(nodeObj.attvalues.attvalue)
242✔
177
                        ? nodeObj.attvalues.attvalue
242!
178
                        : [nodeObj.attvalues.attvalue];
×
179

180
                    for (const attvalue of attvalues) {
242✔
181
                        const attObj = attvalue as { "@_for": string; "@_value": string };
969✔
182
                        const attrId = attObj["@_for"];
969✔
183
                        const value = attObj["@_value"];
969✔
184

185
                        const attrDef = attributes.get(attrId);
969✔
186
                        if (attrDef) {
969✔
187
                            nodeData[attrDef.title] = this.parseValue(value, attrDef.type);
969✔
188
                        }
969✔
189
                    }
969✔
190
                }
242✔
191

192
                // Parse viz namespace elements
193
                if (nodeObj["viz:position"]) {
5,339✔
194
                    const pos = nodeObj["viz:position"];
7✔
195
                    nodeData.position = {
7✔
196
                        x: pos["@_x"] ? parseFloat(pos["@_x"]) : 0,
7!
197
                        y: pos["@_y"] ? parseFloat(pos["@_y"]) : 0,
7!
198
                        z: pos["@_z"] ? parseFloat(pos["@_z"]) : 0,
7!
199
                    };
7✔
200
                }
7✔
201

202
                if (nodeObj["viz:color"]) {
5,339✔
203
                    const color = nodeObj["viz:color"];
242✔
204
                    nodeData.color = {
242✔
205
                        r: color["@_r"] ? parseInt(color["@_r"], 10) : 0,
242!
206
                        g: color["@_g"] ? parseInt(color["@_g"], 10) : 0,
242!
207
                        b: color["@_b"] ? parseInt(color["@_b"], 10) : 0,
242!
208
                        a: color["@_a"] ? parseFloat(color["@_a"]) : 1.0,
242!
209
                    };
242✔
210
                }
242✔
211

212
                if (nodeObj["viz:size"]) {
5,339✔
213
                    const size = nodeObj["viz:size"];
7✔
214
                    nodeData.size = size["@_value"] ? parseFloat(size["@_value"]) : 1.0;
7!
215
                }
7✔
216

217
                nodes.push(nodeData);
5,338✔
218
            } catch (error) {
5,339!
219
                const canContinue = this.errorAggregator.addError({
×
220
                    message: `Failed to parse node: ${error instanceof Error ? error.message : String(error)}`,
×
221
                    category: "parse-error",
×
222
                });
×
223

224
                if (!canContinue) {
×
225
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
226
                }
×
227
            }
×
228
        }
5,339✔
229

230
        return nodes as AdHocData[];
15✔
231
    }
18✔
232

233
    private parseEdges(edgeData: unknown, attributes: Map<string, GEXFAttribute>): AdHocData[] {
18✔
234
        if (!edgeData) {
18!
235
            return [] as AdHocData[];
4✔
236
        }
4✔
237

238
        const edgeArray = Array.isArray(edgeData) ? edgeData : [edgeData];
18!
239
        const edges: Record<string, unknown>[] = [];
18✔
240

241
        for (const edge of edgeArray) {
18✔
242
            try {
1,570✔
243
                const edgeObj = edge as {
1,570✔
244
                    "@_id"?: string;
245
                    "@_source": string;
246
                    "@_target": string;
247
                    "@_weight"?: string;
248
                    "@_type"?: string;
249
                    "@_label"?: string;
250
                    attvalues?: { attvalue?: unknown[] };
251
                };
252

253
                const src = edgeObj["@_source"];
1,570✔
254
                const dst = edgeObj["@_target"];
1,570✔
255

256
                if (!src || !dst) {
1,570!
257
                    this.errorAggregator.addError({
1✔
258
                        message: "Edge missing source or target attribute",
1✔
259
                        category: "missing-value",
1✔
260
                        field: !src ? "source" : "target",
1!
261
                    });
1✔
262
                    continue;
1✔
263
                }
1✔
264

265
                const edgeData: Record<string, unknown> = { src, dst };
1,569✔
266

267
                // Add optional attributes
268
                if (edgeObj["@_id"]) {
1,569✔
269
                    edgeData.id = edgeObj["@_id"];
1,569✔
270
                }
1,569✔
271

272
                if (edgeObj["@_label"]) {
1,570!
273
                    edgeData.label = edgeObj["@_label"];
×
274
                }
✔
275

276
                if (edgeObj["@_weight"]) {
1,570✔
277
                    edgeData.weight = parseFloat(edgeObj["@_weight"]);
164✔
278
                }
164✔
279

280
                if (edgeObj["@_type"]) {
1,570!
281
                    edgeData.type = edgeObj["@_type"];
1✔
282
                }
1✔
283

284
                // Parse attribute values
285
                if (edgeObj.attvalues?.attvalue) {
1,570✔
286
                    const attvalues = Array.isArray(edgeObj.attvalues.attvalue)
7✔
287
                        ? edgeObj.attvalues.attvalue
7!
288
                        : [edgeObj.attvalues.attvalue];
×
289

290
                    for (const attvalue of attvalues) {
7✔
291
                        const attObj = attvalue as { "@_for": string; "@_value": string };
19✔
292
                        const attrId = attObj["@_for"];
19✔
293
                        const value = attObj["@_value"];
19✔
294

295
                        const attrDef = attributes.get(attrId);
19✔
296
                        if (attrDef) {
19✔
297
                            edgeData[attrDef.title] = this.parseValue(value, attrDef.type);
19✔
298
                        }
19✔
299
                    }
19✔
300
                }
7✔
301

302
                edges.push(edgeData);
1,569✔
303
            } catch (error) {
1,570!
304
                const canContinue = this.errorAggregator.addError({
×
305
                    message: `Failed to parse edge: ${error instanceof Error ? error.message : String(error)}`,
×
306
                    category: "parse-error",
×
307
                });
×
308

309
                if (!canContinue) {
×
310
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
311
                }
×
312
            }
×
313
        }
1,570✔
314

315
        return edges as AdHocData[];
14✔
316
    }
18✔
317

318
    private parseValue(value: string, type: string): string | number | boolean {
18✔
319
        switch (type) {
988✔
320
            case "integer":
988✔
321
            case "long":
988✔
322
                return parseInt(value, 10);
13✔
323
            case "float":
988✔
324
            case "double":
988✔
325
                return parseFloat(value);
487✔
326
            case "boolean":
988!
327
                return value === "true" || value === "1";
5✔
328
            default:
988✔
329
                return value;
483✔
330
        }
988✔
331
    }
988✔
332
}
18✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc