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

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

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

79.5
/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(
18✔
88
        attributesData: unknown,
36✔
89
        forClass: "node" | "edge",
36✔
90
    ): Map<string, GEXFAttribute> {
36✔
91
        const attributes = new Map<string, GEXFAttribute>();
36✔
92

93
        if (!attributesData) {
36!
94
            return attributes;
30✔
95
        }
30✔
96

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

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

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

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

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

123
                attributes.set(id, {id, title, type});
15✔
124
            }
15✔
125
        }
5✔
126

127
        return attributes;
6✔
128
    }
36✔
129

130
    private parseNodes(
18✔
131
        nodeData: unknown,
18✔
132
        attributes: Map<string, GEXFAttribute>,
18✔
133
    ): AdHocData[] {
18✔
134
        if (!nodeData) {
18!
135
            return [] as AdHocData[];
3✔
136
        }
3✔
137

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

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

163
                const id = nodeObj["@_id"];
5,339✔
164
                if (!id) {
5,339!
165
                    this.errorAggregator.addError({
1✔
166
                        message: "Node missing id attribute",
1✔
167
                        category: "missing-value",
1✔
168
                        field: "id",
1✔
169
                    });
1✔
170
                    continue;
1✔
171
                }
1✔
172

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

175
                // Add label if present
176
                if (nodeObj["@_label"]) {
5,338✔
177
                    nodeData.label = nodeObj["@_label"];
332✔
178
                }
332✔
179

180
                // Parse attribute values
181
                if (nodeObj.attvalues?.attvalue) {
5,339✔
182
                    const attvalues = Array.isArray(nodeObj.attvalues.attvalue) ?
242✔
183
                        nodeObj.attvalues.attvalue :
242!
184
                        [nodeObj.attvalues.attvalue];
×
185

186
                    for (const attvalue of attvalues) {
242✔
187
                        const attObj = attvalue as {"@_for": string, "@_value": string};
969✔
188
                        const attrId = attObj["@_for"];
969✔
189
                        const value = attObj["@_value"];
969✔
190

191
                        const attrDef = attributes.get(attrId);
969✔
192
                        if (attrDef) {
969✔
193
                            nodeData[attrDef.title] = this.parseValue(value, attrDef.type);
969✔
194
                        }
969✔
195
                    }
969✔
196
                }
242✔
197

198
                // Parse viz namespace elements
199
                if (nodeObj["viz:position"]) {
5,339✔
200
                    const pos = nodeObj["viz:position"];
7✔
201
                    nodeData.position = {
7✔
202
                        x: pos["@_x"] ? parseFloat(pos["@_x"]) : 0,
7!
203
                        y: pos["@_y"] ? parseFloat(pos["@_y"]) : 0,
7!
204
                        z: pos["@_z"] ? parseFloat(pos["@_z"]) : 0,
7!
205
                    };
7✔
206
                }
7✔
207

208
                if (nodeObj["viz:color"]) {
5,339✔
209
                    const color = nodeObj["viz:color"];
242✔
210
                    nodeData.color = {
242✔
211
                        r: color["@_r"] ? parseInt(color["@_r"], 10) : 0,
242!
212
                        g: color["@_g"] ? parseInt(color["@_g"], 10) : 0,
242!
213
                        b: color["@_b"] ? parseInt(color["@_b"], 10) : 0,
242!
214
                        a: color["@_a"] ? parseFloat(color["@_a"]) : 1.0,
242!
215
                    };
242✔
216
                }
242✔
217

218
                if (nodeObj["viz:size"]) {
5,339✔
219
                    const size = nodeObj["viz:size"];
7✔
220
                    nodeData.size = size["@_value"] ? parseFloat(size["@_value"]) : 1.0;
7!
221
                }
7✔
222

223
                nodes.push(nodeData);
5,338✔
224
            } catch (error) {
5,339!
225
                const canContinue = this.errorAggregator.addError({
×
226
                    message: `Failed to parse node: ${error instanceof Error ? error.message : String(error)}`,
×
227
                    category: "parse-error",
×
228
                });
×
229

230
                if (!canContinue) {
×
231
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
232
                }
×
233
            }
