@noeldemartin/solid-utils 0.1.0 → 0.1.1-next.2c9924cd927c0b708aef3ff4426982469f3b86fd

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,6 +1,8 @@
1
- import { arr, arrayFilter, arrayReplace,objectWithoutEmpty } from '@noeldemartin/utils';
2
- import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
3
1
  import md5 from 'md5';
2
+ import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
3
+ import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
4
+ import { fromRDF, toRDF } from 'jsonld';
5
+ import type { JsonLdDocument } from 'jsonld';
4
6
  import type { Quad } from 'rdf-js';
5
7
  import type { Term as N3Term } from 'n3';
6
8
 
@@ -10,9 +12,11 @@ import MalformedSolidDocumentError, { SolidDocumentFormat } from '@/errors/Malfo
10
12
  import NetworkRequestError from '@/errors/NetworkRequestError';
11
13
  import NotFoundError from '@/errors/NotFoundError';
12
14
  import UnauthorizedError from '@/errors/UnauthorizedError';
15
+ import { isJsonLDGraph } from '@/helpers/jsonld';
16
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
13
17
 
14
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- export declare type AnyFetch = (input: any, options?: any) => Promise<any>;
19
+ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
16
20
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
17
21
  export declare type Fetch = TypedFetch | AnyFetch;
18
22
 
@@ -40,7 +44,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
40
44
  if (error instanceof UnauthorizedError)
41
45
  throw error;
42
46
 
43
- throw new NetworkRequestError(url);
47
+ if (error instanceof NotFoundError)
48
+ throw error;
49
+
50
+ throw new NetworkRequestError(url, { cause: error });
44
51
  }
45
52
  }
46
53
 
@@ -48,26 +55,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
48
55
  const normalizedQuads = quads.slice(0);
49
56
  const quadsIndexes: Record<string, Set<number>> = {};
50
57
  const blankNodeIds = arr(quads)
51
- .flatMap((quad, index) => {
52
- const ids = arrayFilter([
53
- quad.object.termType === 'BlankNode' ? quad.object.value : null,
54
- quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
55
- ]);
56
-
57
- for (const id of ids) {
58
- quadsIndexes[id] = quadsIndexes[id] ?? new Set();
59
- quadsIndexes[id].add(index);
60
- }
61
-
62
- return ids;
63
- })
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
+ )
64
67
  .filter()
65
68
  .unique();
66
69
 
67
70
  for (const originalId of blankNodeIds) {
71
+ const quadIndexes = quadsIndexes[originalId] as Set<number>;
68
72
  const normalizedId = md5(
69
- arr(quadsIndexes[originalId])
70
- .map(index => quads[index])
73
+ arr(quadIndexes)
74
+ .map(index => quads[index] as Quad)
71
75
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
72
76
  .map(
73
77
  ({ predicate, object }) => object.termType === 'BlankNode'
@@ -78,9 +82,12 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
78
82
  .join(),
79
83
  );
80
84
 
81
- for (const index of quadsIndexes[originalId]) {
82
- const quad = normalizedQuads[index];
83
- 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
+ };
84
91
 
85
92
  for (const [termName, termValue] of Object.entries(terms)) {
86
93
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
@@ -89,7 +96,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
89
96
  terms[termName] = new N3BlankNode(normalizedId);
90
97
  }
91
98
 
92
- 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
+ );
93
108
  }
94
109
  }
95
110
 
@@ -102,7 +117,7 @@ export interface ParsingOptions {
102
117
  }
103
118
 
104
119
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
105
- fetch = fetch ?? window.fetch;
120
+ fetch = fetch ?? window.fetch.bind(window);
106
121
 
107
122
  const statements = await turtleToQuads(body);
108
123
 
@@ -122,6 +137,29 @@ export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<So
122
137
  return new SolidDocument(url, statements, headers);
123
138
  }
