feat: Integrate PATCH functionality

This commit is contained in:
Joachim Van Herwegen 2020-07-22 13:49:22 +02:00
parent 04a12c723e
commit 0e486cf6a6
5 changed files with 174 additions and 95 deletions

View File

@ -2,21 +2,29 @@ import yargs from 'yargs';
import { import {
AcceptPreferenceParser, AcceptPreferenceParser,
AuthenticatedLdpHandler, AuthenticatedLdpHandler,
BodyParser,
CompositeAsyncHandler, CompositeAsyncHandler,
ExpressHttpServer, ExpressHttpServer,
HttpRequest,
Operation, Operation,
PatchingStore,
Representation,
ResponseDescription, ResponseDescription,
SimpleAuthorizer, SimpleAuthorizer,
SimpleBodyParser, SimpleBodyParser,
SimpleCredentialsExtractor, SimpleCredentialsExtractor,
SimpleDeleteOperationHandler, SimpleDeleteOperationHandler,
SimpleGetOperationHandler, SimpleGetOperationHandler,
SimplePatchOperationHandler,
SimplePermissionsExtractor, SimplePermissionsExtractor,
SimplePostOperationHandler, SimplePostOperationHandler,
SimpleRequestParser, SimpleRequestParser,
SimpleResourceStore, SimpleResourceStore,
SimpleResponseWriter, SimpleResponseWriter,
SimpleSparqlUpdateBodyParser,
SimpleSparqlUpdatePatchHandler,
SimpleTargetExtractor, SimpleTargetExtractor,
SingleThreadedResourceLocker,
} from '..'; } from '..';
const { argv } = yargs const { argv } = yargs
@ -29,10 +37,14 @@ const { argv } = yargs
const { port } = argv; const { port } = argv;
// This is instead of the dependency injection that still needs to be added // This is instead of the dependency injection that still needs to be added
const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
new SimpleBodyParser(),
new SimpleSparqlUpdateBodyParser(),
]);
const requestParser = new SimpleRequestParser({ const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(), targetExtractor: new SimpleTargetExtractor(),
preferenceParser: new AcceptPreferenceParser(), preferenceParser: new AcceptPreferenceParser(),
bodyParser: new SimpleBodyParser(), bodyParser,
}); });
const credentialsExtractor = new SimpleCredentialsExtractor(); const credentialsExtractor = new SimpleCredentialsExtractor();
@ -41,10 +53,15 @@ const authorizer = new SimpleAuthorizer();
// Will have to see how to best handle this // Will have to see how to best handle this
const store = new SimpleResourceStore(`http://localhost:${port}/`); const store = new SimpleResourceStore(`http://localhost:${port}/`);
const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([ const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
new SimpleGetOperationHandler(store), new SimpleDeleteOperationHandler(patchingStore),
new SimplePostOperationHandler(store), new SimpleGetOperationHandler(patchingStore),
new SimpleDeleteOperationHandler(store), new SimplePatchOperationHandler(patchingStore),
new SimplePostOperationHandler(patchingStore),
]); ]);
const responseWriter = new SimpleResponseWriter(); const responseWriter = new SimpleResponseWriter();

View File

@ -17,7 +17,9 @@ export * from './src/ldp/http/ResponseWriter';
export * from './src/ldp/http/SimpleBodyParser'; export * from './src/ldp/http/SimpleBodyParser';
export * from './src/ldp/http/SimpleRequestParser'; export * from './src/ldp/http/SimpleRequestParser';
export * from './src/ldp/http/SimpleResponseWriter'; export * from './src/ldp/http/SimpleResponseWriter';
export * from './src/ldp/http/SimpleSparqlUpdateBodyParser';
export * from './src/ldp/http/SimpleTargetExtractor'; export * from './src/ldp/http/SimpleTargetExtractor';
export * from './src/ldp/http/SparqlUpdatePatch';
export * from './src/ldp/http/TargetExtractor'; export * from './src/ldp/http/TargetExtractor';
// LDP/Operations // LDP/Operations
@ -26,6 +28,7 @@ export * from './src/ldp/operations/OperationHandler';
export * from './src/ldp/operations/ResponseDescription'; export * from './src/ldp/operations/ResponseDescription';
export * from './src/ldp/operations/SimpleDeleteOperationHandler'; export * from './src/ldp/operations/SimpleDeleteOperationHandler';
export * from './src/ldp/operations/SimpleGetOperationHandler'; export * from './src/ldp/operations/SimpleGetOperationHandler';
export * from './src/ldp/operations/SimplePatchOperationHandler';
export * from './src/ldp/operations/SimplePostOperationHandler'; export * from './src/ldp/operations/SimplePostOperationHandler';
// LDP/Permissions // LDP/Permissions
@ -52,11 +55,16 @@ export * from './src/server/HttpHandler';
export * from './src/server/HttpRequest'; export * from './src/server/HttpRequest';
export * from './src/server/HttpResponse'; export * from './src/server/HttpResponse';
// Storage/Patch
export * from './src/storage/patch/PatchHandler';
export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler';
// Storage // Storage
export * from './src/storage/AtomicResourceStore'; export * from './src/storage/AtomicResourceStore';
export * from './src/storage/Conditions'; export * from './src/storage/Conditions';
export * from './src/storage/Lock'; export * from './src/storage/Lock';
export * from './src/storage/LockingResourceStore'; export * from './src/storage/LockingResourceStore';
export * from './src/storage/PatchingStore';
export * from './src/storage/RepresentationConverter'; export * from './src/storage/RepresentationConverter';
export * from './src/storage/ResourceLocker'; export * from './src/storage/ResourceLocker';
export * from './src/storage/ResourceMapper'; export * from './src/storage/ResourceMapper';

