@noeldemartin/solid-utils 0.1.1-next.0138d472d679413be54bc014f8cf21f03a1e1c3c → 0.1.1-next.0d98b8599ef41e8aa2544f3da0243b4dcb5f78f5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,57 +1,95 @@
1
1
  import { uuid } from '@noeldemartin/utils';
2
2
 
3
3
  import { createSolidDocument, fetchSolidDocument, solidDocumentExists, updateSolidDocument } from '@/helpers/io';
4
- import type SolidThing from '@/models/SolidThing';
5
4
  import type { Fetch } from '@/helpers/io';
6
5
  import type { SolidUserProfile } from '@/helpers/auth';
7
6
 
8
- async function mintPrivateTypeIndexUrl(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
9
- fetch = fetch ?? window.fetch;
7
+ type TypeIndexType = 'public' | 'private';
8
+
9
+ async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch): Promise<string> {
10
+ fetch = fetch ?? window.fetch.bind(fetch);
10
11
 
11
12
  const storageUrl = user.storageUrls[0];
12
- const typeIndexUrl = `${storageUrl}settings/privateTypeIndex`;
13
+ const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
13
14
 
14
15
  return await solidDocumentExists(typeIndexUrl, fetch)
15
- ? `${storageUrl}settings/privateTypeIndex-${uuid()}`
16
+ ? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
16
17
  : typeIndexUrl;
17
18
  }
18
19
 
19
- export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
20
- fetch = fetch ?? window.fetch;
20
+ async function createTypeIndex(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch) {
21
+ if (user.writableProfileUrl === null) {
22
+ throw new Error('Can\'t create type index without a writable profile document');
23
+ }
21
24
 
22
- const typeIndexUrl = await mintPrivateTypeIndexUrl(user, fetch);
23
- const typeIndexBody = `
24
- <> a
25
- <http://www.w3.org/ns/solid/terms#TypeIndex>,
26
- <http://www.w3.org/ns/solid/terms#UnlistedDocument> .
27
- `;
25
+ fetch = fetch ?? window.fetch.bind(fetch);
26
+
27
+ const typeIndexUrl = await mintTypeIndexUrl(user, type, fetch);
28
+ const typeIndexBody = type === 'public'
29
+ ? '<> a <http://www.w3.org/ns/solid/terms#TypeIndex> .'
30
+ : `
31
+ <> a
32
+ <http://www.w3.org/ns/solid/terms#TypeIndex>,
33
+ <http://www.w3.org/ns/solid/terms#UnlistedDocument> .
34
+ `;
28
35
  const profileUpdateBody = `
29
36
  INSERT DATA {
30
- <${user.webId}> <http://www.w3.org/ns/solid/terms#privateTypeIndex> <${typeIndexUrl}> .
37
+ <${user.webId}> <http://www.w3.org/ns/solid/terms#${type}TypeIndex> <${typeIndexUrl}> .
31
38
  }
32
39
  `;
33
40
 
34
- createSolidDocument(typeIndexUrl, typeIndexBody, fetch);
35
- updateSolidDocument(user.webId, profileUpdateBody, fetch);
41
+ await Promise.all([
42
+ createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
43
+ updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
44
+ ]);
45
+
46
+ if (type === 'public') {
47
+ // TODO Implement updating ACLs for the listing itself to public
48
+ }
36
49
 
37
50
  return typeIndexUrl;
38
51
  }
39
52
 
