@noeldemartin/solid-utils 0.1.1 → 0.2.0

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.
Files changed (35) hide show
  1. package/.nvmrc +1 -0
  2. package/.semaphore/semaphore.yml +1 -1
  3. package/dist/noeldemartin-solid-utils.cjs.js +1 -1
  4. package/dist/noeldemartin-solid-utils.cjs.js.map +1 -1
  5. package/dist/noeldemartin-solid-utils.d.ts +94 -16
  6. package/dist/noeldemartin-solid-utils.esm.js +1 -1
  7. package/dist/noeldemartin-solid-utils.esm.js.map +1 -1
  8. package/dist/noeldemartin-solid-utils.umd.js +90 -0
  9. package/dist/noeldemartin-solid-utils.umd.js.map +1 -0
  10. package/noeldemartin.config.js +4 -2
  11. package/package.json +11 -11
  12. package/src/errors/MalformedSolidDocumentError.ts +2 -2
  13. package/src/errors/NetworkRequestError.ts +5 -4
  14. package/src/errors/NotFoundError.ts +2 -2
  15. package/src/errors/UnauthorizedError.ts +2 -2
  16. package/src/errors/UnsuccessfulNetworkRequestError.ts +23 -0
  17. package/src/errors/UnsupportedAuthorizationProtocolError.ts +16 -0
  18. package/src/errors/index.ts +2 -0
  19. package/src/helpers/auth.ts +91 -18
  20. package/src/helpers/identifiers.ts +56 -0
  21. package/src/helpers/index.ts +2 -0
  22. package/src/helpers/interop.ts +68 -30
  23. package/src/helpers/io.ts +147 -60
  24. package/src/helpers/jsonld.ts +25 -1
  25. package/src/helpers/testing.ts +103 -28
  26. package/src/helpers/vocabs.ts +4 -2
  27. package/src/helpers/wac.ts +55 -0
  28. package/src/models/SolidDocument.ts +41 -35
  29. package/src/models/SolidStore.ts +61 -0
  30. package/src/models/index.ts +2 -1
  31. package/src/plugins/chai/assertions.ts +11 -2
  32. package/src/plugins/cypress/types.d.ts +1 -0
  33. package/src/plugins/jest/matchers.ts +45 -32
  34. package/src/plugins/jest/types.d.ts +1 -0
  35. package/src/types/n3.d.ts +9 -0
package/src/helpers/io.ts CHANGED
@@ -1,8 +1,15 @@
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 {
3
+ TurtleParser,
4
+ TurtleWriter,
5
+ createBlankNode,
6
+ createQuad,
7
+ jsonLDFromRDF,
8
+ jsonLDToRDF,
9
+ } from '@noeldemartin/solid-utils-external';
10
+ import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
4
11
  import type { Quad } from 'rdf-js';
5
- import type { Term as N3Term } from 'n3';
12
+ import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
6
13
 
7
14
  import SolidDocument from '@/models/SolidDocument';
8
15
 
@@ -10,9 +17,11 @@ import MalformedSolidDocumentError, { SolidDocumentFormat } from '@/errors/Malfo
10
17
  import NetworkRequestError from '@/errors/NetworkRequestError';
11
18
  import NotFoundError from '@/errors/NotFoundError';
12
19
  import UnauthorizedError from '@/errors/UnauthorizedError';
20
+ import { isJsonLDGraph } from '@/helpers/jsonld';
21
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
13
22
 
14
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- export declare type AnyFetch = (input: any, options?: any) => Promise<any>;
24
+ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
16
25
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
17
26
  export declare type Fetch = TypedFetch | AnyFetch;
18
27
 
@@ -40,7 +49,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
40
49
  if (error instanceof UnauthorizedError)
41
50
  throw error;
42
51
 
43
- throw new NetworkRequestError(url);
52
+ if (error instanceof NotFoundError)
53
+ throw error;
54
+
55
+ throw new NetworkRequestError(url, { cause: error });
44
56
  }
