@noeldemartin/solid-utils 0.5.0 → 0.6.0-next.3e3ceb79b047f4ec87a416c2f920a13eda7a0df1
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/io-CMHtz5bu.js +401 -0
- package/dist/io-CMHtz5bu.js.map +1 -0
- package/dist/noeldemartin-solid-utils.d.ts +13 -65
- package/dist/noeldemartin-solid-utils.js +224 -0
- package/dist/noeldemartin-solid-utils.js.map +1 -0
- package/dist/testing.d.ts +45 -0
- package/dist/testing.js +157 -0
- package/dist/testing.js.map +1 -0
- package/package.json +61 -63
- 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 +3 -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 +2 -2
- package/src/testing/vitest/index.ts +13 -0
- package/src/testing/vitest/matchers.ts +68 -0
- package/src/types/n3.d.ts +0 -2
- 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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { jsonldToQuads, normalizeSparql, quadsToJsonLD, sparqlToQuadsSync, turtleToQuadsSync } from './io';
|
|
4
|
+
import type { Quad } from '@rdfjs/types';
|
|
5
|
+
|
|
6
|
+
describe('IO', () => {
|
|
7
|
+
|
|
8
|
+
it('parses jsonld', async () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const jsonld = {
|
|
11
|
+
'@context': { '@vocab': 'https://schema.org/' },
|
|
12
|
+
'@type': 'Movie',
|
|
13
|
+
'name': 'Spirited Away',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Act
|
|
17
|
+
const quads = (await jsonldToQuads(jsonld)) as [Quad, Quad];
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(quads).toHaveLength(2);
|
|
21
|
+
|
|
22
|
+
expect(quads[0].subject.termType).toEqual('BlankNode');
|
|
23
|
+
expect(quads[0].predicate.termType).toEqual('NamedNode');
|
|
24
|
+
expect(quads[0].predicate.value).toEqual('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
|
|
25
|
+
expect(quads[0].object.termType).toEqual('NamedNode');
|
|
26
|
+
expect(quads[0].object.value).toEqual('https://schema.org/Movie');
|
|
27
|
+
|
|
28
|
+
expect(quads[1].subject.termType).toEqual('BlankNode');
|
|
29
|
+
expect(quads[1].predicate.termType).toEqual('NamedNode');
|
|
30
|
+
expect(quads[1].predicate.value).toEqual('https://schema.org/name');
|
|
31
|
+
expect(quads[1].object.termType).toEqual('Literal');
|
|
32
|
+
expect(quads[1].object.value).toEqual('Spirited Away');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parses jsonld with anonymous subjects', async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const jsonld = {
|
|
38
|
+
'@context': { '@vocab': 'https://schema.org/' },
|
|
39
|
+
'@id': '#it',
|
|
40
|
+
'@type': 'Movie',
|
|
41
|
+
'name': 'Spirited Away',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Act
|
|
45
|
+
const quads = (await jsonldToQuads(jsonld)) as [Quad, Quad];
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(quads).toHaveLength(2);
|
|
49
|
+
|
|
50
|
+
expect(quads[0].subject.termType).toEqual('NamedNode');
|
|
51
|
+
expect(quads[0].subject.value).toEqual('#it');
|
|
52
|
+
expect(quads[0].predicate.termType).toEqual('NamedNode');
|
|
53
|
+
expect(quads[0].predicate.value).toEqual('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
|
|
54
|
+
expect(quads[0].object.termType).toEqual('NamedNode');
|
|
55
|
+
expect(quads[0].object.value).toEqual('https://schema.org/Movie');
|
|
56
|
+
|
|
57
|
+
expect(quads[1].subject.termType).toEqual('NamedNode');
|
|
58
|
+
expect(quads[1].subject.value).toEqual('#it');
|
|
59
|
+
expect(quads[1].predicate.termType).toEqual('NamedNode');
|
|
60
|
+
expect(quads[1].predicate.value).toEqual('https://schema.org/name');
|
|
61
|
+
expect(quads[1].object.termType).toEqual('Literal');
|
|
62
|
+
expect(quads[1].object.value).toEqual('Spirited Away');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('parses jsonld graphs', async () => {
|
|
66
|
+
// Arrange
|
|
67
|
+
const jsonld = {
|
|
68
|
+
'@graph': [
|
|
69
|
+
{
|
|
70
|
+
'@context': { '@vocab': 'https://schema.org/' },
|
|
71
|
+
'@id': 'solid://movies/spirited-away',
|
|
72
|
+
'@type': 'Movie',
|
|
73
|
+
'name': 'Spirited Away',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
'@context': { '@vocab': 'https://schema.org/' },
|
|
77
|
+
'@id': 'solid://movies/spirited-away',
|
|
78
|
+
'@type': 'Movie',
|
|
79
|
+
'name': 'Spirited Away',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Act
|
|
85
|
+
const quads = await jsonldToQuads(jsonld);
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
expect(quads).toHaveLength(4);
|
|
89
|
+
|
|
90
|
+
[0, 2].forEach((index) => {
|
|
91
|
+
expect(quads[index]?.subject.termType).toEqual('NamedNode');
|
|
92
|
+
expect(quads[index]?.subject.value).toEqual('solid://movies/spirited-away');
|
|
93
|
+
expect(quads[index]?.predicate.termType).toEqual('NamedNode');
|
|
94
|
+
expect(quads[index]?.predicate.value).toEqual('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
|
|
95
|
+
expect(quads[index]?.object.termType).toEqual('NamedNode');
|
|
96
|
+
expect(quads[index]?.object.value).toEqual('https://schema.org/Movie');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
[1, 3].forEach((index) => {
|
|
100
|
+
expect(quads[index]?.subject.termType).toEqual('NamedNode');
|
|
101
|
+
expect(quads[index]?.subject.value).toEqual('solid://movies/spirited-away');
|
|
102
|
+
expect(quads[index]?.predicate.termType).toEqual('NamedNode');
|
|
103
|
+
expect(quads[index]?.predicate.value).toEqual('https://schema.org/name');
|
|
104
|
+
expect(quads[index]?.object.termType).toEqual('Literal');
|
|
105
|
+
expect(quads[index]?.object.value).toEqual('Spirited Away');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('normalizes sparql', () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const insertTurtle = `
|
|
112
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
113
|
+
|
|
114
|
+
<#me>
|
|
115
|
+
foaf:name "Amy" ;
|
|
116
|
+
foaf:lastName "Doe" .
|
|
117
|
+
`;
|
|
118
|
+
const deleteTurtle = `
|
|
119
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
120
|
+
|
|
121
|
+
<#me> foaf:name "John Doe" .
|
|
122
|
+
`;
|
|
123
|
+
const sparql = `
|
|
124
|
+
INSERT DATA { ${insertTurtle} } ;
|
|
125
|
+
DELETE DATA { ${deleteTurtle} }
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
const normalized = normalizeSparql(sparql);
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(normalized).toEqual(
|
|
133
|
+
[
|
|
134
|
+
'INSERT DATA {',
|
|
135
|
+
' <#me> <http://xmlns.com/foaf/0.1/lastName> "Doe" .',
|
|
136
|
+
' <#me> <http://xmlns.com/foaf/0.1/name> "Amy" .',
|
|
137
|
+
'} ;',
|
|
138
|
+
'DELETE DATA {',
|
|
139
|
+
' <#me> <http://xmlns.com/foaf/0.1/name> "John Doe" .',
|
|
140
|
+
'}',
|
|
141
|
+
].join('\n'),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('parses sparql', () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const insertTurtle = `
|
|
148
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
149
|
+
|
|
150
|
+
<#me> foaf:name "Amy Doe" .
|
|
151
|
+
`;
|
|
152
|
+
const deleteTurtle = `
|
|
153
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
154
|
+
|
|
155
|
+
<#me> foaf:name "John Doe" .
|
|
156
|
+
`;
|
|
157
|
+
const sparql = `
|
|
158
|
+
INSERT DATA { ${insertTurtle} } ;
|
|
159
|
+
DELETE DATA { ${deleteTurtle} }
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
// Act
|
|
163
|
+
const quads = sparqlToQuadsSync(sparql);
|
|
164
|
+
|
|
165
|
+
// Assert
|
|
166
|
+
expect(Object.keys(quads)).toHaveLength(2);
|
|
167
|
+
|
|
168
|
+
expect(quads.insert).toEqual(turtleToQuadsSync(insertTurtle));
|
|
169
|
+
expect(quads.delete).toEqual(turtleToQuadsSync(deleteTurtle));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('parses turtle synchronously', () => {
|
|
173
|
+
// Arrange
|
|
174
|
+
const turtle = `
|
|
175
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
176
|
+
|
|
177
|
+
<#me>
|
|
178
|
+
a foaf:Person ;
|
|
179
|
+
foaf:name "John Doe" .
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
const quads = turtleToQuadsSync(turtle) as [Quad, Quad];
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(quads).toHaveLength(2);
|
|
187
|
+
|
|
188
|
+
expect(quads[0].subject.termType).toEqual('NamedNode');
|
|
189
|
+
expect(quads[0].subject.value).toEqual('#me');
|
|
190
|
+
expect(quads[0].predicate.termType).toEqual('NamedNode');
|
|
191
|
+
expect(quads[0].predicate.value).toEqual('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
|
|
192
|
+
expect(quads[0].object.termType).toEqual('NamedNode');
|
|
193
|
+
expect(quads[0].object.value).toEqual('http://xmlns.com/foaf/0.1/Person');
|
|
194
|
+
|
|
195
|
+
expect(quads[1].subject.termType).toEqual('NamedNode');
|
|
196
|
+
expect(quads[1].subject.value).toEqual('#me');
|
|
197
|
+
expect(quads[1].predicate.termType).toEqual('NamedNode');
|
|
198
|
+
expect(quads[1].predicate.value).toEqual('http://xmlns.com/foaf/0.1/name');
|
|
199
|
+
expect(quads[1].object.termType).toEqual('Literal');
|
|
200
|
+
expect(quads[1].object.value).toEqual('John Doe');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('converts quads to jsonld', async () => {
|
|
204
|
+
// Arrange
|
|
205
|
+
const quads = turtleToQuadsSync(`
|
|
206
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
207
|
+
|
|
208
|
+
<#me>
|
|
209
|
+
a foaf:Person ;
|
|
210
|
+
foaf:name "John Doe" .
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
// Act
|
|
214
|
+
const jsonld = await quadsToJsonLD(quads);
|
|
215
|
+
|
|
216
|
+
// Assert
|
|
217
|
+
expect(jsonld).toEqual({
|
|
218
|
+
'@graph': [
|
|
219
|
+
{
|
|
220
|
+
'@id': '#me',
|
|
221
|
+
'@type': ['http://xmlns.com/foaf/0.1/Person'],
|
|
222
|
+
'http://xmlns.com/foaf/0.1/name': [{ '@value': 'John Doe' }],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
});
|
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