@noeldemartin/solid-utils 0.1.1-next.8c064afc67573f7cf20fa4cbeecfad10a949a9ef → 0.1.1-next.9e1ba757a71d124d509b14f4ed0de95a13e0c196
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 +78 -17
- package/dist/noeldemartin-solid-utils.esm.js +1 -1
- package/dist/noeldemartin-solid-utils.esm.js.map +1 -1
- package/package.json +10 -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 +17 -0
- package/src/helpers/testing.ts +69 -33
- 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/index.ts
CHANGED
package/src/helpers/interop.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
|
13
|
+
const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
|
|
13
14
|
|
|
14
15
|
return await solidDocumentExists(typeIndexUrl, fetch)
|
|
15
|
-
? `${storageUrl}settings
|
|
16
|
+
? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
|
|
16
17
|
: typeIndexUrl;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
37
|
+
<${user.webId}> <http://www.w3.org/ns/solid/terms#${type}TypeIndex> <${typeIndexUrl}> .
|
|
31
38
|
}
|
|
32
39
|
`;
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
+
async function findRegistrations(
|
|
41
54
|
typeIndexUrl: string,
|
|
42
|
-
|
|
55
|
+
type: string | string[],
|
|
56
|
+
predicate: string,
|
|
43
57
|
fetch?: Fetch,
|
|
44
|
-
): Promise<
|
|
58
|
+
): Promise<string[]> {
|
|
45
59
|
const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,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;
|
|
@@ -7,6 +10,20 @@ export type JsonLD = Partial<{
|
|
|
7
10
|
export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
|
|
8
11
|
export type JsonLDGraph = { '@graph': JsonLDResource[] };
|
|
9
12
|
|
|
13
|
+
export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
|
|
14
|
+
const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
|
|
15
|
+
|
|
16
|
+
if ('@graph' in compactedJsonLD) {
|
|
17
|
+
return compactedJsonLD as JsonLDGraph;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if ('@id' in compactedJsonLD) {
|
|
21
|
+
return { '@graph': [compactedJsonLD] } as JsonLDGraph;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { '@graph': [] };
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
|
|
11
28
|
return '@graph' in jsonld;
|
|
12
29
|
}
|
package/src/helpers/testing.ts
CHANGED
|
@@ -1,46 +1,67 @@
|
|
|
1
|
+
import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
|
|
1
2
|
import type { JsonLD } from '@/helpers/jsonld';
|
|
2
|
-
import { pull } from '@noeldemartin/utils';
|
|
3
3
|
import type { Quad, Quad_Object } from 'rdf-js';
|
|
4
4
|
|
|
5
5
|
import { jsonldToQuads, quadToTurtle, quadsToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
|
|
6
6
|
|
|
7
7
|
let patternsRegExpsIndex: Record<string, RegExp> = {};
|
|
8
|
+
const builtInPatterns: Record<string, string> = {
|
|
9
|
+
'%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class ExpectedQuadAssertionError extends JSError {
|
|
13
|
+
|
|
14
|
+
constructor(public readonly expectedQuad: Quad) {
|
|
15
|
+
super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
|
|
16
|
+
}
|
|
8
17
|
|
|
9
|
-
function containsPatterns(value: string): boolean {
|
|
10
|
-
return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
function assertExpectedQuadsExist(expectedQuads: Quad[], actualQuads: Quad[]): void {
|
|
21
|
+
for (const expectedQuad of expectedQuads) {
|
|
22
|
+
const matchingQuad = actualQuads.find(actualQuad => quadEquals(expectedQuad, actualQuad));
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
if (!matchingQuad)
|
|
25
|
+
throw new ExpectedQuadAssertionError(expectedQuad);
|
|
26
|
+
|
|
27
|
+
arrayRemove(actualQuads, matchingQuad);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let expectedRegExp = expected;
|
|
31
|
+
function containsPatterns(value: string): boolean {
|
|
32
|
+
return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
function createPatternRegexp(expected: string): RegExp {
|
|
36
|
+
const patternAliases = [];
|
|
37
|
+
const patternMatches = stringMatchAll<4, 1 | 2>(
|
|
38
|
+
expected,
|
|
39
|
+
/\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
|
|
40
|
+
);
|
|
41
|
+
const patterns: string[] = [];
|
|
42
|
+
let expectedRegExp = expected;
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
for (const patternMatch of patternMatches) {
|
|
45
|
+
patternMatch[2] && patternAliases.push(patternMatch[2]);
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
}
|
|
47
|
+
patterns.push(patternMatch[3]);
|
|
33
48
|
|
|
34
|
-
expectedRegExp = expectedRegExp.replace(
|
|
49
|
+
expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
|
|
50
|
+
}
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, pattern);
|
|
38
|
-
}
|
|
52
|
+
expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
for (const [patternIndex, pattern] of Object.entries(patterns)) {
|
|
55
|
+
expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
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;
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
|
|
@@ -95,9 +116,13 @@ export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<Eq
|
|
|
95
116
|
if (expectedQuads.length !== actualQuads.length)
|
|
96
117
|
return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
try {
|
|
120
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
123
|
+
throw error;
|
|
124
|
+
|
|
125
|
+
return result(false, error.message);
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
return result(true, 'jsonld matches');
|
|
@@ -121,9 +146,16 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
|
|
|
121
146
|
if (expectedQuads.length !== actualQuads.length)
|
|
122
147
|
return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
try {
|
|
150
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
153
|
+
throw error;
|
|
154
|
+
|
|
155
|
+
return result(
|
|
156
|
+
false,
|
|
157
|
+
`Couldn't find the following ${operation} triple: ${quadToTurtle(error.expectedQuad)}`,
|
|
158
|
+
);
|
|
127
159
|
}
|
|
128
160
|
}
|
|
129
161
|
|
|
@@ -145,9 +177,13 @@ export function turtleEquals(expected: string, actual: string): EqualityResult {
|
|
|
145
177
|
if (expectedQuads.length !== actualQuads.length)
|
|
146
178
|
return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
|
|
147
179
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
try {
|
|
181
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
184
|
+
throw error;
|
|
185
|
+
|
|
186
|
+
return result(false, error.message);
|
|
151
187
|
}
|
|
152
188
|
|
|
153
189
|
return result(true, 'turtle matches');
|
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)
|