feat: Add BodyParser for SPARQL updates

This commit is contained in:
Joachim Van Herwegen 2020-07-22 13:40:47 +02:00
parent 4ac9e92d06
commit 95c65c86a7
7 changed files with 146 additions and 9 deletions

41
package-lock.json generated
View File

@ -3080,8 +3080,7 @@
"fast-deep-equal": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
"dev": true
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
},
"fast-json-stable-stringify": {
"version": "2.1.0",
@ -4747,8 +4746,7 @@
"lodash.uniqwith": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz",
"integrity": "sha1-egy/ZfQ7WShiWp1NDcVLGMrcfvM=",
"dev": true
"integrity": "sha1-egy/ZfQ7WShiWp1NDcVLGMrcfvM="
},
"lodash.zip": {
"version": "4.2.0",
@ -4889,8 +4887,7 @@
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mixin-deep": {
"version": "1.3.2",
@ -5672,7 +5669,6 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/rdf-string/-/rdf-string-1.4.2.tgz",
"integrity": "sha512-74yYjS0W4N3nYDGbXBZrNsqDmhBTjqChTETO9heC2G2M3iMYaIPtEfUikNsBWUj4+4bIKyqL7vAntWBTfJpFFA==",
"dev": true,
"requires": {
"@rdfjs/data-model": "^1.1.1"
}
@ -5681,7 +5677,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/rdf-terms/-/rdf-terms-1.5.1.tgz",
"integrity": "sha512-dDhpUYxTAOWKT3Ln93A5k5UB5SftG8bPAzeZEjGeP4e7eboMhITUTDks8HDmUt9X1P+HpfnY/o6VSTSpf3Advw==",
"dev": true,
"requires": {
"@rdfjs/data-model": "^1.1.1",
"lodash.uniqwith": "^4.5.0"
@ -6551,6 +6546,36 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true
},
"sparqlalgebrajs": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.3.1.tgz",
"integrity": "sha512-zJ9EEX2BtHjdiSQoWP7fKt2IqD4zi24n3CQtRGOIzh8Hyj2JFMtje68CweaIJgQLN5kNYpNx/VT2F6nxpeIRJg==",
"requires": {
"@rdfjs/data-model": "^1.1.2",
"fast-deep-equal": "^3.1.1",
"minimist": "^1.2.5",
"rdf-string": "^1.3.1",
"sparqljs": "^3.0.1"
}
},
"sparqljs": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.0.2.tgz",
"integrity": "sha512-qmXKWx0ENeLUlvo0tgzcVfnvZnOBkyySvysIf6Y3HgWFKmu1bSrDB9K8siQYJO7jnSihoebtAPv89jvA6by/iw==",
"requires": {
"n3": "^1.5.0"
},
"dependencies": {
"n3": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/n3/-/n3-1.5.0.tgz",
"integrity": "sha512-k4R/EOMnnRYFt+hXgqyKOHjzmshaLuHUFgrz5nsp9nAojCZuAHrro/DsIM2tS0Bgx6ed7DM5Ks3q2teJ8n7HnQ==",
"requires": {
"queue-microtask": "^1.1.2"
}
}
}
},
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",

View File

@ -40,6 +40,8 @@
"cors": "^2.8.5",
"express": "^4.17.1",
"n3": "^1.4.0",
"rdf-terms": "^1.5.1",
"sparqlalgebrajs": "^2.3.1",
"yargs": "^15.4.1"
},
"devDependencies": {

View File

@ -3,4 +3,9 @@ import { Representation } from '../representation/Representation';
/**
* Represents the changes needed for a PATCH request.
*/
export interface Patch extends Representation {}
export interface Patch extends Representation {
/**
* The raw body of the PATCH request.
*/
raw: string;
}

View File

@ -0,0 +1,47 @@
import { BodyParser } from './BodyParser';
import { HttpRequest } from '../../server/HttpRequest';
import { Readable } from 'stream';
import { readableToString } from '../../util/Util';
import { SparqlUpdatePatch } from './SparqlUpdatePatch';
import { translate } from 'sparqlalgebrajs';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
/**
* {@link BodyParser} that supports `application/sparql-update` content.
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
* Simple since metadata parsing is not yet implemented.
*/
export class SimpleSparqlUpdateBodyParser extends BodyParser {
public async canHandle(input: HttpRequest): Promise<void> {
const contentType = input.headers['content-type'];
if (!contentType || contentType !== 'application/sparql-update') {
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
}
}
public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> {
try {
const sparql = await readableToString(input);
const algebra = translate(sparql, { quads: true });
// Prevent body from being requested again
return {
algebra,
dataType: 'sparql-algebra',
raw: sparql,
get data(): Readable {
throw new Error('Body already parsed');
},
metadata: {
raw: [],
profiles: [],
contentType: 'application/sparql-update',
},
};
} catch (error) {
throw new UnsupportedHttpError(error);
}
}
}

View File

@ -0,0 +1,12 @@
import { Patch } from './Patch';
import { Update } from 'sparqlalgebrajs/lib/algebra';
/**
* A specific type of {@link Patch} corresponding to a SPARQL update.
*/
export interface SparqlUpdatePatch extends Patch {
/**
* Algebra corresponding to the SPARQL update.
*/
algebra: Update;
}

View File

@ -1,3 +1,6 @@
import arrayifyStream from 'arrayify-stream';
import { Readable } from 'stream';
/**
* Makes sure the input path has exactly 1 slash at the end.
* Multiple slashes will get merged into one.
@ -8,3 +11,5 @@
* @returns The potentially changed path.
*/
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');

View File

@ -0,0 +1,41 @@
import { Algebra } from 'sparqlalgebrajs';
import { HttpRequest } from '../../../../src/server/HttpRequest';
import { SimpleSparqlUpdateBodyParser } from '../../../../src/ldp/http/SimpleSparqlUpdateBodyParser';
import streamifyArray from 'streamify-array';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { namedNode, quad } from '@rdfjs/data-model';
describe('A SimpleSparqlUpdateBodyParser', (): void => {
const bodyParser = new SimpleSparqlUpdateBodyParser();
it('only accepts application/sparql-update content.', async(): Promise<void> => {
await expect(bodyParser.canHandle({ headers: {}} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError);
await expect(bodyParser.canHandle({ headers: { 'content-type': 'text/plain' }} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError);
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/sparql-update' }} as HttpRequest)).resolves.toBeUndefined();
});
it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
await expect(bodyParser.handle(streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest)).rejects.toThrow(UnsupportedHttpError);
});
it('converts SPARQL updates to algebra.', async(): Promise<void> => {
const result = await bodyParser.handle(streamifyArray(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ],
) as HttpRequest);
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
namedNode('http://test.com/s'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
) ]);
expect(result.dataType).toBe('sparql-algebra');
expect(result.raw).toBe('DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}');
expect(result.metadata).toEqual({
raw: [],
profiles: [],
contentType: 'application/sparql-update',
});
expect((): any => result.data).toThrow('Body already parsed');
});
});