Skip to content

Commit

Permalink
Added support for updating values of optional properties (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
karelklima authored Nov 28, 2023
1 parent a664e21 commit a8c90a4
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 5 deletions.
2 changes: 1 addition & 1 deletion library/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class Decoder {
);
} else {
// No data, optional property
return property["@array"] ? [] : undefined;
return property["@array"] ? [] : null;
}
}

Expand Down
4 changes: 2 additions & 2 deletions library/lens/query_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ export class QueryBuilder {
${this.getResourceSignature()}
${this.getShape(true, false, true)}
`.WHERE`
${this.getShape(true, true, false)}
VALUES ?iri {
${iris.map(this.df.namedNode)}
}
${this.getShape(true, true, false)}
`.build();

return query;
Expand Down Expand Up @@ -170,6 +170,6 @@ export class QueryBuilder {
}

return DELETE`${helper.deleteQuads}`.INSERT`${helper.insertQuads}`
.WHERE`${helper.deleteQuads}`.build();
.WHERE`${helper.whereQuads}`.build();
}
}
10 changes: 9 additions & 1 deletion library/lens/update_helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context, RDF } from "../rdf.ts";
import { OPTIONAL, type SparqlValue } from "../sparql/mod.ts";
import {
getSchemaProperties,
type Property,
Expand All @@ -16,7 +17,7 @@ export class UpdateHelper {

public readonly deleteQuads: RDF.Quad[] = [];
public readonly insertQuads: RDF.Quad[] = [];
public readonly whereQuads: RDF.Quad[] = [];
public readonly whereQuads: SparqlValue[] = [];

constructor(
schema: Schema,
Expand Down Expand Up @@ -68,6 +69,12 @@ export class UpdateHelper {
const quadsToDelete = this.encode(deletePattern);
this.deleteQuads.push(...quadsToDelete);

if (property["@optional"]) {
this.whereQuads.push(OPTIONAL`${quadsToDelete}`);
} else {
this.whereQuads.push(...quadsToDelete);
}

if (property["@optional"] && propertyValue === null) {
// The intention was to delete a value of an optional property, nothing to insert
return;
Expand Down Expand Up @@ -110,6 +117,7 @@ export class UpdateHelper {
};
const quadsToDelete = this.encode(deletePattern);
this.deleteQuads.push(...quadsToDelete);
this.whereQuads.push(...quadsToDelete);

const insertPattern = {
$id: entity.$id,
Expand Down
1 change: 1 addition & 0 deletions library/sparql/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { sparql, type SparqlValue } from "./sparql_tag.ts";
export { ASK, CONSTRUCT, DESCRIBE, SELECT } from "./sparql_query_builders.ts";
export { DELETE, INSERT, WITH } from "./sparql_update_builders.ts";
export { OPTIONAL } from "./sparql_expression_builders.ts";
22 changes: 22 additions & 0 deletions library/sparql/sparql_expression_builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { bracesInline, SparqlBuilder } from "./sparql_shared_builders.ts";

import { type SparqlValue } from "./sparql_tag.ts";

type Builders<T extends keyof SparqlExpressionBuilder> = Pick<
SparqlExpressionBuilder,
T
>;

class SparqlExpressionBuilder extends SparqlBuilder {
public OPTIONAL(
strings: TemplateStringsArray,
...values: SparqlValue[]
): Builders<"build"> {
return this.template(strings, values, "OPTIONAL", bracesInline);
}
}

export const OPTIONAL = (
strings: TemplateStringsArray,
...values: SparqlValue[]
) => new SparqlExpressionBuilder().OPTIONAL(strings, ...values);
2 changes: 2 additions & 0 deletions library/sparql/sparql_shared_builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { sparql, type SparqlValue } from "./sparql_tag.ts";
type Wrap = (keyword: string, value: string) => string;
export const braces: Wrap = (keyword: string, value: string) =>
`${keyword} {\n${value}\n}\n`;
export const bracesInline: Wrap = (keyword: string, value: string) =>
`${keyword} { ${value} }`;
export const parentheses: Wrap = (keyword: string, value: string) =>
`${keyword} (${value})\n`;
const none: Wrap = (keyword: string, value: string) => `${keyword} ${value}\n`;
Expand Down
5 changes: 5 additions & 0 deletions library/sparql/sparql_tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type SparqlValue =
| boolean
| Date
| Iterable<SparqlValue>
| { build: () => string }
| null
| undefined;

Expand Down Expand Up @@ -71,6 +72,10 @@ const valueToString = (value: SparqlValue): string => {
return result;
}

if ("build" in value) {
return value.build();
}

if (value.termType) {
return stringify(value);
}
Expand Down
11 changes: 11 additions & 0 deletions library/sparql/sparql_update_builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class SparqlUpdateBuilder extends SparqlBuilder {
return this.template(strings, values, "DELETE DATA", braces);
}

public DELETE_WHERE(
strings: TemplateStringsArray,
...values: SparqlValue[]
): Builders<"build"> {
return this.template(strings, values, "DELETE WHERE", braces);
}

public WITH(
stringOrNamedNode: string | RDF.NamedNode<string>,
): Builders<"INSERT" | "DELETE"> {
Expand All @@ -81,6 +88,10 @@ export const DELETE = Object.assign((
strings: TemplateStringsArray,
...values: SparqlValue[]
) => new SparqlUpdateBuilder().DELETE_DATA(strings, ...values),
WHERE: (
strings: TemplateStringsArray,
...values: SparqlValue[]
) => new SparqlUpdateBuilder().DELETE_WHERE(strings, ...values),
});

export const WITH = (
Expand Down
1 change: 1 addition & 0 deletions tests/decoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Deno.test("Decoder / Optional property missing", () => {
const output = [
{
$id: x.A,
optional: null,
},
];

Expand Down
189 changes: 189 additions & 0 deletions tests/e2e/optional.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { assert, Comunica } from "../test_deps.ts";

import { initStore, x } from "../test_utils.ts";

import { createLens } from "ldkit";

const engine = new Comunica();

const EntitySchema = {
"@type": x.Entity,
requiredString: {
"@id": x.requiredString,
},
optionalString: {
"@id": x.optionalString,
"@optional": true,
},
optionalArray: {
"@id": x.optionalArray,
"@optional": true,
"@array": true,
},
} as const;

const init = () => {
const { context, assertStore } = initStore();
const Entities = createLens(EntitySchema, context, engine);
return { Entities, assertStore };
};

Deno.test("E2E / Optional / Insert without optional properties", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" .
`);
});

