/
InMemoryResourceStore.ts
167 lines (153 loc) · 5.71 KB
/
InMemoryResourceStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { PassThrough } from 'stream';
import arrayifyStream from 'arrayify-stream';
import streamifyArray from 'streamify-array';
import { RuntimeConfig } from '../init/RuntimeConfig';
import { Representation } from '../ldp/representation/Representation';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { TEXT_TURTLE } from '../util/ContentTypes';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { ensureTrailingSlash } from '../util/Util';
import { ResourceStore } from './ResourceStore';
/**
* Resource store storing its data in an in-memory map.
* Current Solid functionality support is quite basic: containers are not really supported for example.
*/
export class InMemoryResourceStore implements ResourceStore {
private readonly store: { [id: string]: Representation };
private readonly runtimeConfig: RuntimeConfig;
private index = 0;
/**
* @param runtimeConfig - Config containing base that will be stripped of all incoming URIs
* and added to all outgoing ones to find the relative path.
*/
public constructor(runtimeConfig: RuntimeConfig) {
this.runtimeConfig = runtimeConfig;
this.store = {
// Default root entry (what you get when the identifier is equal to the base)
'': {
binary: true,
data: streamifyArray([]),
metadata: { raw: [], profiles: [], contentType: TEXT_TURTLE },
},
};
}
/**
* 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.
*
* @returns The newly generated identifier.
*/
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
const containerPath = this.parseIdentifier(container);
this.checkPath(containerPath);
const newID = { path: `${ensureTrailingSlash(container.path)}${this.index}` };
const newPath = this.parseIdentifier(newID);
this.index += 1;
this.store[newPath] = await this.copyRepresentation(representation);
return newID;
}
/**
* Deletes the given resource.
* @param identifier - Identifier of resource to delete.
*/
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const path = this.parseIdentifier(identifier);
this.checkPath(path);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.store[path];
}
/**
* Returns the stored representation for the given identifier.
* Preferences will be ignored, data will be returned as it was received.
*
* @param identifier - Identifier to retrieve.
*
* @returns The corresponding Representation.
*/
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
const path = this.parseIdentifier(identifier);
this.checkPath(path);
return this.generateRepresentation(path);
}
/**
* @throws Not supported.
*/
public async modifyResource(): Promise<void> {
throw new Error('Not supported.');
}
/**
* Puts the given data in the given location.
* @param identifier - Identifier to replace.
* @param representation - New Representation.
*/
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
const path = this.parseIdentifier(identifier);
this.store[path] = await this.copyRepresentation(representation);
}
/**
* Strips the base from the identifier and checks if it is valid.
* @param identifier - Incoming identifier.
*
* @throws {@link NotFoundHttpError}
* If the identifier doesn't start with the base ID.
*
* @returns A string representing the relative path.
*/
private parseIdentifier(identifier: ResourceIdentifier): string {
const path = identifier.path.slice(this.runtimeConfig.base.length);
if (!identifier.path.startsWith(this.runtimeConfig.base)) {
throw new NotFoundHttpError();
}
return path;
}
/**
* Checks if the relative path is in the store.
* @param path - Incoming identifier.
*
* @throws {@link NotFoundHttpError}
* If the path is not in the store.
*/
private checkPath(path: string): void {
if (!this.store[path]) {
throw new NotFoundHttpError();
}
}
/**
* Copies the Representation by draining the original data stream and creating a new one.
*
* @param source - Incoming Representation.
*/
private async copyRepresentation(source: Representation): Promise<Representation> {
const arr = await arrayifyStream(source.data);
return {
binary: source.binary,
data: streamifyArray([ ...arr ]),
metadata: source.metadata,
};
}
/**
* 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.
*
* @param path - Path in store of Representation.
*
* @returns The resulting Representation.
*/
private async generateRepresentation(path: string): Promise<Representation> {
// Note: when converting to a complete ResourceStore and using readable-stream
// object mode should be set correctly here (now fixed due to Node 10)
const source = this.store[path];
const streamInternal = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
const streamOutput = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
source.data.pipe(streamInternal);
source.data.pipe(streamOutput);
source.data = streamInternal;
return {
binary: source.binary,
data: streamOutput,
metadata: source.metadata,
};
}
}