40
- export async function findContainerRegistration(
53
+ async function findRegistrations(
41
54
  typeIndexUrl: string,
42
- childrenType: string,
55
+ type: string | string[],
56
+ predicate: string,
43
57
  fetch?: Fetch,
44
- ): Promise<SolidThing | null> {
58
+ ): Promise<string[]> {
45
59
  const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
46
- const containerQuad = typeIndex
47
- .statements(undefined, 'rdfs:type', 'solid:TypeRegistration')
48
- .find(
49
- statement =>
50
- typeIndex.contains(statement.subject.value, 'solid:forClass', childrenType) &&
51
- typeIndex.contains(statement.subject.value, 'solid:instanceContainer'),
52
- );
53
-
54
- return containerQuad
55
- ? typeIndex.getThing(containerQuad.subject.value) ?? null
56
- : null;
60
+ const types = Array.isArray(type) ? type : [type];
61
+
62
+ return types.map(
63
+ type => typeIndex
64
+ .statements(undefined, 'rdf:type', 'solid:TypeRegistration')
65
+ .filter(statement => typeIndex.contains(statement.subject.value, 'solid:forClass', type))
66
+ .map(statement => typeIndex.statements(statement.subject.value, predicate))
67
+ .flat()
68
+ .map(statement => statement.object.value)
69
+ .filter(url => !!url),
70
+ ).flat();
71
+ }
72
+
73
+ export async function createPublicTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
74
+ return createTypeIndex(user, 'public', fetch);
75
+ }
76
+
77
+ export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
78
+ return createTypeIndex(user, 'private', fetch);
79
+ }
80
+
81
+ export async function findContainerRegistrations(
82
+ typeIndexUrl: string,
83
+ type: string | string[],
84
+ fetch?: Fetch,
85
+ ): Promise<string[]> {
86
+ return findRegistrations(typeIndexUrl, type, 'solid:instanceContainer', fetch);
87
+ }
88
+
89
+ export async function findInstanceRegistrations(
90
+ typeIndexUrl: string,
91
+ type: string | string[],
92
+ fetch?: Fetch,
93
+ ): Promise<string[]> {
94
+ return findRegistrations(typeIndexUrl, type, 'solid:instance', fetch);
57
95
  }
package/src/helpers/io.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { arr, arrayFilter, arrayReplace,objectWithoutEmpty } from '@noeldemartin/utils';
1
+ import md5 from 'md5';
2
+ import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
2
3
  import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
4
+ import { fromRDF, toRDF } from 'jsonld';
3
5
  import type { JsonLdDocument } from 'jsonld';
4
- import { toRDF } from 'jsonld';
5
- import md5 from 'md5';
6
6
  import type { Quad } from 'rdf-js';
7
7
  import type { Term as N3Term } from 'n3';
8
8
 
@@ -13,10 +13,10 @@ import NetworkRequestError from '@/errors/NetworkRequestError';
13
13
  import NotFoundError from '@/errors/NotFoundError';
14
14
  import UnauthorizedError from '@/errors/UnauthorizedError';
15
15
  import { isJsonLDGraph } from '@/helpers/jsonld';
16
- import type { JsonLD } from '@/helpers/jsonld';
16
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
17
17
 
18
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- export declare type AnyFetch = (input: any, options?: any) => Promise<any>;
19
+ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
20
20
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
21
21
  export declare type Fetch = TypedFetch | AnyFetch;
22
22
 
@@ -44,7 +44,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
44
44
  if (error instanceof UnauthorizedError)
45
45
  throw error;
46
46
 
47
- throw new NetworkRequestError(url);
47
+ if (error instanceof NotFoundError)
48
+ throw error;
49
+
50
+ throw new NetworkRequestError(url, { cause: error });
48
51
  }
49
52
  }
50
53
 
@@ -52,26 +55,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
52
55
  const normalizedQuads = quads.slice(0);
53
56
  const quadsIndexes: Record<string, Set<number>> = {};
54
57
  const blankNodeIds = arr(quads)
55
- .flatMap((quad, index) => {
56
- const ids = arrayFilter([
57
- quad.object.termType === 'BlankNode' ? quad.object.value : null,
58
- quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
59
- ]);
60
-
61
- for (const id of ids) {
62
- quadsIndexes[id] = quadsIndexes[id] ?? new Set();
63
- quadsIndexes[id].add(index);
64
- }
65
-
66
- return ids;
67
- })
58
+ .flatMap(
59
+ (quad, index) => tap(
60
+ arrayFilter([
61
+ quad.object.termType === 'BlankNode' ? quad.object.value : null,
62
+ quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
63
+ ]),
64
+ ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
65
+ ),
66
+ )
68
67
  .filter()
69
68
  .unique();
70
69
 