45
57
  }
46
58
 
@@ -48,26 +60,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
48
60
  const normalizedQuads = quads.slice(0);
49
61
  const quadsIndexes: Record<string, Set<number>> = {};
50
62
  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
- })
63
+ .flatMap(
64
+ (quad, index) => tap(
65
+ arrayFilter([
66
+ quad.object.termType === 'BlankNode' ? quad.object.value : null,
67
+ quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
68
+ ]),
69
+ ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
70
+ ),
71
+ )
64
72
  .filter()
65
73
  .unique();
66
74
 
67
75
  for (const originalId of blankNodeIds) {
76
+ const quadIndexes = quadsIndexes[originalId] as Set<number>;
68
77
  const normalizedId = md5(
69
- arr(quadsIndexes[originalId])
70
- .map(index => quads[index])
78
+ arr(quadIndexes)
79
+ .map(index => quads[index] as Quad)
71
80
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
72
81
  .map(
73
82
  ({ predicate, object }) => object.termType === 'BlankNode'
@@ -78,18 +87,29 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
78
87
  .join(),
79
88
  );
80
89
 
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 };
90
+ for (const index of quadIndexes) {
91
+ const quad = normalizedQuads[index] as Quad;
92
+ const terms: Record<string, Term> = {
93
+ subject: quad.subject as Term,
94
+ object: quad.object as Term,
95
+ };
84
96
 
85
97
  for (const [termName, termValue] of Object.entries(terms)) {
86
98
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
87
99
  continue;
88
100
 
89
- terms[termName] = new N3BlankNode(normalizedId);
101
+ terms[termName] = createBlankNode(normalizedId) as Term;
90
102
  }
91
103
 
92
- arrayReplace(normalizedQuads, quad, new N3Quad(terms.subject, quad.predicate as N3Term, terms.object));
104
+ arrayReplace(
105
+ normalizedQuads,
106
+ quad,
107
+ createQuad(
108
+ terms.subject as Term,
109
+ quad.predicate as Term,
110
+ terms.object as Term,
111
+ ),
112
+ );
93
113
  }
94
114
  }
95
115
 
@@ -97,12 +117,17 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
97
117
  }
98
118
 
99
119
  export interface ParsingOptions {
100
- documentUrl: string;
120
+ baseIRI: string;
101
121
  normalizeBlankNodes: boolean;
102
122
  }
103
123
 
124
+ export interface RDFGraphData {
125
+ quads: Quad[];
126
+ containsRelativeIRIs: boolean;
127
+ }
128
+
104
129
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
105
- fetch = fetch ?? window.fetch;
130
+ fetch = fetch ?? window.fetch.bind(window);
106
131
 
107
132
  const statements = await turtleToQuads(body);
108
133
 
@@ -117,11 +142,34 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
117
142
 
118
143
  export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
119
144
  const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
120
- const statements = await turtleToQuads(data, { documentUrl: url });
145
+ const statements = await turtleToQuads(data, { baseIRI: url });
121
146
 
122
147
  return new SolidDocument(url, statements, headers);
123
148
  }
124
149
 
