Skip to content

Commit

Permalink
feat: add simple resource store
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Jun 24, 2020
1 parent d983fca commit 12fd00e
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
89 changes: 89 additions & 0 deletions src/storage/SimpleResourceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import arrayifyStream from 'arrayify-stream';
import { BinaryRepresentation } from '../ldp/representation/BinaryRepresentation';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { Quad } from 'rdf-js';
import { QuadRepresentation } from '../ldp/representation/QuadRepresentation';
import { Readable } from 'stream';
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';

export class SimpleResourceStore implements ResourceStore {
private readonly store: { [id: string]: Quad[] } = { '': []};
private readonly base: string;
private index = 0;

public constructor(base: string) {
this.base = base;
}

public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
const containerPath = this.parseIdentifier(container);
const newPath = `${containerPath}/${this.index}`;
this.index += 1;
this.store[newPath] = await this.parseRepresentation(representation);
return { path: `${this.base}${newPath}` };
}

public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const path = this.parseIdentifier(identifier);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.store[path];
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences): Promise<Representation> {
const path = this.parseIdentifier(identifier);
return this.generateRepresentation(this.store[path], preferences);
}

public async modifyResource(): Promise<void> {
throw new Error('Not supported.');
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
const path = this.parseIdentifier(identifier);
this.store[path] = await this.parseRepresentation(representation);
}

private parseIdentifier(identifier: ResourceIdentifier): string {
const path = identifier.path.slice(this.base.length);
if (!this.store[path] || !identifier.path.startsWith(this.base)) {
throw new NotFoundHttpError();
}
return path;
}

private async parseRepresentation(representation: Representation): Promise<Quad[]> {
if (representation.dataType !== 'quad') {
throw new UnsupportedMediaTypeHttpError('SimpleResourceStore only supports quad representations.');
}
return arrayifyStream(representation.data);
}

private generateRepresentation(data: Quad[], preferences: RepresentationPreferences): Representation {
if (preferences.type && preferences.type.some((preference): boolean => preference.value.includes('text/turtle'))) {
return this.generateBinaryRepresentation(data);
}
return this.generateQuadRepresentation(data);
}

private generateBinaryRepresentation(data: Quad[]): BinaryRepresentation {
return {
dataType: 'binary',
data: streamifyArray(data).pipe(new StreamWriter({ format: 'text/turtle' })) as unknown as Readable,
metadata: { raw: [], profiles: [], contentType: 'text/turtle' },
};
}

private generateQuadRepresentation(data: Quad[]): QuadRepresentation {
return {
dataType: 'quad',
data: streamifyArray(data),
metadata: { raw: [], profiles: []},
};
}
}
7 changes: 7 additions & 0 deletions src/util/errors/NotFoundHttpError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { HttpError } from './HttpError';

export class NotFoundHttpError extends HttpError {
public constructor(message?: string) {
super(404, 'NotFoundHttpError', message);
}
}
98 changes: 98 additions & 0 deletions test/unit/storage/SimpleResourceStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import arrayifyStream from 'arrayify-stream';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
import { Readable } from 'stream';
import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
import streamifyArray from 'streamify-array';
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
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'),
);

beforeEach(async(): Promise<void> => {
store = new SimpleResourceStore(base);

representation = {
data: streamifyArray([ quad ]),
dataType: 'quad',
metadata: null,
};
});

it('errors if a resource was not found.', async(): Promise<void> => {
await expect(store.getRepresentation({ path: `${base}wrong` }, {})).rejects.toThrow(NotFoundHttpError);
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, null)).rejects.toThrow(NotFoundHttpError);
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, null)).rejects.toThrow(NotFoundHttpError);
});

it('errors when modifying resources.', async(): Promise<void> => {
await expect(store.modifyResource()).rejects.toThrow(Error);
});

it('errors for wrong input data types.', async(): Promise<void> => {
(representation as any).dataType = '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, {});
expect(result).toEqual({
dataType: 'quad',
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
},
});
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]);
});

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: '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 }, {});
expect(result).toEqual({
dataType: 'quad',
data: expect.any(Readable),
metadata: {
profiles: [],
raw: [],
},
});
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]);
});

it('can delete data.', async(): Promise<void> => {
await store.deleteResource({ path: base });
await expect(store.getRepresentation({ path: base }, {})).rejects.toThrow(NotFoundHttpError);
});
});

0 comments on commit 12fd00e

Please sign in to comment.