Skip to content

Custom Data Sources ​

Guide to creating custom data source handlers.

Overview ​

Graphty's data source system is extensible. Create custom data sources to load graphs from APIs, databases, or specialized file formats.

DataSource Interface ​

All data sources extend the abstract DataSource class:

typescript
abstract class DataSource {
    static type: string;

    abstract load(config: object): AsyncGenerator<GraphData>;
}

interface GraphData {
    nodes: NodeData[];
    edges: EdgeData[];
}

Creating a Custom Data Source ​

Basic Example ​

typescript
import { DataSource, GraphData, NodeData, EdgeData } from "@graphty/graphty-element";

class MyDataSource extends DataSource {
    static type = "my-api";

    async *load(config: { url: string }): AsyncGenerator<GraphData> {
        const response = await fetch(config.url);
        const data = await response.json();

        yield {
            nodes: data.nodes,
            edges: data.edges,
        };
    }
}

// Register the data source
DataSource.register(MyDataSource);

Using Your Data Source ​

typescript
await graph.loadFromDataSource("my-api", {
    url: "https://api.example.com/graph",
});

Complete Example: REST API ​

typescript
import { DataSource, GraphData, NodeData, EdgeData } from "@graphty/graphty-element";

interface ApiConfig {
    baseUrl: string;
    apiKey?: string;
    graphId: string;
}

class RestApiDataSource extends DataSource {
    static type = "rest-api";

    async *load(config: ApiConfig): AsyncGenerator<GraphData> {
        const headers: Record<string, string> = {
            "Content-Type": "application/json",
        };

        if (config.apiKey) {
            headers["Authorization"] = `Bearer ${config.apiKey}`;
        }

        // Fetch nodes
        const nodesResponse = await fetch(`${config.baseUrl}/graphs/${config.graphId}/nodes`, { headers });
        const nodesData = await nodesResponse.json();

        // Fetch edges
        const edgesResponse = await fetch(`${config.baseUrl}/graphs/${config.graphId}/edges`, { headers });
        const edgesData = await edgesResponse.json();

        // Transform to Graphty format
        const nodes: NodeData[] = nodesData.map((n: any) => ({
            id: n.nodeId,
            label: n.name,
            ...n.attributes,
        }));

        const edges: EdgeData[] = edgesData.map((e: any) => ({
            source: e.fromNode,
            target: e.toNode,
            weight: e.weight,
        }));

        yield { nodes, edges };
    }
}

DataSource.register(RestApiDataSource);

Usage:

typescript
await graph.loadFromDataSource("rest-api", {
    baseUrl: "https://api.example.com",
    apiKey: "your-api-key",
    graphId: "graph-123",
});

Chunked Loading ​

For large datasets, yield data in chunks:

typescript
class PaginatedDataSource extends DataSource {
    static type = "paginated";

    async *load(config: { url: string; pageSize: number }): AsyncGenerator<GraphData> {
        let page = 0;
        let hasMore = true;

        while (hasMore) {
            const response = await fetch(`${config.url}?page=${page}&size=${config.pageSize}`);
            const data = await response.json();

            yield {
                nodes: data.nodes,
                edges: data.edges,
            };

            hasMore = data.hasNextPage;
            page++;
        }
    }
}

DataSource.register(PaginatedDataSource);

This progressively loads the graph, with the UI updating after each chunk.

Streaming Data Source ​

For real-time data streams:

typescript
class WebSocketDataSource extends DataSource {
    static type = "websocket";

    async *load(config: { wsUrl: string }): AsyncGenerator<GraphData> {
        const ws = new WebSocket(config.wsUrl);

        // Create a queue for incoming data
        const queue: GraphData[] = [];
        let resolve: (() => void) | null = null;

        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            queue.push({
                nodes: data.nodes || [],
                edges: data.edges || [],
            });
            if (resolve) {
                resolve();
                resolve = null;
            }
        };

        // Wait for initial connection
        await new Promise<void>((r) => {
            ws.onopen = () => r();
        });

        // Yield data as it arrives
        try {
            while (ws.readyState === WebSocket.OPEN) {
                if (queue.length > 0) {
                    yield queue.shift()!;
                } else {
                    await new Promise<void>((r) => {
                        resolve = r;
                    });
                }
            }
        } finally {
            ws.close();
        }
    }
}

