Skip to content

Commit

Permalink
Implement details queries for vertex and edges (#761)
Browse files Browse the repository at this point in the history
* Add EdgeRef

* Extract function to create stable keys in queries

* Add scaffold for explorer functions

* Implement in Gremlin

* Implement in openCypher

* Implement SPARQL

* Add cache updates

* Add hooks for details queries

* Fix log message

* Use existing response typing logic
  • Loading branch information
kmcginnes authored Jan 24, 2025
1 parent 90cc8c0 commit 38700d3
Show file tree
Hide file tree
Showing 21 changed files with 740 additions and 19 deletions.
49 changes: 49 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/edgeDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { logger, query } from "@/utils";
import {
EdgeDetailsRequest,
EdgeDetailsResponse,
ErrorResponse,
} from "../useGEFetchTypes";
import { GEdge, GremlinFetch } from "./types";
import { mapResults } from "./mappers/mapResults";
import isErrorResponse from "../utils/isErrorResponse";
import { idParam } from "./idParam";

type Response = {
requestId: string;
status: {
message: string;
code: number;
};
result: {
data: {
"@type": "g:List";
"@value": Array<GEdge>;
};
};
};

export async function edgeDetails(
gremlinFetch: GremlinFetch,
request: EdgeDetailsRequest
): Promise<EdgeDetailsResponse> {
const template = query`
g.E(${idParam(request.edge)})
`;

// Fetch the vertex details
const data = await gremlinFetch<Response | ErrorResponse>(template);
if (isErrorResponse(data)) {
logger.error(data.detailedMessage);
throw new Error(data.detailedMessage);
}

// Map the results
const entities = mapResults(data.result.data);
const edge = entities.edges.length > 0 ? entities.edges[0] : null;
if (!edge) {
logger.warn("Edge not found", request.edge);
}

return { edge };
}
24 changes: 24 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Explorer, ExplorerRequestOptions } from "../useGEFetchTypes";
import { logger } from "@/utils";
import { createLoggerFromConnection } from "@/core/connector";
import { FeatureFlags } from "@/core";
import { vertexDetails } from "./vertexDetails";
import { edgeDetails } from "./edgeDetails";

function _gremlinFetch(
connection: ConnectionConfig,
Expand Down Expand Up @@ -113,5 +115,27 @@ export function createGremlinExplorer(
req
);
},
async vertexDetails(req, options) {
options ??= {};
options.queryId = v4();

remoteLogger.info("[Gremlin Explorer] Fetching vertex details...");
const result = await vertexDetails(
_gremlinFetch(connection, featureFlags, options),
req
);
return result;
},
async edgeDetails(req, options) {
options ??= {};
options.queryId = v4();

remoteLogger.info("[Gremlin Explorer] Fetching edge details...");
const result = await edgeDetails(
_gremlinFetch(connection, featureFlags, options),
req
);
return result;
},
} satisfies Explorer;
}
6 changes: 6 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/idParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { EdgeRef, VertexRef } from "../useGEFetchTypes";

/** Formats the ID parameter for a gremlin query based on the ID type. */
export function idParam(entity: VertexRef | EdgeRef) {
return entity.idType === "number" ? `${entity.id}L` : `"${entity.id}"`;
}
48 changes: 48 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/vertexDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { logger, query } from "@/utils";
import {
ErrorResponse,
VertexDetailsRequest,
VertexDetailsResponse,
} from "../useGEFetchTypes";
import { GremlinFetch, GVertex } from "./types";
import { mapResults } from "./mappers/mapResults";
import isErrorResponse from "../utils/isErrorResponse";
import { idParam } from "./idParam";

type Response = {
requestId: string;
status: {
message: string;
code: number;
};
result: {
data: {
"@type": "g:List";
"@value": Array<GVertex>;
};
};
};