View File

@ -27,7 +27,7 @@ export class SimpleSparqlUpdatePatchHandler extends PatchHandler {
} }
public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> { public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
if (input.patch.dataType !== 'algebra' || !input.patch.algebra) { if (input.patch.dataType !== 'sparql-algebra' || !input.patch.algebra) {
throw new UnsupportedHttpError('Only SPARQL update patch requests are supported.'); throw new UnsupportedHttpError('Only SPARQL update patch requests are supported.');
} }
} }

View File

@ -1,29 +1,59 @@
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
import { BodyParser } from '../../src/ldp/http/BodyParser';
import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { HttpHandler } from '../../src/server/HttpHandler';
import { HttpRequest } from '../../src/server/HttpRequest'; import { HttpRequest } from '../../src/server/HttpRequest';
import { IncomingHttpHeaders } from 'http';
import { Operation } from '../../src/ldp/operations/Operation'; import { Operation } from '../../src/ldp/operations/Operation';
import { Parser } from 'n3';
import { PatchingStore } from '../../src/storage/PatchingStore';
import { Representation } from '../../src/ldp/representation/Representation';
import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer'; import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer';
import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor'; import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler'; import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler';
import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler'; import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler';
import { SimplePatchOperationHandler } from '../../src/ldp/operations/SimplePatchOperationHandler';
import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor'; import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor';
import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler'; import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler';
import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser'; import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore'; import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore';
import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter'; import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter';
import { SimpleSparqlUpdateBodyParser } from '../../src/ldp/http/SimpleSparqlUpdateBodyParser';
import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSparqlUpdatePatchHandler';
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor'; import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { createResponse, MockResponse } from 'node-mocks-http'; import { createResponse, MockResponse } from 'node-mocks-http';
import { namedNode, quad } from '@rdfjs/data-model';
import * as url from 'url'; import * as url from 'url';
describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => { const call = async(handler: HttpHandler, requestUrl: url.URL, method: string, headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
let handler: AuthenticatedLdpHandler; const request = streamifyArray(data) as HttpRequest;
request.url = requestUrl.pathname;
request.method = method;
request.headers = headers;
request.headers.host = requestUrl.host;
const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
beforeEach(async(): Promise<void> => { const endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
return response;
};
describe('An AuthenticatedLdpHandler', (): void => {
describe('with simple handlers', (): void => {
const requestParser = new SimpleRequestParser({ const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(), targetExtractor: new SimpleTargetExtractor(),
preferenceParser: new AcceptPreferenceParser(), preferenceParser: new AcceptPreferenceParser(),
@ -43,7 +73,7 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
const responseWriter = new SimpleResponseWriter(); const responseWriter = new SimpleResponseWriter();
handler = new AuthenticatedLdpHandler({ const handler = new AuthenticatedLdpHandler({
requestParser, requestParser,
credentialsExtractor, credentialsExtractor,
permissionsExtractor, permissionsExtractor,
@ -51,101 +81,125 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
operationHandler, operationHandler,
responseWriter, responseWriter,
}); });
});
it('can add, read and delete data based on incoming requests.', async(): Promise<void> => { it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
// POST // POST
let requestUrl = new url.URL('http://test.com/'); let requestUrl = new url.URL('http://test.com/');
let request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest; let response: MockResponse<any> = await call(
request.url = requestUrl.pathname; handler,
request.method = 'POST'; requestUrl,
request.headers = { 'POST',
'content-type': 'text/turtle', { 'content-type': 'text/turtle' },
host: requestUrl.host, [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
}; );
let response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
let id;
let endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response._getData()).toHaveLength(0); expect(response._getData()).toHaveLength(0);
id = response._getHeaders().location; const id = response._getHeaders().location;
expect(id).toContain(url.format(requestUrl)); expect(id).toContain(url.format(requestUrl));
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
// GET // GET
requestUrl = new url.URL(id); requestUrl = new url.URL(id);
request = {} as HttpRequest; response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
request.url = requestUrl.pathname;
request.method = 'GET';
request.headers = {
accept: 'text/turtle',
host: requestUrl.host,
};
response = createResponse({ eventEmitter: EventEmitter });
endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.'); expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
expect(response._getHeaders().location).toBe(url.format(requestUrl)); expect(response._getHeaders().location).toBe(id);
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
// DELETE // DELETE
request = {} as HttpRequest; response = await call(handler, requestUrl, 'DELETE', {}, []);
request.url = requestUrl.pathname;
request.method = 'DELETE';
request.headers = {
host: requestUrl.host,
};
response = createResponse({ eventEmitter: EventEmitter });
endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response._getData()).toHaveLength(0); expect(response._getData()).toHaveLength(0);
expect(response._getHeaders().location).toBe(url.format(requestUrl)); expect(response._getHeaders().location).toBe(url.format(requestUrl));
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
// GET // GET
request = {} as HttpRequest; response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
request.url = requestUrl.pathname;
request.method = 'GET';
request.headers = {
accept: 'text/turtle',
host: requestUrl.host,
};
response = createResponse({ eventEmitter: EventEmitter });
endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
expect(response._getData()).toContain('NotFoundHttpError'); expect(response._getData()).toContain('NotFoundHttpError');
resolve();
}); });
}); });
await handler.handleSafe({ request, response }); describe('with simple PATCH handlers', (): void => {
await endPromise; const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
new SimpleBodyParser(),
new SimpleSparqlUpdateBodyParser(),
]);
const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(),
preferenceParser: new AcceptPreferenceParser(),
bodyParser,
});
const credentialsExtractor = new SimpleCredentialsExtractor();
const permissionsExtractor = new SimplePermissionsExtractor();
const authorizer = new SimpleAuthorizer();
const store = new SimpleResourceStore('http://test.com/');
const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
new SimpleGetOperationHandler(patchingStore),
new SimplePostOperationHandler(patchingStore),
new SimpleDeleteOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore),
]);
const responseWriter = new SimpleResponseWriter();
const handler = new AuthenticatedLdpHandler({
requestParser,
credentialsExtractor,
permissionsExtractor,
authorizer,
operationHandler,
responseWriter,
});
it('can handle simple SPARQL updates.', async(): Promise<void> => {
// POST
let requestUrl = new url.URL('http://test.com/');
let response: MockResponse<any> = await call(
handler,
requestUrl,
'POST',
{ 'content-type': 'text/turtle' },
[ '<http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.',
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.' ],
);
expect(response.statusCode).toBe(200);
expect(response._getData()).toHaveLength(0);
const id = response._getHeaders().location;
expect(id).toContain(url.format(requestUrl));
// PATCH
requestUrl = new url.URL(id);
response = await call(
handler,
requestUrl,
'PATCH',
{ 'content-type': 'application/sparql-update' },
[ 'DELETE { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1> }',
'INSERT {<http://test.com/s3> <http://test.com/p3> <http://test.com/o3>}',
'WHERE {}' ],
);
expect(response.statusCode).toBe(200);
expect(response._getData()).toHaveLength(0);
expect(response._getHeaders().location).toBe(id);
// GET
requestUrl = new url.URL(id);
response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
expect(response.statusCode).toBe(200);
expect(response._getData()).toContain('<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.');
expect(response._getHeaders().location).toBe(id);
const parser = new Parser();
const triples = parser.parse(response._getData());
expect(triples).toBeRdfIsomorphic(
[
quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')),
quad(namedNode('http://test.com/s3'), namedNode('http://test.com/p3'), namedNode('http://test.com/o3')),
],
);
});
}); });
}); });

View File

@ -78,7 +78,7 @@ describe('A SimpleSparqlUpdatePatchHandler', (): void => {
}; };
it('only accepts SPARQL updates.', async(): Promise<void> => { it('only accepts SPARQL updates.', async(): Promise<void> => {
const input = { identifier: { path: 'path' }, patch: { dataType: 'algebra', algebra: {}} as SparqlUpdatePatch }; const input = { identifier: { path: 'path' }, patch: { dataType: 'sparql-algebra', algebra: {}} as SparqlUpdatePatch };
await expect(handler.canHandle(input)).resolves.toBeUndefined(); await expect(handler.canHandle(input)).resolves.toBeUndefined();
input.patch.dataType = 'notAlgebra'; input.patch.dataType = 'notAlgebra';
await expect(handler.canHandle(input)).rejects.toThrow(UnsupportedHttpError); await expect(handler.canHandle(input)).rejects.toThrow(UnsupportedHttpError);