71
70
  for (const originalId of blankNodeIds) {
71
+ const quadIndexes = quadsIndexes[originalId] as Set<number>;
72
72
  const normalizedId = md5(
73
- arr(quadsIndexes[originalId])
74
- .map(index => quads[index])
73
+ arr(quadIndexes)
74
+ .map(index => quads[index] as Quad)
75
75
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
76
76
  .map(
77
77
  ({ predicate, object }) => object.termType === 'BlankNode'
@@ -82,9 +82,12 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
82
82
  .join(),
83
83
  );
84
84
 
85
- for (const index of quadsIndexes[originalId]) {
86
- const quad = normalizedQuads[index];
87
- const terms: Record<string, N3Term> = { subject: quad.subject as N3Term, object: quad.object as N3Term };
85
+ for (const index of quadIndexes) {
86
+ const quad = normalizedQuads[index] as Quad;
87
+ const terms: Record<string, N3Term> = {
88
+ subject: quad.subject as N3Term,
89
+ object: quad.object as N3Term,
90
+ };
88
91
 
89
92
  for (const [termName, termValue] of Object.entries(terms)) {
90
93
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
@@ -93,7 +96,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
93
96
  terms[termName] = new N3BlankNode(normalizedId);
94
97
  }
95
98
 
96
- arrayReplace(normalizedQuads, quad, new N3Quad(terms.subject, quad.predicate as N3Term, terms.object));
99
+ arrayReplace(
100
+ normalizedQuads,
101
+ quad,
102
+ new N3Quad(
103
+ terms.subject as N3Term,
104
+ quad.predicate as N3Term,
105
+ terms.object as N3Term,
106
+ ),
107
+ );
97
108
  }
98
109
  }
99
110
 
@@ -106,7 +117,7 @@ export interface ParsingOptions {
106
117
  }
107
118
 
108
119
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
109
- fetch = fetch ?? window.fetch;
120
+ fetch = fetch ?? window.fetch.bind(window);
110
121
 
111
122
  const statements = await turtleToQuads(body);
112
123
 
@@ -126,6 +137,19 @@ export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<So
126
137
  return new SolidDocument(url, statements, headers);
127
138
  }
128
139
 
140
+ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
141
+ try {
142
+ const document = await fetchSolidDocument(url, fetch);
143
+
144
+ return document;
145
+ } catch (error) {
146
+ if (!(error instanceof NotFoundError))
147
+ throw error;
148
+
149
+ return null;
150
+ }
151
+ }
152
+
129
153
  export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
