@noeldemartin/solid-utils 0.5.0 → 0.6.0-next.508449b33de64b0bcade86b642c9793381434231
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.
- package/dist/chai.d.ts +31 -0
- package/dist/chai.js +23 -0
- package/dist/chai.js.map +1 -0
- package/dist/helpers-DGXMj9cx.js +107 -0
- package/dist/helpers-DGXMj9cx.js.map +1 -0
- package/dist/io-wCcrq4b9.js +401 -0
- package/dist/io-wCcrq4b9.js.map +1 -0
- package/dist/noeldemartin-solid-utils.d.ts +61 -65
- package/dist/noeldemartin-solid-utils.js +224 -0
- package/dist/noeldemartin-solid-utils.js.map +1 -0
- package/dist/testing.d.ts +40 -0
- package/dist/testing.js +7 -0
- package/dist/testing.js.map +1 -0
- package/dist/vitest.d.ts +50 -0
- package/dist/vitest.js +55 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +67 -63
- package/src/chai/assertions.ts +35 -0
- package/src/chai/index.ts +19 -0
- package/src/errors/UnauthorizedError.ts +1 -3
- package/src/errors/UnsuccessfulNetworkRequestError.ts +2 -2
- package/src/helpers/auth.test.ts +221 -0
- package/src/helpers/auth.ts +28 -27
- package/src/helpers/identifiers.test.ts +76 -0
- package/src/helpers/identifiers.ts +14 -17
- package/src/helpers/index.ts +0 -1
- package/src/helpers/interop.ts +23 -16
- package/src/helpers/io.test.ts +228 -0
- package/src/helpers/io.ts +57 -77
- package/src/helpers/jsonld.ts +6 -6
- package/src/helpers/vocabs.ts +3 -6
- package/src/helpers/wac.test.ts +64 -0
- package/src/helpers/wac.ts +10 -6
- package/src/index.ts +4 -0
- package/src/models/SolidDocument.test.ts +77 -0
- package/src/models/SolidDocument.ts +14 -18
- package/src/models/SolidStore.ts +22 -12
- package/src/models/SolidThing.ts +5 -7
- package/src/models/index.ts +2 -0
- package/src/{helpers/testing.ts → testing/helpers.ts} +24 -27
- package/src/testing/hepers.test.ts +329 -0
- package/src/testing/index.ts +1 -2
- package/src/types/index.ts +2 -0
- package/src/types/n3.d.ts +0 -2
- package/src/vitest/index.ts +20 -0
- package/src/vitest/matchers.ts +68 -0
- package/.github/workflows/ci.yml +0 -16
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -70
- package/dist/noeldemartin-solid-utils.cjs.js +0 -2
- package/dist/noeldemartin-solid-utils.cjs.js.map +0 -1
- package/dist/noeldemartin-solid-utils.esm.js +0 -2
- package/dist/noeldemartin-solid-utils.esm.js.map +0 -1
- package/dist/noeldemartin-solid-utils.umd.js +0 -90
- package/dist/noeldemartin-solid-utils.umd.js.map +0 -1
- package/noeldemartin.config.js +0 -9
- package/src/main.ts +0 -5
- package/src/plugins/chai/assertions.ts +0 -40
- package/src/plugins/chai/index.ts +0 -5
- package/src/plugins/cypress/types.d.ts +0 -15
- package/src/plugins/index.ts +0 -2
- package/src/plugins/jest/index.ts +0 -5
- package/src/plugins/jest/matchers.ts +0 -65
- package/src/plugins/jest/types.d.ts +0 -14
- package/src/testing/ResponseStub.ts +0 -46
- package/src/testing/mocking.ts +0 -33
package/src/helpers/io.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
+
import jsonld from 'jsonld';
|
|
1
2
|
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
3
|
import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
|
|
11
|
-
import
|
|
12
|
-
import type { JsonLdDocument
|
|
4
|
+
import { BlankNode as N3BlankNode, Quad as N3Quad, Parser, Writer } from 'n3';
|
|
5
|
+
import type { JsonLdDocument } from 'jsonld';
|
|
6
|
+
import type { Quad } from '@rdfjs/types';
|
|
7
|
+
import type { Term } from 'n3';
|
|
13
8
|
|
|
14
|
-
import SolidDocument from '
|
|
9
|
+
import SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
11
|
+
// eslint-disable-next-line max-len
|
|
12
|
+
import MalformedSolidDocumentError, { SolidDocumentFormat } from '@noeldemartin/solid-utils/errors/MalformedSolidDocumentError';
|
|
13
|
+
import NetworkRequestError from '@noeldemartin/solid-utils/errors/NetworkRequestError';
|
|
14
|
+
import NotFoundError from '@noeldemartin/solid-utils/errors/NotFoundError';
|
|
15
|
+
import UnauthorizedError from '@noeldemartin/solid-utils/errors/UnauthorizedError';
|
|
16
|
+
import { isJsonLDGraph } from '@noeldemartin/solid-utils/helpers/jsonld';
|
|
17
|
+
import type { JsonLD, JsonLDGraph, JsonLDResource } from '@noeldemartin/solid-utils/helpers/jsonld';
|
|
22
18
|
|
|
23
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
20
|
export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
|
|
@@ -44,11 +40,9 @@ async function fetchRawSolidDocument(
|
|
|
44
40
|
const fetch = options?.fetch ?? window.fetch;
|
|
45
41
|
const response = await fetch(url, requestOptions);
|
|
46
42
|
|
|
47
|
-
if (response.status === 404)
|
|
48
|
-
throw new NotFoundError(url);
|
|
43
|
+
if (response.status === 404) throw new NotFoundError(url);
|
|
49
44
|
|
|
50
|
-
if ([401, 403].includes(response.status))
|
|
51
|
-
throw new UnauthorizedError(url, response.status);
|
|
45
|
+
if ([401, 403].includes(response.status)) throw new UnauthorizedError(url, response.status);
|
|
52
46
|
|
|
53
47
|
const body = await response.text();
|
|
54
48
|
|
|
@@ -57,11 +51,9 @@ async function fetchRawSolidDocument(
|
|
|
57
51
|
headers: response.headers,
|
|
58
52
|
};
|
|
59
53
|
} catch (error) {
|
|
60
|
-
if (error instanceof UnauthorizedError)
|
|
61
|
-
throw error;
|
|
54
|
+
if (error instanceof UnauthorizedError) throw error;
|
|
62
55
|
|
|
63
|
-
if (error instanceof NotFoundError)
|
|
64
|
-
throw error;
|
|
56
|
+
if (error instanceof NotFoundError) throw error;
|
|
65
57
|
|
|
66
58
|
throw new NetworkRequestError(url, { cause: error });
|
|
67
59
|
}
|
|
@@ -71,15 +63,14 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
71
63
|
const normalizedQuads = quads.slice(0);
|
|
72
64
|
const quadsIndexes: Record<string, Set<number>> = {};
|
|
73
65
|
const blankNodeIds = arr(quads)
|
|
74
|
-
.flatMap(
|
|
75
|
-
|
|
66
|
+
.flatMap((quad, index) =>
|
|
67
|
+
tap(
|
|
76
68
|
arrayFilter([
|
|
77
69
|
quad.object.termType === 'BlankNode' ? quad.object.value : null,
|
|
78
70
|
quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
|
|
79
71
|
]),
|
|
80
|
-
ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
|
|
81
|
-
)
|
|
82
|
-
)
|
|
72
|
+
(ids) => ids.forEach((id) => (quadsIndexes[id] ??= new Set()).add(index)),
|
|
73
|
+
))
|
|
83
74
|
.filter()
|
|
84
75
|
.unique();
|
|
85
76
|
|
|
@@ -87,13 +78,10 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
87
78
|
const quadIndexes = quadsIndexes[originalId] as Set<number>;
|
|
88
79
|
const normalizedId = md5(
|
|
89
80
|
arr(quadIndexes)
|
|
90
|
-
.map(index => quads[index] as Quad)
|
|
81
|
+
.map((index) => quads[index] as Quad)
|
|
91
82
|
.filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
|
|
92
|
-
.map(
|
|
93
|
-
|
|
94
|
-
? predicate.value
|
|
95
|
-
: predicate.value + object.value,
|
|
96
|
-
)
|
|
83
|
+
.map(({ predicate, object }) =>
|
|
84
|
+
object.termType === 'BlankNode' ? predicate.value : predicate.value + object.value)
|
|
97
85
|
.sorted()
|
|
98
86
|
.join(),
|
|
99
87
|
);
|
|
@@ -106,20 +94,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
106
94
|
};
|
|
107
95
|
|
|
108
96
|
for (const [termName, termValue] of Object.entries(terms)) {
|
|
109
|
-
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
|
|
110
|
-
continue;
|
|
97
|
+
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId) continue;
|
|
111
98
|
|
|
112
|
-
terms[termName] =
|
|
99
|
+
terms[termName] = new N3BlankNode(normalizedId) as Term;
|
|
113
100
|
}
|
|
114
101
|
|
|
115
102
|
arrayReplace(
|
|
116
103
|
normalizedQuads,
|
|
117
104
|
quad,
|
|
118
|
-
|
|
119
|
-
terms.subject as Term,
|
|
120
|
-
quad.predicate as Term,
|
|
121
|
-
terms.object as Term,
|
|
122
|
-
),
|
|
105
|
+
new N3Quad(terms.subject as Term, quad.predicate as Term, terms.object as Term),
|
|
123
106
|
);
|
|
124
107
|
}
|
|
125
108
|
}
|
|
@@ -128,15 +111,18 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
128
111
|
}
|
|
129
112
|
|
|
130
113
|
function normalizeQuads(quads: Quad[]): string {
|
|
131
|
-
return quads
|
|
114
|
+
return quads
|
|
115
|
+
.map((quad) => ' ' + quadToTurtle(quad))
|
|
116
|
+
.sort()
|
|
117
|
+
.join('\n');
|
|
132
118
|
}
|
|
133
119
|
|
|
134
|
-
function preprocessSubjects(
|
|
135
|
-
if (!
|
|
120
|
+
function preprocessSubjects(json: JsonLD): void {
|
|
121
|
+
if (!json['@id']?.startsWith('#')) {
|
|
136
122
|
return;
|
|
137
123
|
}
|
|
138
124
|
|
|
139
|
-
|
|
125
|
+
json['@id'] = ANONYMOUS_PREFIX + json['@id'];
|
|
140
126
|
}
|
|
141
127
|
|
|
142
128
|
function postprocessSubjects(quads: Quad[]): void {
|
|
@@ -194,23 +180,22 @@ export async function fetchSolidDocumentIfFound(
|
|
|
194
180
|
|
|
195
181
|
return document;
|
|
196
182
|
} catch (error) {
|
|
197
|
-
if (!(error instanceof NotFoundError))
|
|
198
|
-
throw error;
|
|
183
|
+
if (!(error instanceof NotFoundError)) throw error;
|
|
199
184
|
|
|
200
185
|
return null;
|
|
201
186
|
}
|
|
202
187
|
}
|
|
203
188
|
|
|
204
|
-
export async function jsonldToQuads(
|
|
205
|
-
if (isJsonLDGraph(
|
|
206
|
-
const graphQuads = await Promise.all(
|
|
189
|
+
export async function jsonldToQuads(json: JsonLD, baseIRI?: string): Promise<Quad[]> {
|
|
190
|
+
if (isJsonLDGraph(json)) {
|
|
191
|
+
const graphQuads = await Promise.all(json['@graph'].map((resource) => jsonldToQuads(resource, baseIRI)));
|
|
207
192
|
|
|
208
193
|
return graphQuads.flat();
|
|
209
194
|
}
|
|
210
195
|
|
|
211
|
-
preprocessSubjects(
|
|
196
|
+
preprocessSubjects(json);
|
|
212
197
|
|
|
213
|
-
const quads = await (
|
|
198
|
+
const quads = await (jsonld.toRDF(json as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
|
|
214
199
|
|
|
215
200
|
postprocessSubjects(quads);
|
|
216
201
|
|
|
@@ -220,10 +205,9 @@ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Q
|
|
|
220
205
|
export function normalizeSparql(sparql: string): string {
|
|
221
206
|
const quads = sparqlToQuadsSync(sparql);
|
|
222
207
|
|
|
223
|
-
return Object
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
const normalizedQuads = normalizeQuads(quads);
|
|
208
|
+
return Object.entries(quads)
|
|
209
|
+
.reduce((normalizedOperations, [operation, _quads]) => {
|
|
210
|
+
const normalizedQuads = normalizeQuads(_quads);
|
|
227
211
|
|
|
228
212
|
return normalizedOperations.concat(`${operation.toUpperCase()} DATA {\n${normalizedQuads}\n}`);
|
|
229
213
|
}, [] as string[])
|
|
@@ -238,7 +222,7 @@ export function normalizeTurtle(sparql: string): string {
|
|
|
238
222
|
|
|
239
223
|
export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
|
|
240
224
|
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
241
|
-
const parser = new
|
|
225
|
+
const parser = new Parser(parserOptions);
|
|
242
226
|
const data: RDFGraphData = {
|
|
243
227
|
quads: [],
|
|
244
228
|
containsRelativeIRIs: false,
|
|
@@ -257,11 +241,7 @@ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {
|
|
|
257
241
|
parser.parse(turtle, (error, quad) => {
|
|
258
242
|
if (error) {
|
|
259
243
|
reject(
|
|
260
|
-
new MalformedSolidDocumentError(
|
|
261
|
-
options.baseIRI ?? null,
|
|
262
|
-
SolidDocumentFormat.Turtle,
|
|
263
|
-
error.message,
|
|
264
|
-
),
|
|
244
|
+
new MalformedSolidDocumentError(options.baseIRI ?? null, SolidDocumentFormat.Turtle, error.message),
|
|
265
245
|
);
|
|
266
246
|
|
|
267
247
|
return;
|
|
@@ -283,7 +263,7 @@ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {
|
|
|
283
263
|
}
|
|
284
264
|
|
|
285
265
|
export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
|
|
286
|
-
const graph = await
|
|
266
|
+
const graph = await jsonld.fromRDF(quads);
|
|
287
267
|
|
|
288
268
|
return {
|
|
289
269
|
'@graph': graph as JsonLDResource[],
|
|
@@ -291,13 +271,13 @@ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
|
|
|
291
271
|
}
|
|
292
272
|
|
|
293
273
|
export function quadsToTurtle(quads: Quad[]): string {
|
|
294
|
-
const writer = new
|
|
274
|
+
const writer = new Writer();
|
|
295
275
|
|
|
296
276
|
return writer.quadsToString(quads);
|
|
297
277
|
}
|
|
298
278
|
|
|
299
279
|
export function quadToTurtle(quad: Quad): string {
|
|
300
|
-
const writer = new
|
|
280
|
+
const writer = new Writer();
|
|
301
281
|
|
|
302
282
|
return writer.quadsToString([quad]).slice(0, -1);
|
|
303
283
|
}
|
|
@@ -319,12 +299,14 @@ export async function sparqlToQuads(
|
|
|
319
299
|
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
320
300
|
const quads: Record<string, Quad[]> = {};
|
|
321
301
|
|
|
322
|
-
await Promise.all(
|
|
323
|
-
|
|
324
|
-
|
|
302
|
+
await Promise.all(
|
|
303
|
+
[...operations].map(async (operation) => {
|
|
304
|
+
const operationName = operation[1].toLowerCase();
|
|
305
|
+
const operationBody = operation[2];
|
|
325
306
|
|
|
326
|
-
|
|
327
|
-
|
|
307
|
+
quads[operationName] = await turtleToQuads(operationBody, options);
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
328
310
|
|
|
329
311
|
return quads;
|
|
330
312
|
}
|
|
@@ -351,14 +333,12 @@ export async function turtleToQuads(turtle: string, options: Partial<ParsingOpti
|
|
|
351
333
|
|
|
352
334
|
export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
|
|
353
335
|
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
354
|
-
const parser = new
|
|
336
|
+
const parser = new Parser(parserOptions);
|
|
355
337
|
|
|
356
338
|
try {
|
|
357
339
|
const quads = parser.parse(turtle);
|
|
358
340
|
|
|
359
|
-
return options.normalizeBlankNodes
|
|
360
|
-
? normalizeBlankNodes(quads)
|
|
361
|
-
: quads;
|
|
341
|
+
return options.normalizeBlankNodes ? normalizeBlankNodes(quads) : quads;
|
|
362
342
|
} catch (error) {
|
|
363
343
|
throw new MalformedSolidDocumentError(
|
|
364
344
|
options.baseIRI ?? null,
|
package/src/helpers/jsonld.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { JsonLdDocument } from '
|
|
1
|
+
import jsonld from 'jsonld';
|
|
2
|
+
import type { JsonLdDocument } from 'jsonld';
|
|
3
3
|
|
|
4
4
|
export type JsonLD = Partial<{
|
|
5
5
|
'@context': Record<string, unknown>;
|
|
@@ -13,8 +13,8 @@ export type JsonLDGraph = {
|
|
|
13
13
|
'@graph': JsonLDResource[];
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export async function compactJsonLDGraph(
|
|
17
|
-
const compactedJsonLD = await
|
|
16
|
+
export async function compactJsonLDGraph(json: JsonLDGraph): Promise<JsonLDGraph> {
|
|
17
|
+
const compactedJsonLD = await jsonld.compact(json as JsonLdDocument, {});
|
|
18
18
|
|
|
19
19
|
if ('@graph' in compactedJsonLD) {
|
|
20
20
|
return compactedJsonLD as JsonLDGraph;
|
|
@@ -27,6 +27,6 @@ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGra
|
|
|
27
27
|
return { '@graph': [] };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function isJsonLDGraph(
|
|
31
|
-
return '@graph' in
|
|
30
|
+
export function isJsonLDGraph(json: JsonLD): json is JsonLDGraph {
|
|
31
|
+
return '@graph' in json;
|
|
32
32
|
}
|
package/src/helpers/vocabs.ts
CHANGED
|
@@ -23,22 +23,19 @@ export function defineIRIPrefix(name: string, value: string): void {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}): string {
|
|
26
|
-
if (iri.startsWith('http'))
|
|
27
|
-
return iri;
|
|
26
|
+
if (iri.startsWith('http')) return iri;
|
|
28
27
|
|
|
29
28
|
const [prefix, name] = iri.split(':');
|
|
30
29
|
|
|
31
30
|
if (prefix && name) {
|
|
32
31
|
const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
|
|
33
32
|
|
|
34
|
-
if (!expandedPrefix)
|
|
35
|
-
throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
|
|
33
|
+
if (!expandedPrefix) throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
|
|
36
34
|
|
|
37
35
|
return expandedPrefix + name;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
if (!options.defaultPrefix)
|
|
41
|
-
throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
|
|
38
|
+
if (!options.defaultPrefix) throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
|
|
42
39
|
|
|
43
40
|
return options.defaultPrefix + prefix;
|
|
44
41
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { FakeResponse, FakeServer } from '@noeldemartin/testing';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line max-len
|
|
5
|
+
import UnsupportedAuthorizationProtocolError from '@noeldemartin/solid-utils/errors/UnsupportedAuthorizationProtocolError';
|
|
6
|
+
|
|
7
|
+
import { fetchSolidDocumentACL } from './wac';
|
|
8
|
+
|
|
9
|
+
describe('WAC helpers', () => {
|
|
10
|
+
|
|
11
|
+
it('resolves relative ACL urls', async () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const server = new FakeServer();
|
|
14
|
+
const documentUrl = 'https://example.com/alice/movies/my-favorite-movie';
|
|
15
|
+
|
|
16
|
+
server.respondOnce(documentUrl, FakeResponse.success(undefined, { Link: '<my-favorite-movie.acl>;rel="acl"' }));
|
|
17
|
+
server.respondOnce(
|
|
18
|
+
`${documentUrl}.acl`,
|
|
19
|
+
FakeResponse.success(`
|
|
20
|
+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
21
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
22
|
+
|
|
23
|
+
<#owner>
|
|
24
|
+
a acl:Authorization;
|
|
25
|
+
acl:agent <https://example.com/alice/profile/card#me>;
|
|
26
|
+
acl:accessTo <./my-favorite-movie> ;
|
|
27
|
+
acl:mode acl:Read, acl:Write, acl:Control .
|
|
28
|
+
`),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const { url, effectiveUrl, document } = await fetchSolidDocumentACL(documentUrl, server.fetch);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(url).toEqual(`${documentUrl}.acl`);
|
|
36
|
+
expect(effectiveUrl).toEqual(url);
|
|
37
|
+
expect(document.contains(`${documentUrl}.acl#owner`, 'rdf:type', 'acl:Authorization')).toBe(true);
|
|
38
|
+
|
|
39
|
+
expect(server.getRequests()).toHaveLength(2);
|
|
40
|
+
expect(server.getRequest(documentUrl)?.method).toEqual('HEAD');
|
|
41
|
+
expect(server.getRequest(url)).not.toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('fails with ACP resources', async () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const server = new FakeServer();
|
|
47
|
+
const documentUrl = 'https://example.com/alice/movies/my-favorite-movie';
|
|
48
|
+
|
|
49
|
+
server.respondOnce(documentUrl, FakeResponse.success(undefined, { Link: '<my-favorite-movie.acl>;rel="acl"' }));
|
|
50
|
+
server.respondOnce(
|
|
51
|
+
`${documentUrl}.acl`,
|
|
52
|
+
FakeResponse.success(undefined, {
|
|
53
|
+
Link: '<http://www.w3.org/ns/solid/acp#AccessControlResource>; rel="type"',
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Act
|
|
58
|
+
const promisedDocument = fetchSolidDocumentACL(documentUrl, server.fetch);
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
await expect(promisedDocument).rejects.toBeInstanceOf(UnsupportedAuthorizationProtocolError);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
});
|
package/src/helpers/wac.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import type
|
|
3
|
+
// eslint-disable-next-line max-len
|
|
4
|
+
import UnsupportedAuthorizationProtocolError from '@noeldemartin/solid-utils/errors/UnsupportedAuthorizationProtocolError';
|
|
5
|
+
import { fetchSolidDocumentIfFound } from '@noeldemartin/solid-utils/helpers/io';
|
|
6
|
+
import type SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
|
|
7
|
+
import type { Fetch } from '@noeldemartin/solid-utils/helpers/io';
|
|
7
8
|
|
|
8
9
|
async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
|
|
9
10
|
fetch = fetch ?? window.fetch.bind(window);
|
|
@@ -24,7 +25,7 @@ async function fetchEffectiveACL(
|
|
|
24
25
|
fetch: Fetch,
|
|
25
26
|
aclResourceUrl?: string | null,
|
|
26
27
|
): Promise<SolidDocument> {
|
|
27
|
-
aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
|
|
28
|
+
aclResourceUrl = aclResourceUrl ?? (await fetchACLResourceUrl(resourceUrl, fetch));
|
|
28
29
|
|
|
29
30
|
const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', { fetch });
|
|
30
31
|
|
|
@@ -39,7 +40,10 @@ async function fetchEffectiveACL(
|
|
|
39
40
|
return aclDocument;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export async function fetchSolidDocumentACL(
|
|
43
|
+
export async function fetchSolidDocumentACL(
|
|
44
|
+
documentUrl: string,
|
|
45
|
+
fetch: Fetch,
|
|
46
|
+
): Promise<{
|
|
43
47
|
url: string;
|
|
44
48
|
effectiveUrl: string;
|
|
45
49
|
document: SolidDocument;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
|
|
4
|
+
import { turtleToQuadsSync } from '@noeldemartin/solid-utils/helpers';
|
|
5
|
+
|
|
6
|
+
describe('SolidDocument', () => {
|
|
7
|
+
|
|
8
|
+
it('Identifies storage documents', () => {
|
|
9
|
+
const hasStorageHeader = (link: string) => {
|
|
10
|
+
const document = new SolidDocument('', [], new Headers({ Link: link }));
|
|
11
|
+
|
|
12
|
+
return document.isStorage();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/* eslint-disable max-len */
|
|
16
|
+
expect(hasStorageHeader('')).toBe(false);
|
|
17
|
+
expect(hasStorageHeader('<http://www.w3.org/ns/pim/space#Storage>; rel="type"')).toBe(true);
|
|
18
|
+
expect(hasStorageHeader('<http://www.w3.org/ns/pim/space#Storage>; rel="something-else"; rel="type"')).toBe(
|
|
19
|
+
true,
|
|
20
|
+
);
|
|
21
|
+
expect(
|
|
22
|
+
hasStorageHeader(
|
|
23
|
+
'<http://www.w3.org/ns/pim/space#Storage>; rel="something-else", <http://example.com>; rel="type"',
|
|
24
|
+
),
|
|
25
|
+
).toBe(false);
|
|
26
|
+
/* eslint-enable max-len */
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('Parses last modified from header', () => {
|
|
30
|
+
const document = new SolidDocument('', [], new Headers({ 'Last-Modified': 'Fri, 03 Sept 2021 16:09:12 GMT' }));
|
|
31
|
+
|
|
32
|
+
expect(document.getLastModified()).toEqual(new Date(1630685352000));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('Parses last modified from document purl:modified', () => {
|
|
36
|
+
const document = new SolidDocument(
|
|
37
|
+
'https://pod.example.org/my-document',
|
|
38
|
+
turtleToQuadsSync(
|
|
39
|
+
`
|
|
40
|
+
<./fallback>
|
|
41
|
+
<http://purl.org/dc/terms/modified>
|
|
42
|
+
"2021-09-03T16:23:25.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
43
|
+
|
|
44
|
+
<>
|
|
45
|
+
<http://purl.org/dc/terms/modified>
|
|
46
|
+
"2021-09-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
47
|
+
`,
|
|
48
|
+
{ baseIRI: 'https://pod.example.org/my-document' },
|
|
49
|
+
),
|
|
50
|
+
new Headers({ 'Last-Modified': 'invalid date' }),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(document.getLastModified()).toEqual(new Date(1630685352000));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('Parses last modified from any purl date', () => {
|
|
57
|
+
const document = new SolidDocument(
|
|
58
|
+
'https://pod.example.org/my-document',
|
|
59
|
+
turtleToQuadsSync(
|
|
60
|
+
`
|
|
61
|
+
<./fallback-one>
|
|
62
|
+
<http://purl.org/dc/terms/modified>
|
|
63
|
+
"2021-05-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
64
|
+
|
|
65
|
+
<./fallback-two>
|
|
66
|
+
<http://purl.org/dc/terms/created>
|
|
67
|
+
"2021-09-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
68
|
+
`,
|
|
69
|
+
{ baseIRI: 'https://pod.example.org/my-document' },
|
|
70
|
+
),
|
|
71
|
+
new Headers({ 'Last-Modified': 'invalid date' }),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(document.getLastModified()).toEqual(new Date(1630685352000));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { arrayFilter, parseDate, stringMatch } from '@noeldemartin/utils';
|
|
2
|
-
import type { Quad } from '
|
|
2
|
+
import type { Quad } from '@rdfjs/types';
|
|
3
3
|
|
|
4
|
-
import { expandIRI } from '
|
|
4
|
+
import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
|
|
5
5
|
|
|
6
6
|
import SolidStore from './SolidStore';
|
|
7
7
|
|
|
@@ -25,16 +25,13 @@ export default class SolidDocument extends SolidStore {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
public isACPResource(): boolean {
|
|
28
|
-
return !!this.headers
|
|
28
|
+
return !!this.headers
|
|
29
|
+
.get('Link')
|
|
29
30
|
?.match(/<http:\/\/www\.w3\.org\/ns\/solid\/acp#AccessControlResource>;[^,]+rel="type"/);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
public isPersonalProfile(): boolean {
|
|
33
|
-
return !!this.statement(
|
|
34
|
-
this.url,
|
|
35
|
-
expandIRI('rdf:type'),
|
|
36
|
-
expandIRI('foaf:PersonalProfileDocument'),
|
|
37
|
-
);
|
|
34
|
+
return !!this.statement(this.url, expandIRI('rdf:type'), expandIRI('foaf:PersonalProfileDocument'));
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
public isStorage(): boolean {
|
|
@@ -54,10 +51,12 @@ export default class SolidDocument extends SolidStore {
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
public getLastModified(): Date | null {
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
??
|
|
54
|
+
return (
|
|
55
|
+
parseDate(this.headers.get('last-modified')) ??
|
|
56
|
+
parseDate(this.statement(this.url, 'purl:modified')?.object.value) ??
|
|
57
|
+
this.getLatestDocumentDate() ??
|
|
58
|
+
null
|
|
59
|
+
);
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
protected expandIRI(iri: string): string {
|
|
@@ -65,14 +64,11 @@ export default class SolidDocument extends SolidStore {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
private getLatestDocumentDate(): Date | null {
|
|
68
|
-
const dates = [
|
|
69
|
-
|
|
70
|
-
...this.statements(undefined, 'purl:created'),
|
|
71
|
-
]
|
|
72
|
-
.map(statement => parseDate(statement.object.value))
|
|
67
|
+
const dates = [...this.statements(undefined, 'purl:modified'), ...this.statements(undefined, 'purl:created')]
|
|
68
|
+
.map((statement) => parseDate(statement.object.value))
|
|
73
69
|
.filter((date): date is Date => date !== null);
|
|
74
70
|
|
|
75
|
-
return dates.length > 0 ? dates.reduce((a, b) => a > b ? a : b) : null;
|
|
71
|
+
return dates.length > 0 ? dates.reduce((a, b) => (a > b ? a : b)) : null;
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
private getPermissionsFromWAC(type: string): SolidDocumentPermission[] {
|
package/src/models/SolidStore.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Quad } from '
|
|
1
|
+
import type { BlankNode, Literal, NamedNode, Quad, Variable } from '@rdfjs/types';
|
|
2
2
|
|
|
3
|
-
import { expandIRI } from '
|
|
3
|
+
import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
|
|
4
4
|
|
|
5
5
|
import SolidThing from './SolidThing';
|
|
6
6
|
|
|
7
|
+
export type Term = NamedNode | Literal | BlankNode | Quad | Variable;
|
|
8
|
+
|
|
7
9
|
export default class SolidStore {
|
|
8
10
|
|
|
9
11
|
private quads: Quad[];
|
|
@@ -24,21 +26,21 @@ export default class SolidStore {
|
|
|
24
26
|
this.quads.push(...quads);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
public statements(subject?: string, predicate?: string, object?: string): Quad[] {
|
|
29
|
+
public statements(subject?: Term | string, predicate?: Term | string, object?: Term | string): Quad[] {
|
|
28
30
|
return this.quads.filter(
|
|
29
|
-
statement =>
|
|
30
|
-
(!object || statement.object
|
|
31
|
-
(!subject || statement.subject
|
|
32
|
-
(!predicate || statement.predicate
|
|
31
|
+
(statement) =>
|
|
32
|
+
(!object || this.termMatches(statement.object, object)) &&
|
|
33
|
+
(!subject || this.termMatches(statement.subject, subject)) &&
|
|
34
|
+
(!predicate || this.termMatches(statement.predicate, predicate)),
|
|
33
35
|
);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
public statement(subject?: string, predicate?: string, object?: string): Quad | null {
|
|
38
|
+
public statement(subject?: Term | string, predicate?: Term | string, object?: Term | string): Quad | null {
|
|
37
39
|
const statement = this.quads.find(
|
|
38
|
-
|
|
39
|
-
(!object ||
|
|
40
|
-
(!subject ||
|
|
41
|
-
(!predicate ||
|
|
40
|
+
(_statement) =>
|
|
41
|
+
(!object || this.termMatches(_statement.object, object)) &&
|
|
42
|
+
(!subject || this.termMatches(_statement.subject, subject)) &&
|
|
43
|
+
(!predicate || this.termMatches(_statement.predicate, predicate)),
|
|
42
44
|
);
|
|
43
45
|
|
|
44
46
|
return statement ?? null;
|
|
@@ -58,4 +60,12 @@ export default class SolidStore {
|
|
|
58
60
|
return expandIRI(iri);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
protected termMatches(term: Term, value: Term | string): boolean {
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
return this.expandIRI(value) === term.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return term.termType === term.termType && term.value === value.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
}
|
package/src/models/SolidThing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Quad } from '
|
|
1
|
+
import type { Quad } from '@rdfjs/types';
|
|
2
2
|
|
|
3
|
-
import { expandIRI } from '
|
|
3
|
+
import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
|
|
4
4
|
|
|
5
5
|
export default class SolidThing {
|
|
6
6
|
|
|
@@ -13,15 +13,13 @@ export default class SolidThing {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
public value(property: string): string | undefined {
|
|
16
|
-
return this.quads
|
|
17
|
-
.find(quad => quad.predicate.value === expandIRI(property))
|
|
18
|
-
?.object.value;
|
|
16
|
+
return this.quads.find((quad) => quad.predicate.value === expandIRI(property))?.object.value;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
public values(property: string): string[] {
|
|
22
20
|
return this.quads
|
|
23
|
-
.filter(quad => quad.predicate.value === expandIRI(property))
|
|
24
|
-
.map(quad => quad.object.value);
|
|
21
|
+
.filter((quad) => quad.predicate.value === expandIRI(property))
|
|
22
|
+
.map((quad) => quad.object.value);
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
}
|