Deno.test("E2E / Optional / Insert with optional properties", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
optionalString: "test",
optionalArray: ["testArray", "otherArray"],
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" ;
x:optionalString "test" ;
x:optionalArray "testArray", "otherArray" .
`);
});

Deno.test("E2E / Optional / Unset optional existing property", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
optionalString: "test",
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" ;
x:optionalString "test" .
`);

await Entities.update({
$id: x.Entity,
requiredString: "different",
optionalString: null,
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "different" .
`);
});

Deno.test("E2E / Optional / Unset optional existing array property", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
optionalArray: ["test"],
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" ;
x:optionalArray "test" .
`);

await Entities.update({
$id: x.Entity,
requiredString: "different",
optionalArray: [],
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "different" .
`);
});

Deno.test("E2E / Optional / Unset optional non-existing property", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" .
`);

await Entities.update({
$id: x.Entity,
requiredString: "different",
optionalString: null,
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "different" .
`);
});

Deno.test("E2E / Optional / Should return null for optional property without value", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" .
`);

const entity = await Entities.findByIri(x.Entity);

assert(entity !== null);
assert(entity.optionalString === null);
});

Deno.test("E2E / Optional / Should return empty array for optional array property without value", async () => {
const { Entities, assertStore } = init();

await Entities.insert({
$id: x.Entity,
requiredString: "required",
});

assertStore(`
x:Entity
a x:Entity ;
x:requiredString "required" .
`);

const entity = await Entities.findByIri(x.Entity);

assert(entity !== null);
assert(Array.isArray(entity.optionalArray));
assert(entity.optionalArray.length === 0);
});
47 changes: 47 additions & 0 deletions tests/sparql_expression_builders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { assertEquals } from "./test_deps.ts";

import { DataFactory } from "../library/rdf.ts";
import { OPTIONAL } from "../library/sparql/sparql_expression_builders.ts";
import { DELETE } from "../library/sparql/sparql_update_builders.ts";

const df = new DataFactory();
const s = df.variable("s");
const p = df.variable("p");
const o = df.variable("o");
const spo = df.quad(s, p, o);

Deno.test("SPARQL / Sparql builder OPTIONAL #1", () => {
const expected = "OPTIONAL { ?s ?p ?o . }";
const query = OPTIONAL`${s} ${p} ${o} .`.build();

assertEquals(query, expected);
});

Deno.test("SPARQL / Sparql builder OPTIONAL #2", () => {
const expected = "OPTIONAL { ?s ?p ?o . }";
const query = OPTIONAL`${spo}`.build();

assertEquals(query, expected);
});

Deno.test("SPARQL / Sparql builder OPTIONAL #3", () => {
const expected = "OPTIONAL { ?s ?p ?o .\n?s ?p ?o . }";
const query = OPTIONAL`${[spo, spo]}`.build();

assertEquals(query, expected);
});

Deno.test("SPARQL / Sparql builder OPTIONAL #4", () => {
const expected = "DELETE WHERE {\nOPTIONAL { ?s ?p ?o .\n?s ?p ?o . }\n}\n";
const query = DELETE.WHERE`${OPTIONAL`${[spo, spo]}`}`.build();

assertEquals(query, expected);
});

Deno.test("SPARQL / Sparql builder OPTIONAL #5", () => {
const expected =
"DELETE WHERE {\nOPTIONAL { ?s ?p ?o . }\nOPTIONAL { ?s ?p ?o . }\n}\n";
const query = DELETE.WHERE`${[OPTIONAL`${spo}`, OPTIONAL`${spo}`]}`.build();

assertEquals(query, expected);
});
Loading

0 comments on commit a8c90a4

Please sign in to comment.