import 'jest-rdf';
import { namedNode, quad } from '@rdfjs/data-model';
import arrayifyStream from 'arrayify-stream';
import type { Quad } from 'rdf-js';
import { translate } from 'sparqlalgebrajs';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { SparqlUpdatePatch } from '../../../../src/http/representation/SparqlUpdatePatch';
import type { RepresentationPatcherInput } from '../../../../src/storage/patch/RepresentationPatcher';
import { SparqlUpdatePatcher } from '../../../../src/storage/patch/SparqlUpdatePatcher';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
function getPatch(query: string): SparqlUpdatePatch {
const prefixedQuery = `prefix : \n${query}`;
return {
algebra: translate(prefixedQuery, { quads: true }),
data: guardedStreamFrom(prefixedQuery),
metadata: new RepresentationMetadata(),
binary: true,
};
}
describe('A SparqlUpdatePatcher', (): void => {
let patcher: SparqlUpdatePatcher;
let startQuads: Quad[];
let representation: Representation;
const identifier = { path: 'http://test.com/foo' };
const fulfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
beforeEach(async(): Promise => {
startQuads = [ quad(
namedNode('http://test.com/startS1'),
namedNode('http://test.com/startP1'),
namedNode('http://test.com/startO1'),
), quad(
namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2'),
) ];
representation = new BasicRepresentation(startQuads, 'internal/quads');
patcher = new SparqlUpdatePatcher();
});
it('only accepts SPARQL updates.', async(): Promise => {
const input = { identifier, patch: { algebra: {}} as SparqlUpdatePatch };
await expect(patcher.canHandle(input)).resolves.toBeUndefined();
delete (input.patch as any).algebra;
await expect(patcher.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('handles NOP operations by not doing anything.', async(): Promise => {
let patch = getPatch('');
let input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).resolves.toBe(representation);
patch = getPatch('');
input = { identifier, patch };
const result = await patcher.handle(input);
expect(result.metadata.contentType).toBe('internal/quads');
await expect(arrayifyStream(result.data)).resolves.toEqual([]);
});
it('handles INSERT DATA updates.', async(): Promise => {
const patch = getPatch(fulfilledDataInsert);
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
expect(result.metadata.contentType).toBe('internal/quads');
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
...startQuads,
quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')),
]);
});
it('handles DELETE DATA updates.', async(): Promise => {
const patch = getPatch('DELETE DATA { :startS1 :startP1 :startO1 }');
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
]);
});
it('handles DELETE WHERE updates with no variables.', async(): Promise => {
const patch = getPatch('DELETE WHERE { :startS1 :startP1 :startO1 }');
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
]);
});
it('handles DELETE WHERE updates with variables.', async(): Promise => {
const patch = getPatch('DELETE WHERE { :startS1 :startP1 ?o }');
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
]);
});
it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise => {
const patch = getPatch('DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}');
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
quad(namedNode('http://test.com/s1'),
namedNode('http://test.com/p1'),
namedNode('http://test.com/o1')),
]);
});
it('handles composite INSERT/DELETE updates.', async(): Promise => {
const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
quad(namedNode('http://test.com/s2'),
namedNode('http://test.com/p2'),
namedNode('http://test.com/o2')),
]);
});
it('handles composite DELETE/INSERT updates.', async(): Promise => {
const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
const result = await patcher.handle(input);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
namedNode('http://test.com/startO2')),
quad(namedNode('http://test.com/s1'),
namedNode('http://test.com/p1'),
namedNode('http://test.com/o1')),
quad(namedNode('http://test.com/s2'),
namedNode('http://test.com/p2'),
namedNode('http://test.com/o2')),
]);
});
it('rejects GRAPH inserts.', async(): Promise => {
const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects GRAPH deletes.', async(): Promise => {
const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects DELETE/INSERT updates with non-BGP WHERE.', async(): Promise => {
const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o. FILTER (?o > 5) }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects INSERT WHERE updates with a UNION.', async(): Promise => {
const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects non-DELETE/INSERT updates.', async(): Promise => {
const query = 'MOVE DEFAULT TO GRAPH :newGraph';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow(NotImplementedHttpError);
});
it('creates a new resource if it does not exist yet.', async(): Promise => {
const query = 'INSERT DATA { . }';
const patch = getPatch(query);
const input: RepresentationPatcherInput = { identifier, patch };
const result = await patcher.handle(input);
expect(result.metadata.contentType).toBe('internal/quads');
expect(result.metadata.identifier.value).toBe(identifier.path);
const resultQuads = await arrayifyStream(result.data);
expect(resultQuads).toBeRdfIsomorphic([
quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
]);
});
it('requires the input body to contain quads.', async(): Promise => {
const query = 'INSERT DATA { . }';
const patch = getPatch(query);
representation.metadata.contentType = 'text/turtle';
const input = { identifier, patch, representation };
await expect(patcher.handle(input)).rejects.toThrow('Quad stream was expected for patching.');
});
});