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

View File

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

View File

@ -51,6 +51,14 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
return op.type === Algebra.types.COMPOSITE_UPDATE; 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) * 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. * 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 { private validateDeleteInsert(op: Algebra.DeleteInsert): void {
const def = defaultGraph(); const def = defaultGraph();
@ -81,8 +89,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
this.logger.warn('GRAPH statement in INSERT clause'); this.logger.warn('GRAPH statement in INSERT clause');
throw new NotImplementedHttpError('GRAPH statements are not supported'); throw new NotImplementedHttpError('GRAPH statements are not supported');
} }
if (op.where ?? deletes.some((pattern): boolean => if (!(typeof op.where === 'undefined' || this.isBasicGraphPatternWithoutVariables(op.where))) {
someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
this.logger.warn('WHERE statements are not supported'); this.logger.warn('WHERE statements are not supported');
throw new NotImplementedHttpError('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 handler: SparqlUpdatePatchHandler;
let source: ResourceStore; let source: ResourceStore;
let startQuads: Quad[]; let startQuads: Quad[];
const fullfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
startQuads = [ quad( startQuads = [ quad(
@ -56,6 +57,12 @@ describe('A SparqlUpdatePatchHandler', (): void => {
return true; 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> => { it('only accepts SPARQL updates.', async(): Promise<void> => {
const input = { identifier: { path: 'path' }, const input = { identifier: { path: 'path' },
patch: { algebra: {}} as SparqlUpdatePatch }; patch: { algebra: {}} as SparqlUpdatePatch };
@ -65,12 +72,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('handles INSERT DATA updates.', async(): Promise<void> => { it('handles INSERT DATA updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, await handle(fullfilledDataInsert);
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 });
expect(await basicChecks(startQuads.concat( 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/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')) ], 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> => { it('handles DELETE DATA updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, await handle('DELETE DATA { :startS1 :startP1 :startO1 }');
patch: { algebra: translate(
'DELETE DATA { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
expect(await basicChecks( expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'), [ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'), namedNode('http://test.com/startP2'),
@ -91,11 +89,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('handles DELETE WHERE updates with no variables.', async(): Promise<void> => { it('handles DELETE WHERE updates with no variables.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, const query = 'DELETE WHERE { :startS1 :startP1 :startO1 }';
patch: { algebra: translate( await handle(query);
'DELETE WHERE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
expect(await basicChecks( expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'), [ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'), namedNode('http://test.com/startP2'),
@ -104,13 +99,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise<void> => { it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, const query = 'DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}';
patch: { algebra: translate( await handle(query);
'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 });
expect(await basicChecks([ expect(await basicChecks([
quad(namedNode('http://test.com/startS2'), quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'), namedNode('http://test.com/startP2'),
@ -122,14 +112,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('handles composite INSERT/DELETE updates.', async(): Promise<void> => { it('handles composite INSERT/DELETE updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
patch: { algebra: translate( 'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' + await handle(query);
'<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 });
expect(await basicChecks([ expect(await basicChecks([
quad(namedNode('http://test.com/startS2'), quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'), namedNode('http://test.com/startP2'),
@ -141,14 +126,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('handles composite DELETE/INSERT updates.', async(): Promise<void> => { it('handles composite DELETE/INSERT updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' }, const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
patch: { algebra: translate( 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
'DELETE DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.' + await handle(query);
'<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 });
expect(await basicChecks([ expect(await basicChecks([
quad(namedNode('http://test.com/startS2'), quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'), namedNode('http://test.com/startP2'),
@ -163,80 +143,49 @@ describe('A SparqlUpdatePatchHandler', (): void => {
}); });
it('rejects GRAPH inserts.', async(): Promise<void> => { it('rejects GRAPH inserts.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' }, const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
patch: { algebra: translate( await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
'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');
}); });
it('rejects GRAPH deletes.', async(): Promise<void> => { it('rejects GRAPH deletes.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' }, const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
patch: { algebra: translate( await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
'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');
}); });
it('rejects DELETE/INSERT updates with a non-empty WHERE.', async(): Promise<void> => { it('rejects DELETE/INSERT updates with a non-empty WHERE.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' }, const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o }';
patch: { algebra: translate( await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
'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 }', it('rejects INSERT WHERE updates with a UNION.', async(): Promise<void> => {
{ quads: true }, const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
) } as SparqlUpdatePatch }); await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
await expect(handle).rejects.toThrow('WHERE statements are not supported');
}); });
it('rejects DELETE WHERE updates with variables.', async(): Promise<void> => { it('rejects DELETE WHERE updates with variables.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' }, const query = 'DELETE WHERE { ?v :startP1 :startO1 }';
patch: { algebra: translate( await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
'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');
}); });
it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => { it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => {
const handle = handler.handle({ identifier: { path: 'path' }, const query = 'MOVE DEFAULT TO GRAPH :newGraph';
patch: { algebra: translate( await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
'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');
}); });
it('throws the error returned by the store if there is one.', async(): Promise<void> => { it('throws the error returned by the store if there is one.', async(): Promise<void> => {
source.getRepresentation = jest.fn(async(): Promise<any> => { source.getRepresentation = jest.fn(async(): Promise<any> => {
throw new Error('error'); throw new Error('error');
}); });
await expect(handle(fullfilledDataInsert)).rejects.toThrow('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');
}); });
it('creates a new resource if it does not exist yet.', async(): Promise<void> => { it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
// There is no initial data
startQuads = []; startQuads = [];
source.getRepresentation = jest.fn((): any => { source.getRepresentation = jest.fn((): any => {
throw new NotFoundHttpError(); throw new NotFoundHttpError();
}); });
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
await handler.handle({ identifier: { path: 'path' }, await handle(query);
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }',
{ quads: true },
) } as SparqlUpdatePatch });
expect(await basicChecks(startQuads.concat( 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/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ],
))).toBe(true); ))).toBe(true);