From d171e7dfdfadad7266b8e71e6e2d7184d865ad2f Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Tue, 7 Oct 2025 10:47:20 -0500 Subject: [PATCH] Add PatchEntityDetailsError --- .../queries/executeUserQuery.test.ts | 4 +- .../queries/patchEntityDetails.test.ts | 78 ++++++++++++++++++- .../connector/queries/patchEntityDetails.ts | 48 +++++++++++- .../src/utils/createDisplayError.ts | 8 ++ 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/packages/graph-explorer/src/connector/queries/executeUserQuery.test.ts b/packages/graph-explorer/src/connector/queries/executeUserQuery.test.ts index 62007a416..a7c4af07c 100644 --- a/packages/graph-explorer/src/connector/queries/executeUserQuery.test.ts +++ b/packages/graph-explorer/src/connector/queries/executeUserQuery.test.ts @@ -165,7 +165,9 @@ describe("executeUserQuery", () => { executeUserQuery("query", mockUpdateSchema) ); - await expect(result).rejects.toThrow("Failed to fetch entity details"); + await expect(result).rejects.toThrow( + "Failed to fetch the details of 2 vertices and 1 edge." + ); expect(rawQuerySpy).toBeCalledTimes(1); expect(vertexDetailsSpy).toBeCalledTimes(1); expect(edgeDetailsSpy).toBeCalledTimes(1); diff --git a/packages/graph-explorer/src/connector/queries/patchEntityDetails.test.ts b/packages/graph-explorer/src/connector/queries/patchEntityDetails.test.ts index c87bfb6a4..6cbffcfdc 100644 --- a/packages/graph-explorer/src/connector/queries/patchEntityDetails.test.ts +++ b/packages/graph-explorer/src/connector/queries/patchEntityDetails.test.ts @@ -3,7 +3,10 @@ import { createTestableVertex, FakeExplorer, } from "@/utils/testing"; -import { patchEntityDetails } from "./patchEntityDetails"; +import { + patchEntityDetails, + PatchEntityDetailsError, +} from "./patchEntityDetails"; import { createQueryClient } from "@/core/queryClient"; import { createResultScalar, @@ -222,4 +225,77 @@ describe("patchEntityDetails", () => { expect(result).toStrictEqual([expectedBundle]); }); + + it("should throw when source vertex not found", async () => { + const explorer = new FakeExplorer(); + const client = createQueryClient({ explorer }); + + const edge = createTestableEdge(); + explorer.addTestableEdge(edge); + explorer.vertexMap.delete(edge.source.id); + + await expect(() => + patchEntityDetails(client, [edge.asFragmentResult()]) + ).rejects.toThrow( + new PatchEntityDetailsError("Failed to fetch the details of 1 vertex.") + ); + }); + + it("should throw when target vertex not found", async () => { + const explorer = new FakeExplorer(); + const client = createQueryClient({ explorer }); + + const edge = createTestableEdge(); + explorer.addTestableEdge(edge); + explorer.vertexMap.delete(edge.target.id); + + await expect(() => + patchEntityDetails(client, [edge.asFragmentResult()]) + ).rejects.toThrow( + new PatchEntityDetailsError("Failed to fetch the details of 1 vertex.") + ); + }); + + it("should throw when edge full details not found", async () => { + const explorer = new FakeExplorer(); + const client = createQueryClient({ explorer }); + + const edge = createTestableEdge(); + explorer.addTestableEdge(edge); + explorer.edgeMap.delete(edge.id); + + await expect(() => + patchEntityDetails(client, [edge.asFragmentResult()]) + ).rejects.toThrow( + new PatchEntityDetailsError("Failed to fetch the details of 1 edge.") + ); + }); + + it("should throw when multiple vertices and edges are not found", async () => { + const explorer = new FakeExplorer(); + const client = createQueryClient({ explorer }); + + const vertex = createTestableVertex(); + const edge1 = createTestableEdge(); + const edge2 = createTestableEdge(); + explorer.addTestableVertex(vertex); + explorer.addTestableEdge(edge1); + explorer.addTestableEdge(edge2); + explorer.vertexMap.delete(vertex.id); + explorer.vertexMap.delete(edge1.source.id); + explorer.edgeMap.delete(edge1.id); + explorer.edgeMap.delete(edge2.id); + + await expect(() => + patchEntityDetails(client, [ + vertex.asFragmentResult(), + edge1.asFragmentResult(), + edge2.asFragmentResult(), + ]) + ).rejects.toThrow( + new PatchEntityDetailsError( + "Failed to fetch the details of 2 vertices and 2 edges." + ) + ); + }); }); diff --git a/packages/graph-explorer/src/connector/queries/patchEntityDetails.ts b/packages/graph-explorer/src/connector/queries/patchEntityDetails.ts index cfb024cd7..a746ca40f 100644 --- a/packages/graph-explorer/src/connector/queries/patchEntityDetails.ts +++ b/packages/graph-explorer/src/connector/queries/patchEntityDetails.ts @@ -35,11 +35,30 @@ export async function patchEntityDetails( // Ensure all details are fetched if (missingVertices.length > 0 || missingEdges.length > 0) { + const vertexCount = + missingVertices.length === 0 + ? null + : missingVertices.length === 1 + ? "1 vertex" + : `${missingVertices.length} vertices`; + const edgeCount = + missingEdges.length === 0 + ? null + : missingEdges.length === 1 + ? "1 edge" + : `${missingEdges.length} edges`; + + const missingCount = + vertexCount && edgeCount + ? `${vertexCount} and ${edgeCount}` + : vertexCount || edgeCount || ""; logger.error("Failed to fetch fragment entity details", { missingVertices, missingEdges, }); - throw new Error("Failed to fetch entity details"); + throw new PatchEntityDetailsError( + `Failed to fetch the details of ${missingCount}.` + ); } // Create a mapping from vertex ID to full vertex details @@ -76,7 +95,7 @@ function patchVertex( const fullVertex = vertexDetailsMap.get(vertex.id); if (!fullVertex) { - throw new Error("Failed to fetch vertex details"); + throw new PatchEntityDetailsError("Failed to fetch vertex details"); } return createPatchedResultVertex({ @@ -94,8 +113,21 @@ function patchEdge( const fullSource = vertexDetailsMap.get(edge.sourceId); const fullTarget = vertexDetailsMap.get(edge.targetId); - if (!fullEdge || !fullSource || !fullTarget) { - throw new Error("Failed to fetch edge details"); + if (!fullEdge) { + throw new PatchEntityDetailsError( + "Could not find the full details of the edge" + ); + } + + if (!fullSource) { + throw new PatchEntityDetailsError( + "Could not find the full details of the source vertex" + ); + } + if (!fullTarget) { + throw new PatchEntityDetailsError( + "Could not find the full details of the target vertex" + ); } return createPatchedResultEdge({ @@ -118,3 +150,11 @@ function patchBundle( ), }; } + +export class PatchEntityDetailsError extends Error { + constructor(message: string) { + super(message); + this.name = "PatchEntityDetailsError"; + Object.setPrototypeOf(this, PatchEntityDetailsError.prototype); + } +} diff --git a/packages/graph-explorer/src/utils/createDisplayError.ts b/packages/graph-explorer/src/utils/createDisplayError.ts index 6acf68485..70e8bfea7 100644 --- a/packages/graph-explorer/src/utils/createDisplayError.ts +++ b/packages/graph-explorer/src/utils/createDisplayError.ts @@ -1,6 +1,7 @@ import { ZodError } from "zod"; import { NetworkError } from "./NetworkError"; import { isCancellationError } from "./isCancellationError"; +import { PatchEntityDetailsError } from "@/connector/queries/patchEntityDetails"; export type DisplayError = { title: string; @@ -118,6 +119,13 @@ export function createDisplayError(error: any): DisplayError { }; } + if (error instanceof PatchEntityDetailsError) { + return { + title: "Failed to update entity details", + message: error.message, + }; + } + if (error instanceof ZodError) { return { title: "Unrecognized Result Format",