Skip to content

Commit 12fd00e

Browse files
committed
feat: add simple resource store
1 parent d983fca commit 12fd00e

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed

src/storage/SimpleResourceStore.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import arrayifyStream from 'arrayify-stream';
2+
import { BinaryRepresentation } from '../ldp/representation/BinaryRepresentation';
3+
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
4+
import { Quad } from 'rdf-js';
5+
import { QuadRepresentation } from '../ldp/representation/QuadRepresentation';
6+
import { Readable } from 'stream';
7+
import { Representation } from '../ldp/representation/Representation';
8+
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
9+
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
10+
import { ResourceStore } from './ResourceStore';
11+
import streamifyArray from 'streamify-array';
12+
import { StreamWriter } from 'n3';
13+
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
14+
15+
export class SimpleResourceStore implements ResourceStore {
16+
private readonly store: { [id: string]: Quad[] } = { '': []};
17+
private readonly base: string;
18+
private index = 0;
19+
20+
public constructor(base: string) {
21+
this.base = base;
22+
}
23+
24+
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
25+
const containerPath = this.parseIdentifier(container);
26+
const newPath = `${containerPath}/${this.index}`;
27+
this.index += 1;
28+
this.store[newPath] = await this.parseRepresentation(representation);
29+
return { path: `${this.base}${newPath}` };
30+
}
31+
32+
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
33+
const path = this.parseIdentifier(identifier);
34+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
35+
delete this.store[path];
36+
}
37+
38+
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences): Promise<Representation> {
39+
const path = this.parseIdentifier(identifier);
40+
return this.generateRepresentation(this.store[path], preferences);
41+
}
42+
43+
public async modifyResource(): Promise<void> {
44+
throw new Error('Not supported.');
45+
}
46+
47+
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
48+
const path = this.parseIdentifier(identifier);
49+
this.store[path] = await this.parseRepresentation(representation);
50+
}
51+
52+
private parseIdentifier(identifier: ResourceIdentifier): string {
53+
const path = identifier.path.slice(this.base.length);
54+
if (!this.store[path] || !identifier.path.startsWith(this.base)) {
55+
throw new NotFoundHttpError();
56+
}
57+
return path;
58+
}
59+
60+
private async parseRepresentation(representation: Representation): Promise<Quad[]> {
61+
if (representation.dataType !== 'quad') {
62+
throw new UnsupportedMediaTypeHttpError('SimpleResourceStore only supports quad representations.');
63+
}
64+
return arrayifyStream(representation.data);
65+
}
66+
67+
private generateRepresentation(data: Quad[], preferences: RepresentationPreferences): Representation {
68+
if (preferences.type && preferences.type.some((preference): boolean => preference.value.includes('text/turtle'))) {
69+
return this.generateBinaryRepresentation(data);
70+
}
71+
return this.generateQuadRepresentation(data);
72+
}
73+
74+
private generateBinaryRepresentation(data: Quad[]): BinaryRepresentation {
75+
return {
76+
dataType: 'binary',
77+
data: streamifyArray(data).pipe(new StreamWriter({ format: 'text/turtle' })) as unknown as Readable,
78+
metadata: { raw: [], profiles: [], contentType: 'text/turtle' },
79+
};
80+
}
81+
82+
private generateQuadRepresentation(data: Quad[]): QuadRepresentation {
83+
return {
84+
dataType: 'quad',
85+
data: streamifyArray(data),
86+
metadata: { raw: [], profiles: []},
87+
};
88+
}
89+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { HttpError } from './HttpError';
2+
3+
export class NotFoundHttpError extends HttpError {
4+
public constructor(message?: string) {
5+
super(404, 'NotFoundHttpError', message);
6+
}
7+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import arrayifyStream from 'arrayify-stream';
2+
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
3+
import { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
4+
import { Readable } from 'stream';
5+
import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore';
6+
import streamifyArray from 'streamify-array';
7+
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
8+
import { namedNode, triple } from '@rdfjs/data-model';
9+
10+
const base = 'http://test.com/';
11+
12+
describe('A SimpleResourceStore', (): void => {
13+
let store: SimpleResourceStore;
14+
let representation: QuadRepresentation;
15+
const quad = triple(
16+
namedNode('http://test.com/s'),
17+
namedNode('http://test.com/p'),
18+
namedNode('http://test.com/o'),
19+
);
20+
21+
beforeEach(async(): Promise<void> => {
22+
store = new SimpleResourceStore(base);
23+
24+
representation = {
25+
data: streamifyArray([ quad ]),
26+
dataType: 'quad',
27+
metadata: null,
28+
};
29+
});
30+
31+
it('errors if a resource was not found.', async(): Promise<void> => {
32+
await expect(store.getRepresentation({ path: `${base}wrong` }, {})).rejects.toThrow(NotFoundHttpError);
33+
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, null)).rejects.toThrow(NotFoundHttpError);
34+
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
35+
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, null)).rejects.toThrow(NotFoundHttpError);
36+
});
37+
38+
it('errors when modifying resources.', async(): Promise<void> => {
39+
await expect(store.modifyResource()).rejects.toThrow(Error);
40+
});
41+
42+
it('errors for wrong input data types.', async(): Promise<void> => {
43+
(representation as any).dataType = 'binary';
44+
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(UnsupportedMediaTypeHttpError);
45+
});
46+
47+
it('can write and read data.', async(): Promise<void> => {
48+
const identifier = await store.addResource({ path: base }, representation);
49+
expect(identifier.path.startsWith(base)).toBeTruthy();
50+
const result = await store.getRepresentation(identifier, {});
51+
expect(result).toEqual({
52+
dataType: 'quad',
53+
data: expect.any(Readable),
54+
metadata: {
55+
profiles: [],
56+
raw: [],
57+
},
58+
});
59+
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]);
60+
});
61+
62+
it('can read binary data.', async(): Promise<void> => {
63+
const identifier = await store.addResource({ path: base }, representation);
64+
expect(identifier.path.startsWith(base)).toBeTruthy();
65+
const result = await store.getRepresentation(identifier, { type: [{ value: 'text/turtle', weight: 1 }]});
66+
expect(result).toEqual({
67+
dataType: 'binary',
68+
data: expect.any(Readable),
69+
metadata: {
70+
profiles: [],
71+
raw: [],
72+
contentType: 'text/turtle',
73+
},
74+
});
75+
await expect(arrayifyStream(result.data)).resolves.toContain(
76+
`<${quad.subject.value}> <${quad.predicate.value}> <${quad.object.value}>`,
77+
);
78+
});
79+
80+
it('can set data.', async(): Promise<void> => {
81+
await store.setRepresentation({ path: base }, representation);
82+
const result = await store.getRepresentation({ path: base }, {});
83+
expect(result).toEqual({
84+
dataType: 'quad',
85+
data: expect.any(Readable),
86+
metadata: {
87+
profiles: [],
88+
raw: [],
89+
},
90+
});
91+
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]);
92+
});
93+
94+
it('can delete data.', async(): Promise<void> => {
95+
await store.deleteResource({ path: base });
96+
await expect(store.getRepresentation({ path: base }, {})).rejects.toThrow(NotFoundHttpError);
97+
});
98+
});

0 commit comments

Comments
 (0)