diff --git a/package-lock.json b/package-lock.json index 8f787eadf..4531b285e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index 6e7385686..2b5a12321 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index c0671278e..455c09372 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -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'); } diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts index 96f5381c2..67602d7e3 100644 --- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts +++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts @@ -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 => { startQuads = [ quad( @@ -56,6 +57,12 @@ describe('A SparqlUpdatePatchHandler', (): void => { return true; } + async function handle(query: string): Promise { + const sparqlPrefix = 'prefix : \n'; + return handler.handle({ identifier: { path: 'path' }, + patch: { algebra: translate(sparqlPrefix.concat(query), { quads: true }) } as SparqlUpdatePatch }); + } + it('only accepts SPARQL updates.', async(): Promise => { const input = { identifier: { path: 'path' }, patch: { algebra: {}} as SparqlUpdatePatch }; @@ -65,12 +72,7 @@ describe('A SparqlUpdatePatchHandler', (): void => { }); it('handles INSERT DATA updates.', async(): Promise => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'INSERT DATA { . ' + - ' }', - { 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 => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE DATA { }', - { 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 => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE WHERE { }', - { 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 => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE { }\n' + - 'INSERT { . }\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 => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'INSERT DATA { . ' + - ' };' + - 'DELETE WHERE { .' + - ' }', - { 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 => { - await handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE DATA { .' + - ' };' + - 'INSERT DATA { . ' + - ' }', - { 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 => { - const handle = handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'INSERT DATA { GRAPH { ' + - ' } }', - { 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 => { - const handle = handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE DATA { GRAPH { ' + - ' } }', - { 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 => { - const handle = handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE { }\n' + - 'INSERT { . }\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 => { + 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 => { - const handle = handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'DELETE WHERE { ?v }', - { 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 => { - const handle = handler.handle({ identifier: { path: 'path' }, - patch: { algebra: translate( - 'MOVE DEFAULT TO GRAPH ', - { 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 => { source.getRepresentation = jest.fn(async(): Promise => { throw new Error('error'); }); - - const input = { identifier: { path: 'path' }, - patch: { algebra: translate( - 'INSERT DATA { . ' + - ' }', - { 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 => { - // 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 { . }', - { quads: true }, - ) } as SparqlUpdatePatch }); + const query = 'INSERT DATA { . }'; + 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);