From 8718734835e80e913c7becca261b63921bff6d8a Mon Sep 17 00:00:00 2001 From: Stefan Avesand Date: Tue, 31 Mar 2026 13:09:51 +0200 Subject: [PATCH] feat: make SPARQL endpoint path configurable (#23) Add a configurable `sparqlEndpointPath` field to ConnectionConfig, allowing users to specify the SPARQL query endpoint path for their triple store. Defaults to "/sparql" for backward compatibility with Amazon Neptune and Blazegraph. This enables Graph Explorer to work with triple stores that use different SPARQL endpoint paths, such as Apache Jena Fuseki which uses "/query". Changes: - Add optional `sparqlEndpointPath` to ConnectionConfig type - Use configurable path in frontend SPARQL connector instead of hardcoded "/sparql" - Pass path to proxy server via "sparql-endpoint-path" header - Use configurable path in proxy server for query execution and query cancellation - Add "SPARQL Endpoint Path" input field to connection form UI, shown when SPARQL query language is selected - Add GRAPH_EXP_SPARQL_ENDPOINT_PATH to default connection schema Closes #23 --- .../src/node-server.ts | 6 ++++-- .../src/connector/fetchDatabaseRequest.ts | 3 +++ .../src/connector/sparql/sparqlExplorer.ts | 2 +- .../src/core/defaultConnection.ts | 3 +++ .../CreateConnection/CreateConnection.tsx | 21 +++++++++++++++++++ packages/shared/src/types/index.ts | 6 ++++++ 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index 8d96bcd78..ada75e6ce 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -27,6 +27,7 @@ interface DbQueryIncomingHttpHeaders extends IncomingHttpHeaders { "aws-neptune-region"?: string; "service-type"?: string; "db-query-logging-enabled"?: string; + "sparql-endpoint-path"?: string; } interface LoggerIncomingHttpHeaders extends IncomingHttpHeaders { @@ -202,6 +203,7 @@ app.post("/sparql", async (req, res, next) => { const headers = req.headers as DbQueryIncomingHttpHeaders; const queryId = headers["queryid"]; const graphDbConnectionUrl = headers["graph-db-connection-url"]; + const sparqlEndpointPath = headers["sparql-endpoint-path"] || "/sparql"; const shouldLogDbQuery = BooleanStringSchema.default(false).parse( headers["db-query-logging-enabled"], ); @@ -219,7 +221,7 @@ app.post("/sparql", async (req, res, next) => { proxyLogger.debug(`Cancelling request ${queryId}...`); try { await retryFetch( - new URL(`${graphDbConnectionUrl}/sparql/status`), + new URL(`${graphDbConnectionUrl}${sparqlEndpointPath}/status`), { method: "POST", headers: { @@ -264,7 +266,7 @@ app.post("/sparql", async (req, res, next) => { proxyLogger.debug("[SPARQL] Received database query:\n%s", queryString); } - const rawUrl = `${graphDbConnectionUrl}/sparql`; + const rawUrl = `${graphDbConnectionUrl}${sparqlEndpointPath}`; let body = `query=${encodeURIComponent(queryString)}`; if (queryId) { body += `&queryId=${encodeURIComponent(queryId)}`; diff --git a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts index bf7f0fa39..6941a8cb7 100644 --- a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts +++ b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts @@ -69,6 +69,9 @@ function getAuthHeaders( headers["db-query-logging-enabled"] = String( featureFlags.allowLoggingDbQuery, ); + if (connection.sparqlEndpointPath) { + headers["sparql-endpoint-path"] = connection.sparqlEndpointPath; + } } if (connection.awsAuthEnabled) { headers["aws-neptune-region"] = connection.awsRegion || ""; diff --git a/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts b/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts index 44160068d..b293049fa 100644 --- a/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts +++ b/packages/graph-explorer/src/connector/sparql/sparqlExplorer.ts @@ -54,7 +54,7 @@ function _sparqlFetch( return fetchDatabaseRequest( connection, featureFlags, - `${connection.url}/sparql`, + `${connection.url}${connection.sparqlEndpointPath ?? "/sparql"}`, { method: "POST", headers, diff --git a/packages/graph-explorer/src/core/defaultConnection.ts b/packages/graph-explorer/src/core/defaultConnection.ts index 93125a636..8f3ff9a62 100644 --- a/packages/graph-explorer/src/core/defaultConnection.ts +++ b/packages/graph-explorer/src/core/defaultConnection.ts @@ -21,6 +21,8 @@ export const DefaultConnectionDataSchema = z.object({ .enum(neptuneServiceTypeOptions) .default(DEFAULT_SERVICE_TYPE) .catch(DEFAULT_SERVICE_TYPE), + // SPARQL options + GRAPH_EXP_SPARQL_ENDPOINT_PATH: z.string().optional(), // Connection options GRAPH_EXP_FETCH_REQUEST_TIMEOUT: z.number().default(240000), GRAPH_EXP_NODE_EXPANSION_LIMIT: z.number().optional(), @@ -116,6 +118,7 @@ export function mapToConnection(data: DefaultConnectionData): RawConfiguration { awsAuthEnabled: data.GRAPH_EXP_IAM, awsRegion: data.GRAPH_EXP_AWS_REGION, serviceType: data.GRAPH_EXP_SERVICE_TYPE, + sparqlEndpointPath: data.GRAPH_EXP_SPARQL_ENDPOINT_PATH, fetchTimeoutMs: data.GRAPH_EXP_FETCH_REQUEST_TIMEOUT, nodeExpansionLimit: data.GRAPH_EXP_NODE_EXPANSION_LIMIT, }, diff --git a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx index b5f7b6e96..c21cf1acf 100644 --- a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx +++ b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx @@ -43,6 +43,7 @@ type ConnectionForm = { awsAuthEnabled?: boolean; serviceType?: NeptuneServiceType; awsRegion?: string; + sparqlEndpointPath?: string; fetchTimeoutEnabled: boolean; fetchTimeoutMs?: number; nodeExpansionLimitEnabled: boolean; @@ -72,6 +73,7 @@ function mapToConnection(data: Required): ConnectionConfig { awsAuthEnabled: data.awsAuthEnabled, serviceType: data.serviceType, awsRegion: data.awsRegion, + sparqlEndpointPath: data.sparqlEndpointPath || undefined, fetchTimeoutMs: data.fetchTimeoutEnabled ? data.fetchTimeoutMs : undefined, nodeExpansionLimit: data.nodeExpansionLimitEnabled ? data.nodeExpansionLimit @@ -275,6 +277,25 @@ const CreateConnection = ({ disabled={form.serviceType === "neptune-graph"} /> + {form.queryEngine === "sparql" && ( + + + + + )}