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 {
AcceptPreferenceParser,
AuthenticatedLdpHandler,
BodyParser,
CompositeAsyncHandler,
ExpressHttpServer,
HttpRequest,
Operation,
PatchingStore,
QuadToTurtleConverter,
Representation,
ResponseDescription,
RepresentationConvertingStore,
SimpleAuthorizer,
SimpleBodyParser,
SimpleCredentialsExtractor,
@ -25,6 +24,7 @@ import {
SimpleSparqlUpdatePatchHandler,
SimpleTargetExtractor,
SingleThreadedResourceLocker,
TurtleToQuadConverter,
} from '..';
const { argv } = yargs
@ -37,9 +37,9 @@ const { argv } = yargs
const { port } = argv;
// This is instead of the dependency injection that still needs to be added
const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleBodyParser(),
const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleSparqlUpdateBodyParser(),
new SimpleBodyParser(),
]);
const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(),
@ -53,11 +53,16 @@ const authorizer = new SimpleAuthorizer();
// Will have to see how to best handle this
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 patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(convertingStore, patcher);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
const operationHandler = new CompositeAsyncHandler([
new SimpleDeleteOperationHandler(patchingStore),
new SimpleGetOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore),

View File

@ -1,37 +1,22 @@
import { BinaryRepresentation } from '../representation/BinaryRepresentation';
import { BodyParser } from './BodyParser';
import { DATA_TYPE_QUAD } from '../../util/ContentTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { PassThrough } from 'stream';
import { QuadRepresentation } from '../representation/QuadRepresentation';
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.
* Naively parses the content-type header to determine the body type.
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the mediatype from the content-type header.
* Metadata is not generated (yet).
*/
export class SimpleBodyParser extends BodyParser {
private static readonly contentTypes = [
'application/n-quads',
'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.');
}
public async canHandle(): Promise<void> {
// Default BodyParser supports all content-types
}
// 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
public async handle(input: HttpRequest): Promise<QuadRepresentation | undefined> {
public async handle(input: HttpRequest): Promise<BinaryRepresentation | undefined> {
const contentType = input.headers['content-type'];
if (!contentType) {
@ -46,16 +31,9 @@ export class SimpleBodyParser extends BodyParser {
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 {
dataType: DATA_TYPE_QUAD,
data: errorStream,
dataType: DATA_TYPE_BINARY,
data: input,
metadata,
};
}

View File

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

View File

@ -4,6 +4,8 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
/**
* Handler that combines several other handlers,
* 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> {
private readonly handlers: AsyncHandler<TIn, TOut>[];

View File

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

View File

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

View File

@ -1,68 +1,37 @@
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 { Readable } from 'stream';
import { SimpleBodyParser } from '../../../../src/ldp/http/SimpleBodyParser';
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';
const contentTypes = [
'application/n-quads',
'application/trig',
'application/n-triples',
'text/turtle',
'text/n3',
];
describe('A SimpleBodyparser', (): void => {
const bodyParser = new SimpleBodyParser();
it('rejects input with unsupported content type.', async(): Promise<void> => {
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/rdf+xml' }} as HttpRequest))
.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('accepts all input.', async(): Promise<void> => {
await expect(bodyParser.canHandle()).resolves.toBeUndefined();
});
it('returns empty output if there was no content-type.', async(): Promise<void> => {
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;
input.headers = { 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!;
expect(result).toEqual({
data: expect.any(Readable),
dataType: DATA_TYPE_QUAD,
dataType: DATA_TYPE_BINARY,
metadata: {
contentType: 'text/turtle',
profiles: [],
raw: [],
},
});
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
namedNode('http://test.com/s'),
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);
await expect(arrayifyStream(result.data)).resolves.toEqual(
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
});
});

View File

@ -1,37 +1,31 @@
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 { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
import { Readable } from 'stream';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
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/';
describe('A SimpleResourceStore', (): void => {
let store: SimpleResourceStore;
let representation: QuadRepresentation;
const quad = triple(
namedNode('http://test.com/s'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
);
let representation: BinaryRepresentation;
const dataString = '<http://test.com/s> <http://test.com/p> <http://test.com/o>.';
beforeEach(async(): Promise<void> => {
store = new SimpleResourceStore(base);
representation = {
data: streamifyArray([ quad ]),
dataType: DATA_TYPE_QUAD,
data: streamifyArray([ dataString ]),
dataType: DATA_TYPE_BINARY,
metadata: {} as RepresentationMetadata,
};
});
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))
.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);
});
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> => {
const identifier = await store.addResource({ path: base }, representation);
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({
dataType: DATA_TYPE_QUAD,
dataType: representation.dataType,
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
},
metadata: representation.metadata,
});
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> => {
const identifier = await store.addResource({ path: base }, representation);
representation.data = streamifyArray([ quad ]);
representation.data = streamifyArray([ ]);
const childIdentifier = await store.addResource(identifier, representation);
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> => {
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({
dataType: DATA_TYPE_QUAD,
dataType: representation.dataType,
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
},
metadata: representation.metadata,
});
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]);
await expect(arrayifyStream(result.data)).resolves.toEqual([ dataString ]);
});
it('can delete data.', async(): Promise<void> => {
await store.deleteResource({ path: base });
await expect(store.getRepresentation({ path: base }, {})).rejects.toThrow(NotFoundHttpError);
await expect(store.getRepresentation({ path: base })).rejects.toThrow(NotFoundHttpError);
});
});