diff --git a/bin/server.ts b/bin/server.ts index 7928f876f..cce5161ad 100644 --- a/bin/server.ts +++ b/bin/server.ts @@ -2,21 +2,29 @@ import yargs from 'yargs'; import { AcceptPreferenceParser, AuthenticatedLdpHandler, + BodyParser, CompositeAsyncHandler, ExpressHttpServer, + HttpRequest, Operation, + PatchingStore, + Representation, ResponseDescription, SimpleAuthorizer, SimpleBodyParser, SimpleCredentialsExtractor, SimpleDeleteOperationHandler, SimpleGetOperationHandler, + SimplePatchOperationHandler, SimplePermissionsExtractor, SimplePostOperationHandler, SimpleRequestParser, SimpleResourceStore, SimpleResponseWriter, + SimpleSparqlUpdateBodyParser, + SimpleSparqlUpdatePatchHandler, SimpleTargetExtractor, + SingleThreadedResourceLocker, } from '..'; const { argv } = yargs @@ -29,10 +37,14 @@ const { argv } = yargs const { port } = argv; // This is instead of the dependency injection that still needs to be added +const bodyParser: BodyParser = new CompositeAsyncHandler([ + new SimpleBodyParser(), + new SimpleSparqlUpdateBodyParser(), +]); const requestParser = new SimpleRequestParser({ targetExtractor: new SimpleTargetExtractor(), preferenceParser: new AcceptPreferenceParser(), - bodyParser: new SimpleBodyParser(), + bodyParser, }); const credentialsExtractor = new SimpleCredentialsExtractor(); @@ -41,10 +53,15 @@ const authorizer = new SimpleAuthorizer(); // Will have to see how to best handle this 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([ - new SimpleGetOperationHandler(store), - new SimplePostOperationHandler(store), - new SimpleDeleteOperationHandler(store), + new SimpleDeleteOperationHandler(patchingStore), + new SimpleGetOperationHandler(patchingStore), + new SimplePatchOperationHandler(patchingStore), + new SimplePostOperationHandler(patchingStore), ]); const responseWriter = new SimpleResponseWriter(); diff --git a/index.ts b/index.ts index 871778ceb..04cea1ac1 100644 --- a/index.ts +++ b/index.ts @@ -17,7 +17,9 @@ export * from './src/ldp/http/ResponseWriter'; export * from './src/ldp/http/SimpleBodyParser'; export * from './src/ldp/http/SimpleRequestParser'; export * from './src/ldp/http/SimpleResponseWriter'; +export * from './src/ldp/http/SimpleSparqlUpdateBodyParser'; export * from './src/ldp/http/SimpleTargetExtractor'; +export * from './src/ldp/http/SparqlUpdatePatch'; export * from './src/ldp/http/TargetExtractor'; // LDP/Operations @@ -26,6 +28,7 @@ export * from './src/ldp/operations/OperationHandler'; export * from './src/ldp/operations/ResponseDescription'; export * from './src/ldp/operations/SimpleDeleteOperationHandler'; export * from './src/ldp/operations/SimpleGetOperationHandler'; +export * from './src/ldp/operations/SimplePatchOperationHandler'; export * from './src/ldp/operations/SimplePostOperationHandler'; // LDP/Permissions @@ -52,11 +55,16 @@ export * from './src/server/HttpHandler'; export * from './src/server/HttpRequest'; export * from './src/server/HttpResponse'; +// Storage/Patch +export * from './src/storage/patch/PatchHandler'; +export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler'; + // Storage export * from './src/storage/AtomicResourceStore'; export * from './src/storage/Conditions'; export * from './src/storage/Lock'; export * from './src/storage/LockingResourceStore'; +export * from './src/storage/PatchingStore'; export * from './src/storage/RepresentationConverter'; export * from './src/storage/ResourceLocker'; export * from './src/storage/ResourceMapper'; diff --git a/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts index dc99ad817..b2111daaa 100644 --- a/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts @@ -27,7 +27,7 @@ export class SimpleSparqlUpdatePatchHandler extends PatchHandler { } public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise { - 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.'); } } diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts index 233bcf5d6..4ddb7037c 100644 --- a/test/integration/AuthenticatedLdpHandler.test.ts +++ b/test/integration/AuthenticatedLdpHandler.test.ts @@ -1,29 +1,59 @@ import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; +import { BodyParser } from '../../src/ldp/http/BodyParser'; import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; import { EventEmitter } from 'events'; +import { HttpHandler } from '../../src/server/HttpHandler'; import { HttpRequest } from '../../src/server/HttpRequest'; +import { IncomingHttpHeaders } from 'http'; 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 { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer'; import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor'; import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler'; import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler'; +import { SimplePatchOperationHandler } from '../../src/ldp/operations/SimplePatchOperationHandler'; import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor'; import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler'; import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser'; import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore'; 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 { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker'; import streamifyArray from 'streamify-array'; import { createResponse, MockResponse } from 'node-mocks-http'; +import { namedNode, quad } from '@rdfjs/data-model'; import * as url from 'url'; -describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => { - let handler: AuthenticatedLdpHandler; +const call = async(handler: HttpHandler, requestUrl: url.URL, method: string, headers: IncomingHttpHeaders, data: string[]): Promise> => { + const request = streamifyArray(data) as HttpRequest; + request.url = requestUrl.pathname; + request.method = method; + request.headers = headers; + request.headers.host = requestUrl.host; + const response: MockResponse = createResponse({ eventEmitter: EventEmitter }); - beforeEach(async(): Promise => { + 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({ targetExtractor: new SimpleTargetExtractor(), preferenceParser: new AcceptPreferenceParser(), @@ -43,7 +73,7 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => { const responseWriter = new SimpleResponseWriter(); - handler = new AuthenticatedLdpHandler({ + const handler = new AuthenticatedLdpHandler({ requestParser, credentialsExtractor, permissionsExtractor, @@ -51,101 +81,125 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => { operationHandler, responseWriter, }); + + it('can add, read and delete data based on incoming requests.', async(): Promise => { + // POST + let requestUrl = new url.URL('http://test.com/'); + let response: MockResponse = await call( + handler, + requestUrl, + 'POST', + { 'content-type': 'text/turtle' }, + [ ' .' ], + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + const id = response._getHeaders().location; + expect(id).toContain(url.format(requestUrl)); + + // GET + requestUrl = new url.URL(id); + response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + expect(response.statusCode).toBe(200); + expect(response._getData()).toContain(' .'); + expect(response._getHeaders().location).toBe(id); + + // DELETE + response = await call(handler, requestUrl, 'DELETE', {}, []); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(url.format(requestUrl)); + + // GET + response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + expect(response.statusCode).toBe(404); + expect(response._getData()).toContain('NotFoundHttpError'); + }); }); - it('can add, read and delete data based on incoming requests.', async(): Promise => { - // POST - let requestUrl = new url.URL('http://test.com/'); - let request = streamifyArray([ ' .' ]) as HttpRequest; - request.url = requestUrl.pathname; - request.method = 'POST'; - request.headers = { - 'content-type': 'text/turtle', - host: requestUrl.host, - }; - let response: MockResponse = 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._getData()).toHaveLength(0); - id = response._getHeaders().location; - expect(id).toContain(url.format(requestUrl)); - resolve(); - }); + describe('with simple PATCH handlers', (): void => { + const bodyParser: BodyParser = new CompositeAsyncHandler([ + new SimpleBodyParser(), + new SimpleSparqlUpdateBodyParser(), + ]); + const requestParser = new SimpleRequestParser({ + targetExtractor: new SimpleTargetExtractor(), + preferenceParser: new AcceptPreferenceParser(), + bodyParser, }); - await handler.handleSafe({ request, response }); - await endPromise; + const credentialsExtractor = new SimpleCredentialsExtractor(); + const permissionsExtractor = new SimplePermissionsExtractor(); + const authorizer = new SimpleAuthorizer(); - // GET - requestUrl = new url.URL(id); - request = {} as HttpRequest; - request.url = requestUrl.pathname; - request.method = 'GET'; - request.headers = { - accept: 'text/turtle', - host: requestUrl.host, - }; - response = createResponse({ eventEmitter: EventEmitter }); + const store = new SimpleResourceStore('http://test.com/'); + const locker = new SingleThreadedResourceLocker(); + const patcher = new SimpleSparqlUpdatePatchHandler(store, locker); + const patchingStore = new PatchingStore(store, patcher); - endPromise = new Promise((resolve): void => { - response.on('end', (): void => { - expect(response._isEndCalled()).toBeTruthy(); - expect(response.statusCode).toBe(200); - expect(response._getData()).toContain(' .'); - expect(response._getHeaders().location).toBe(url.format(requestUrl)); - resolve(); - }); + const operationHandler = new CompositeAsyncHandler([ + 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, }); - await handler.handleSafe({ request, response }); - await endPromise; + it('can handle simple SPARQL updates.', async(): Promise => { + // POST + let requestUrl = new url.URL('http://test.com/'); + let response: MockResponse = await call( + handler, + requestUrl, + 'POST', + { 'content-type': 'text/turtle' }, + [ ' .', + ' .' ], + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + const id = response._getHeaders().location; + expect(id).toContain(url.format(requestUrl)); - // DELETE - request = {} as HttpRequest; - request.url = requestUrl.pathname; - request.method = 'DELETE'; - request.headers = { - host: requestUrl.host, - }; - response = createResponse({ eventEmitter: EventEmitter }); + // PATCH + requestUrl = new url.URL(id); + response = await call( + handler, + requestUrl, + 'PATCH', + { 'content-type': 'application/sparql-update' }, + [ 'DELETE { }', + 'INSERT { }', + 'WHERE {}' ], + ); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(id); - endPromise = new Promise((resolve): void => { - response.on('end', (): void => { - expect(response._isEndCalled()).toBeTruthy(); - expect(response.statusCode).toBe(200); - expect(response._getData()).toHaveLength(0); - expect(response._getHeaders().location).toBe(url.format(requestUrl)); - resolve(); - }); + // GET + requestUrl = new url.URL(id); + response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); + expect(response.statusCode).toBe(200); + expect(response._getData()).toContain(' .'); + 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')), + ], + ); }); - - await handler.handleSafe({ request, response }); - await endPromise; - - // GET - request = {} as HttpRequest; - 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._getData()).toContain('NotFoundHttpError'); - resolve(); - }); - }); - - await handler.handleSafe({ request, response }); - await endPromise; }); }); diff --git a/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts index debe4cc63..4a83bc6f3 100644 --- a/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts +++ b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts @@ -78,7 +78,7 @@ describe('A SimpleSparqlUpdatePatchHandler', (): void => { }; it('only accepts SPARQL updates.', async(): Promise => { - 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(); input.patch.dataType = 'notAlgebra'; await expect(handler.canHandle(input)).rejects.toThrow(UnsupportedHttpError);