mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Integrate data conversion with rest of server
This commit is contained in:
parent
5e1bb10f81
commit
4403421c49
@ -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),
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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: []},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>[];
|
||||||
|
@ -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),
|
||||||
|
@ -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'),
|
|
||||||
) ]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user