DataSource.register(WebSocketDataSource);

Custom File Format ​

Parse a custom file format:

typescript
class CustomFormatDataSource extends DataSource {
    static type = "custom-format";

    async *load(config: { content: string }): AsyncGenerator<GraphData> {
        const lines = config.content.split("\n");
        const nodes: NodeData[] = [];
        const edges: EdgeData[] = [];

        let section = "none";

        for (const line of lines) {
            const trimmed = line.trim();

            if (trimmed === "[NODES]") {
                section = "nodes";
                continue;
            }
            if (trimmed === "[EDGES]") {
                section = "edges";
                continue;
            }
            if (!trimmed || trimmed.startsWith("#")) {
                continue;
            }

            if (section === "nodes") {
                const [id, label] = trimmed.split(":");
                nodes.push({ id, label });
            }

            if (section === "edges") {
                const [source, target, weight] = trimmed.split(",");
                edges.push({
                    source,
                    target,
                    weight: parseFloat(weight) || 1,
                });
            }
        }

        yield { nodes, edges };
    }
}

DataSource.register(CustomFormatDataSource);

Example custom format file:

# My Graph
[NODES]
a:Node A
b:Node B
c:Node C

[EDGES]
a,b,1.5
b,c,2.0
c,a,0.5

Schema Validation ​

Use Zod for input validation:

typescript
import { z } from "zod";
import { DataSource, GraphData } from "@graphty/graphty-element";

const ConfigSchema = z.object({
    url: z.string().url(),
    timeout: z.number().optional().default(5000),
    retries: z.number().optional().default(3),
});

type Config = z.infer<typeof ConfigSchema>;

class ValidatedDataSource extends DataSource {
    static type = "validated";

    async *load(config: unknown): AsyncGenerator<GraphData> {
        // Validate config
        const validConfig = ConfigSchema.parse(config);

        // Use validated config
        let lastError: Error | null = null;

        for (let attempt = 0; attempt < validConfig.retries; attempt++) {
            try {
                const controller = new AbortController();
                const timeoutId = setTimeout(() => controller.abort(), validConfig.timeout);

                const response = await fetch(validConfig.url, {
                    signal: controller.signal,
                });

                clearTimeout(timeoutId);

                const data = await response.json();
                yield { nodes: data.nodes, edges: data.edges };
                return;
            } catch (error) {
                lastError = error as Error;
            }
        }

        throw lastError || new Error("Failed to load data");
    }
}

DataSource.register(ValidatedDataSource);

Error Handling ​

Handle errors gracefully:

typescript
class RobustDataSource extends DataSource {
    static type = "robust";

    async *load(config: { urls: string[] }): AsyncGenerator<GraphData> {
        const allNodes: NodeData[] = [];
        const allEdges: EdgeData[] = [];
        const errors: Error[] = [];

        for (const url of config.urls) {
            try {
                const response = await fetch(url);

                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const data = await response.json();

                if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
                    throw new Error("Invalid data format");
                }

                allNodes.push(...data.nodes);
                allEdges.push(...data.edges);

                // Yield partial results
                yield { nodes: data.nodes, edges: data.edges };
            } catch (error) {
                errors.push(error as Error);
                console.warn(`Failed to load ${url}:`, error);
            }
        }

        if (allNodes.length === 0 && errors.length > 0) {
            throw new AggregateError(errors, "All data sources failed");
        }
    }
}

DataSource.register(RobustDataSource);

Configuration Types ​

Export your config type for TypeScript users:

typescript
// my-data-source.ts
export interface MyDataSourceConfig {
    url: string;
    apiKey?: string;
    options?: {
        includeMetadata?: boolean;
        maxNodes?: number;
    };
}

class MyDataSource extends DataSource {
    static type = "my-source";

    async *load(config: MyDataSourceConfig): AsyncGenerator<GraphData> {
        // ...
    }
}

Usage with type checking:

typescript
import type { MyDataSourceConfig } from "./my-data-source";

const config: MyDataSourceConfig = {
    url: "https://api.example.com/graph",
    options: { maxNodes: 1000 },
};

await graph.loadFromDataSource("my-source", config);