@noeldemartin/solid-utils 0.1.1-next.7b4ae3cff8eddb646fb583cde8416341461a0f08 → 0.1.1-next.8987fd38d01bd74fdde315a5f60c5f95f5183cd2
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/.nvmrc +1 -1
- package/dist/noeldemartin-solid-utils.cjs.js +1 -1
- package/dist/noeldemartin-solid-utils.cjs.js.map +1 -1
- package/dist/noeldemartin-solid-utils.d.ts +79 -17
- package/dist/noeldemartin-solid-utils.esm.js +1 -1
- package/dist/noeldemartin-solid-utils.esm.js.map +1 -1
- package/dist/noeldemartin-solid-utils.umd.js +90 -0
- package/dist/noeldemartin-solid-utils.umd.js.map +1 -0
- package/package.json +11 -13
- package/src/errors/MalformedSolidDocumentError.ts +2 -2
- package/src/errors/NetworkRequestError.ts +5 -4
- package/src/errors/NotFoundError.ts +2 -2
- package/src/errors/UnauthorizedError.ts +2 -2
- package/src/errors/UnsuccessfulNetworkRequestError.ts +23 -0
- package/src/errors/index.ts +1 -0
- package/src/helpers/auth.ts +82 -16
- package/src/helpers/identifiers.ts +56 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/interop.ts +68 -30
- package/src/helpers/io.ts +133 -66
- package/src/helpers/jsonld.ts +21 -1
- package/src/helpers/testing.ts +24 -25
- package/src/helpers/vocabs.ts +4 -2
- package/src/helpers/wac.ts +45 -0
- package/src/models/SolidDocument.ts +38 -37
- package/src/models/SolidStore.ts +61 -0
- package/src/models/index.ts +2 -1
- package/src/plugins/jest/matchers.ts +5 -7
- package/src/types/n3.d.ts +9 -0
package/src/helpers/io.ts
CHANGED
|
@@ -1,10 +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
|
-
import type { JsonLdDocument } from 'jsonld';
|
|
4
|
-
import { toRDF } from 'jsonld';
|
|
5
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';
|
|
6
11
|
import type { Quad } from 'rdf-js';
|
|
7
|
-
import type { Term
|
|
12
|
+
import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
|
|
8
13
|
|
|
9
14
|
import SolidDocument from '@/models/SolidDocument';
|
|
10
15
|
|
|
@@ -13,10 +18,10 @@ import NetworkRequestError from '@/errors/NetworkRequestError';
|
|
|
13
18
|
import NotFoundError from '@/errors/NotFoundError';
|
|
14
19
|
import UnauthorizedError from '@/errors/UnauthorizedError';
|
|
15
20
|
import { isJsonLDGraph } from '@/helpers/jsonld';
|
|
16
|
-
import type { JsonLD } from '@/helpers/jsonld';
|
|
21
|
+
import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
|
|
17
22
|
|
|
18
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
export declare type AnyFetch = (input: any, options?: any) => Promise<
|
|
24
|
+
export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
|
|
20
25
|
export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
|
|
21
26
|
export declare type Fetch = TypedFetch | AnyFetch;
|
|
22
27
|
|
|
@@ -44,7 +49,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
|
|
|
44
49
|
if (error instanceof UnauthorizedError)
|
|
45
50
|
throw error;
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
if (error instanceof NotFoundError)
|
|
53
|
+
throw error;
|
|
54
|
+
|
|
55
|
+
throw new NetworkRequestError(url, { cause: error });
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -52,26 +60,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
52
60
|
const normalizedQuads = quads.slice(0);
|
|
53
61
|
const quadsIndexes: Record<string, Set<number>> = {};
|
|
54
62
|
const blankNodeIds = arr(quads)
|
|
55
|
-
.flatMap(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return ids;
|
|
67
|
-
})
|
|
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
|
+
)
|
|
68
72
|
.filter()
|
|
69
73
|
.unique();
|
|
70
74
|
|
|
71
75
|
for (const originalId of blankNodeIds) {
|
|
76
|
+
const quadIndexes = quadsIndexes[originalId] as Set<number>;
|
|
72
77
|
const normalizedId = md5(
|
|
73
|
-
arr(
|
|
74
|
-
.map(index => quads[index])
|
|
78
|
+
arr(quadIndexes)
|
|
79
|
+
.map(index => quads[index] as Quad)
|
|
75
80
|
.filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
|
|
76
81
|
.map(
|
|
77
82
|
({ predicate, object }) => object.termType === 'BlankNode'
|
|
@@ -82,18 +87,29 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
82
87
|
.join(),
|
|
83
88
|
);
|
|
84
89
|
|
|
85
|
-
for (const index of
|
|
86
|
-
const quad = normalizedQuads[index];
|
|
87
|
-
const terms: Record<string,
|
|
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
|
+
};
|
|
88
96
|
|
|
89
97
|
for (const [termName, termValue] of Object.entries(terms)) {
|
|
90
98
|
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
|
|
91
99
|
continue;
|
|
92
100
|
|
|
93
|
-
terms[termName] =
|
|
101
|
+
terms[termName] = createBlankNode(normalizedId) as Term;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
arrayReplace(
|
|
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
|
+
);
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
|
|
@@ -101,12 +117,17 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
export interface ParsingOptions {
|
|
104
|
-
|
|
120
|
+
baseIRI: string;
|
|
105
121
|
normalizeBlankNodes: boolean;
|
|
106
122
|
}
|
|
107
123
|
|
|
124
|
+
export interface RDFGraphData {
|
|
125
|
+
quads: Quad[];
|
|
126
|
+
containsRelativeIRIs: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
108
129
|
export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
109
|
-
fetch = fetch ?? window.fetch;
|
|
130
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
110
131
|
|
|
111
132
|
const statements = await turtleToQuads(body);
|
|
112
133
|
|
|
@@ -121,19 +142,32 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
|
|
|
121
142
|
|
|
122
143
|
export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
123
144
|
const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
|
|
124
|
-
const statements = await turtleToQuads(data, {
|
|
145
|
+
const statements = await turtleToQuads(data, { baseIRI: url });
|
|
125
146
|
|
|
126
147
|
return new SolidDocument(url, statements, headers);
|
|
127
148
|
}
|
|
128
149
|
|
|
129
|
-
export async function
|
|
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[]> {
|
|
130
164
|
if (isJsonLDGraph(jsonld)) {
|
|
131
|
-
const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
|
|
165
|
+
const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
|
|
132
166
|
|
|
133
167
|
return graphQuads.flat();
|
|
134
168
|
}
|
|
135
169
|
|
|
136
|
-
return
|
|
170
|
+
return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
|
|
137
171
|
}
|
|
138
172
|
|
|
139
173
|
export function normalizeSparql(sparql: string): string {
|
|
@@ -149,6 +183,60 @@ export function normalizeSparql(sparql: string): string {
|
|
|
149
183
|
.join(' ;\n');
|
|
150
184
|
}
|
|
151
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
|
+
|
|
152
240
|
export function quadsToTurtle(quads: Quad[]): string {
|
|
153
241
|
const writer = new TurtleWriter;
|
|
154
242
|
|
|
@@ -175,7 +263,7 @@ export async function sparqlToQuads(
|
|
|
175
263
|
sparql: string,
|
|
176
264
|
options: Partial<ParsingOptions> = {},
|
|
177
265
|
): Promise<Record<string, Quad[]>> {
|
|
178
|
-
const operations = sparql
|
|
266
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
179
267
|
const quads: Record<string, Quad[]> = {};
|
|
180
268
|
|
|
181
269
|
await Promise.all([...operations].map(async operation => {
|
|
@@ -189,7 +277,7 @@ export async function sparqlToQuads(
|
|
|
189
277
|
}
|
|
190
278
|
|
|
191
279
|
export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
|
|
192
|
-
const operations = sparql
|
|
280
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
193
281
|
const quads: Record<string, Quad[]> = {};
|
|
194
282
|
|
|
195
283
|
for (const operation of operations) {
|
|
@@ -203,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
|
|
|
203
291
|
}
|
|
204
292
|
|
|
205
293
|
export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
|
|
206
|
-
const
|
|
207
|
-
const parser = new TurtleParser(parserOptions);
|
|
208
|
-
const quads: Quad[] = [];
|
|
294
|
+
const { quads } = await parseTurtle(turtle, options);
|
|
209
295
|
|
|
210
|
-
return
|
|
211
|
-
parser.parse(turtle, (error, quad) => {
|
|
212
|
-
if (error) {
|
|
213
|
-
reject(
|
|
214
|
-
new MalformedSolidDocumentError(
|
|
215
|
-
options.documentUrl ?? null,
|
|
216
|
-
SolidDocumentFormat.Turtle,
|
|
217
|
-
error.message,
|
|
218
|
-
),
|
|
219
|
-
);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!quad) {
|
|
224
|
-
options.normalizeBlankNodes
|
|
225
|
-
? resolve(normalizeBlankNodes(quads))
|
|
226
|
-
: resolve(quads);
|
|
227
|
-
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
quads.push(quad);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
296
|
+
return quads;
|
|
234
297
|
}
|
|
235
298
|
|
|
236
299
|
export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
|
|
237
|
-
const parserOptions = objectWithoutEmpty({ baseIRI: options.
|
|
300
|
+
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
238
301
|
const parser = new TurtleParser(parserOptions);
|
|
239
302
|
|
|
240
303
|
try {
|
|
@@ -244,12 +307,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
|
|
|
244
307
|
? normalizeBlankNodes(quads)
|
|
245
308
|
: quads;
|
|
246
309
|
} catch (error) {
|
|
247
|
-
throw new MalformedSolidDocumentError(
|
|
310
|
+
throw new MalformedSolidDocumentError(
|
|
311
|
+
options.baseIRI ?? null,
|
|
312
|
+
SolidDocumentFormat.Turtle,
|
|
313
|
+
(error as Error).message ?? '',
|
|
314
|
+
);
|
|
248
315
|
}
|
|
249
316
|
}
|
|
250
317
|
|
|
251
318
|
export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
|
|
252
|
-
fetch = fetch ?? window.fetch;
|
|
319
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
253
320
|
|
|
254
321
|
await fetch(url, {
|
|
255
322
|
method: 'PATCH',
|
package/src/helpers/jsonld.ts
CHANGED
|
@@ -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,7 +8,24 @@ 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 = {
|
|
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
|
+
}
|
|
9
29
|
|
|
10
30
|
export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
|
|
11
31
|
return '@graph' in jsonld;
|
package/src/helpers/testing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
|
|
2
2
|
import type { JsonLD } from '@/helpers/jsonld';
|
|
3
3
|
import type { Quad, Quad_Object } from 'rdf-js';
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ const builtInPatterns: Record<string, string> = {
|
|
|
9
9
|
'%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
class ExpectedQuadAssertionError extends
|
|
12
|
+
class ExpectedQuadAssertionError extends JSError {
|
|
13
13
|
|
|
14
14
|
constructor(public readonly expectedQuad: Quad) {
|
|
15
15
|
super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
|
|
@@ -32,37 +32,36 @@ function containsPatterns(value: string): boolean {
|
|
|
32
32
|
return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
if (!containsPatterns(expected))
|
|
37
|
-
return expected === actual;
|
|
38
|
-
|
|
35
|
+
function createPatternRegexp(expected: string): RegExp {
|
|
39
36
|
const patternAliases = [];
|
|
37
|
+
const patternMatches = stringMatchAll<4, 1 | 2>(
|
|
38
|
+
expected,
|
|
39
|
+
/\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
|
|
40
|
+
);
|
|
41
|
+
const patterns: string[] = [];
|
|
42
|
+
let expectedRegExp = expected;
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const patterns: string[] = [];
|
|
44
|
-
let expectedRegExp = expected;
|
|
45
|
-
|
|
46
|
-
for (const patternMatch of patternMatches) {
|
|
47
|
-
if (patternMatch[2]) {
|
|
48
|
-
patternAliases.push(patternMatch[2]);
|
|
49
|
-
}
|
|
44
|
+
for (const patternMatch of patternMatches) {
|
|
45
|
+
patternMatch[2] && patternAliases.push(patternMatch[2]);
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
|
|
54
|
-
}
|
|
47
|
+
patterns.push(patternMatch[3]);
|
|
55
48
|
|
|
56
|
-
expectedRegExp = expectedRegExp.replace(
|
|
49
|
+
expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
|
|
50
|
+
}
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
|
|
60
|
-
}
|
|
52
|
+
expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
for (const [patternIndex, pattern] of Object.entries(patterns)) {
|
|
55
|
+
expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
|
|
63
56
|
}
|
|
64
57
|
|
|
65
|
-
return
|
|
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;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
|
package/src/helpers/vocabs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
6
|
+
import SolidStore from './SolidStore';
|
|
7
7
|
|
|
8
|
-
export
|
|
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('
|
|
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
|
|
37
|
-
return
|
|
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
|
|
44
|
-
return this.
|
|
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
|
|
53
|
-
|
|
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
|
|
64
|
-
return this.
|
|
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
|
-
|
|
68
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Quad } from 'rdf-js';
|
|
2
|
+
|
|
3
|
+
import { expandIRI } from '@/helpers/vocabs';
|
|
4
|
+
|
|
5
|
+
import SolidThing from './SolidThing';
|
|
6
|
+
|
|
7
|
+
export default class SolidStore {
|
|
8
|
+
|
|
9
|
+
private quads: Quad[];
|
|
10
|
+
|
|
11
|
+
public constructor(quads: Quad[] = []) {
|
|
12
|
+
this.quads = quads;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public isEmpty(): boolean {
|
|
16
|
+
return this.statements.length === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public getQuads(): Quad[] {
|
|
20
|
+
return this.quads.slice(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public addQuads(quads: Quad[]): void {
|
|
24
|
+
this.quads.push(...quads);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public statements(subject?: string, predicate?: string, object?: string): Quad[] {
|
|
28
|
+
return this.quads.filter(
|
|
29
|
+
statement =>
|
|
30
|
+
(!object || statement.object.value === this.expandIRI(object)) &&
|
|
31
|
+
(!subject || statement.subject.value === this.expandIRI(subject)) &&
|
|
32
|
+
(!predicate || statement.predicate.value === this.expandIRI(predicate)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public statement(subject?: string, predicate?: string, object?: string): Quad | null {
|
|
37
|
+
const statement = this.quads.find(
|
|
38
|
+
statement =>
|
|
39
|
+
(!object || statement.object.value === this.expandIRI(object)) &&
|
|
40
|
+
(!subject || statement.subject.value === this.expandIRI(subject)) &&
|
|
41
|
+
(!predicate || statement.predicate.value === this.expandIRI(predicate)),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return statement ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public contains(subject: string, predicate?: string, object?: string): boolean {
|
|
48
|
+
return this.statement(subject, predicate, object) !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getThing(subject: string): SolidThing {
|
|
52
|
+
const statements = this.statements(subject);
|
|
53
|
+
|
|
54
|
+
return new SolidThing(subject, statements);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected expandIRI(iri: string): string {
|
|
58
|
+
return expandIRI(iri);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}
|
package/src/models/index.ts
CHANGED