124
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
+
153
+ export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
154
+ if (isJsonLDGraph(jsonld)) {
155
+ const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
156
+
157
+ return graphQuads.flat();
158
+ }
159
+
160
+ return toRDF(jsonld as JsonLdDocument) as Promise<Quad[]>;
161
+ }
162
+
125
163
  export function normalizeSparql(sparql: string): string {
126
164
  const quads = sparqlToQuadsSync(sparql);
127
165
 
@@ -135,6 +173,20 @@ export function normalizeSparql(sparql: string): string {
135
173
  .join(' ;\n');
136
174
  }
137
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
+
184
+ export function quadsToTurtle(quads: Quad[]): string {
185
+ const writer = new TurtleWriter;
186
+
187
+ return writer.quadsToString(quads);
188
+ }
189
+
138
190
  export function quadToTurtle(quad: Quad): string {
139
191
  const writer = new TurtleWriter;
140
192
 
@@ -155,7 +207,7 @@ export async function sparqlToQuads(
155
207
  sparql: string,
156
208
  options: Partial<ParsingOptions> = {},
157
209
  ): Promise<Record<string, Quad[]>> {
158
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
210
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
159
211
  const quads: Record<string, Quad[]> = {};
160
212
 
161
213
  await Promise.all([...operations].map(async operation => {
@@ -169,7 +221,7 @@ export async function sparqlToQuads(
169
221
  }
170
222
 
171
223
  export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
172
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
224
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
173
225
  const quads: Record<string, Quad[]> = {};
174
226
 
175
227
  for (const operation of operations) {
@@ -224,12 +276,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
224
276
  ? normalizeBlankNodes(quads)
225
277
  : quads;
226
278
  } catch (error) {
227
- 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
+ );
228
284
  }
229
285
  }
230
286
 
231
287
  export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
232
- fetch = fetch ?? window.fetch;
288
+ fetch = fetch ?? window.fetch.bind(window);
233
289
 
234
290
  await fetch(url, {
235
291
  method: 'PATCH',
@@ -6,3 +6,7 @@ export type JsonLD = Partial<{
6
6
 
7
7
  export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
8
8
  export type JsonLDGraph = { '@graph': JsonLDResource[] };
9
+
10
+ export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
11
+ return '@graph' in jsonld;
12
+ }
@@ -1,39 +1,67 @@
1
- import { pull } from '@noeldemartin/utils';
1
+ import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
2
+ import type { JsonLD } from '@/helpers/jsonld';
2
3
  import type { Quad, Quad_Object } from 'rdf-js';
3
4
 
4
- import { quadToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
5
+ import { jsonldToQuads, quadToTurtle, quadsToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
5
6
 
6
- const patternRegExps: Record<string, RegExp> = {};
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
+ }
7
17
 
8
- function containsPatterns(value: string): boolean {
9
- return /\[\[([^\]]+)\]\]/.test(value);
10
18
  }
11
19
 
12
- function quadValueEquals(expected: string, actual: string): boolean {
13
- if (!containsPatterns(expected))
14
- 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));
15
23
 
16
- if (!(expected in patternRegExps)) {
17
- const patternMatches = expected.matchAll(/\[\[([^\]]+)\]\]/g);
18
- const patterns: string[] = [];
19
- let expectedRegExp = expected;
24
+ if (!matchingQuad)
25
+ throw new ExpectedQuadAssertionError(expectedQuad);
20
26
 
21
- for (const patternMatch of patternMatches) {
22
- patterns.push(patternMatch[1]);
27
+ arrayRemove(actualQuads, matchingQuad);
28
+ }
29
+ }
23
30
 
24
- expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
25
- }
31
+ function containsPatterns(value: string): boolean {
32
+ return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
33
+ }
26
34
 
27
- expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
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
- for (const [patternIndex, pattern] of Object.entries(patterns)) {
30
- expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, pattern);
31
- }
44
+ for (const patternMatch of patternMatches) {
45
+ patternMatch[2] && patternAliases.push(patternMatch[2]);
46
+
47
+ patterns.push(patternMatch[3]);
32
48
 
33
- patternRegExps[expected] = new RegExp(expectedRegExp);
49
+ expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
34
50
  }
35
51
 
36
- return patternRegExps[expected].test(actual);
52
+ expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
53
+
54
+ for (const [patternIndex, pattern] of Object.entries(patterns)) {
55
+ expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
56
+ }
57
+
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;
37
65
  }
38
66
 
39
67
  function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
@@ -59,6 +87,10 @@ function quadEquals(expected: Quad, actual: Quad): boolean {
59
87
  && quadValueEquals(expected.predicate.value, actual.predicate.value);
60
88
  }
61
89
 
90
+ function resetPatterns(): void {
91
+ patternsRegExpsIndex = {};
92
+ }
93
+
62
94
  export interface EqualityResult {
63
95
  success: boolean;
64
96
  message: string;
@@ -66,8 +98,39 @@ export interface EqualityResult {
66
98
  actual: string;
67
99
  }
68
100
 
101
+ export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<EqualityResult> {
102
+ // TODO catch parsing errors and improve message.
103
+ resetPatterns();
104
+
105
+ const expectedQuads = await jsonldToQuads(expected);
106
+ const actualQuads = await jsonldToQuads(actual);
107
+ const expectedTurtle = quadsToTurtle(expectedQuads);
108
+ const actualTurtle = quadsToTurtle(actualQuads);
109
+ const result = (success: boolean, message: string) => ({
110
+ success,
111
+ message,
112
+ expected: expectedTurtle,
113
+ actual: actualTurtle,
114
+ });
115
+
116
+ if (expectedQuads.length !== actualQuads.length)
117
+ return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
118
+
119
+ try {
120
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
121
+ } catch (error) {
122
+ if (!(error instanceof ExpectedQuadAssertionError))
123
+ throw error;
124
+
125
+ return result(false, error.message);
126
+ }
127
+
128
+ return result(true, 'jsonld matches');
129
+ }
130
+
69
131
  export function sparqlEquals(expected: string, actual: string): EqualityResult {
70
132
  // TODO catch parsing errors and improve message.
133
+ resetPatterns();
71
134
 
72
135
  const expectedOperations = sparqlToQuadsSync(expected, { normalizeBlankNodes: true });
73
136
  const actualOperations = sparqlToQuadsSync(actual, { normalizeBlankNodes: true });
@@ -83,9 +146,16 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
83
146
  if (expectedQuads.length !== actualQuads.length)
84
147
  return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
85
148
 
86
- for (const expectedQuad of expectedQuads) {
87
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
88
- 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
+ );
89
159
  }
90
160
  }
91
161
 
@@ -98,6 +168,7 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
98
168
 
99
169
  export function turtleEquals(expected: string, actual: string): EqualityResult {
100
170
  // TODO catch parsing errors and improve message.
171
+ resetPatterns();
101
172
 
102
173
  const expectedQuads = turtleToQuadsSync(expected, { normalizeBlankNodes: true });
103
174
  const actualQuads = turtleToQuadsSync(actual, { normalizeBlankNodes: true });
@@ -106,9 +177,13 @@ export function turtleEquals(expected: string, actual: string): EqualityResult {
106
177
  if (expectedQuads.length !== actualQuads.length)
107
178
  return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
108
179
 
109
- for (const expectedQuad of expectedQuads) {
110
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
111
- 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);
112
187
  }
113
188
 
114
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
+ }