×
234
        }
5,339✔
235

236
        return nodes as AdHocData[];
15✔
237
    }
18✔
238

239
    private parseEdges(
18✔
240
        edgeData: unknown,
18✔
241
        attributes: Map<string, GEXFAttribute>,
18✔
242
    ): AdHocData[] {
18✔
243
        if (!edgeData) {
18!
244
            return [] as AdHocData[];
4✔
245
        }
4✔
246

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

250
        for (const edge of edgeArray) {
18✔
251
            try {
1,570✔
252
                const edgeObj = edge as {
1,570✔
253
                    "@_id"?: string;
254
                    "@_source": string;
255
                    "@_target": string;
256
                    "@_weight"?: string;
257
                    "@_type"?: string;
258
                    "@_label"?: string;
259
                    "attvalues"?: {attvalue?: unknown[]};
260
                };
261

262
                const src = edgeObj["@_source"];
1,570✔
263
                const dst = edgeObj["@_target"];
1,570✔
264

265
                if (!src || !dst) {
1,570!
266
                    this.errorAggregator.addError({
1✔
267
                        message: "Edge missing source or target attribute",
1✔
268
                        category: "missing-value",
1✔
269
                        field: !src ? "source" : "target",
1!
270
                    });
1✔
271
                    continue;
1✔
272
                }
1✔
273

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

276
                // Add optional attributes
277
                if (edgeObj["@_id"]) {
1,569✔
278
                    edgeData.id = edgeObj["@_id"];
1,569✔
279
                }
1,569✔
280

281
                if (edgeObj["@_label"]) {
1,570!
282
                    edgeData.label = edgeObj["@_label"];
×
283
                }
✔
284

285
                if (edgeObj["@_weight"]) {
1,570✔
286
                    edgeData.weight = parseFloat(edgeObj["@_weight"]);
164✔
287
                }
164✔
288

289
                if (edgeObj["@_type"]) {
1,570!
290
                    edgeData.type = edgeObj["@_type"];
1✔
291
                }
1✔
292

293
                // Parse attribute values
294
                if (edgeObj.attvalues?.attvalue) {
1,570✔
295
                    const attvalues = Array.isArray(edgeObj.attvalues.attvalue) ?
7✔
296
                        edgeObj.attvalues.attvalue :
7!
297
                        [edgeObj.attvalues.attvalue];
×
298

299
                    for (const attvalue of attvalues) {
7✔
300
                        const attObj = attvalue as {"@_for": string, "@_value": string};
19✔
301
                        const attrId = attObj["@_for"];
19✔
302
                        const value = attObj["@_value"];
19✔
303

304
                        const attrDef = attributes.get(attrId);
19✔
305
                        if (attrDef) {
19✔
306
                            edgeData[attrDef.title] = this.parseValue(value, attrDef.type);
19✔
307
                        }
19✔
308
                    }
19✔
309
                }
7✔
310

311
                edges.push(edgeData);
1,569✔
312
            } catch (error) {
1,570!
313
                const canContinue = this.errorAggregator.addError({
×
314
                    message: `Failed to parse edge: ${error instanceof Error ? error.message : String(error)}`,
×
315
                    category: "parse-error",
×
316
                });
×
317

318
                if (!canContinue) {
×
319
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
320
                }
×
321
            }
×
322
        }
1,570✔
323

324
        return edges as AdHocData[];
14✔
325
    }
18✔
326

327
    private parseValue(value: string, type: string): string | number | boolean {
18✔
328
        switch (type) {
988✔
329
            case "integer":
988✔
330
            case "long":
988✔
331
                return parseInt(value, 10);
13✔
332
            case "float":
988✔
333
            case "double":
988✔
334
                return parseFloat(value);
487✔
335
            case "boolean":
988!
336
                return value === "true" || value === "1";
5✔
337
            default:
988✔
338
                return value;
483✔
339
        }
988✔
340
    }
988✔
341
}
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

© 2025 Coveralls, Inc