export async function vertexDetails(
gremlinFetch: GremlinFetch,
request: VertexDetailsRequest
): Promise<VertexDetailsResponse> {
const template = query`
g.V(${idParam(request.vertex)})
`;

// Fetch the vertex details
const data = await gremlinFetch<Response | ErrorResponse>(template);
if (isErrorResponse(data)) {
throw new Error(data.detailedMessage);
}

// Map the results
const entities = mapResults(data.result.data);
const vertex = entities.vertices.length > 0 ? entities.vertices[0] : null;
if (!vertex) {
logger.warn("Vertex not found", request.vertex);
}

return { vertex };
}
54 changes: 54 additions & 0 deletions packages/graph-explorer/src/connector/openCypher/edgeDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
EdgeDetailsRequest,
EdgeDetailsResponse,
ErrorResponse,
} from "@/connector/useGEFetchTypes";
import { OCEdge, OpenCypherFetch } from "./types";
import isErrorResponse from "@/connector/utils/isErrorResponse";
import { logger, query } from "@/utils";
import mapApiEdge from "./mappers/mapApiEdge";

type Response = {
results: [
{
edge: OCEdge;
sourceLabels: Array<string>;
targetLabels: Array<string>;
},
];
};

export async function edgeDetails(
openCypherFetch: OpenCypherFetch,
req: EdgeDetailsRequest
): Promise<EdgeDetailsResponse> {
const template = query`
MATCH ()-[edge]-()
WHERE ID(edge) = "${String(req.edge.id)}"
RETURN edge, labels(startNode(edge)) as sourceLabels, labels(endNode(edge)) as targetLabels
`;
const data = await openCypherFetch<Response | ErrorResponse>(template);

if (isErrorResponse(data)) {
logger.error(
"Failed to fetch edge details",
req.edge,
data.detailedMessage
);
throw new Error(data.detailedMessage);
}

const value = data.results[0];

if (!value) {
console.warn("Edge not found", req.edge);
return { edge: null };
}

const sourceLabels = value.sourceLabels.join("::");
const targetLabels = value.targetLabels.join("::");

const edge = mapApiEdge(value.edge, sourceLabels, targetLabels);

return { edge };
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Explorer, ExplorerRequestOptions } from "../useGEFetchTypes";
import { env, logger } from "@/utils";
import { createLoggerFromConnection } from "@/core/connector";
import { FeatureFlags } from "@/core";
import { vertexDetails } from "./vertexDetails";
import { edgeDetails } from "./edgeDetails";

function _openCypherFetch(
connection: ConnectionConfig,
Expand Down Expand Up @@ -87,6 +89,20 @@ export function createOpenCypherExplorer(
req
);
},
async vertexDetails(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching vertex details...");
return vertexDetails(
_openCypherFetch(connection, featureFlags, options),
req
);
},
async edgeDetails(req, options) {
remoteLogger.info("[openCypher Explorer] Fetching edge details...");
return edgeDetails(
_openCypherFetch(connection, featureFlags, options),
req
);
},
} satisfies Explorer;
}

Expand Down
44 changes: 44 additions & 0 deletions packages/graph-explorer/src/connector/openCypher/vertexDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
ErrorResponse,
VertexDetailsRequest,
VertexDetailsResponse,
} from "@/connector/useGEFetchTypes";
import { OCVertex, OpenCypherFetch } from "./types";
import isErrorResponse from "@/connector/utils/isErrorResponse";
import mapApiVertex from "./mappers/mapApiVertex";
import { query } from "@/utils";

type Response = {
results: [
{
vertex: OCVertex;
},
];
};

export async function vertexDetails(
openCypherFetch: OpenCypherFetch,
req: VertexDetailsRequest
): Promise<VertexDetailsResponse> {
const idTemplate = `"${String(req.vertex.id)}"`;
const template = query`
MATCH (vertex) WHERE ID(vertex) = ${idTemplate} RETURN vertex
`;

// Fetch the vertex details
const data = await openCypherFetch<Response | ErrorResponse>(template);
if (isErrorResponse(data)) {
throw new Error(data.detailedMessage);
}

// Map the results
const ocVertex = data.results[0]?.vertex;

if (!ocVertex) {
console.warn("Vertex not found", req.vertex);
return { vertex: null };
}

const vertex = mapApiVertex(ocVertex);
return { vertex };
}
Loading

0 comments on commit 38700d3

Please sign in to comment.