150
+ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
151
+ try {
152
+ const document = await fetchSolidDocument(url, fetch);
153
+
154
+ return document;
155
+ } catch (error) {
156
+ if (!(error instanceof NotFoundError))
157
+ throw error;
158
+
159
+ return null;
160
+ }
161
+ }
162
+
163
+ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
164
+ if (isJsonLDGraph(jsonld)) {
165
+ const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
166
+
167
+ return graphQuads.flat();
168
+ }
169
+
170
+ return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
171
+ }
172
+
125
173
  export function normalizeSparql(sparql: string): string {
126
174
  const quads = sparqlToQuadsSync(sparql);
127
175
 
@@ -135,6 +183,66 @@ export function normalizeSparql(sparql: string): string {
135
183
  .join(' ;\n');
136
184
  }
137
185
 
186
+ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
187
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
188
+ const parser = new TurtleParser(parserOptions);
189
+ const data: RDFGraphData = {
190
+ quads: [],
191
+ containsRelativeIRIs: false,
192
+ };
193
+
194
+ return new Promise((resolve, reject) => {
195
+ const resolveRelativeIRI = parser._resolveRelativeIRI;
196
+
197
+ parser._resolveRelativeIRI = (...args) => {
198
+ data.containsRelativeIRIs = true;
199
+ parser._resolveRelativeIRI = resolveRelativeIRI;
200
+
201
+ return parser._resolveRelativeIRI(...args);
202
+ };
203
+
204
+ parser.parse(turtle, (error, quad) => {
205
+ if (error) {
206
+ reject(
207
+ new MalformedSolidDocumentError(
208
+ options.baseIRI ?? null,
209
+ SolidDocumentFormat.Turtle,
210
+ error.message,
211
+ ),
212
+ );
213
+
214
+ return;
215
+ }
216
+
217
+ if (!quad) {
218
+ // data.quads = options.normalizeBlankNodes
219
+ // ? normalizeBlankNodes(data.quads)
220
+ // : data.quads;
221
+
222
+ resolve(data);
223
+
224
+ return;
225
+ }
226
+
227
+ data.quads.push(quad);
228
+ });
229
+ });
230
+ }
231
+
232
+ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
233
+ const graph = await jsonLDFromRDF(quads);
234
+
235
+ return {
236
+ '@graph': graph as JsonLDResource[],
237
+ };
238
+ }
239
+
240
+ export function quadsToTurtle(quads: Quad[]): string {
241
+ const writer = new TurtleWriter;
242
+
243
+ return writer.quadsToString(quads);
244
+ }
245
+
138
246
  export function quadToTurtle(quad: Quad): string {
139
247
  const writer = new TurtleWriter;
140
248
 
@@ -155,7 +263,7 @@ export async function sparqlToQuads(
155
263
  sparql: string,
156
264
  options: Partial<ParsingOptions> = {},
157
265
  ): Promise<Record<string, Quad[]>> {
158
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
266
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
159
267
  const quads: Record<string, Quad[]> = {};
160
268
 
161
269
  await Promise.all([...operations].map(async operation => {
@@ -169,7 +277,7 @@ export async function sparqlToQuads(
169
277
  }
170
278
 
171
279
  export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
172
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
280
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
173
281
  const quads: Record<string, Quad[]> = {};
174
282
 
175
283
  for (const operation of operations) {
@@ -183,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
183
291
  }
184
292
 
185
293
  export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
186
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
187
- const parser = new TurtleParser(parserOptions);
188
- const quads: Quad[] = [];
189
-
190
- return new Promise((resolve, reject) => {
191
- parser.parse(turtle, (error, quad) => {
192
- if (error) {
193
- reject(
194
- new MalformedSolidDocumentError(
195
- options.documentUrl ?? null,
196
- SolidDocumentFormat.Turtle,
197
- error.message,
198
- ),
199
- );
200
- return;
201
- }
202
-
203
- if (!quad) {
204
- options.normalizeBlankNodes
205
- ? resolve(normalizeBlankNodes(quads))
206
- : resolve(quads);
207
-
208
- return;
209
- }
294
+ const { quads } = await parseTurtle(turtle, options);
210
295
 
211
- quads.push(quad);
212
- });
213
- });
296
+ return quads;
214
297
  }
215
298
 
216
299
  export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
217
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
300
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
218
301
  const parser = new TurtleParser(parserOptions);
219
302
 
220
303
  try {
@@ -224,12 +307,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
224
307
  ? normalizeBlankNodes(quads)
225
308
  : quads;
226
309
  } catch (error) {
227
- throw new MalformedSolidDocumentError(options.documentUrl ?? null, SolidDocumentFormat.Turtle, error.message);
310
+ throw new MalformedSolidDocumentError(
311
+ options.baseIRI ?? null,
312
+ SolidDocumentFormat.Turtle,
313
+ (error as Error).message ?? '',
314
+ );
228
315
  }
229
316
  }
230
317
 
231
318
  export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
232
- fetch = fetch ?? window.fetch;
319
+ fetch = fetch ?? window.fetch.bind(window);
233
320
 
234
321
  await fetch(url, {
235
322
  method: 'PATCH',
@@ -1,3 +1,6 @@
1
+ import { compactJsonLD } from '@noeldemartin/solid-utils-external';
2
+ import type { JsonLdDocument } from '@noeldemartin/solid-utils-external';
3
+
1
4
  export type JsonLD = Partial<{
2
5
  '@context': Record<string, unknown>;
3
6
  '@id': string;
@@ -5,4 +8,25 @@ export type JsonLD = Partial<{
5
8
  }> & { [k: string]: unknown };
6
9
 
7
10
  export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
8
- export type JsonLDGraph = { '@graph': JsonLDResource[] };
11
+ export type JsonLDGraph = {
12
+ '@context'?: Record<string, unknown>;
13
+ '@graph': JsonLDResource[];
14
+ };
15
+
16
+ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
17
+ const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
18
+
19
+ if ('@graph' in compactedJsonLD) {
20
+ return compactedJsonLD as JsonLDGraph;
21
+ }
22
+
23
+ if ('@id' in compactedJsonLD) {
24
+ return { '@graph': [compactedJsonLD] } as JsonLDGraph;
25
+ }
26
+
27
+ return { '@graph': [] };
28
+ }
29
+
30
+ export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
31
+ return '@graph' in jsonld;
32
+ }
@@ -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,55 @@
1
+ import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
2
+
3
+ import UnsupportedAuthorizationProtocolError from '@/errors/UnsupportedAuthorizationProtocolError';
4
+ import { fetchSolidDocumentIfFound } from '@/helpers/io';
5
+ import type SolidDocument from '@/models/SolidDocument';
6
+ import type { Fetch } from '@/helpers/io';
7
+
8
+ async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
9
+ fetch = fetch ?? window.fetch.bind(window);
10
+
11
+ const resourceHead = await fetch(resourceUrl, { method: 'HEAD' });
12
+ const linkHeader = resourceHead.headers.get('Link') ?? '';
13
+ const url = linkHeader.match(/<([^>]+)>;\s*rel="acl"/)?.[1] ?? null;
14
+
15
+ if (!url) {
16
+ throw new Error(`Could not find ACL Resource for '${resourceUrl}'`);
17
+ }
18
+
19
+ return urlResolve(requireUrlParentDirectory(resourceUrl), url);
20
+ }
21
+
22
+ async function fetchEffectiveACL(
23
+ resourceUrl: string,
24
+ fetch: Fetch,
25
+ aclResourceUrl?: string | null,
26
+ ): Promise<SolidDocument> {
27
+ aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
28
+
29
+ const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch);
30
+
31
+ if (!aclDocument) {
32
+ return fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
33
+ }
34
+
35
+ if (aclDocument.isACPResource()) {
36
+ throw new UnsupportedAuthorizationProtocolError(resourceUrl, 'ACP');
37
+ }
38
+
39
+ return aclDocument;
40
+ }
41
+
42
+ export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
43
+ url: string;
44
+ effectiveUrl: string;
45
+ document: SolidDocument;
46
+ }> {
47
+ const url = await fetchACLResourceUrl(documentUrl, fetch);
48
+ const document = await fetchEffectiveACL(documentUrl, fetch, url);
49
+
50
+ return objectWithoutEmpty({
51
+ url,
52
+ effectiveUrl: document.url,
53
+ document,
54
+ });
55
+ }