1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | import { Quad, NamedNode } from "rdf-js";
|
23 | import LinkHeader from "http-link-header";
|
24 | import { dataset, DataFactory } from "./rdfjs";
|
25 | import { fetch } from "./fetcher";
|
26 | import { turtleToTriples, triplesToTurtle } from "./formats/turtle";
|
27 | import { isLocalNode, resolveIriForLocalNodes } from "./datatypes";
|
28 | import { internal_fetchResourceAcl, internal_fetchFallbackAcl } from "./acl";
|
29 | import {
|
30 | UrlString,
|
31 | LitDataset,
|
32 | DatasetInfo,
|
33 | ChangeLog,
|
34 | hasChangelog,
|
35 | hasDatasetInfo,
|
36 | LocalNode,
|
37 | unstable_Acl,
|
38 | unstable_hasAccessibleAcl,
|
39 | unstable_AccessModes,
|
40 | } from "./interfaces";
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | export function createLitDataset(): LitDataset {
|
48 | return dataset();
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | export const defaultFetchOptions = {
|
55 | fetch: fetch,
|
56 | };
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | export async function fetchLitDataset(
|
65 | url: UrlString,
|
66 | options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
|
67 | ): Promise<LitDataset & DatasetInfo> {
|
68 | const config = {
|
69 | ...defaultFetchOptions,
|
70 | ...options,
|
71 | };
|
72 |
|
73 | const response = await config.fetch(url);
|
74 | if (!response.ok) {
|
75 | throw new Error(
|
76 | `Fetching the Resource failed: ${response.status} ${response.statusText}.`
|
77 | );
|
78 | }
|
79 | const data = await response.text();
|
80 | const triples = await turtleToTriples(data, url);
|
81 | const resource = dataset();
|
82 | triples.forEach((triple) => resource.add(triple));
|
83 |
|
84 | const datasetInfo = parseDatasetInfo(response);
|
85 |
|
86 | const resourceWithDatasetInfo: LitDataset &
|
87 | DatasetInfo = Object.assign(resource, { datasetInfo: datasetInfo });
|
88 |
|
89 | return resourceWithDatasetInfo;
|
90 | }
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | export async function internal_fetchLitDatasetInfo(
|
96 | url: UrlString,
|
97 | options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
|
98 | ): Promise<DatasetInfo> {
|
99 | const config = {
|
100 | ...defaultFetchOptions,
|
101 | ...options,
|
102 | };
|
103 |
|
104 | const response = await config.fetch(url, { method: "HEAD" });
|
105 | if (!response.ok) {
|
106 | throw new Error(
|
107 | `Fetching the Resource metadata failed: ${response.status} ${response.statusText}.`
|
108 | );
|
109 | }
|
110 |
|
111 | const datasetInfo = parseDatasetInfo(response);
|
112 |
|
113 | return { datasetInfo: datasetInfo };
|
114 | }
|
115 |
|
116 | function parseDatasetInfo(response: Response): DatasetInfo["datasetInfo"] {
|
117 | const datasetInfo: DatasetInfo["datasetInfo"] = {
|
118 | fetchedFrom: response.url,
|
119 | };
|
120 | const linkHeader = response.headers.get("Link");
|
121 | if (linkHeader) {
|
122 | const parsedLinks = LinkHeader.parse(linkHeader);
|
123 | const aclLinks = parsedLinks.get("rel", "acl");
|
124 | if (aclLinks.length === 1) {
|
125 | datasetInfo.unstable_aclUrl = new URL(
|
126 | aclLinks[0].uri,
|
127 | datasetInfo.fetchedFrom
|
128 | ).href;
|
129 | }
|
130 | }
|
131 |
|
132 | const wacAllowHeader = response.headers.get("WAC-Allow");
|
133 | if (wacAllowHeader) {
|
134 | datasetInfo.unstable_permissions = parseWacAllowHeader(wacAllowHeader);
|
135 | }
|
136 |
|
137 | return datasetInfo;
|
138 | }
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | export async function unstable_fetchLitDatasetWithAcl(
|
160 | url: UrlString,
|
161 | options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
|
162 | ): Promise<LitDataset & DatasetInfo & (unstable_Acl | { acl: null })> {
|
163 | const litDataset = await fetchLitDataset(url, options);
|
164 |
|
165 | if (!unstable_hasAccessibleAcl(litDataset)) {
|
166 | return Object.assign(litDataset, { acl: null });
|
167 | }
|
168 |
|
169 | const [resourceAcl, fallbackAcl] = await Promise.all([
|
170 | internal_fetchResourceAcl(litDataset, options),
|
171 | internal_fetchFallbackAcl(litDataset, options),
|
172 | ]);
|
173 |
|
174 | const acl: unstable_Acl["acl"] = {
|
175 | fallbackAcl: fallbackAcl,
|
176 | resourceAcl: resourceAcl !== null ? resourceAcl : undefined,
|
177 | };
|
178 |
|
179 | return Object.assign(litDataset, { acl: acl });
|
180 | }
|
181 |
|
182 | const defaultSaveOptions = {
|
183 | fetch: fetch,
|
184 | };
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 | export async function saveLitDatasetAt(
|
194 | url: UrlString,
|
195 | litDataset: LitDataset,
|
196 | options: Partial<typeof defaultSaveOptions> = defaultSaveOptions
|
197 | ): Promise<LitDataset & DatasetInfo & ChangeLog> {
|
198 | const config = {
|
199 | ...defaultSaveOptions,
|
200 | ...options,
|
201 | };
|
202 |
|
203 | let requestInit: RequestInit;
|
204 |
|
205 | if (isUpdate(litDataset, url)) {
|
206 | const deleteStatement =
|
207 | litDataset.changeLog.deletions.length > 0
|
208 | ? `DELETE DATA {${(
|
209 | await triplesToTurtle(
|
210 | litDataset.changeLog.deletions.map(getNamedNodesForLocalNodes)
|
211 | )
|
212 | ).trim()}};`
|
213 | : "";
|
214 | const insertStatement =
|
215 | litDataset.changeLog.additions.length > 0
|
216 | ? `INSERT DATA {${(
|
217 | await triplesToTurtle(
|
218 | litDataset.changeLog.additions.map(getNamedNodesForLocalNodes)
|
219 | )
|
220 | ).trim()}};`
|
221 | : "";
|
222 |
|
223 | requestInit = {
|
224 | method: "PATCH",
|
225 | body: `${deleteStatement} ${insertStatement}`,
|
226 | headers: {
|
227 | "Content-Type": "application/sparql-update",
|
228 | },
|
229 | };
|
230 | } else {
|
231 | requestInit = {
|
232 | method: "PUT",
|
233 | body: await triplesToTurtle(
|
234 | Array.from(litDataset).map(getNamedNodesForLocalNodes)
|
235 | ),
|
236 | headers: {
|
237 | "Content-Type": "text/turtle",
|
238 | "If-None-Match": "*",
|
239 | Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
|
240 | },
|
241 | };
|
242 | }
|
243 |
|
244 | const response = await config.fetch(url, requestInit);
|
245 |
|
246 | if (!response.ok) {
|
247 | throw new Error(
|
248 | `Storing the Resource failed: ${response.status} ${response.statusText}.`
|
249 | );
|
250 | }
|
251 |
|
252 | const datasetInfo: DatasetInfo["datasetInfo"] = hasDatasetInfo(litDataset)
|
253 | ? { ...litDataset.datasetInfo, fetchedFrom: url }
|
254 | : { fetchedFrom: url };
|
255 | const storedDataset: LitDataset & ChangeLog & DatasetInfo = Object.assign(
|
256 | litDataset,
|
257 | {
|
258 | changeLog: { additions: [], deletions: [] },
|
259 | datasetInfo: datasetInfo,
|
260 | }
|
261 | );
|
262 |
|
263 | const storedDatasetWithResolvedIris = resolveLocalIrisInLitDataset(
|
264 | storedDataset
|
265 | );
|
266 |
|
267 | return storedDatasetWithResolvedIris;
|
268 | }
|
269 |
|
270 | function isUpdate(
|
271 | litDataset: LitDataset,
|
272 | url: UrlString
|
273 | ): litDataset is LitDataset &
|
274 | ChangeLog &
|
275 | DatasetInfo & { datasetInfo: { fetchedFrom: string } } {
|
276 | return (
|
277 | hasChangelog(litDataset) &&
|
278 | hasDatasetInfo(litDataset) &&
|
279 | typeof litDataset.datasetInfo.fetchedFrom === "string" &&
|
280 | litDataset.datasetInfo.fetchedFrom === url
|
281 | );
|
282 | }
|
283 |
|
284 | const defaultSaveInContainerOptions = {
|
285 | fetch: fetch,
|
286 | };
|
287 | type SaveInContainerOptions = Partial<
|
288 | typeof defaultSaveInContainerOptions & {
|
289 | slugSuggestion: string;
|
290 | }
|
291 | >;
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 | export async function saveLitDatasetInContainer(
|
301 | containerUrl: UrlString,
|
302 | litDataset: LitDataset,
|
303 | options: SaveInContainerOptions = defaultSaveInContainerOptions
|
304 | ): Promise<LitDataset & DatasetInfo> {
|
305 | const config = {
|
306 | ...defaultSaveOptions,
|
307 | ...options,
|
308 | };
|
309 |
|
310 | const rawTurtle = await triplesToTurtle(
|
311 | Array.from(litDataset).map(getNamedNodesForLocalNodes)
|
312 | );
|
313 | const headers: RequestInit["headers"] = {
|
314 | "Content-Type": "text/turtle",
|
315 | Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
|
316 | };
|
317 | if (options.slugSuggestion) {
|
318 | headers.slug = options.slugSuggestion;
|
319 | }
|
320 | const response = await config.fetch(containerUrl, {
|
321 | method: "POST",
|
322 | body: rawTurtle,
|
323 | headers: headers,
|
324 | });
|
325 |
|
326 | if (!response.ok) {
|
327 | throw new Error(
|
328 | `Storing the Resource in the Container failed: ${response.status} ${response.statusText}.`
|
329 | );
|
330 | }
|
331 |
|
332 | const locationHeader = response.headers.get("Location");
|
333 | if (locationHeader === null) {
|
334 | throw new Error(
|
335 | "Could not determine the location for the newly saved LitDataset."
|
336 | );
|
337 | }
|
338 |
|
339 | const resourceIri = new URL(locationHeader, new URL(containerUrl).origin)
|
340 | .href;
|
341 | const datasetInfo: DatasetInfo["datasetInfo"] = {
|
342 | fetchedFrom: resourceIri,
|
343 | };
|
344 | const resourceWithDatasetInfo: LitDataset &
|
345 | DatasetInfo = Object.assign(litDataset, { datasetInfo: datasetInfo });
|
346 |
|
347 | const resourceWithResolvedIris = resolveLocalIrisInLitDataset(
|
348 | resourceWithDatasetInfo
|
349 | );
|
350 |
|
351 | return resourceWithResolvedIris;
|
352 | }
|
353 |
|
354 | function getNamedNodesForLocalNodes(quad: Quad): Quad {
|
355 | const subject = isLocalNode(quad.subject)
|
356 | ? getNamedNodeFromLocalNode(quad.subject)
|
357 | : quad.subject;
|
358 | const object = isLocalNode(quad.object)
|
359 | ? getNamedNodeFromLocalNode(quad.object)
|
360 | : quad.object;
|
361 |
|
362 | return {
|
363 | ...quad,
|
364 | subject: subject,
|
365 | object: object,
|
366 | };
|
367 | }
|
368 |
|
369 | function getNamedNodeFromLocalNode(localNode: LocalNode): NamedNode {
|
370 | return DataFactory.namedNode("#" + localNode.name);
|
371 | }
|
372 |
|
373 | function resolveLocalIrisInLitDataset<Dataset extends LitDataset & DatasetInfo>(
|
374 | litDataset: Dataset
|
375 | ): Dataset {
|
376 | const resourceIri = litDataset.datasetInfo.fetchedFrom;
|
377 | const unresolvedQuads = Array.from(litDataset);
|
378 |
|
379 | unresolvedQuads.forEach((unresolvedQuad) => {
|
380 | const resolvedQuad = resolveIriForLocalNodes(unresolvedQuad, resourceIri);
|
381 | litDataset.delete(unresolvedQuad);
|
382 | litDataset.add(resolvedQuad);
|
383 | });
|
384 |
|
385 | return litDataset;
|
386 | }
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
394 | function parseWacAllowHeader(wacAllowHeader: string) {
|
395 | function parsePermissionStatement(
|
396 | permissionStatement: string
|
397 | ): unstable_AccessModes {
|
398 | const permissions = permissionStatement.split(" ");
|
399 | const writePermission = permissions.includes("write");
|
400 | return writePermission
|
401 | ? {
|
402 | read: permissions.includes("read"),
|
403 | append: true,
|
404 | write: true,
|
405 | control: permissions.includes("control"),
|
406 | }
|
407 | : {
|
408 | read: permissions.includes("read"),
|
409 | append: permissions.includes("append"),
|
410 | write: false,
|
411 | control: permissions.includes("control"),
|
412 | };
|
413 | }
|
414 | function getStatementFor(header: string, scope: "user" | "public") {
|
415 | const relevantEntries = header
|
416 | .split(",")
|
417 | .map((rawEntry) => rawEntry.split("="))
|
418 | .filter((parts) => parts.length === 2 && parts[0].trim() === scope);
|
419 |
|
420 |
|
421 | if (relevantEntries.length !== 1) {
|
422 | return "";
|
423 | }
|
424 | const relevantStatement = relevantEntries[0][1].trim();
|
425 |
|
426 |
|
427 | if (
|
428 | relevantStatement.charAt(0) !== '"' ||
|
429 | relevantStatement.charAt(relevantStatement.length - 1) !== '"'
|
430 | ) {
|
431 | return "";
|
432 | }
|
433 |
|
434 | return relevantStatement.substring(1, relevantStatement.length - 1);
|
435 | }
|
436 |
|
437 | return {
|
438 | user: parsePermissionStatement(getStatementFor(wacAllowHeader, "user")),
|
439 | public: parsePermissionStatement(getStatementFor(wacAllowHeader, "public")),
|
440 | };
|
441 | }
|