Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ describe("parseAndMapQuads", () => {
expect(() => parseAndMapQuads(invalidData)).toThrow();
expect(logger.error).toHaveBeenCalled();
});

it("should throw validation error for partial shorthand matches (e.g., s, predicate, object)", () => {
const invalidData = {
head: { vars: ["s", "predicate", "object"] },
results: {
bindings: [
{
s: { type: "uri", value: "http://example.com/resource" },
predicate: { type: "uri", value: "http://example.com/predicate" },
object: { type: "literal", value: "test value" },
},
],
},
};

expect(() => parseAndMapQuads(invalidData)).toThrow();
expect(logger.error).toHaveBeenCalled();
});
});

describe("successful parsing", () => {
Expand Down Expand Up @@ -138,6 +156,31 @@ describe("parseAndMapQuads", () => {
expect(logger.error).not.toHaveBeenCalled();
});

it("should parse shorthand variables (?s ?p ?o) correctly", () => {
const vertex = createTestableVertex().withRdfValues();
const bindings = createQuadBindingsForEntities([vertex], []);

// Convert standard bindings to shorthand variables
const shorthandBindings = bindings.map(b => ({
s: b.subject,
p: b.predicate,
o: b.object,
}));

const data = {
head: { vars: ["s", "p", "o"] },
results: { bindings: shorthandBindings },
};

const result = parseAndMapQuads(data);

expect(result).toEqual({
vertices: [vertex.asResult()],
edges: [],
});
expect(logger.error).not.toHaveBeenCalled();
});

it("should parse single vertex with blank node", () => {
const vertex = createTestableVertex().withRdfValues({
isBlankNode: true,
Expand Down
67 changes: 67 additions & 0 deletions packages/graph-explorer/src/connector/sparql/rawQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,35 @@ describe("rawQuery", () => {
expect(result.rawResponse).toEqual(mockResponse);
});

it("should treat SELECT queries with exactly s/p/o variables as CONSTRUCT", async () => {
const vertex1 = createTestableVertex().withRdfValues();
const vertex2 = createTestableVertex().withRdfValues();

const bindings = createQuadBindingsForEntities([vertex1, vertex2], []);
const shorthandBindings = bindings.map(b => ({
s: b.subject,
p: b.predicate,
o: b.object,
}));

const mockResponse = {
head: { vars: ["s", "p", "o"] },
results: { bindings: shorthandBindings },
};

const mockFetch = vi.fn().mockResolvedValue(mockResponse);
const result = await rawQuery(mockFetch, {
query: "SELECT ?s ?p ?o WHERE { ?s ?p ?o }",
});

// It should map them to fragments as if it was a CONSTRUCT response
expect(result.results).toStrictEqual([
vertex1.asFragmentResult(),
vertex2.asFragmentResult(),
]);
expect(result.rawResponse).toEqual(mockResponse);
});

it("should not treat SELECT queries with subject/predicate/object variables as CONSTRUCT", async () => {
const mockResponse = {
head: { vars: ["subject", "predicate", "object", "extra"] },
Expand Down Expand Up @@ -262,6 +291,44 @@ describe("rawQuery", () => {
expect(result.rawResponse).toEqual(mockResponse);
});

it("should not treat SELECT queries with partial shorthand variables (e.g., s/predicate/object) as CONSTRUCT", async () => {
const mockResponse = {
head: { vars: ["s", "predicate", "object"] },
results: {
bindings: [
{
s: createUriValue("http://example.org/person1"),
predicate: createUriValue("http://example.org/name"),
object: createLiteralValue("John Doe"),
},
],
},
};

const mockFetch = vi.fn().mockResolvedValue(mockResponse);
const result = await rawQuery(mockFetch, {
query: "SELECT ?s ?predicate ?object WHERE { ?s ?predicate ?object }",
});

// Should be treated as SELECT query (bundle with 3 scalars), not CONSTRUCT
expect(result.results).toStrictEqual([
createResultBundle({
values: [
createResultScalar({
name: "?s",
value: "http://example.org/person1",
}),
createResultScalar({
name: "?predicate",
value: "http://example.org/name",
}),
createResultScalar({ name: "?object", value: "John Doe" }),
],
}),
]);
expect(result.rawResponse).toEqual(mockResponse);
});

it("should not treat SELECT queries with non-URI subjects as CONSTRUCT", async () => {
const mockResponse = {
head: { vars: ["subject", "predicate", "object"] },
Expand Down
23 changes: 22 additions & 1 deletion packages/graph-explorer/src/connector/sparql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,33 @@ export const sparqlAskResponseSchema = z.object({
boolean: z.boolean(),
});

export const sparqlQuadBindingSchema = z
const sparqlQuadBindingLongSchema = z
.object({
subject: sparqlResourceValueSchema,
predicate: sparqlUriValueSchema,
object: sparqlValueSchema,
graph: sparqlValueSchema.optional(),
})
.strict();

const sparqlQuadBindingShortSchema = z
.object({
s: sparqlResourceValueSchema,
p: sparqlUriValueSchema,
o: sparqlValueSchema,
g: sparqlValueSchema.optional(),
c: sparqlValueSchema.optional(),
})
.strict()
.transform(val => ({
subject: val.s,
predicate: val.p,
object: val.o,
graph: val.g ?? val.c,
}));

export const sparqlQuadBindingSchema = z.union([
sparqlQuadBindingLongSchema,
sparqlQuadBindingShortSchema,
]);
Comment thread
kmcginnes marked this conversation as resolved.
export type SparqlQuadBinding = z.infer<typeof sparqlQuadBindingSchema>;
Loading