@noeldemartin/solid-utils 0.1.1-next.f279ff39536b39493ea8febae8d8052a9a3b1365 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +65 -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 +7 -9
- package/src/errors/MalformedSolidDocumentError.ts +2 -2
- package/src/errors/NetworkRequestError.ts +4 -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/UnsupportedAuthorizationProtocolError.ts +16 -0
- package/src/errors/index.ts +2 -0
- package/src/helpers/auth.ts +90 -17
- package/src/helpers/interop.ts +63 -27
- package/src/helpers/io.ts +78 -47
- package/src/helpers/jsonld.ts +21 -1
- package/src/helpers/testing.ts +2 -2
- package/src/helpers/vocabs.ts +2 -1
- package/src/helpers/wac.ts +12 -2
- package/src/models/SolidDocument.ts +41 -35
- package/src/models/SolidStore.ts +61 -0
- package/src/models/index.ts +2 -1
- package/src/types/n3.d.ts +9 -0
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noeldemartin/solid-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "My JavaScript utilities for Solid",
|
|
5
5
|
"main": "dist/noeldemartin-solid-utils.cjs.js",
|
|
6
6
|
"module": "dist/noeldemartin-solid-utils.esm.js",
|
|
7
|
+
"browser": "dist/noeldemartin-solid-utils.umd.js",
|
|
7
8
|
"types": "dist/noeldemartin-solid-utils.d.ts",
|
|
8
9
|
"scripts": {
|
|
9
10
|
"build": "rm dist -rf && npm run build:js && npm run build:types",
|
|
@@ -29,13 +30,12 @@
|
|
|
29
30
|
"homepage": "https://github.com/noeldemartin/solid-utils",
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@babel/runtime": "^7.14.0",
|
|
32
|
-
"@noeldemartin/utils": "0.
|
|
33
|
+
"@noeldemartin/solid-utils-external": "^0.1.0",
|
|
34
|
+
"@noeldemartin/utils": "^0.3.0",
|
|
33
35
|
"@types/rdf-js": "^4.0.1",
|
|
34
36
|
"core-js": "^3.12.1",
|
|
35
37
|
"jest-diff": "^26.6.2",
|
|
36
|
-
"
|
|
37
|
-
"md5": "^2.3.0",
|
|
38
|
-
"n3": "^1.10.0"
|
|
38
|
+
"md5": "^2.3.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@babel/core": "^7.14.3",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"@babel/preset-env": "^7.14.2",
|
|
45
45
|
"@babel/preset-typescript": "^7.13.0",
|
|
46
46
|
"@microsoft/api-extractor": "^7.15.2",
|
|
47
|
-
"@noeldemartin/eslint-config-typescript": "^0.1.
|
|
48
|
-
"@noeldemartin/scripts": "^0.1.
|
|
47
|
+
"@noeldemartin/eslint-config-typescript": "^0.1.1",
|
|
48
|
+
"@noeldemartin/scripts": "^0.1.2",
|
|
49
49
|
"@rollup/plugin-alias": "^3.1.2",
|
|
50
50
|
"@rollup/plugin-babel": "^5.3.0",
|
|
51
51
|
"@rollup/plugin-commonjs": "^19.0.0",
|
|
@@ -54,9 +54,7 @@
|
|
|
54
54
|
"@types/chai": "^4.2.18",
|
|
55
55
|
"@types/jest": "^26.0.23",
|
|
56
56
|
"@types/jest-diff": "^24.3.0",
|
|
57
|
-
"@types/jsonld": "^1.5.6",
|
|
58
57
|
"@types/md5": "^2.3.0",
|
|
59
|
-
"@types/n3": "^1.8.0",
|
|
60
58
|
"babel-plugin-transform-remove-imports": "^1.5.4",
|
|
61
59
|
"eslint": "^7.26.0",
|
|
62
60
|
"jest": "^26.6.3",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
function errorMessage(
|
|
4
4
|
documentUrl: string | null,
|
|
@@ -14,7 +14,7 @@ export enum SolidDocumentFormat {
|
|
|
14
14
|
Turtle = 'Turtle',
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export default class MalformedSolidDocumentError extends
|
|
17
|
+
export default class MalformedSolidDocumentError extends JSError {
|
|
18
18
|
|
|
19
19
|
public readonly documentUrl: string | null;
|
|
20
20
|
public readonly documentFormat: SolidDocumentFormat;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
import type { JSErrorOptions } from '@noeldemartin/utils';
|
|
3
3
|
|
|
4
|
-
export default class NetworkRequestError extends
|
|
4
|
+
export default class NetworkRequestError extends JSError {
|
|
5
5
|
|
|
6
6
|
public readonly url: string;
|
|
7
7
|
|
|
8
|
-
constructor(url: string, options?:
|
|
8
|
+
constructor(url: string, options?: JSErrorOptions) {
|
|
9
9
|
super(`Request failed trying to fetch ${url}`, options);
|
|
10
10
|
|
|
11
11
|
this.url = url;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
function errorMessage(url: string, responseStatus?: number): string {
|
|
4
4
|
const typeInfo = responseStatus === 403 ? ' (Forbidden)' : '';
|
|
@@ -6,7 +6,7 @@ function errorMessage(url: string, responseStatus?: number): string {
|
|
|
6
6
|
return `Unauthorized${typeInfo}: ${url}`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export default class UnauthorizedError extends
|
|
9
|
+
export default class UnauthorizedError extends JSError {
|
|
10
10
|
|
|
11
11
|
public readonly url: string;
|
|
12
12
|
public readonly responseStatus?: number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
function getErrorMessage(messageOrResponse: string | Response, response?: Response): string {
|
|
4
|
+
response = response ?? messageOrResponse as Response;
|
|
5
|
+
|
|
6
|
+
return typeof messageOrResponse === 'string'
|
|
7
|
+
? `${messageOrResponse} (returned ${response.status} status code)`
|
|
8
|
+
: `Request to ${response.url} returned ${response.status} status code`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class UnsuccessfulRequestError extends JSError {
|
|
12
|
+
|
|
13
|
+
public response: Response;
|
|
14
|
+
|
|
15
|
+
constructor(response: Response);
|
|
16
|
+
constructor(message: string, response: Response);
|
|
17
|
+
constructor(messageOrResponse: string | Response, response?: Response) {
|
|
18
|
+
super(getErrorMessage(messageOrResponse, response));
|
|
19
|
+
|
|
20
|
+
this.response = response ?? messageOrResponse as Response;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
import type { JSErrorOptions } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
export default class UnsupportedAuthorizationProtocolError extends JSError {
|
|
5
|
+
|
|
6
|
+
public readonly url: string;
|
|
7
|
+
public readonly protocol: string;
|
|
8
|
+
|
|
9
|
+
constructor(url: string, protocol: string, options?: JSErrorOptions) {
|
|
10
|
+
super(`The resource at ${url} is using an unsupported authorization protocol (${protocol})`, options);
|
|
11
|
+
|
|
12
|
+
this.url = url;
|
|
13
|
+
this.protocol = protocol;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { default as MalformedSolidDocumentError, SolidDocumentFormat } from './M
|
|
|
2
2
|
export { default as NetworkRequestError } from './NetworkRequestError';
|
|
3
3
|
export { default as NotFoundError } from './NotFoundError';
|
|
4
4
|
export { default as UnauthorizedError } from './UnauthorizedError';
|
|
5
|
+
export { default as UnsuccessfulNetworkRequestError } from './UnsuccessfulNetworkRequestError';
|
|
6
|
+
export { default as UnsupportedAuthorizationProtocolError } from './UnsupportedAuthorizationProtocolError';
|
package/src/helpers/auth.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { objectWithoutEmpty, silenced, urlParentDirectory, urlRoot, urlRoute } from '@noeldemartin/utils';
|
|
1
|
+
import { arrayUnique, objectWithoutEmpty, silenced, urlParentDirectory, urlRoot, urlRoute } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import SolidStore from '../models/SolidStore';
|
|
4
|
+
import UnauthorizedError from '../errors/UnauthorizedError';
|
|
5
|
+
import type SolidDocument from '../models/SolidDocument';
|
|
2
6
|
|
|
3
7
|
import { fetchSolidDocument } from './io';
|
|
4
8
|
import type { Fetch } from './io';
|
|
@@ -6,6 +10,8 @@ import type { Fetch } from './io';
|
|
|
6
10
|
export interface SolidUserProfile {
|
|
7
11
|
webId: string;
|
|
8
12
|
storageUrls: string[];
|
|
13
|
+
cloaked: boolean;
|
|
14
|
+
writableProfileUrl: string | null;
|
|
9
15
|
name?: string;
|
|
10
16
|
avatarUrl?: string;
|
|
11
17
|
oidcIssuerUrl?: string;
|
|
@@ -13,16 +19,79 @@ export interface SolidUserProfile {
|
|
|
13
19
|
privateTypeIndexUrl?: string;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
async function fetchExtendedUserProfile(webIdDocument: SolidDocument, fetch?: Fetch): Promise<{
|
|
23
|
+
store: SolidStore;
|
|
24
|
+
cloaked: boolean;
|
|
25
|
+
writableProfileUrl: string | null;
|
|
26
|
+
}> {
|
|
27
|
+
const store = new SolidStore(webIdDocument.getQuads());
|
|
28
|
+
const documents: Record<string, SolidDocument | false | null> = { [webIdDocument.url]: webIdDocument };
|
|
29
|
+
const addReferencedDocumentUrls = (document: SolidDocument) => {
|
|
30
|
+
document
|
|
31
|
+
.statements(undefined, 'foaf:isPrimaryTopicOf')
|
|
32
|
+
.map(quad => quad.object.value)
|
|
33
|
+
.forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
|
|
34
|
+
document
|
|
35
|
+
.statements(undefined, 'foaf:primaryTopic')
|
|
36
|
+
.map(quad => quad.subject.value)
|
|
37
|
+
.forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
|
|
38
|
+
};
|
|
39
|
+
const loadProfileDocuments = async (): Promise<void> => {
|
|
40
|
+
for (const [url, document] of Object.entries(documents)) {
|
|
41
|
+
if (document !== null) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const document = await fetchSolidDocument(url, fetch);
|
|
47
|
+
|
|
48
|
+
documents[url] = document;
|
|
49
|
+
store.addQuads(document.getQuads());
|
|
50
|
+
|
|
51
|
+
addReferencedDocumentUrls(document);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof UnauthorizedError) {
|
|
54
|
+
documents[url] = false;
|
|
55
|
+
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
addReferencedDocumentUrls(webIdDocument);
|
|
65
|
+
|
|
66
|
+
do {
|
|
67
|
+
await loadProfileDocuments();
|
|
68
|
+
} while (Object.values(documents).some(document => document === null));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
store,
|
|
72
|
+
cloaked: Object.values(documents).some(document => document === false),
|
|
73
|
+
writableProfileUrl:
|
|
74
|
+
webIdDocument.isUserWritable()
|
|
75
|
+
? webIdDocument.url
|
|
76
|
+
: Object
|
|
77
|
+
.values(documents)
|
|
78
|
+
.find((document): document is SolidDocument => !!document && document.isUserWritable())
|
|
79
|
+
?.url ?? null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
16
83
|
async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUserProfile> {
|
|
17
84
|
const documentUrl = urlRoute(webId);
|
|
18
85
|
const document = await fetchSolidDocument(documentUrl, fetch);
|
|
19
86
|
|
|
20
|
-
if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer'))
|
|
87
|
+
if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer')) {
|
|
21
88
|
throw new Error(`${webId} is not a valid webId.`);
|
|
89
|
+
}
|
|
22
90
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
91
|
+
const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, fetch);
|
|
92
|
+
const storageUrls = store.statements(webId, 'pim:storage').map(storage => storage.object.value);
|
|
93
|
+
const publicTypeIndex = store.statement(webId, 'solid:publicTypeIndex');
|
|
94
|
+
const privateTypeIndex = store.statement(webId, 'solid:privateTypeIndex');
|
|
26
95
|
|
|
27
96
|
let parentUrl = urlParentDirectory(documentUrl);
|
|
28
97
|
while (parentUrl && storageUrls.length === 0) {
|
|
@@ -37,19 +106,23 @@ async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUser
|
|
|
37
106
|
parentUrl = urlParentDirectory(parentUrl);
|
|
38
107
|
}
|
|
39
108
|
|
|
40
|
-
return
|
|
109
|
+
return {
|
|
41
110
|
webId,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
111
|
+
cloaked,
|
|
112
|
+
writableProfileUrl,
|
|
113
|
+
storageUrls: arrayUnique(storageUrls),
|
|
114
|
+
...objectWithoutEmpty({
|
|
115
|
+
name:
|
|
116
|
+
store.statement(webId, 'vcard:fn')?.object.value ??
|
|
117
|
+
store.statement(webId, 'foaf:name')?.object.value,
|
|
118
|
+
avatarUrl:
|
|
119
|
+
store.statement(webId, 'vcard:hasPhoto')?.object.value ??
|
|
120
|
+
store.statement(webId, 'foaf:img')?.object.value,
|
|
121
|
+
oidcIssuerUrl: store.statement(webId, 'solid:oidcIssuer')?.object.value,
|
|
122
|
+
publicTypeIndexUrl: publicTypeIndex?.object.value,
|
|
123
|
+
privateTypeIndexUrl: privateTypeIndex?.object.value,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
53
126
|
}
|
|
54
127
|
|
|
55
128
|
export async function fetchLoginUserProfile(loginUrl: string, fetch?: Fetch): Promise<SolidUserProfile | null> {
|
package/src/helpers/interop.ts
CHANGED
|
@@ -1,59 +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
|
-
|
|
7
|
+
type TypeIndexType = 'public' | 'private';
|
|
8
|
+
|
|
9
|
+
async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch): Promise<string> {
|
|
9
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
|
+
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
|
+
}
|
|
24
|
+
|
|
20
25
|
fetch = fetch ?? window.fetch.bind(fetch);
|
|
21
26
|
|
|
22
|
-
const typeIndexUrl = await
|
|
23
|
-
const typeIndexBody =
|
|
24
|
-
<> a
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
41
|
await Promise.all([
|
|
35
42
|
createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
|
|
36
|
-
updateSolidDocument(user.
|
|
43
|
+
updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
|
|
37
44
|
]);
|
|
38
45
|
|
|
46
|
+
if (type === 'public') {
|
|
47
|
+
// TODO Implement updating ACLs for the listing itself to public
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
return typeIndexUrl;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
async function findRegistrations(
|
|
43
54
|
typeIndexUrl: string,
|
|
44
|
-
|
|
55
|
+
type: string | string[],
|
|
56
|
+
predicate: string,
|
|
45
57
|
fetch?: Fetch,
|
|
46
|
-
): Promise<
|
|
58
|
+
): Promise<string[]> {
|
|
47
59
|
const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
59
95
|
}
|
package/src/helpers/io.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
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';
|
|
2
10
|
import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
|
|
3
|
-
import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
|
|
4
|
-
import { fromRDF, toRDF } from 'jsonld';
|
|
5
|
-
import type { JsonLdDocument } from 'jsonld';
|
|
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
|
|
|
@@ -84,25 +89,25 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
84
89
|
|
|
85
90
|
for (const index of quadIndexes) {
|
|
86
91
|
const quad = normalizedQuads[index] as Quad;
|
|
87
|
-
const terms: Record<string,
|
|
88
|
-
subject: quad.subject as
|
|
89
|
-
object: quad.object as
|
|
92
|
+
const terms: Record<string, Term> = {
|
|
93
|
+
subject: quad.subject as Term,
|
|
94
|
+
object: quad.object as Term,
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
for (const [termName, termValue] of Object.entries(terms)) {
|
|
93
98
|
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
|
|
94
99
|
continue;
|
|
95
100
|
|
|
96
|
-
terms[termName] =
|
|
101
|
+
terms[termName] = createBlankNode(normalizedId) as Term;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
arrayReplace(
|
|
100
105
|
normalizedQuads,
|
|
101
106
|
quad,
|
|
102
|
-
|
|
103
|
-
terms.subject as
|
|
104
|
-
quad.predicate as
|
|
105
|
-
terms.object as
|
|
107
|
+
createQuad(
|
|
108
|
+
terms.subject as Term,
|
|
109
|
+
quad.predicate as Term,
|
|
110
|
+
terms.object as Term,
|
|
106
111
|
),
|
|
107
112
|
);
|
|
108
113
|
}
|
|
@@ -112,10 +117,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
export interface ParsingOptions {
|
|
115
|
-
|
|
120
|
+
baseIRI: string;
|
|
116
121
|
normalizeBlankNodes: boolean;
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
export interface RDFGraphData {
|
|
125
|
+
quads: Quad[];
|
|
126
|
+
containsRelativeIRIs: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
120
130
|
fetch = fetch ?? window.fetch.bind(window);
|
|
121
131
|
|
|
@@ -132,7 +142,7 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
|
|
|
132
142
|
|
|
133
143
|
export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
134
144
|
const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
|
|
135
|
-
const statements = await turtleToQuads(data, {
|
|
145
|
+
const statements = await turtleToQuads(data, { baseIRI: url });
|
|
136
146
|
|
|
137
147
|
return new SolidDocument(url, statements, headers);
|
|
138
148
|
}
|
|
@@ -150,14 +160,14 @@ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Pro
|
|
|
150
160
|
}
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
|
|
163
|
+
export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
|
|
154
164
|
if (isJsonLDGraph(jsonld)) {
|
|
155
|
-
const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
|
|
165
|
+
const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
|
|
156
166
|
|
|
157
167
|
return graphQuads.flat();
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
return
|
|
170
|
+
return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
export function normalizeSparql(sparql: string): string {
|
|
@@ -173,8 +183,54 @@ export function normalizeSparql(sparql: string): string {
|
|
|
173
183
|
.join(' ;\n');
|
|
174
184
|
}
|
|
175
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
|
+
|
|
176
232
|
export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
|
|
177
|
-
const graph = await
|
|
233
|
+
const graph = await jsonLDFromRDF(quads);
|
|
178
234
|
|
|
179
235
|
return {
|
|
180
236
|
'@graph': graph as JsonLDResource[],
|
|
@@ -235,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
|
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
|
|
238
|
-
const
|
|
239
|
-
const parser = new TurtleParser(parserOptions);
|
|
240
|
-
const quads: Quad[] = [];
|
|
241
|
-
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
parser.parse(turtle, (error, quad) => {
|
|
244
|
-
if (error) {
|
|
245
|
-
reject(
|
|
246
|
-
new MalformedSolidDocumentError(
|
|
247
|
-
options.documentUrl ?? null,
|
|
248
|
-
SolidDocumentFormat.Turtle,
|
|
249
|
-
error.message,
|
|
250
|
-
),
|
|
251
|
-
);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (!quad) {
|
|
256
|
-
options.normalizeBlankNodes
|
|
257
|
-
? resolve(normalizeBlankNodes(quads))
|
|
258
|
-
: resolve(quads);
|
|
294
|
+
const { quads } = await parseTurtle(turtle, options);
|
|
259
295
|
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
quads.push(quad);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
296
|
+
return quads;
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
|
|
269
|
-
const parserOptions = objectWithoutEmpty({ baseIRI: options.
|
|
300
|
+
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
270
301
|
const parser = new TurtleParser(parserOptions);
|
|
271
302
|
|
|
272
303
|
try {
|
|
@@ -277,7 +308,7 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
|
|
|
277
308
|
: quads;
|
|
278
309
|
} catch (error) {
|
|
279
310
|
throw new MalformedSolidDocumentError(
|
|
280
|
-
options.
|
|
311
|
+
options.baseIRI ?? null,
|
|
281
312
|
SolidDocumentFormat.Turtle,
|
|
282
313
|
(error as Error).message ?? '',
|
|
283
314
|
);
|
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)}`);
|