130
154
  if (isJsonLDGraph(jsonld)) {
131
155
  const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
@@ -149,6 +173,14 @@ export function normalizeSparql(sparql: string): string {
149
173
  .join(' ;\n');
150
174
  }
151
175
 
176
+ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
177
+ const graph = await fromRDF(quads);
178
+
179
+ return {
180
+ '@graph': graph as JsonLDResource[],
181
+ };
182
+ }
183
+
152
184
  export function quadsToTurtle(quads: Quad[]): string {
153
185
  const writer = new TurtleWriter;
154
186
 
@@ -175,7 +207,7 @@ export async function sparqlToQuads(
175
207
  sparql: string,
176
208
  options: Partial<ParsingOptions> = {},
177
209
  ): Promise<Record<string, Quad[]>> {
178
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
210
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
179
211
  const quads: Record<string, Quad[]> = {};
180
212
 
181
213
  await Promise.all([...operations].map(async operation => {
@@ -189,7 +221,7 @@ export async function sparqlToQuads(
189
221
  }
190
222
 
191
223
  export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
192
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
224
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
193
225
  const quads: Record<string, Quad[]> = {};
194
226
 
195
227
  for (const operation of operations) {
@@ -244,12 +276,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
244
276
  ? normalizeBlankNodes(quads)
245
277
  : quads;
246
278
  } catch (error) {
247
- throw new MalformedSolidDocumentError(options.documentUrl ?? null, SolidDocumentFormat.Turtle, error.message);
279
+ throw new MalformedSolidDocumentError(
280
+ options.documentUrl ?? null,
281
+ SolidDocumentFormat.Turtle,
282
+ (error as Error).message ?? '',
283
+ );
248
284
  }
249
285
  }
250
286
 
251
287
  export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
252
- fetch = fetch ?? window.fetch;
288
+ fetch = fetch ?? window.fetch.bind(window);
253
289
 
254
290
  await fetch(url, {
255
291
  method: 'PATCH',
@@ -1,46 +1,67 @@
1
+ import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
1
2
  import type { JsonLD } from '@/helpers/jsonld';
2
- import { pull } from '@noeldemartin/utils';
3
3
  import type { Quad, Quad_Object } from 'rdf-js';
4
4
 
5
5
  import { jsonldToQuads, quadToTurtle, quadsToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
6
6
 
7
7
  let patternsRegExpsIndex: Record<string, RegExp> = {};
8
+ const builtInPatterns: Record<string, string> = {
9
+ '%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
10
+ };
11
+
12
+ class ExpectedQuadAssertionError extends JSError {
13
+
14
+ constructor(public readonly expectedQuad: Quad) {
15
+ super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
16
+ }
8
17
 
9
- function containsPatterns(value: string): boolean {
10
- return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
11
18
  }
12
19
 
13
- function quadValueEquals(expected: string, actual: string): boolean {
14
- if (!containsPatterns(expected))
15
- return expected === actual;
20
+ function assertExpectedQuadsExist(expectedQuads: Quad[], actualQuads: Quad[]): void {
21
+ for (const expectedQuad of expectedQuads) {
22
+ const matchingQuad = actualQuads.find(actualQuad => quadEquals(expectedQuad, actualQuad));
16
23
 
17
- const patternAliases = [];
24
+ if (!matchingQuad)
25
+ throw new ExpectedQuadAssertionError(expectedQuad);
26
+
27
+ arrayRemove(actualQuads, matchingQuad);
28
+ }
29
+ }
18
30
 
19
- if (!(expected in patternsRegExpsIndex)) {
20
- const patternMatches = expected.matchAll(/\[\[((.*)\]\[)?([^\]]+)\]\]/g);
21
- const patterns: string[] = [];
22
- let expectedRegExp = expected;
31
+ function containsPatterns(value: string): boolean {
32
+ return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
33
+ }
23
34
 
24
- for (const patternMatch of patternMatches) {
25
- if (patternMatch[2]) {
26
- patternAliases.push(patternMatch[2]);
27
- }
35
+ function createPatternRegexp(expected: string): RegExp {
36
+ const patternAliases = [];
37
+ const patternMatches = stringMatchAll<4, 1 | 2>(
38
+ expected,
39
+ /\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
40
+ );
41
+ const patterns: string[] = [];
42
+ let expectedRegExp = expected;
28
43
 
29
- patterns.push(patternMatch[3]);
44
+ for (const patternMatch of patternMatches) {
45
+ patternMatch[2] && patternAliases.push(patternMatch[2]);
30
46
 
31
- expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
32
- }
47
+ patterns.push(patternMatch[3]);
33
48
 
34
- expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
49
+ expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
50
+ }
35
51
 
36
- for (const [patternIndex, pattern] of Object.entries(patterns)) {
37
- expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, pattern);
38
- }
52
+ expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
39
53
 
40
- patternsRegExpsIndex[expected] = new RegExp(expectedRegExp);
54
+ for (const [patternIndex, pattern] of Object.entries(patterns)) {
55
+ expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
41
56
  }
42
57
 
43
- return patternsRegExpsIndex[expected].test(actual);
58
+ return new RegExp(expectedRegExp);
59
+ }
60
+
61
+ function quadValueEquals(expected: string, actual: string): boolean {
62
+ return containsPatterns(expected)
63
+ ? (patternsRegExpsIndex[expected] ??= createPatternRegexp(expected)).test(actual)
64
+ : expected === actual;
44
65
  }
45
66
 
46
67
  function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
@@ -95,9 +116,13 @@ export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<Eq
95
116
  if (expectedQuads.length !== actualQuads.length)
96
117
  return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
97
118
 
98
- for (const expectedQuad of expectedQuads) {
99
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
100
- return result(false, `Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
119
+ try {
120
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
121
+ } catch (error) {
122
+ if (!(error instanceof ExpectedQuadAssertionError))
123
+ throw error;
124
+
125
+ return result(false, error.message);
101
126
  }
102
127
 
103
128
  return result(true, 'jsonld matches');
@@ -121,9 +146,16 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
121
146
  if (expectedQuads.length !== actualQuads.length)
122
147
  return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
123
148
 
124
- for (const expectedQuad of expectedQuads) {
125
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
126
- return result(false, `Couldn't find the following ${operation} triple: ${quadToTurtle(expectedQuad)}`);
149
+ try {
150
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
151
+ } catch (error) {
152
+ if (!(error instanceof ExpectedQuadAssertionError))
153
+ throw error;
154
+
155
+ return result(
156
+ false,
157
+ `Couldn't find the following ${operation} triple: ${quadToTurtle(error.expectedQuad)}`,
158
+ );
127
159
  }
128
160
  }
129
161
 
@@ -145,9 +177,13 @@ export function turtleEquals(expected: string, actual: string): EqualityResult {
145
177
  if (expectedQuads.length !== actualQuads.length)
146
178
  return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
147
179
 
148
- for (const expectedQuad of expectedQuads) {
149
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
150
- return result(false, `Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
180
+ try {
181
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
182
+ } catch (error) {
183
+ if (!(error instanceof ExpectedQuadAssertionError))
184
+ throw error;
185
+
186
+ return result(false, error.message);
151
187
  }
152
188
 
153
189
  return result(true, 'turtle matches');
@@ -6,10 +6,12 @@ export interface ExpandIRIOptions {
6
6
  }
7
7
 
8
8
  const knownPrefixes: RDFContext = {
9
+ acl: 'http://www.w3.org/ns/auth/acl#',
9
10
  foaf: 'http://xmlns.com/foaf/0.1/',
10
11
  pim: 'http://www.w3.org/ns/pim/space#',
11
12
  purl: 'http://purl.org/dc/terms/',
12
- rdfs: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
14
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
13
15
  schema: 'https://schema.org/',
14
16
  solid: 'http://www.w3.org/ns/solid/terms#',
15
17
  vcard: 'http://www.w3.org/2006/vcard/ns#',
@@ -25,7 +27,7 @@ export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}):
25
27
 
26
28
  const [prefix, name] = iri.split(':');
27
29
 
28
- if (name) {
30
+ if (prefix && name) {
29
31
  const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
30
32
 
31
33
  if (!expandedPrefix)
@@ -0,0 +1,45 @@
1
+ import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
2
+
3
+ import { fetchSolidDocumentIfFound } from '@/helpers/io';
4
+ import type SolidDocument from '@/models/SolidDocument';
5
+ import type { Fetch } from '@/helpers/io';
6
+
7
+ async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
8
+ fetch = fetch ?? window.fetch.bind(window);
9
+
10
+ const resourceHead = await fetch(resourceUrl, { method: 'HEAD' });
11
+ const linkHeader = resourceHead.headers.get('Link') ?? '';
12
+ const url = linkHeader.match(/<([^>]+)>;\s*rel="acl"/)?.[1] ?? null;
13
+
14
+ if (!url) {
15
+ throw new Error(`Could not find ACL Resource for '${resourceUrl}'`);
16
+ }
17
+
18
+ return urlResolve(requireUrlParentDirectory(resourceUrl), url);
19
+ }
20
+
21
+ async function fetchEffectiveACL(
22
+ resourceUrl: string,
23
+ fetch: Fetch,
24
+ aclResourceUrl?: string | null,
25
+ ): Promise<SolidDocument> {
26
+ aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
27
+
28
+ return await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch)
29
+ ?? await fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
30
+ }
31
+
32
+ export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
33
+ url: string;
34
+ effectiveUrl: string;
35
+ document: SolidDocument;
36
+ }> {
37
+ const url = await fetchACLResourceUrl(documentUrl, fetch);
38
+ const document = await fetchEffectiveACL(documentUrl, fetch, url);
39
+
40
+ return objectWithoutEmpty({
41
+ url,
42
+ effectiveUrl: document.url,
43
+ document,
44
+ });
45
+ }
@@ -1,30 +1,33 @@
1
- import { parseDate } from '@noeldemartin/utils';
1
+ import { arrayFilter, parseDate, stringMatch } from '@noeldemartin/utils';
2
2
  import type { Quad } from 'rdf-js';
3
3
 
4
4
  import { expandIRI } from '@/helpers/vocabs';
5
5
 
6
- import SolidThing from './SolidThing';
6
+ import SolidStore from './SolidStore';
7
7
 
8
- export default class SolidDocument {
8
+ export enum SolidDocumentPermission {
9
+ Read = 'read',
10
+ Write = 'write',
11
+ Append = 'append',
12
+ Control = 'control',
13
+ }
14
+
15
+ export default class SolidDocument extends SolidStore {
9
16
 
10
17
  public readonly url: string;
11
18
  public readonly headers: Headers;
12
- private quads: Quad[];
13
19
 
14
20
  public constructor(url: string, quads: Quad[], headers: Headers) {
21
+ super(quads);
22
+
15
23
  this.url = url;
16
- this.quads = quads;
17
24
  this.headers = headers;
18
25
  }
19
26
 
20
- public isEmpty(): boolean {
21
- return this.statements.length === 0;
22
- }
23
-
24
27
  public isPersonalProfile(): boolean {
25
28
  return !!this.statement(
26
29
  this.url,
27
- expandIRI('rdfs:type'),
30
+ expandIRI('rdf:type'),
28
31
  expandIRI('foaf:PersonalProfileDocument'),
29
32
  );
30
33
  }
@@ -33,41 +36,27 @@ export default class SolidDocument {
33
36
  return !!this.headers.get('Link')?.match(/<http:\/\/www\.w3\.org\/ns\/pim\/space#Storage>;[^,]+rel="type"/);
34
37
  }
35
38
 
36
- public getLastModified(): Date | null {
37
- return parseDate(this.headers.get('last-modified'))
38
- ?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
39
- ?? this.getLatestDocumentDate()
40
- ?? null;
39
+ public isUserWritable(): boolean {
40
+ return this.getUserPermissions().includes(SolidDocumentPermission.Write);
41
41
  }
42
42
 
43
- public statements(subject?: string, predicate?: string, object?: string): Quad[] {
44
- return this.quads.filter(
45
- statement =>
46
- (!object || statement.object.value === expandIRI(object, { defaultPrefix: this.url })) &&
47
- (!subject || statement.subject.value === expandIRI(subject, { defaultPrefix: this.url })) &&
48
- (!predicate || statement.predicate.value === expandIRI(predicate, { defaultPrefix: this.url })),
49
- );
43
+ public getUserPermissions(): SolidDocumentPermission[] {
44
+ return this.getPermissionsFromWAC('user');
50
45
  }
51
46
 
52
- public statement(subject?: string, predicate?: string, object?: string): Quad | null {
53
- const statement = this.quads.find(
54
- statement =>
55
- (!object || statement.object.value === expandIRI(object, { defaultPrefix: this.url })) &&
56
- (!subject || statement.subject.value === expandIRI(subject, { defaultPrefix: this.url })) &&
57
- (!predicate || statement.predicate.value === expandIRI(predicate, { defaultPrefix: this.url })),
58
- );
59
-
60
- return statement ?? null;
47
+ public getPublicPermissions(): SolidDocumentPermission[] {
48
+ return this.getPermissionsFromWAC('public');
61
49
  }
62
50
 
63
- public contains(subject: string, predicate?: string, object?: string): boolean {
64
- return this.statement(subject, predicate, object) !== null;
51
+ public getLastModified(): Date | null {
52
+ return parseDate(this.headers.get('last-modified'))
53
+ ?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
54
+ ?? this.getLatestDocumentDate()
55
+ ?? null;
65
56
  }
66
57
 
67
- public getThing(subject: string): SolidThing {
68
- const statements = this.statements(subject);
69
-
70
- return new SolidThing(subject, statements);
58
+ protected expandIRI(iri: string): string {
59
+ return expandIRI(iri, { defaultPrefix: this.url });
71
60
  }
72
61
 
73
62
  private getLatestDocumentDate(): Date | null {
@@ -81,4 +70,16 @@ export default class SolidDocument {
81
70
  return dates.length > 0 ? dates.reduce((a, b) => a > b ? a : b) : null;
82
71
  }
83
72
 
73
+ private getPermissionsFromWAC(type: string): SolidDocumentPermission[] {
74
+ const wacAllow = this.headers.get('WAC-Allow') ?? '';
75
+ const publicModes = stringMatch<2>(wacAllow, new RegExp(`${type}="([^"]+)"`))?.[1] ?? '';
76
+
77
+ return arrayFilter([
78
+ publicModes.includes('read') && SolidDocumentPermission.Read,
79
+ publicModes.includes('write') && SolidDocumentPermission.Write,
80
+ publicModes.includes('append') && SolidDocumentPermission.Append,
81
+ publicModes.includes('control') && SolidDocumentPermission.Control,
82
+ ]);
83
+ }
84
+
84
85
  }