feat: Integrate data conversion with rest of server

This commit is contained in:
Joachim Van Herwegen 2020-08-04 10:24:59 +02:00
parent 5e1bb10f81
commit 4403421c49
8 changed files with 102 additions and 226 deletions

View File

@ -2,14 +2,13 @@ import yargs from 'yargs';
import { import {
AcceptPreferenceParser, AcceptPreferenceParser,
AuthenticatedLdpHandler, AuthenticatedLdpHandler,
BodyParser,
CompositeAsyncHandler, CompositeAsyncHandler,
ExpressHttpServer, ExpressHttpServer,
HttpRequest, HttpRequest,
Operation,
PatchingStore, PatchingStore,
QuadToTurtleConverter,
Representation, Representation,
ResponseDescription, RepresentationConvertingStore,
SimpleAuthorizer, SimpleAuthorizer,
SimpleBodyParser, SimpleBodyParser,
SimpleCredentialsExtractor, SimpleCredentialsExtractor,
@ -25,6 +24,7 @@ import {
SimpleSparqlUpdatePatchHandler, SimpleSparqlUpdatePatchHandler,
SimpleTargetExtractor, SimpleTargetExtractor,
SingleThreadedResourceLocker, SingleThreadedResourceLocker,
TurtleToQuadConverter,
} from '..'; } from '..';
const { argv } = yargs const { argv } = yargs
@ -37,9 +37,9 @@ 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 | undefined>([ const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleBodyParser(),
new SimpleSparqlUpdateBodyParser(), new SimpleSparqlUpdateBodyParser(),
new SimpleBodyParser(),
]); ]);
const requestParser = new SimpleRequestParser({ const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(), targetExtractor: new SimpleTargetExtractor(),
@ -53,11 +53,16 @@ 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 converter = new CompositeAsyncHandler([
new TurtleToQuadConverter(),
new QuadToTurtleConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const locker = new SingleThreadedResourceLocker(); const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker); const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(store, patcher); const patchingStore = new PatchingStore(convertingStore, patcher);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([ const operationHandler = new CompositeAsyncHandler([
new SimpleDeleteOperationHandler(patchingStore), new SimpleDeleteOperationHandler(patchingStore),
new SimpleGetOperationHandler(patchingStore), new SimpleGetOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore), new SimplePatchOperationHandler(patchingStore),

View File

@ -1,37 +1,22 @@
import { BinaryRepresentation } from '../representation/BinaryRepresentation';
import { BodyParser } from './BodyParser'; import { BodyParser } from './BodyParser';
import { DATA_TYPE_QUAD } from '../../util/ContentTypes'; import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest'; import { HttpRequest } from '../../server/HttpRequest';
import { PassThrough } from 'stream';
import { QuadRepresentation } from '../representation/QuadRepresentation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata'; import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { StreamParser } from 'n3';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
/** /**
* Parses the incoming {@link HttpRequest} if there is no body or if it contains turtle (or similar) RDF data. * Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the content-type header to determine the body type. * Naively parses the mediatype from the content-type header.
* Metadata is not generated (yet).
*/ */
export class SimpleBodyParser extends BodyParser { export class SimpleBodyParser extends BodyParser {
private static readonly contentTypes = [ public async canHandle(): Promise<void> {
'application/n-quads', // Default BodyParser supports all content-types
'application/trig',
'application/n-triples',
'text/turtle',
'text/n3',
];
public async canHandle(input: HttpRequest): Promise<void> {
const contentType = input.headers['content-type'];
if (contentType && !SimpleBodyParser.contentTypes.some((type): boolean => contentType.includes(type))) {
throw new UnsupportedMediaTypeHttpError('This parser only supports RDF data.');
}
} }
// Note that the only reason this is a union is in case the body is empty. // Note that the only reason this is a union is in case the body is empty.
// If this check gets moved away from the BodyParsers this union could be removed // If this check gets moved away from the BodyParsers this union could be removed
public async handle(input: HttpRequest): Promise<QuadRepresentation | undefined> { public async handle(input: HttpRequest): Promise<BinaryRepresentation | undefined> {
const contentType = input.headers['content-type']; const contentType = input.headers['content-type'];
if (!contentType) { if (!contentType) {
@ -46,16 +31,9 @@ export class SimpleBodyParser extends BodyParser {
contentType: mediaType, contentType: mediaType,
}; };
// Catch parsing errors and emit correct error
// Node 10 requires both writableObjectMode and readableObjectMode
const errorStream = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
const data = input.pipe(new StreamParser());
data.pipe(errorStream);
data.on('error', (error): boolean => errorStream.emit('error', new UnsupportedHttpError(error.message)));
return { return {
dataType: DATA_TYPE_QUAD, dataType: DATA_TYPE_BINARY,
data: errorStream, data: input,
metadata, metadata,
}; };
} }

View File

@ -1,24 +1,18 @@
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { BinaryRepresentation } from '../ldp/representation/BinaryRepresentation'; import { DATA_TYPE_BINARY } from '../util/ContentTypes';
import { ensureTrailingSlash } from '../util/Util'; import { ensureTrailingSlash } from '../util/Util';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { Quad } from 'rdf-js';
import { QuadRepresentation } from '../ldp/representation/QuadRepresentation';
import { Representation } from '../ldp/representation/Representation'; import { Representation } from '../ldp/representation/Representation';
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore'; import { ResourceStore } from './ResourceStore';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { StreamWriter } from 'n3';
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../util/ContentTypes';
/** /**
* Resource store storing its data as Quads in an in-memory map. * Resource store storing its data in an in-memory map.
* All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed. * All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed.
*/ */
export class SimpleResourceStore implements ResourceStore { export class SimpleResourceStore implements ResourceStore {
private readonly store: { [id: string]: Quad[] } = { '': []}; private readonly store: { [id: string]: Representation };
private readonly base: string; private readonly base: string;
private index = 0; private index = 0;
@ -27,13 +21,22 @@ export class SimpleResourceStore implements ResourceStore {
*/ */
public constructor(base: string) { public constructor(base: string) {
this.base = base; this.base = base;
this.store = {
// Default root entry (what you get when the identifier is equal to the base)
'': {
dataType: DATA_TYPE_BINARY,
data: streamifyArray([]),
metadata: { raw: [], profiles: []},
},
};
} }
/** /**
* Stores the incoming data under a new URL corresponding to `container.path + number`. * Stores the incoming data under a new URL corresponding to `container.path + number`.
* Slash added when needed. * Slash added when needed.
* @param container - The identifier to store the new data under. * @param container - The identifier to store the new data under.
* @param representation - Data to store. Only Quad streams are supported. * @param representation - Data to store.
* *
* @returns The newly generated identifier. * @returns The newly generated identifier.
*/ */
@ -41,7 +44,7 @@ export class SimpleResourceStore implements ResourceStore {
const containerPath = this.parseIdentifier(container); const containerPath = this.parseIdentifier(container);
const newPath = `${ensureTrailingSlash(containerPath)}${this.index}`; const newPath = `${ensureTrailingSlash(containerPath)}${this.index}`;
this.index += 1; this.index += 1;
this.store[newPath] = await this.parseRepresentation(representation); this.store[newPath] = await this.copyRepresentation(representation);
return { path: `${this.base}${newPath}` }; return { path: `${this.base}${newPath}` };
} }
@ -57,18 +60,15 @@ export class SimpleResourceStore implements ResourceStore {
/** /**
* Returns the stored representation for the given identifier. * Returns the stored representation for the given identifier.
* The only preference that is supported is `type === 'text/turtle'`. * Preferences will be ignored, data will be returned as it was received.
* In all other cases a stream of Quads will be returned.
* *
* @param identifier - Identifier to retrieve. * @param identifier - Identifier to retrieve.
* @param preferences - Preferences for resulting Representation.
* *
* @returns The corresponding Representation. * @returns The corresponding Representation.
*/ */
public async getRepresentation(identifier: ResourceIdentifier, public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
preferences: RepresentationPreferences): Promise<Representation> {
const path = this.parseIdentifier(identifier); const path = this.parseIdentifier(identifier);
return this.generateRepresentation(this.store[path], preferences); return this.generateRepresentation(path);
} }
/** /**
@ -85,7 +85,7 @@ export class SimpleResourceStore implements ResourceStore {
*/ */
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> { public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
const path = this.parseIdentifier(identifier); const path = this.parseIdentifier(identifier);
this.store[path] = await this.parseRepresentation(representation); this.store[path] = await this.copyRepresentation(representation);
} }
/** /**
@ -106,66 +106,36 @@ export class SimpleResourceStore implements ResourceStore {
} }
/** /**
* Converts the Representation to an array of Quads. * Copies the Representation by draining the original data stream and creating a new one.
* @param representation - Incoming Representation.
* @throws {@link UnsupportedMediaTypeHttpError}
* If the representation is not a Quad stream.
* *
* @returns Promise of array of Quads pulled from the stream. * @param data - Incoming Representation.
*/ */
private async parseRepresentation(representation: Representation): Promise<Quad[]> { private async copyRepresentation(source: Representation): Promise<Representation> {
if (representation.dataType !== DATA_TYPE_QUAD) { const arr = await arrayifyStream(source.data);
throw new UnsupportedMediaTypeHttpError('SimpleResourceStore only supports quad representations.'); return {
} dataType: source.dataType,
return arrayifyStream(representation.data); data: streamifyArray([ ...arr ]),
metadata: source.metadata,
};
} }
/** /**
* Converts an array of Quads to a Representation. * Generates a Representation that is identical to the one stored,
* If preferences.type contains 'text/turtle' the result will be a stream of turtle strings, * but makes sure to duplicate the data stream so it stays readable for later calls.
* otherwise a stream of Quads.
* *
* Note that in general this should be done by resource store specifically made for converting to turtle, * @param path - Path in store of Representation.
* this is just here to make this simple resource store work.
*
* @param data - Quads to transform.
* @param preferences - Requested preferences.
* *
* @returns The resulting Representation. * @returns The resulting Representation.
*/ */
private generateRepresentation(data: Quad[], preferences: RepresentationPreferences): Representation { private async generateRepresentation(path: string): Promise<Representation> {
// Always return turtle unless explicitly asked for quads const source = this.store[path];
if (preferences.type?.some((preference): boolean => preference.value.includes(CONTENT_TYPE_QUADS))) { const arr = await arrayifyStream(source.data);
return this.generateQuadRepresentation(data); source.data = streamifyArray([ ...arr ]);
}
return this.generateBinaryRepresentation(data);
}
/**
* Creates a {@link BinaryRepresentation} of the incoming Quads.
* @param data - Quads to transform to text/turtle.
*
* @returns The resulting binary Representation.
*/
private generateBinaryRepresentation(data: Quad[]): BinaryRepresentation {
return { return {
dataType: DATA_TYPE_BINARY, dataType: source.dataType,
data: streamifyArray([ ...data ]).pipe(new StreamWriter({ format: 'text/turtle' })), data: streamifyArray([ ...arr ]),
metadata: { raw: [], profiles: [], contentType: 'text/turtle' }, metadata: source.metadata,
};
}
/**
* Creates a {@link QuadRepresentation} of the incoming Quads.
* @param data - Quads to transform to a stream of Quads.
*
* @returns The resulting quad Representation.
*/
private generateQuadRepresentation(data: Quad[]): QuadRepresentation {
return {
dataType: DATA_TYPE_QUAD,
data: streamifyArray([ ...data ]),
metadata: { raw: [], profiles: []},
}; };
} }
} }

View File

@ -4,6 +4,8 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
/** /**
* Handler that combines several other handlers, * Handler that combines several other handlers,
* thereby allowing other classes that depend on a single handler to still use multiple. * thereby allowing other classes that depend on a single handler to still use multiple.
* The handlers will be checked in the order they appear in the input array,
* allowing for more fine-grained handlers to check before catch-all handlers.
*/ */
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> { export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
private readonly handlers: AsyncHandler<TIn, TOut>[]; private readonly handlers: AsyncHandler<TIn, TOut>[];

View File

@ -9,7 +9,9 @@ import { IncomingHttpHeaders } from 'http';
import { Operation } from '../../src/ldp/operations/Operation'; import { Operation } from '../../src/ldp/operations/Operation';
import { Parser } from 'n3'; import { Parser } from 'n3';
import { PatchingStore } from '../../src/storage/PatchingStore'; import { PatchingStore } from '../../src/storage/PatchingStore';
import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter';
import { Representation } from '../../src/ldp/representation/Representation'; import { Representation } from '../../src/ldp/representation/Representation';
import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
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';
@ -27,6 +29,7 @@ import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSp
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor'; import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker'; import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
import { createResponse, MockResponse } from 'node-mocks-http'; import { createResponse, MockResponse } from 'node-mocks-http';
import { namedNode, quad } from '@rdfjs/data-model'; import { namedNode, quad } from '@rdfjs/data-model';
import * as url from 'url'; import * as url from 'url';
@ -134,9 +137,14 @@ describe('An AuthenticatedLdpHandler', (): void => {
const authorizer = new SimpleAuthorizer(); const authorizer = new SimpleAuthorizer();
const store = new SimpleResourceStore('http://test.com/'); const store = new SimpleResourceStore('http://test.com/');
const converter = new CompositeAsyncHandler([
new QuadToTurtleConverter(),
new TurtleToQuadConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const locker = new SingleThreadedResourceLocker(); const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker); const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(store, patcher); const patchingStore = new PatchingStore(convertingStore, patcher);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([ const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
new SimpleGetOperationHandler(patchingStore), new SimpleGetOperationHandler(patchingStore),

View File

@ -1,13 +1,12 @@
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { DATA_TYPE_QUAD } from '../../src/util/ContentTypes'; import { DATA_TYPE_BINARY } from '../../src/util/ContentTypes';
import { HttpRequest } from '../../src/server/HttpRequest'; import { HttpRequest } from '../../src/server/HttpRequest';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser'; import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor'; import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { namedNode, triple } from '@rdfjs/data-model';
describe('A SimpleRequestParser with simple input parsers', (): void => { describe('A SimpleRequestParser with simple input parsers', (): void => {
const targetExtractor = new SimpleTargetExtractor(); const targetExtractor = new SimpleTargetExtractor();
@ -36,7 +35,7 @@ describe('A SimpleRequestParser with simple input parsers', (): void => {
}, },
body: { body: {
data: expect.any(Readable), data: expect.any(Readable),
dataType: DATA_TYPE_QUAD, dataType: DATA_TYPE_BINARY,
metadata: { metadata: {
contentType: 'text/turtle', contentType: 'text/turtle',
profiles: [], profiles: [],
@ -45,10 +44,8 @@ describe('A SimpleRequestParser with simple input parsers', (): void => {
}, },
}); });
await expect(arrayifyStream(result.body!.data)).resolves.toEqualRdfQuadArray([ triple( await expect(arrayifyStream(result.body!.data)).resolves.toEqual(
namedNode('http://test.com/s'), [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
namedNode('http://test.com/p'), );
namedNode('http://test.com/o'),
) ]);
}); });
}); });

View File

@ -1,68 +1,37 @@
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { DATA_TYPE_QUAD } from '../../../../src/util/ContentTypes'; import { DATA_TYPE_BINARY } from '../../../../src/util/ContentTypes';
import { HttpRequest } from '../../../../src/server/HttpRequest'; import { HttpRequest } from '../../../../src/server/HttpRequest';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { SimpleBodyParser } from '../../../../src/ldp/http/SimpleBodyParser'; import { SimpleBodyParser } from '../../../../src/ldp/http/SimpleBodyParser';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { namedNode, triple } from '@rdfjs/data-model';
import 'jest-rdf'; import 'jest-rdf';
const contentTypes = [
'application/n-quads',
'application/trig',
'application/n-triples',
'text/turtle',
'text/n3',
];
describe('A SimpleBodyparser', (): void => { describe('A SimpleBodyparser', (): void => {
const bodyParser = new SimpleBodyParser(); const bodyParser = new SimpleBodyParser();
it('rejects input with unsupported content type.', async(): Promise<void> => { it('accepts all input.', async(): Promise<void> => {
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/rdf+xml' }} as HttpRequest)) await expect(bodyParser.canHandle()).resolves.toBeUndefined();
.rejects.toThrow(new UnsupportedMediaTypeHttpError('This parser only supports RDF data.'));
});
it('accepts input with no content type.', async(): Promise<void> => {
await expect(bodyParser.canHandle({ headers: { }} as HttpRequest)).resolves.toBeUndefined();
});
it('accepts turtle and similar content types.', async(): Promise<void> => {
for (const type of contentTypes) {
await expect(bodyParser.canHandle({ headers: { 'content-type': type }} as HttpRequest)).resolves.toBeUndefined();
}
}); });
it('returns empty output if there was no content-type.', async(): Promise<void> => { it('returns empty output if there was no content-type.', async(): Promise<void> => {
await expect(bodyParser.handle({ headers: { }} as HttpRequest)).resolves.toBeUndefined(); await expect(bodyParser.handle({ headers: { }} as HttpRequest)).resolves.toBeUndefined();
}); });
it('returns a stream of quads if there was data.', async(): Promise<void> => { it('returns a Representation if there was data.', async(): Promise<void> => {
const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest; const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
input.headers = { 'content-type': 'text/turtle' }; input.headers = { 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!; const result = (await bodyParser.handle(input))!;
expect(result).toEqual({ expect(result).toEqual({
data: expect.any(Readable), data: expect.any(Readable),
dataType: DATA_TYPE_QUAD, dataType: DATA_TYPE_BINARY,
metadata: { metadata: {
contentType: 'text/turtle', contentType: 'text/turtle',
profiles: [], profiles: [],
raw: [], raw: [],
}, },
}); });
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple( await expect(arrayifyStream(result.data)).resolves.toEqual(
namedNode('http://test.com/s'), [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
namedNode('http://test.com/p'), );
namedNode('http://test.com/o'),
) ]);
});
it('throws an UnsupportedHttpError on invalid triple data when reading the stream.', async(): Promise<void> => {
const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>' ]) as HttpRequest;
input.headers = { 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!;
await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError);
}); });
}); });

View File

@ -1,37 +1,31 @@
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { BinaryRepresentation } from '../../../src/ldp/representation/BinaryRepresentation';
import { DATA_TYPE_BINARY } from '../../../src/util/ContentTypes';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore'; import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../../../src/util/ContentTypes';
import { namedNode, triple } from '@rdfjs/data-model';
const base = 'http://test.com/'; const base = 'http://test.com/';
describe('A SimpleResourceStore', (): void => { describe('A SimpleResourceStore', (): void => {
let store: SimpleResourceStore; let store: SimpleResourceStore;
let representation: QuadRepresentation; let representation: BinaryRepresentation;
const quad = triple( const dataString = '<http://test.com/s> <http://test.com/p> <http://test.com/o>.';
namedNode('http://test.com/s'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
);
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
store = new SimpleResourceStore(base); store = new SimpleResourceStore(base);
representation = { representation = {
data: streamifyArray([ quad ]), data: streamifyArray([ dataString ]),
dataType: DATA_TYPE_QUAD, dataType: DATA_TYPE_BINARY,
metadata: {} as RepresentationMetadata, metadata: {} as RepresentationMetadata,
}; };
}); });
it('errors if a resource was not found.', async(): Promise<void> => { it('errors if a resource was not found.', async(): Promise<void> => {
await expect(store.getRepresentation({ path: `${base}wrong` }, {})).rejects.toThrow(NotFoundHttpError); await expect(store.getRepresentation({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation)) await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation))
.rejects.toThrow(NotFoundHttpError); .rejects.toThrow(NotFoundHttpError);
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError); await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
@ -43,85 +37,38 @@ describe('A SimpleResourceStore', (): void => {
await expect(store.modifyResource()).rejects.toThrow(Error); await expect(store.modifyResource()).rejects.toThrow(Error);
}); });
it('errors for wrong input data types.', async(): Promise<void> => {
(representation as any).dataType = DATA_TYPE_BINARY;
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(UnsupportedMediaTypeHttpError);
});
it('can write and read data.', async(): Promise<void> => { it('can write and read data.', async(): Promise<void> => {
const identifier = await store.addResource({ path: base }, representation); const identifier = await store.addResource({ path: base }, representation);
expect(identifier.path.startsWith(base)).toBeTruthy(); expect(identifier.path.startsWith(base)).toBeTruthy();
const result = await store.getRepresentation(identifier, { type: [{ value: CONTENT_TYPE_QUADS, weight: 1 }]}); const result = await store.getRepresentation(identifier);
expect(result).toEqual({ expect(result).toEqual({
dataType: DATA_TYPE_QUAD, dataType: representation.dataType,
data: expect.any(Readable), data: expect.any(Readable),
metadata: { metadata: representation.metadata,
profiles: [],
raw: [],
},
}); });
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]); await expect(arrayifyStream(result.data)).resolves.toEqual([ dataString ]);
}); });
it('can add resources to previously added resources.', async(): Promise<void> => { it('can add resources to previously added resources.', async(): Promise<void> => {
const identifier = await store.addResource({ path: base }, representation); const identifier = await store.addResource({ path: base }, representation);
representation.data = streamifyArray([ quad ]); representation.data = streamifyArray([ ]);
const childIdentifier = await store.addResource(identifier, representation); const childIdentifier = await store.addResource(identifier, representation);
expect(childIdentifier.path).toContain(identifier.path); expect(childIdentifier.path).toContain(identifier.path);
}); });
it('can read binary data.', async(): Promise<void> => {
const identifier = await store.addResource({ path: base }, representation);
expect(identifier.path.startsWith(base)).toBeTruthy();
const result = await store.getRepresentation(identifier, { type: [{ value: 'text/turtle', weight: 1 }]});
expect(result).toEqual({
dataType: DATA_TYPE_BINARY,
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
contentType: 'text/turtle',
},
});
await expect(arrayifyStream(result.data)).resolves.toContain(
`<${quad.subject.value}> <${quad.predicate.value}> <${quad.object.value}>`,
);
});
it('returns turtle data if no preference was set.', async(): Promise<void> => {
const identifier = await store.addResource({ path: base }, representation);
expect(identifier.path.startsWith(base)).toBeTruthy();
const result = await store.getRepresentation(identifier, { });
expect(result).toEqual({
dataType: DATA_TYPE_BINARY,
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
contentType: 'text/turtle',
},
});
await expect(arrayifyStream(result.data)).resolves.toContain(
`<${quad.subject.value}> <${quad.predicate.value}> <${quad.object.value}>`,
);
});
it('can set data.', async(): Promise<void> => { it('can set data.', async(): Promise<void> => {
await store.setRepresentation({ path: base }, representation); await store.setRepresentation({ path: base }, representation);
const result = await store.getRepresentation({ path: base }, { type: [{ value: CONTENT_TYPE_QUADS, weight: 1 }]}); const result = await store.getRepresentation({ path: base });
expect(result).toEqual({ expect(result).toEqual({
dataType: DATA_TYPE_QUAD, dataType: representation.dataType,
data: expect.any(Readable), data: expect.any(Readable),
metadata: { metadata: representation.metadata,
profiles: [],
raw: [],
},
}); });
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]); await expect(arrayifyStream(result.data)).resolves.toEqual([ dataString ]);
}); });
it('can delete data.', async(): Promise<void> => { it('can delete data.', async(): Promise<void> => {
await store.deleteResource({ path: base }); await store.deleteResource({ path: base });
await expect(store.getRepresentation({ path: base }, {})).rejects.toThrow(NotFoundHttpError); await expect(store.getRepresentation({ path: base })).rejects.toThrow(NotFoundHttpError);
}); });
}); });