fix: Allow non-variable BGP boedies in SPARQL updates

* fix: SPARQL algebra update

* fix: SPARQL algebra bgp only

* fix: No SPARQL variables and refactor tests
This commit is contained in:
Matthieu Bosquet 2021-02-24 07:56:31 +00:00 committed by GitHub
parent 14736327e7
commit 894d4589d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 96 deletions

8
package-lock.json generated
View File

@ -7649,7 +7649,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rdf-isomorphic/-/rdf-isomorphic-1.2.0.tgz",
"integrity": "sha512-Dq+iuWrVuK7q3P4/nychbWhRJ1M5yMAekNJN8f5pjarE8SH9Duy/R0JopVF0I0Q2w0poZlsVKKIBpeG+AdOSAw==",
"dev": true,
"requires": {
"rdf-string": "^1.5.0",
"rdf-terms": "^1.6.2"
@ -8881,13 +8880,14 @@
"dev": true
},
"sparqlalgebrajs": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.4.0.tgz",
"integrity": "sha512-6glKn1uWBsdPuQ4D+4r5m8mgWZoMfiNgip4uyblULTmgISqcbsQzrlrIhWQoZSX95QLLlWlYJufhelQAIRAWKg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.5.0.tgz",
"integrity": "sha512-dNJf4xUj5DFZc/9vQnDU5y9u8l4MfvOkMgx6PAefhTjAK0HHChxLZFF4n6GngWfvEZQ3/HcfmQk3cQo6sT/6bQ==",
"requires": {
"fast-deep-equal": "^3.1.1",
"minimist": "^1.2.5",
"rdf-data-factory": "^1.0.0",
"rdf-isomorphic": "^1.2.0",
"rdf-string": "^1.5.0",
"sparqljs": "^3.1.1"
}

View File

@ -104,7 +104,7 @@
"rdf-parse": "^1.7.0",
"rdf-serialize": "^1.1.0",
"rdf-terms": "^1.5.1",
"sparqlalgebrajs": "^2.3.1",
"sparqlalgebrajs": "^2.5.0",
"sparqljs": "^3.1.2",
"streamify-array": "^1.0.1",
"uuid": "^8.3.0",

View File

@ -51,6 +51,14 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
return op.type === Algebra.types.COMPOSITE_UPDATE;
}
private isBasicGraphPatternWithoutVariables(op: Algebra.Operation): op is Algebra.Bgp {
if (op.type !== Algebra.types.BGP) {
return false;
}
return !(op.patterns as BaseQuad[]).some((pattern): boolean =>
someTerms(pattern, (term): boolean => term.termType === 'Variable'));
}
/**
* Checks if the input operation is of a supported type (DELETE/INSERT or composite of those)
*/
@ -67,7 +75,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
/**
* Checks if the input DELETE/INSERT is supported.
* This means: no GRAPH statements, no DELETE WHERE.
* This means: no GRAPH statements, no DELETE WHERE containing terms of type Variable.
*/
private validateDeleteInsert(op: Algebra.DeleteInsert): void {
const def = defaultGraph();
@ -81,8 +89,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
this.logger.warn('GRAPH statement in INSERT clause');
throw new NotImplementedHttpError('GRAPH statements are not supported');
}
if (op.where ?? deletes.some((pattern): boolean =>
someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
if (!(typeof op.where === 'undefined' || this.isBasicGraphPatternWithoutVariables(op.where))) {
this.logger.warn('WHERE statements are not supported');
throw new NotImplementedHttpError('WHERE statements are not supported');
}

View File

@ -16,6 +16,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
let handler: SparqlUpdatePatchHandler;
let source: ResourceStore;
let startQuads: Quad[];
const fullfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
beforeEach(async(): Promise<void> => {
startQuads = [ quad(
@ -56,6 +57,12 @@ describe('A SparqlUpdatePatchHandler', (): void => {
return true;
}
async function handle(query: string): Promise<void> {
const sparqlPrefix = 'prefix : <http://test.com/>\n';
return handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(sparqlPrefix.concat(query), { quads: true }) } as SparqlUpdatePatch });
}
it('only accepts SPARQL updates.', async(): Promise<void> => {
const input = { identifier: { path: 'path' },
patch: { algebra: {}} as SparqlUpdatePatch };
@ -65,12 +72,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles INSERT DATA updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> }',
{ quads: true },
) } as SparqlUpdatePatch });
await handle(fullfilledDataInsert);
expect(await basicChecks(startQuads.concat(
[ 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')) ],
@ -78,11 +80,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles DELETE DATA updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE DATA { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
await handle('DELETE DATA { :startS1 :startP1 :startO1 }');
expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
@ -91,11 +89,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles DELETE WHERE updates with no variables.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE WHERE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'DELETE WHERE { :startS1 :startP1 :startO1 }';
await handle(query);
expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
@ -104,13 +99,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }\n' +
'INSERT { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }\n' +
'WHERE {}',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
@ -122,14 +112,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles composite INSERT/DELETE updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> };' +
'DELETE WHERE { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
@ -141,14 +126,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('handles composite DELETE/INSERT updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> };' +
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
@ -163,80 +143,49 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});
it('rejects GRAPH inserts.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { GRAPH <http://test.com/graph> { ' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects GRAPH deletes.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE DATA { GRAPH <http://test.com/graph> { ' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects DELETE/INSERT updates with a non-empty WHERE.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }\n' +
'INSERT { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }\n' +
'WHERE { ?s ?p ?o }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('WHERE statements are not supported');
const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects INSERT WHERE updates with a UNION.', async(): Promise<void> => {
const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects DELETE WHERE updates with variables.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE WHERE { ?v <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('WHERE statements are not supported');
const query = 'DELETE WHERE { ?v :startP1 :startO1 }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'MOVE DEFAULT TO GRAPH <http://test.com/newGraph>',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported');
const query = 'MOVE DEFAULT TO GRAPH :newGraph';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});
it('throws the error returned by the store if there is one.', async(): Promise<void> => {
source.getRepresentation = jest.fn(async(): Promise<any> => {
throw new Error('error');
});
const input = { identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> }',
{ quads: true },
) } as SparqlUpdatePatch };
await expect(handler.handle(input)).rejects.toThrow('error');
await expect(handle(fullfilledDataInsert)).rejects.toThrow('error');
});
it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
// There is no initial data
startQuads = [];
source.getRepresentation = jest.fn((): any => {
throw new NotFoundHttpError();
});
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
await handle(query);
expect(await basicChecks(startQuads.concat(
[ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ],
))).toBe(true);