From 7ea1ca26de9829716b8df01845953d494346e0e7 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Mon, 27 Jan 2025 13:51:59 -0600 Subject: [PATCH] Serialize id type in to id string (#768) --- .../src/connector/gremlin/edgeDetails.ts | 2 +- .../gremlin/fetchNeighbors/index.test.ts | 92 +++++++++---------- .../fetchNeighbors/oneHopTemplate.test.ts | 14 +-- .../gremlin/fetchNeighbors/oneHopTemplate.ts | 4 +- .../neighborsCountTemplate.test.ts | 10 +- .../neighborsCountTemplate.ts | 8 +- .../src/connector/gremlin/idParam.ts | 7 +- .../gremlin/keywordSearch/index.test.ts | 5 +- .../connector/gremlin/mappers/detectIdType.ts | 2 +- .../{toStringId.ts => extractRawId.ts} | 18 ++-- .../connector/gremlin/mappers/mapApiEdge.ts | 11 ++- .../connector/gremlin/mappers/mapApiVertex.ts | 6 +- .../src/connector/gremlin/vertexDetails.ts | 2 +- .../src/connector/openCypher/edgeDetails.ts | 3 +- .../fetchNeighbors/oneHopTemplate.ts | 3 +- .../neighborsCountTemplate.ts | 3 +- .../src/connector/openCypher/idParam.ts | 6 ++ .../openCypher/mappers/mapApiEdge.ts | 8 +- .../openCypher/mappers/mapApiVertex.test.ts | 6 +- .../openCypher/mappers/mapApiVertex.ts | 4 +- .../src/connector/openCypher/vertexDetails.ts | 4 +- .../src/connector/sparql/edgeDetails.ts | 5 +- .../blankNodeSubjectPredicatesTemplate.ts | 3 +- .../sparql/fetchBlankNodeNeighbors/index.ts | 5 +- .../connector/sparql/fetchNeighbors/index.ts | 6 +- .../oneHopNeighborsBlankNodesIdsTemplate.ts | 3 +- .../fetchNeighbors/oneHopNeighborsTemplate.ts | 3 +- .../subjectPredicatesTemplate.ts | 20 ++-- .../neighborsCountTemplate.ts | 5 +- .../src/connector/sparql/idParam.ts | 6 ++ .../sparql/mappers/mapIncomingToEdge.ts | 15 +-- .../sparql/mappers/mapOutgoingToEdge.ts | 14 +-- .../sparql/mappers/mapRawResultToVertex.ts | 4 +- .../src/connector/sparql/types.ts | 14 +-- .../src/connector/sparql/vertexDetails.ts | 7 +- .../core/StateProvider/displayEdge.test.ts | 3 +- .../src/core/StateProvider/displayEdge.ts | 15 +-- .../core/StateProvider/displayVertex.test.ts | 18 ++-- .../src/core/StateProvider/displayVertex.ts | 8 +- .../src/core/entityIdType.test.ts | 43 +++++++++ .../graph-explorer/src/core/entityIdType.ts | 63 +++++++++++++ packages/graph-explorer/src/core/index.ts | 1 + .../src/hooks/useEntities.test.tsx | 17 ++-- .../src/utils/testing/randomData.ts | 35 ++++--- 44 files changed, 342 insertions(+), 189 deletions(-) rename packages/graph-explorer/src/connector/gremlin/mappers/{toStringId.ts => extractRawId.ts} (68%) create mode 100644 packages/graph-explorer/src/connector/openCypher/idParam.ts create mode 100644 packages/graph-explorer/src/connector/sparql/idParam.ts create mode 100644 packages/graph-explorer/src/core/entityIdType.test.ts create mode 100644 packages/graph-explorer/src/core/entityIdType.ts diff --git a/packages/graph-explorer/src/connector/gremlin/edgeDetails.ts b/packages/graph-explorer/src/connector/gremlin/edgeDetails.ts index dab1ca6d4..f21202a26 100644 --- a/packages/graph-explorer/src/connector/gremlin/edgeDetails.ts +++ b/packages/graph-explorer/src/connector/gremlin/edgeDetails.ts @@ -28,7 +28,7 @@ export async function edgeDetails( request: EdgeDetailsRequest ): Promise { const template = query` - g.E(${idParam(request.edge)}) + g.E(${idParam(request.edge.id)}) `; // Fetch the vertex details diff --git a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/index.test.ts b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/index.test.ts index b8941143d..ab769d17d 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/index.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/index.test.ts @@ -1,7 +1,7 @@ import globalMockFetch from "@/connector/testUtils/globalMockFetch"; import mockGremlinFetch from "@/connector/testUtils/mockGremlinFetch"; import fetchNeighbors from "."; -import { VertexId } from "@/core"; +import { createEdgeId, createVertexId } from "@/core"; describe("Gremlin > fetchNeighbors", () => { beforeEach(globalMockFetch); @@ -9,7 +9,7 @@ describe("Gremlin > fetchNeighbors", () => { it("Should return all neighbors from node 2018", async () => { const expectedVertices = [ { - id: "486", + id: createVertexId("486"), type: "airport", types: ["airport"], attributes: { @@ -28,7 +28,7 @@ describe("Gremlin > fetchNeighbors", () => { }, }, { - id: "228", + id: createVertexId("228"), type: "airport", types: ["airport"], attributes: { @@ -47,7 +47,7 @@ describe("Gremlin > fetchNeighbors", () => { }, }, { - id: "124", + id: createVertexId("124"), type: "airport", types: ["airport"], attributes: { @@ -66,13 +66,13 @@ describe("Gremlin > fetchNeighbors", () => { }, }, { - id: "3741", + id: createVertexId("3741"), type: "continent", types: ["continent"], attributes: { code: "EU", type: "continent", desc: "Europe" }, }, { - id: "3701", + id: createVertexId("3701"), type: "country", types: ["country"], attributes: { code: "ES", type: "country", desc: "Spain" }, @@ -80,7 +80,7 @@ describe("Gremlin > fetchNeighbors", () => { ]; const response = await fetchNeighbors(mockGremlinFetch(), { - vertex: { id: "2018" as VertexId, idType: "string" }, + vertex: { id: createVertexId("2018"), idType: "string" }, vertexType: "airport", }); @@ -88,74 +88,74 @@ describe("Gremlin > fetchNeighbors", () => { vertices: expectedVertices, edges: [ { - id: "49540", + id: createEdgeId("49540"), type: "route", - source: "2018", + source: createVertexId("2018"), sourceType: "airport", - target: "486", + target: createVertexId("486"), targetType: "airport", attributes: { dist: 82 }, }, { - id: "33133", + id: createEdgeId("33133"), type: "route", - source: "486", + source: createVertexId("486"), sourceType: "airport", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: { dist: 82 }, }, { - id: "49539", + id: createEdgeId("49539"), type: "route", - source: "2018", + source: createVertexId("2018"), sourceType: "airport", - target: "228", + target: createVertexId("228"), targetType: "airport", attributes: { dist: 153 }, }, { - id: "24860", + id: createEdgeId("24860"), type: "route", - source: "228", + source: createVertexId("228"), sourceType: "airport", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: { dist: 153 }, }, { - id: "49538", + id: createEdgeId("49538"), type: "route", - source: "2018", + source: createVertexId("2018"), sourceType: "airport", - target: "124", + target: createVertexId("124"), targetType: "airport", attributes: { dist: 105 }, }, { - id: "18665", + id: createEdgeId("18665"), type: "route", - source: "124", + source: createVertexId("124"), sourceType: "airport", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: { dist: 105 }, }, { - id: "59800", + id: createEdgeId("59800"), type: "contains", - source: "3741", + source: createVertexId("3741"), sourceType: "continent", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: {}, }, { - id: "56297", + id: createEdgeId("56297"), type: "contains", - source: "3701", + source: createVertexId("3701"), sourceType: "country", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: {}, }, @@ -166,7 +166,7 @@ describe("Gremlin > fetchNeighbors", () => { it("Should return filtered neighbors from node 2018", async () => { const expectedVertices = [ { - id: "486", + id: createVertexId("486"), type: "airport", types: ["airport"], attributes: { @@ -185,7 +185,7 @@ describe("Gremlin > fetchNeighbors", () => { }, }, { - id: "124", + id: createVertexId("124"), type: "airport", types: ["airport"], attributes: { @@ -206,7 +206,7 @@ describe("Gremlin > fetchNeighbors", () => { ]; const response = await fetchNeighbors(mockGremlinFetch(), { - vertex: { id: "2018" as VertexId, idType: "string" }, + vertex: { id: createVertexId("2018"), idType: "string" }, vertexType: "airport", filterByVertexTypes: ["airport"], filterCriteria: [{ name: "code", value: "TF", operator: "LIKE" }], @@ -216,38 +216,38 @@ describe("Gremlin > fetchNeighbors", () => { vertices: expectedVertices, edges: [ { - id: "49540", + id: createEdgeId("49540"), type: "route", - source: "2018", + source: createVertexId("2018"), sourceType: "airport", - target: "486", + target: createVertexId("486"), targetType: "airport", attributes: { dist: 82 }, }, { - id: "33133", + id: createEdgeId("33133"), type: "route", - source: "486", + source: createVertexId("486"), sourceType: "airport", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: { dist: 82 }, }, { - id: "49538", + id: createEdgeId("49538"), type: "route", - source: "2018", + source: createVertexId("2018"), sourceType: "airport", - target: "124", + target: createVertexId("124"), targetType: "airport", attributes: { dist: 105 }, }, { - id: "18665", + id: createEdgeId("18665"), type: "route", - source: "124", + source: createVertexId("124"), sourceType: "airport", - target: "2018", + target: createVertexId("2018"), targetType: "airport", attributes: { dist: 105 }, }, diff --git a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.test.ts b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.test.ts index 02036faed..be0a9ad5a 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.test.ts @@ -1,11 +1,11 @@ import { normalizeWithNoSpace as normalize } from "@/utils/testing"; import oneHopTemplate from "./oneHopTemplate"; -import { VertexId } from "@/core"; +import { createVertexId } from "@/core"; describe("Gremlin > oneHopTemplate", () => { it("Should return a template for a simple vertex id", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, }); expect(normalize(template)).toBe( @@ -25,7 +25,7 @@ describe("Gremlin > oneHopTemplate", () => { it("Should return a template for a simple vertex id with number type", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "number" }, + vertex: { id: createVertexId(12), idType: "number" }, }); expect(normalize(template)).toBe( @@ -45,7 +45,7 @@ describe("Gremlin > oneHopTemplate", () => { it("Should return a template with an offset and limit", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, offset: 5, limit: 5, }); @@ -67,7 +67,7 @@ describe("Gremlin > oneHopTemplate", () => { it("Should return a template for specific vertex type", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, filterByVertexTypes: ["country"], offset: 5, limit: 10, @@ -90,7 +90,7 @@ describe("Gremlin > oneHopTemplate", () => { it("Should return a template for multiple vertex type", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, filterByVertexTypes: ["country", "airport", "continent"], offset: 5, limit: 10, @@ -113,7 +113,7 @@ describe("Gremlin > oneHopTemplate", () => { it("Should return a template with specific filter criteria", () => { const template = oneHopTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, filterByVertexTypes: ["country"], filterCriteria: [ { name: "longest", value: 10000, operator: "gte", dataType: "Number" }, diff --git a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.ts b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.ts index 19c6878fe..de1a84e9a 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchNeighbors/oneHopTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import type { Criterion, NeighborsRequest } from "@/connector/useGEFetchTypes"; +import { idParam } from "../idParam"; function criterionNumberTemplate({ name, @@ -134,8 +135,7 @@ export default function oneHopTemplate({ limit = 0, offset = 0, }: Omit): string { - const idTemplate = - vertex.idType === "number" ? `${vertex.id}L` : `"${vertex.id}"`; + const idTemplate = idParam(vertex.id); const range = limit > 0 ? `.range(${offset}, ${offset + limit})` : ""; const vertexTypes = filterByVertexTypes.flatMap(type => type.split("::")); diff --git a/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.test.ts b/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.test.ts index 31618d1fa..710dc5fe5 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.test.ts @@ -1,11 +1,11 @@ -import { VertexId } from "@/core"; +import { createVertexId } from "@/core"; import neighborsCountTemplate from "./neighborsCountTemplate"; import { normalizeWithNoSpace as normalize } from "@/utils/testing"; describe("Gremlin > neighborsCountTemplate", () => { it("Should return a template for the given vertex id", () => { const template = neighborsCountTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, }); expect(normalize(template)).toBe( @@ -17,7 +17,7 @@ describe("Gremlin > neighborsCountTemplate", () => { it("Should return a template for the given vertex id with number type", () => { const template = neighborsCountTemplate({ - vertex: { id: "12" as VertexId, idType: "number" }, + vertex: { id: createVertexId(12), idType: "number" }, }); expect(normalize(template)).toBe( @@ -29,7 +29,7 @@ describe("Gremlin > neighborsCountTemplate", () => { it("Should return a template for the given vertex id with defined limit", () => { const template = neighborsCountTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, limit: 20, }); @@ -42,7 +42,7 @@ describe("Gremlin > neighborsCountTemplate", () => { it("Should return a template for the given vertex id with no limit", () => { const template = neighborsCountTemplate({ - vertex: { id: "12" as VertexId, idType: "string" }, + vertex: { id: createVertexId("12"), idType: "string" }, limit: 0, }); diff --git a/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.ts b/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.ts index cdd90bb29..402fec500 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchNeighborsCount/neighborsCountTemplate.ts @@ -1,4 +1,5 @@ import type { NeighborsCountRequest } from "@/connector/useGEFetchTypes"; +import { idParam } from "../idParam"; /** * Given a single node ids, it returns a Gremlin template with @@ -15,12 +16,7 @@ export default function neighborsCountTemplate({ vertex, limit = 0, }: NeighborsCountRequest) { - let template = ""; - if (vertex.idType === "number") { - template = `g.V(${vertex.id}L).both()`; - } else { - template = `g.V("${vertex.id}").both()`; - } + let template = `g.V(${idParam(vertex.id)}).both()`; if (limit > 0) { template += `.limit(${limit})`; diff --git a/packages/graph-explorer/src/connector/gremlin/idParam.ts b/packages/graph-explorer/src/connector/gremlin/idParam.ts index c62412f89..26d96f94d 100644 --- a/packages/graph-explorer/src/connector/gremlin/idParam.ts +++ b/packages/graph-explorer/src/connector/gremlin/idParam.ts @@ -1,6 +1,7 @@ -import { EdgeRef, VertexRef } from "../useGEFetchTypes"; +import { EdgeId, getRawId, VertexId } from "@/core"; /** 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}"`; +export function idParam(entityId: VertexId | EdgeId) { + const rawId = getRawId(entityId); + return typeof rawId === "number" ? `${rawId}L` : `"${rawId}"`; } diff --git a/packages/graph-explorer/src/connector/gremlin/keywordSearch/index.test.ts b/packages/graph-explorer/src/connector/gremlin/keywordSearch/index.test.ts index 8fee107e2..ea0c37961 100644 --- a/packages/graph-explorer/src/connector/gremlin/keywordSearch/index.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/keywordSearch/index.test.ts @@ -1,6 +1,7 @@ import globalMockFetch from "@/connector/testUtils/globalMockFetch"; import mockGremlinFetch from "@/connector/testUtils/mockGremlinFetch"; import keywordSearch from "."; +import { createVertexId } from "@/core"; describe("Gremlin > keywordSearch", () => { beforeEach(globalMockFetch); @@ -13,7 +14,7 @@ describe("Gremlin > keywordSearch", () => { expect(keywordResponse).toMatchObject({ vertices: [ { - id: "1", + id: createVertexId("1"), type: "airport", types: ["airport"], attributes: { @@ -45,7 +46,7 @@ describe("Gremlin > keywordSearch", () => { expect(keywordResponse).toMatchObject({ vertices: [ { - id: "836", + id: createVertexId("836"), type: "airport", types: ["airport"], attributes: { diff --git a/packages/graph-explorer/src/connector/gremlin/mappers/detectIdType.ts b/packages/graph-explorer/src/connector/gremlin/mappers/detectIdType.ts index ee0fc05d0..e1ef19d71 100644 --- a/packages/graph-explorer/src/connector/gremlin/mappers/detectIdType.ts +++ b/packages/graph-explorer/src/connector/gremlin/mappers/detectIdType.ts @@ -1,6 +1,6 @@ import { EntityIdType } from "@/core"; import { GInt64, JanusID } from "../types"; -import { isJanusID } from "./toStringId"; +import { isJanusID } from "./extractRawId"; /** * This function will detect the type of the id value passed in. diff --git a/packages/graph-explorer/src/connector/gremlin/mappers/toStringId.ts b/packages/graph-explorer/src/connector/gremlin/mappers/extractRawId.ts similarity index 68% rename from packages/graph-explorer/src/connector/gremlin/mappers/toStringId.ts rename to packages/graph-explorer/src/connector/gremlin/mappers/extractRawId.ts index 0fe98107e..37d0c1c56 100644 --- a/packages/graph-explorer/src/connector/gremlin/mappers/toStringId.ts +++ b/packages/graph-explorer/src/connector/gremlin/mappers/extractRawId.ts @@ -12,16 +12,16 @@ export const isJanusID = (id: any): id is JanusID => { ); }; -const toStringId = (id: string | GInt64 | JanusID): string => { - if (typeof id === "string") { - return id; - } - +export function extractRawId(id: string | GInt64 | JanusID): string | number { if (isJanusID(id)) { - return id["@value"]["relationId"]; + const relationId = id["@value"]["relationId"]; + return relationId; } - return String(id["@value"]); -}; + if (typeof id === "string") { + return id; + } -export default toStringId; + const value = id["@value"]; + return value; +} diff --git a/packages/graph-explorer/src/connector/gremlin/mappers/mapApiEdge.ts b/packages/graph-explorer/src/connector/gremlin/mappers/mapApiEdge.ts index 6833c79f9..c95d2793f 100644 --- a/packages/graph-explorer/src/connector/gremlin/mappers/mapApiEdge.ts +++ b/packages/graph-explorer/src/connector/gremlin/mappers/mapApiEdge.ts @@ -1,19 +1,20 @@ -import type { Edge, EdgeId, VertexId } from "@/core"; +import { createEdgeId, createVertexId, type Edge } from "@/core"; import type { GEdge } from "../types"; import parseEdgePropertiesValues from "./parseEdgePropertiesValues"; -import toStringId from "./toStringId"; + import { detectIdType } from "./detectIdType"; +import { extractRawId } from "./extractRawId"; const mapApiEdge = (apiEdge: GEdge): Edge => { const isFragment = apiEdge["@value"].properties == null; return { entityType: "edge", - id: toStringId(apiEdge["@value"].id) as EdgeId, + id: createEdgeId(extractRawId(apiEdge["@value"].id)), idType: detectIdType(apiEdge["@value"].id), type: apiEdge["@value"].label, - source: toStringId(apiEdge["@value"].outV) as VertexId, + source: createVertexId(extractRawId(apiEdge["@value"].outV)), sourceType: apiEdge["@value"].outVLabel, - target: toStringId(apiEdge["@value"].inV) as VertexId, + target: createVertexId(extractRawId(apiEdge["@value"].inV)), targetType: apiEdge["@value"].inVLabel, attributes: parseEdgePropertiesValues(apiEdge["@value"].properties || {}), __isFragment: isFragment, diff --git a/packages/graph-explorer/src/connector/gremlin/mappers/mapApiVertex.ts b/packages/graph-explorer/src/connector/gremlin/mappers/mapApiVertex.ts index 0574ae5d4..e306b84ef 100644 --- a/packages/graph-explorer/src/connector/gremlin/mappers/mapApiVertex.ts +++ b/packages/graph-explorer/src/connector/gremlin/mappers/mapApiVertex.ts @@ -1,8 +1,8 @@ -import type { Vertex, VertexId } from "@/core"; +import { createVertexId, type Vertex } from "@/core"; import type { GVertex } from "../types"; import { detectIdType } from "./detectIdType"; import parsePropertiesValues from "./parsePropertiesValues"; -import toStringId from "./toStringId"; +import { extractRawId } from "./extractRawId"; const mapApiVertex = (apiVertex: GVertex): Vertex => { const labels = apiVertex["@value"].label.split("::"); @@ -11,7 +11,7 @@ const mapApiVertex = (apiVertex: GVertex): Vertex => { return { entityType: "vertex", - id: toStringId(apiVertex["@value"].id) as VertexId, + id: createVertexId(extractRawId(apiVertex["@value"].id)), idType: detectIdType(apiVertex["@value"].id), type: vt, types: labels, diff --git a/packages/graph-explorer/src/connector/gremlin/vertexDetails.ts b/packages/graph-explorer/src/connector/gremlin/vertexDetails.ts index 1ce4388f4..3138765b9 100644 --- a/packages/graph-explorer/src/connector/gremlin/vertexDetails.ts +++ b/packages/graph-explorer/src/connector/gremlin/vertexDetails.ts @@ -28,7 +28,7 @@ export async function vertexDetails( request: VertexDetailsRequest ): Promise { const template = query` - g.V(${idParam(request.vertex)}) + g.V(${idParam(request.vertex.id)}) `; // Fetch the vertex details diff --git a/packages/graph-explorer/src/connector/openCypher/edgeDetails.ts b/packages/graph-explorer/src/connector/openCypher/edgeDetails.ts index 2af7106dc..d07bc5346 100644 --- a/packages/graph-explorer/src/connector/openCypher/edgeDetails.ts +++ b/packages/graph-explorer/src/connector/openCypher/edgeDetails.ts @@ -7,6 +7,7 @@ import { OCEdge, OpenCypherFetch } from "./types"; import isErrorResponse from "@/connector/utils/isErrorResponse"; import { logger, query } from "@/utils"; import mapApiEdge from "./mappers/mapApiEdge"; +import { idParam } from "./idParam"; type Response = { results: [ @@ -24,7 +25,7 @@ export async function edgeDetails( ): Promise { const template = query` MATCH ()-[edge]-() - WHERE ID(edge) = "${String(req.edge.id)}" + WHERE ID(edge) = ${idParam(req.edge.id)} RETURN edge, labels(startNode(edge)) as sourceLabels, labels(endNode(edge)) as targetLabels `; const data = await openCypherFetch(template); diff --git a/packages/graph-explorer/src/connector/openCypher/fetchNeighbors/oneHopTemplate.ts b/packages/graph-explorer/src/connector/openCypher/fetchNeighbors/oneHopTemplate.ts index 0f8ef349c..4af1439fe 100644 --- a/packages/graph-explorer/src/connector/openCypher/fetchNeighbors/oneHopTemplate.ts +++ b/packages/graph-explorer/src/connector/openCypher/fetchNeighbors/oneHopTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import type { Criterion, NeighborsRequest } from "@/connector/useGEFetchTypes"; +import { idParam } from "../idParam"; const criterionNumberTemplate = ({ name, @@ -129,7 +130,7 @@ const oneHopTemplate = ({ // Combine all the WHERE conditions const whereConditions = [ - `ID(v) = "${vertex.id}"`, + `ID(v) = ${idParam(vertex.id)}`, formattedVertexTypes, ...(filterCriteria?.map(criterionTemplate) ?? []), ] diff --git a/packages/graph-explorer/src/connector/openCypher/fetchNeighborsCount/neighborsCountTemplate.ts b/packages/graph-explorer/src/connector/openCypher/fetchNeighborsCount/neighborsCountTemplate.ts index 4cdaa0e42..882522e76 100644 --- a/packages/graph-explorer/src/connector/openCypher/fetchNeighborsCount/neighborsCountTemplate.ts +++ b/packages/graph-explorer/src/connector/openCypher/fetchNeighborsCount/neighborsCountTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import type { NeighborsCountRequest } from "@/connector/useGEFetchTypes"; +import { idParam } from "../idParam"; /** * Given a single nodes id, it returns an OpenCypher template with @@ -21,7 +22,7 @@ export default function neighborsCountTemplate({ }: NeighborsCountRequest) { return query` MATCH (v)-[]-(neighbor) - WHERE ID(v) = "${vertex.id}" + WHERE ID(v) = ${idParam(vertex.id)} WITH DISTINCT neighbor ${limit > 0 ? `LIMIT ${limit}` : ``} RETURN labels(neighbor) AS vertexLabel, count(DISTINCT neighbor) AS count diff --git a/packages/graph-explorer/src/connector/openCypher/idParam.ts b/packages/graph-explorer/src/connector/openCypher/idParam.ts new file mode 100644 index 000000000..467bc7ae7 --- /dev/null +++ b/packages/graph-explorer/src/connector/openCypher/idParam.ts @@ -0,0 +1,6 @@ +import { EdgeId, getRawId, VertexId } from "@/core"; + +/** Formats the ID parameter for an openCypher query based on the ID type. */ +export function idParam(id: VertexId | EdgeId) { + return `"${getRawId(id)}"`; +} diff --git a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiEdge.ts b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiEdge.ts index deb0e28c0..9e43ccfaf 100644 --- a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiEdge.ts +++ b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiEdge.ts @@ -1,4 +1,4 @@ -import type { Edge, EdgeId, VertexId } from "@/core"; +import { createEdgeId, createVertexId, type Edge } from "@/core"; import type { OCEdge } from "../types"; const mapApiEdge = ( @@ -8,12 +8,12 @@ const mapApiEdge = ( ): Edge => { return { entityType: "edge", - id: apiEdge["~id"] as EdgeId, + id: createEdgeId(apiEdge["~id"]), idType: "string", type: apiEdge["~type"], - source: apiEdge["~start"] as VertexId, + source: createVertexId(apiEdge["~start"]), sourceType: sourceType, - target: apiEdge["~end"] as VertexId, + target: createVertexId(apiEdge["~end"]), targetType: targetType, attributes: apiEdge["~properties"] || {}, }; diff --git a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.test.ts b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.test.ts index 9de13ab94..dc1fe55d6 100644 --- a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.test.ts +++ b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.test.ts @@ -1,5 +1,5 @@ import mapApiVertex from "./mapApiVertex"; -import { Vertex, VertexId } from "@/core"; +import { createVertexId, Vertex } from "@/core"; test("maps empty vertex", () => { const input = { @@ -12,7 +12,7 @@ test("maps empty vertex", () => { expect(result).toEqual({ entityType: "vertex", - id: "" as VertexId, + id: createVertexId(""), idType: "string", type: "", types: [], @@ -45,7 +45,7 @@ test("maps airport node", () => { expect(result).toEqual({ entityType: "vertex", - id: "1" as VertexId, + id: createVertexId("1"), idType: "string", type: "airport", types: ["airport"], diff --git a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.ts b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.ts index 4d55c7558..ab6e2b69b 100644 --- a/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.ts +++ b/packages/graph-explorer/src/connector/openCypher/mappers/mapApiVertex.ts @@ -1,4 +1,4 @@ -import type { Vertex, VertexId } from "@/core"; +import { createVertexId, type Vertex } from "@/core"; import type { OCVertex } from "../types"; export default function mapApiVertex(apiVertex: OCVertex): Vertex { @@ -7,7 +7,7 @@ export default function mapApiVertex(apiVertex: OCVertex): Vertex { return { entityType: "vertex", - id: apiVertex["~id"] as VertexId, + id: createVertexId(apiVertex["~id"]), idType: "string", type: vt, types: labels, diff --git a/packages/graph-explorer/src/connector/openCypher/vertexDetails.ts b/packages/graph-explorer/src/connector/openCypher/vertexDetails.ts index 07fcb2c3e..6d4604bd9 100644 --- a/packages/graph-explorer/src/connector/openCypher/vertexDetails.ts +++ b/packages/graph-explorer/src/connector/openCypher/vertexDetails.ts @@ -7,6 +7,7 @@ import { OCVertex, OpenCypherFetch } from "./types"; import isErrorResponse from "@/connector/utils/isErrorResponse"; import mapApiVertex from "./mappers/mapApiVertex"; import { query } from "@/utils"; +import { idParam } from "./idParam"; type Response = { results: [ @@ -20,9 +21,8 @@ export async function vertexDetails( openCypherFetch: OpenCypherFetch, req: VertexDetailsRequest ): Promise { - const idTemplate = `"${String(req.vertex.id)}"`; const template = query` - MATCH (vertex) WHERE ID(vertex) = ${idTemplate} RETURN vertex + MATCH (vertex) WHERE ID(vertex) = ${idParam(req.vertex.id)} RETURN vertex `; // Fetch the vertex details diff --git a/packages/graph-explorer/src/connector/sparql/edgeDetails.ts b/packages/graph-explorer/src/connector/sparql/edgeDetails.ts index aafc2eea3..1c505e540 100644 --- a/packages/graph-explorer/src/connector/sparql/edgeDetails.ts +++ b/packages/graph-explorer/src/connector/sparql/edgeDetails.ts @@ -9,6 +9,7 @@ import { import { logger, query } from "@/utils"; import { z } from "zod"; import isErrorResponse from "../utils/isErrorResponse"; +import { idParam } from "./idParam"; const responseSchema = sparqlResponseSchema( z.object({ @@ -23,8 +24,8 @@ export async function edgeDetails( ): Promise { const { source, target, predicate } = parseEdgeId(request.edge.id); - const sourceIdTemplate = `<${source}>`; - const targetIdTemplate = `<${target}>`; + const sourceIdTemplate = idParam(source); + const targetIdTemplate = idParam(target); const template = query` # Get the resource types of source and target diff --git a/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/blankNodeSubjectPredicatesTemplate.ts b/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/blankNodeSubjectPredicatesTemplate.ts index b88ba8479..2e94f2d2f 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/blankNodeSubjectPredicatesTemplate.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/blankNodeSubjectPredicatesTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import { SPARQLBlankNodeNeighborsPredicatesRequest } from "../types"; +import { idParam } from "../idParam"; /** * Fetch all predicates and their direction of a pairs of subjects @@ -18,7 +19,7 @@ const blankNodeSubjectPredicatesTemplate = ({ let classesValues = "VALUES ?subject {"; subjectURIs.forEach(sURI => { - classesValues += ` <${sURI}>`; + classesValues += ` ${idParam(sURI)}`; }); classesValues += "}"; return classesValues; diff --git a/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/index.ts b/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/index.ts index bb8be04bf..e4e4a061b 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/index.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchBlankNodeNeighbors/index.ts @@ -19,6 +19,7 @@ import { SparqlFetch, } from "../types"; import { logger } from "@/utils"; +import { VertexId } from "@/core"; type RawBlankNodeNeighborsResponse = { results: { @@ -43,9 +44,9 @@ type RawNeighborsPredicatesResponse = { async function fetchBlankNodeNeighborsPredicates( sparqlFetch: SparqlFetch, subQuery: string, - resourceURI: string, + resourceURI: VertexId, resourceClass: string, - subjectURIs: string[] + subjectURIs: VertexId[] ) { const template = blankNodeSubjectPredicatesTemplate({ subQuery, diff --git a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/index.ts b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/index.ts index cf7633d2f..d05ee0b20 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/index.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/index.ts @@ -1,5 +1,5 @@ import groupBy from "lodash/groupBy"; -import { Edge } from "@/core"; +import { Edge, VertexId } from "@/core"; import type { NeighborsResponse } from "@/connector/useGEFetchTypes"; import mapIncomingToEdge, { IncomingPredicate, @@ -103,9 +103,9 @@ const fetchOneHopNeighbors = async ( export const fetchNeighborsPredicates = async ( sparqlFetch: SparqlFetch, - resourceURI: string, + resourceURI: VertexId, resourceClass: string, - subjectURIs: string[] + subjectURIs: VertexId[] ) => { const template = subjectPredicatesTemplate({ resourceURI, diff --git a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsBlankNodesIdsTemplate.ts b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsBlankNodesIdsTemplate.ts index bbcfc77dc..c5b94278c 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsBlankNodesIdsTemplate.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsBlankNodesIdsTemplate.ts @@ -1,6 +1,7 @@ import { query } from "@/utils"; import { SPARQLNeighborsRequest } from "../types"; import { getFilters, getLimit, getSubjectClasses } from "./helpers"; +import { idParam } from "../idParam"; /** * Generate a template with the same constraints that oneHopNeighborsTemplate @@ -18,7 +19,7 @@ export default function oneHopNeighborsBlankNodesIdsTemplate({ return query` # Sub-query to fetch blank node ids for one hop neighbors SELECT DISTINCT (?subject AS ?bNode) { - BIND(<${resourceURI}> AS ?argument) + BIND(${idParam(resourceURI)} AS ?argument) ${getSubjectClasses(subjectClasses)} { ?argument ?pToSubject ?subject. diff --git a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsTemplate.ts b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsTemplate.ts index 445a9ae03..388d11f78 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsTemplate.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/oneHopNeighborsTemplate.ts @@ -1,3 +1,4 @@ +import { idParam } from "../idParam"; import { SPARQLNeighborsRequest } from "../types"; import { getFilters, getLimit, getSubjectClasses } from "./helpers"; import { query } from "@/utils"; @@ -64,7 +65,7 @@ export default function oneHopNeighborsTemplate({ ?subject a ?subjectClass; ?pred ?value { SELECT DISTINCT ?subject ?pToSubject ?pFromSubject { - BIND(<${resourceURI}> AS ?argument) + BIND(${idParam(resourceURI)} AS ?argument) ${getSubjectClasses(subjectClasses)} { ?argument ?pToSubject ?subject. diff --git a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/subjectPredicatesTemplate.ts b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/subjectPredicatesTemplate.ts index d379dda56..a78cff685 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchNeighbors/subjectPredicatesTemplate.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchNeighbors/subjectPredicatesTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import { SPARQLNeighborsPredicatesRequest } from "../types"; +import { idParam } from "../idParam"; /** * Fetch all predicates and their direction of a pairs of subjects @@ -32,24 +33,15 @@ const subjectPredicatesTemplate = ({ resourceURI, subjectURIs = [], }: SPARQLNeighborsPredicatesRequest): string => { - const getSubjectURIs = () => { - if (!subjectURIs?.length) { - return ""; - } - - let classesValues = "VALUES ?subject {"; - subjectURIs.forEach(sURI => { - classesValues += ` <${sURI}>`; - }); - classesValues += "}"; - return classesValues; - }; + const subjectUriTemplate = subjectURIs.length + ? `VALUES ?subject { ${subjectURIs.map(idParam).join(" ")} }` + : ""; return query` # Fetch all predicates and their direction of a pairs of subjects SELECT ?subject ?subjectClass ?predToSubject ?predFromSubject { - BIND(<${resourceURI}> AS ?argument) - ${getSubjectURIs()} + BIND(${idParam(resourceURI)} AS ?argument) + ${subjectUriTemplate} { ?argument ?predToSubject ?subject. ?subject a ?subjectClass. diff --git a/packages/graph-explorer/src/connector/sparql/fetchNeighborsCount/neighborsCountTemplate.ts b/packages/graph-explorer/src/connector/sparql/fetchNeighborsCount/neighborsCountTemplate.ts index f083ce68d..23d7392b3 100644 --- a/packages/graph-explorer/src/connector/sparql/fetchNeighborsCount/neighborsCountTemplate.ts +++ b/packages/graph-explorer/src/connector/sparql/fetchNeighborsCount/neighborsCountTemplate.ts @@ -1,5 +1,6 @@ import { query } from "@/utils"; import { SPARQLNeighborsCountRequest } from "../types"; +import { idParam } from "../idParam"; /** * Count neighbors by class which are related with the given subject URI. @@ -29,9 +30,9 @@ export default function neighborsCountTemplate({ ?subject a ?class { SELECT DISTINCT ?subject ?class { ?subject a ?class . - { ?subject ?p <${resourceURI}> } + { ?subject ?p ${idParam(resourceURI)} } UNION - { <${resourceURI}> ?p ?subject } + { ${idParam(resourceURI)} ?p ?subject } } ${limit > 0 ? `LIMIT ${limit}` : ""} } diff --git a/packages/graph-explorer/src/connector/sparql/idParam.ts b/packages/graph-explorer/src/connector/sparql/idParam.ts new file mode 100644 index 000000000..00a09b60f --- /dev/null +++ b/packages/graph-explorer/src/connector/sparql/idParam.ts @@ -0,0 +1,6 @@ +import { EdgeId, getRawId, VertexId } from "@/core"; + +/** Formats the ID parameter for a sparql query based on the ID type. */ +export function idParam(id: VertexId | EdgeId) { + return `<${getRawId(id)}>`; +} diff --git a/packages/graph-explorer/src/connector/sparql/mappers/mapIncomingToEdge.ts b/packages/graph-explorer/src/connector/sparql/mappers/mapIncomingToEdge.ts index 00bcbe712..4261fda1a 100644 --- a/packages/graph-explorer/src/connector/sparql/mappers/mapIncomingToEdge.ts +++ b/packages/graph-explorer/src/connector/sparql/mappers/mapIncomingToEdge.ts @@ -1,4 +1,4 @@ -import { Edge, EdgeId, VertexId } from "@/core"; +import { createEdgeId, createVertexId, Edge, getRawId, VertexId } from "@/core"; import { RawValue } from "../types"; export type IncomingPredicate = { @@ -8,18 +8,21 @@ export type IncomingPredicate = { }; const mapIncomingToEdge = ( - resourceURI: string, + resourceURI: VertexId, resourceClass: string, result: IncomingPredicate ): Edge => { + const sourceUri = result.subject.value; + const predicate = result.predFromSubject.value; + return { entityType: "edge", - id: `${result.subject.value}-[${result.predFromSubject.value}]->${resourceURI}` as EdgeId, + id: createEdgeId(`${sourceUri}-[${predicate}]->${getRawId(resourceURI)}`), idType: "string", - type: result.predFromSubject.value, - source: result.subject.value as VertexId, + type: predicate, + source: createVertexId(sourceUri), sourceType: result.subjectClass.value, - target: resourceURI as VertexId, + target: resourceURI, targetType: resourceClass, attributes: {}, }; diff --git a/packages/graph-explorer/src/connector/sparql/mappers/mapOutgoingToEdge.ts b/packages/graph-explorer/src/connector/sparql/mappers/mapOutgoingToEdge.ts index 9dbfcfc4e..7215450bd 100644 --- a/packages/graph-explorer/src/connector/sparql/mappers/mapOutgoingToEdge.ts +++ b/packages/graph-explorer/src/connector/sparql/mappers/mapOutgoingToEdge.ts @@ -1,4 +1,4 @@ -import { Edge, EdgeId, VertexId } from "@/core"; +import { createEdgeId, createVertexId, Edge, getRawId, VertexId } from "@/core"; import { RawValue } from "../types"; export type OutgoingPredicate = { @@ -8,18 +8,20 @@ export type OutgoingPredicate = { }; const mapOutgoingToEdge = ( - resourceURI: string, + resourceURI: VertexId, resourceClass: string, result: OutgoingPredicate ): Edge => { + const targetUri = result.subject.value; + const predicate = result.predToSubject.value; return { entityType: "edge", - id: `${resourceURI}-[${result.predToSubject.value}]->${result.subject.value}` as EdgeId, + id: createEdgeId(`${getRawId(resourceURI)}-[${predicate}]->${targetUri}`), idType: "string", - type: result.predToSubject.value, - source: resourceURI as VertexId, + type: predicate, + source: resourceURI, sourceType: resourceClass, - target: result.subject.value as VertexId, + target: createVertexId(targetUri), targetType: result.subjectClass.value, attributes: {}, }; diff --git a/packages/graph-explorer/src/connector/sparql/mappers/mapRawResultToVertex.ts b/packages/graph-explorer/src/connector/sparql/mappers/mapRawResultToVertex.ts index 795ea8cb6..5f1644c05 100644 --- a/packages/graph-explorer/src/connector/sparql/mappers/mapRawResultToVertex.ts +++ b/packages/graph-explorer/src/connector/sparql/mappers/mapRawResultToVertex.ts @@ -1,10 +1,10 @@ -import { Vertex, VertexId } from "@/core"; +import { createVertexId, Vertex } from "@/core"; import { RawResult } from "../types"; const mapRawResultToVertex = (rawResult: RawResult): Vertex => { return { entityType: "vertex", - id: rawResult.uri as VertexId, + id: createVertexId(rawResult.uri), idType: "string", type: rawResult.class, attributes: rawResult.attributes, diff --git a/packages/graph-explorer/src/connector/sparql/types.ts b/packages/graph-explorer/src/connector/sparql/types.ts index ce900b36d..a93486193 100644 --- a/packages/graph-explorer/src/connector/sparql/types.ts +++ b/packages/graph-explorer/src/connector/sparql/types.ts @@ -35,7 +35,7 @@ export type SPARQLNeighborsRequest = { /** * Resource URI. */ - resourceURI: string; + resourceURI: VertexId; /** * Resource Class. */ @@ -63,11 +63,11 @@ export type SPARQLNeighborsPredicatesRequest = { /** * Resource URI. */ - resourceURI: string; + resourceURI: VertexId; /** * All subjects URIs that are related to the resourceURI. */ - subjectURIs: string[]; + subjectURIs: VertexId[]; }; export type SPARQLBlankNodeNeighborsPredicatesRequest = { @@ -78,14 +78,14 @@ export type SPARQLBlankNodeNeighborsPredicatesRequest = { /** * All subjects URIs that are related to the resourceURI. */ - subjectURIs: string[]; + subjectURIs: VertexId[]; }; export type SPARQLNeighborsCountRequest = { /** * Resource URI. */ - resourceURI: string; + resourceURI: VertexId; /** * Limit the number of results. * 0 = No limit. @@ -135,7 +135,7 @@ export type SPARQLKeywordSearchRequest = { }; export type SPARQLBlankNodeNeighborsRequest = { - resourceURI: string; + resourceURI: VertexId; resourceClass: string; subQuery: string; }; @@ -148,7 +148,7 @@ export type SPARQLBlankNodeNeighborsResponse = NeighborsCountResponse & { }; export type BlankNodeItem = { - id: string; + id: VertexId; subQueryTemplate: string; vertex: Vertex; neighborCounts: { diff --git a/packages/graph-explorer/src/connector/sparql/vertexDetails.ts b/packages/graph-explorer/src/connector/sparql/vertexDetails.ts index 85ec3c99d..58f7f7436 100644 --- a/packages/graph-explorer/src/connector/sparql/vertexDetails.ts +++ b/packages/graph-explorer/src/connector/sparql/vertexDetails.ts @@ -14,6 +14,7 @@ import { import { z } from "zod"; import { Vertex, VertexId } from "@/core"; import isErrorResponse from "../utils/isErrorResponse"; +import { idParam } from "./idParam"; const bindingSchema = z.object({ label: sparqlUriValueSchema, @@ -26,8 +27,6 @@ export async function vertexDetails( sparqlFetch: SparqlFetch, request: VertexDetailsRequest ): Promise { - const idTemplate = `<${request.vertex.id}>`; - const template = query` # Get the resource attributes and class SELECT * @@ -36,7 +35,7 @@ export async function vertexDetails( # Get the resource attributes SELECT ?label ?value WHERE { - ${idTemplate} ?label ?value . + ${idParam(request.vertex.id)} ?label ?value . FILTER(isLiteral(?value)) } } @@ -45,7 +44,7 @@ export async function vertexDetails( # Get the resource type SELECT ?label ?value WHERE { - ${idTemplate} a ?value . + ${idParam(request.vertex.id)} a ?value . BIND(IRI("${rdfTypeUri}") AS ?label) } } diff --git a/packages/graph-explorer/src/core/StateProvider/displayEdge.test.ts b/packages/graph-explorer/src/core/StateProvider/displayEdge.test.ts index 60f099901..84d1ef2a2 100644 --- a/packages/graph-explorer/src/core/StateProvider/displayEdge.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/displayEdge.test.ts @@ -21,6 +21,7 @@ import { Schema } from "../ConfigurationProvider"; import { MutableSnapshot } from "recoil"; import { schemaAtom } from "./schema"; import { QueryEngine } from "@shared/types"; +import { getRawId } from "@/core"; describe("useDisplayEdgeFromEdge", () => { it("should keep the same ID", () => { @@ -35,7 +36,7 @@ describe("useDisplayEdgeFromEdge", () => { it("should have a display ID equal to the edge ID", () => { const edge = createEdge(); - expect(act(edge).displayId).toEqual(edge.id); + expect(act(edge).displayId).toEqual(getRawId(edge.id)); }); it("should have the display name be the types", () => { diff --git a/packages/graph-explorer/src/core/StateProvider/displayEdge.ts b/packages/graph-explorer/src/core/StateProvider/displayEdge.ts index d9f7e3e99..3e31ec1d3 100644 --- a/packages/graph-explorer/src/core/StateProvider/displayEdge.ts +++ b/packages/graph-explorer/src/core/StateProvider/displayEdge.ts @@ -1,4 +1,4 @@ -import { Edge, EdgeId, EntityIdType, VertexId } from "@/core"; +import { Edge, EdgeId, EntityIdType, getRawId, VertexId } from "@/core"; import { selector, selectorFamily, useRecoilValue } from "recoil"; import { textTransformSelector } from "@/hooks"; import { @@ -87,7 +87,8 @@ const displayEdgeSelector = selectorFamily({ .join(", "); // For SPARQL, display the edge type as the ID - const displayId = isSparql ? displayTypes : edge.id; + const rawStringId = String(getRawId(edge.id)); + const displayId = isSparql ? displayTypes : rawStringId; const typeAttributes = get(edgeTypeAttributesSelector(edgeTypes)); const sortedAttributes = getSortedDisplayAttributes( @@ -96,12 +97,14 @@ const displayEdgeSelector = selectorFamily({ textTransform ); + const sourceRawStringId = String(getRawId(edge.source)); + const targetRawStringId = String(getRawId(edge.target)); const sourceDisplayId = isSparql - ? textTransform(edge.source) - : edge.source; + ? textTransform(sourceRawStringId) + : sourceRawStringId; const targetDisplayId = isSparql - ? textTransform(edge.target) - : edge.target; + ? textTransform(targetRawStringId) + : targetRawStringId; const sourceDisplayTypes = edge.sourceType .split("::") diff --git a/packages/graph-explorer/src/core/StateProvider/displayVertex.test.ts b/packages/graph-explorer/src/core/StateProvider/displayVertex.test.ts index 80552ee57..4ff9b3d5e 100644 --- a/packages/graph-explorer/src/core/StateProvider/displayVertex.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/displayVertex.test.ts @@ -5,10 +5,15 @@ import { createRandomVertexTypeConfig, renderHookWithRecoilRoot, } from "@/utils/testing"; -import { useDisplayVertexFromVertex } from "./displayVertex"; -import { Vertex, VertexId } from "@/core"; +import { + createVertexId, + DisplayAttribute, + getRawId, + Schema, + useDisplayVertexFromVertex, + Vertex, +} from "@/core"; import { formatDate, sanitizeText } from "@/utils"; -import { Schema } from "../ConfigurationProvider"; import { MutableSnapshot } from "recoil"; import { schemaAtom } from "./schema"; import { @@ -16,7 +21,6 @@ import { configurationAtom, getDefaultVertexTypeConfig, } from "./configuration"; -import { DisplayAttribute } from "./displayAttribute"; import { createRandomDate } from "@shared/utils/testing"; import { MISSING_DISPLAY_VALUE } from "@/utils/constants"; import { mapToDisplayVertexTypeConfig } from "./displayTypeConfigs"; @@ -35,12 +39,12 @@ describe("useDisplayVertexFromVertex", () => { it("should have a display ID equal to the vertex ID", () => { const vertex = createRandomVertex(); - expect(act(vertex).displayId).toEqual(vertex.id); + expect(act(vertex).displayId).toEqual(getRawId(vertex.id)); }); it("should have the display name be the sanitized vertex ID", () => { const vertex = createRandomVertex(); - expect(act(vertex).displayName).toEqual(vertex.id); + expect(act(vertex).displayName).toEqual(getRawId(vertex.id)); }); it("should have the display description be the sanitized vertex type", () => { @@ -208,7 +212,7 @@ describe("useDisplayVertexFromVertex", () => { it("should replace uri with prefixes when available", () => { const vertex = createRandomVertex(); - vertex.id = "http://www.example.com/resources#foo" as VertexId; + vertex.id = createVertexId("http://www.example.com/resources#foo"); vertex.type = "http://www.example.com/class#bar"; const schema = createRandomSchema(); schema.prefixes = [ diff --git a/packages/graph-explorer/src/core/StateProvider/displayVertex.ts b/packages/graph-explorer/src/core/StateProvider/displayVertex.ts index 178339e68..c6f09fc23 100644 --- a/packages/graph-explorer/src/core/StateProvider/displayVertex.ts +++ b/packages/graph-explorer/src/core/StateProvider/displayVertex.ts @@ -1,5 +1,4 @@ import { selector, selectorFamily, useRecoilValue } from "recoil"; -import { Vertex, VertexId, EntityIdType } from "@/core"; import { DisplayAttribute, getSortedDisplayAttributes, @@ -10,6 +9,10 @@ import { displayVertexTypeConfigSelector, queryEngineSelector, nodeSelector, + EntityIdType, + getRawId, + Vertex, + VertexId, } from "@/core"; import { textTransformSelector } from "@/hooks"; import { @@ -87,7 +90,8 @@ const displayVertexSelector = selectorFamily({ const queryEngine = get(queryEngineSelector); const isSparql = queryEngine === "sparql"; - const displayId = isSparql ? textTransform(vertex.id) : vertex.id; + const rawStringId = String(getRawId(vertex.id)); + const displayId = isSparql ? textTransform(rawStringId) : rawStringId; // One type config used for shape, color, icon, etc. const typeConfig = get(displayVertexTypeConfigSelector(vertex.type)); diff --git a/packages/graph-explorer/src/core/entityIdType.test.ts b/packages/graph-explorer/src/core/entityIdType.test.ts new file mode 100644 index 000000000..d8e727ff7 --- /dev/null +++ b/packages/graph-explorer/src/core/entityIdType.test.ts @@ -0,0 +1,43 @@ +import { VertexId } from "./entities"; +import { createEdgeId, createVertexId, getRawId } from "./entityIdType"; + +describe("createVertexId", () => { + it("should create a vertex id out of a string", () => { + const id = createVertexId("123"); + expect(id).toBe("(str)123"); + }); + + it("should create a vertex id out of a number", () => { + const id = createVertexId(123); + expect(id).toBe("(num)123"); + }); +}); + +describe("createEdgeId", () => { + it("should create an edge id out of a string", () => { + const id = createEdgeId("123"); + expect(id).toBe("(str)123"); + }); + + it("should create an edge id out of a number", () => { + const id = createEdgeId(123); + expect(id).toBe("(num)123"); + }); +}); + +describe("getRawId", () => { + it("should return the raw string id without the prefix", () => { + const id = getRawId("(str)123" as VertexId); + expect(id).toBe("123"); + }); + + it("should return the raw number id without the prefix", () => { + const id = getRawId("(num)123" as VertexId); + expect(id).toBe(123); + }); + + it("should return the id as is if it is not marked as a string or number", () => { + const id = getRawId("123" as VertexId); + expect(id).toBe("123"); + }); +}); diff --git a/packages/graph-explorer/src/core/entityIdType.ts b/packages/graph-explorer/src/core/entityIdType.ts new file mode 100644 index 000000000..b8016ad57 --- /dev/null +++ b/packages/graph-explorer/src/core/entityIdType.ts @@ -0,0 +1,63 @@ +import { VertexId, EdgeId } from "@/core"; + +/** + * Creates a VertexId that is a string prefixed with the ID type. + * @param id The original database ID + * @returns A VertexId that is a string prefixed with the ID type + */ +export function createVertexId(id: string | number): VertexId { + return prefixIdWithType(id) as VertexId; +} + +/** + * Creates an EdgeId that is a string prefixed with the ID type. + * @param id The original database ID + * @returns An EdgeId that is a string prefixed with the ID type + */ +export function createEdgeId(id: string | number): EdgeId { + return prefixIdWithType(id) as EdgeId; +} + +/** + * Strips the ID type prefix from the given ID. + * @param id The original database ID + * @returns The original database ID without the ID type prefix + */ +export function getRawId(id: VertexId | EdgeId): string | number { + if (isIdNumber(id)) { + return parseInt(stripIdTypePrefix(id)); + } + if (isIdString(id)) { + return stripIdTypePrefix(id); + } + return id; +} + +const ID_TYPE_NUM_PREFIX = "(num)"; +const ID_TYPE_STR_PREFIX = "(str)"; + +function prefixIdWithType(id: string | number): string { + if (typeof id === "number") { + return `${ID_TYPE_NUM_PREFIX}${id}`; + } + + return `${ID_TYPE_STR_PREFIX}${id}`; +} + +function isIdNumber(id: string): boolean { + return id.startsWith(ID_TYPE_NUM_PREFIX); +} + +function isIdString(id: string): boolean { + return id.startsWith(ID_TYPE_STR_PREFIX); +} + +function stripIdTypePrefix(id: string): string { + if (isIdNumber(id)) { + return id.slice(ID_TYPE_NUM_PREFIX.length); + } + if (isIdString(id)) { + return id.slice(ID_TYPE_STR_PREFIX.length); + } + return id; +} diff --git a/packages/graph-explorer/src/core/index.ts b/packages/graph-explorer/src/core/index.ts index dbb8ceab4..2398130ca 100644 --- a/packages/graph-explorer/src/core/index.ts +++ b/packages/graph-explorer/src/core/index.ts @@ -3,4 +3,5 @@ export * from "./ThemeProvider"; export * from "./featureFlags"; export * from "./StateProvider"; export * from "./connector"; +export * from "./entityIdType"; export * from "./entities"; diff --git a/packages/graph-explorer/src/hooks/useEntities.test.tsx b/packages/graph-explorer/src/hooks/useEntities.test.tsx index 484a56712..c6888ee6d 100644 --- a/packages/graph-explorer/src/hooks/useEntities.test.tsx +++ b/packages/graph-explorer/src/hooks/useEntities.test.tsx @@ -1,6 +1,13 @@ import { useRecoilValue } from "recoil"; import useEntities from "./useEntities"; -import { Edge, Vertex, VertexId } from "@/core"; +import { + createVertexId, + Edge, + Entities, + Schema, + Vertex, + VertexId, +} from "@/core"; import { createRandomEdge, createRandomEntities, @@ -9,8 +16,6 @@ import { } from "@/utils/testing"; import { schemaAtom } from "@/core/StateProvider/schema"; import { activeConfigurationAtom } from "@/core/StateProvider/configuration"; -import { Schema } from "@/core"; -import { Entities } from "@/core/StateProvider/entitiesSelector"; import { renderHookWithRecoilRoot } from "@/utils/testing"; import { waitForValueToChange } from "@/utils/testing/waitForValueToChange"; import { vi } from "vitest"; @@ -66,21 +71,21 @@ describe("useEntities", () => { it("should handle multiple nodes correctly", async () => { const node1: Vertex = { entityType: "vertex", - id: "1" as VertexId, + id: createVertexId("1"), idType: "string", type: "type1", attributes: {}, }; const node2: Vertex = { entityType: "vertex", - id: "2" as VertexId, + id: createVertexId("2"), idType: "string", type: "type2", attributes: {}, }; const node3: Vertex = { entityType: "vertex", - id: "3" as VertexId, + id: createVertexId("3"), idType: "string", type: "type3", attributes: {}, diff --git a/packages/graph-explorer/src/utils/testing/randomData.ts b/packages/graph-explorer/src/utils/testing/randomData.ts index 6801e45bb..e267cb6c3 100644 --- a/packages/graph-explorer/src/utils/testing/randomData.ts +++ b/packages/graph-explorer/src/utils/testing/randomData.ts @@ -1,13 +1,23 @@ import { + ArrowStyle, AttributeConfig, + createEdgeId, + createVertexId, + Edge, + EdgeId, + EdgePreferences, EdgeTypeConfig, + Entities, FeatureFlags, + LineStyle, RawConfiguration, Schema, + UserStyling, + Vertex, + VertexId, + VertexPreferences, VertexTypeConfig, } from "@/core"; -import { Edge, EdgeId, Vertex, VertexId } from "@/core"; -import { Entities } from "@/core/StateProvider/entitiesSelector"; import { createArray, createRandomBoolean, @@ -18,13 +28,6 @@ import { createRecord, randomlyUndefined, } from "@shared/utils/testing"; -import { - ArrowStyle, - EdgePreferences, - LineStyle, - UserStyling, - VertexPreferences, -} from "@/core/StateProvider/userPreferences"; import { toNodeMap } from "@/core/StateProvider/nodes"; import { toEdgeMap } from "@/core/StateProvider/edges"; import { @@ -148,6 +151,16 @@ export function createRandomEntities(): Entities { return { nodes: toNodeMap(nodes), edges: toEdgeMap(edges) }; } +/** Creates a random vertex ID. */ +export function createRandomVertexId(): VertexId { + return createVertexId(createRandomName("VertexId")); +} + +/** Creates a random edge ID. */ +export function createRandomEdgeId(): EdgeId { + return createEdgeId(createRandomName("EdgeId")); +} + /** * Creates a random vertex. * @returns A random Vertex object. @@ -155,7 +168,7 @@ export function createRandomEntities(): Entities { export function createRandomVertex(): Vertex { return { entityType: "vertex", - id: createRandomName("VertexId") as VertexId, + id: createRandomVertexId(), idType: pickRandomElement(["number", "string"]), type: createRandomName("VertexType"), attributes: createRecord(3, createRandomEntityAttribute), @@ -169,7 +182,7 @@ export function createRandomVertex(): Vertex { export function createRandomEdge(source: Vertex, target: Vertex): Edge { return { entityType: "edge", - id: createRandomName("EdgeId") as EdgeId, + id: createRandomEdgeId(), idType: pickRandomElement(["number", "string"]), type: createRandomName("EdgeType"), attributes: createRecord(3, createRandomEntityAttribute),