@noeldemartin/solid-utils 0.1.1-next.1f0cf6ccc237588ae655211348e94eba9ba16c8d → 0.1.1-next.2c9924cd927c0b708aef3ff4426982469f3b86fd
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 +53 -15
- package/dist/noeldemartin-solid-utils.esm.js +1 -1
- package/dist/noeldemartin-solid-utils.esm.js.map +1 -1
- package/package.json +2 -2
- 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/index.ts +1 -0
- package/src/helpers/interop.ts +65 -29
- package/src/helpers/io.ts +61 -29
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noeldemartin/solid-utils",
|
|
3
|
-
"version": "0.1.1-next.
|
|
3
|
+
"version": "0.1.1-next.2c9924cd927c0b708aef3ff4426982469f3b86fd",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"homepage": "https://github.com/noeldemartin/solid-utils",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@babel/runtime": "^7.14.0",
|
|
32
|
-
"@noeldemartin/utils": "^0.2.1-next.
|
|
32
|
+
"@noeldemartin/utils": "^0.2.1-next.9dca1131badcdf7e51828a07ba72e4a428503066",
|
|
33
33
|
"@types/rdf-js": "^4.0.1",
|
|
34
34
|
"core-js": "^3.12.1",
|
|
35
35
|
"jest-diff": "^26.6.2",
|
|
@@ -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,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
import type { JSErrorOptions } from '@noeldemartin/utils';
|
|
2
3
|
|
|
3
|
-
export default class NetworkRequestError extends
|
|
4
|
+
export default class NetworkRequestError extends JSError {
|
|
4
5
|
|
|
5
6
|
public readonly url: string;
|
|
6
7
|
|
|
7
|
-
constructor(url: string) {
|
|
8
|
-
super(`Request failed trying to fetch ${url}
|
|
8
|
+
constructor(url: string, options?: JSErrorOptions) {
|
|
9
|
+
super(`Request failed trying to fetch ${url}`, options);
|
|
9
10
|
|
|
10
11
|
this.url = url;
|
|
11
12
|
}
|
|
@@ -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
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -2,3 +2,4 @@ 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 UnsuccessfulRequestError } from './NetworkRequestError';
|
package/src/helpers/auth.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { objectWithoutEmpty, silenced, urlParentDirectory, urlRoot, urlRoute } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
+
import SolidStore from '../models/SolidStore';
|
|
4
|
+
import UnauthorizedError from '../errors/UnauthorizedError';
|
|
5
|
+
import type SolidDocument from '../models/SolidDocument';
|
|
6
|
+
|
|
3
7
|
import { fetchSolidDocument } from './io';
|
|
4
8
|
import type { Fetch } from './io';
|
|
5
9
|
|
|
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,72 @@ 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) => document
|
|
30
|
+
.statements(undefined, 'foaf:isPrimaryTopicOf')
|
|
31
|
+
.map(quad => quad.object.value)
|
|
32
|
+
.forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
|
|
33
|
+
const loadProfileDocuments = async (): Promise<void> => {
|
|
34
|
+
for (const [url, document] of Object.entries(documents)) {
|
|
35
|
+
if (document !== null) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const document = await fetchSolidDocument(url, fetch);
|
|
41
|
+
|
|
42
|
+
documents[url] = document;
|
|
43
|
+
store.addQuads(document.getQuads());
|
|
44
|
+
|
|
45
|
+
addReferencedDocumentUrls(document);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof UnauthorizedError) {
|
|
48
|
+
documents[url] = false;
|
|
49
|
+
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
addReferencedDocumentUrls(webIdDocument);
|
|
59
|
+
|
|
60
|
+
do {
|
|
61
|
+
await loadProfileDocuments();
|
|
62
|
+
} while (Object.values(documents).some(document => document === null));
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
store,
|
|
66
|
+
cloaked: Object.values(documents).some(document => document === false),
|
|
67
|
+
writableProfileUrl:
|
|
68
|
+
webIdDocument.isUserWritable()
|
|
69
|
+
? webIdDocument.url
|
|
70
|
+
: Object
|
|
71
|
+
.values(documents)
|
|
72
|
+
.find((document): document is SolidDocument => !!document && document.isUserWritable())
|
|
73
|
+
?.url ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
16
77
|
async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUserProfile> {
|
|
17
78
|
const documentUrl = urlRoute(webId);
|
|
18
79
|
const document = await fetchSolidDocument(documentUrl, fetch);
|
|
19
80
|
|
|
20
|
-
if (!document.isPersonalProfile())
|
|
21
|
-
throw new Error(
|
|
81
|
+
if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer'))
|
|
82
|
+
throw new Error(`${webId} is not a valid webId.`);
|
|
22
83
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
84
|
+
const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, fetch);
|
|
85
|
+
const storageUrls = store.statements(webId, 'pim:storage').map(storage => storage.object.value);
|
|
86
|
+
const publicTypeIndex = store.statement(webId, 'solid:publicTypeIndex');
|
|
87
|
+
const privateTypeIndex = store.statement(webId, 'solid:privateTypeIndex');
|
|
26
88
|
|
|
27
89
|
let parentUrl = urlParentDirectory(documentUrl);
|
|
28
90
|
while (parentUrl && storageUrls.length === 0) {
|
|
@@ -37,19 +99,23 @@ async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUser
|
|
|
37
99
|
parentUrl = urlParentDirectory(parentUrl);
|
|
38
100
|
}
|
|
39
101
|
|
|
40
|
-
return
|
|
102
|
+
return {
|
|
41
103
|
webId,
|
|
42
104
|
storageUrls,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
105
|
+
cloaked,
|
|
106
|
+
writableProfileUrl,
|
|
107
|
+
...objectWithoutEmpty({
|
|
108
|
+
name:
|
|
109
|
+
store.statement(webId, 'vcard:fn')?.object.value ??
|
|
110
|
+
store.statement(webId, 'foaf:name')?.object.value,
|
|
111
|
+
avatarUrl:
|
|
112
|
+
store.statement(webId, 'vcard:hasPhoto')?.object.value ??
|
|
113
|
+
store.statement(webId, 'foaf:img')?.object.value,
|
|
114
|
+
oidcIssuerUrl: store.statement(webId, 'solid:oidcIssuer')?.object.value,
|
|
115
|
+
publicTypeIndexUrl: publicTypeIndex?.object.value,
|
|
116
|
+
privateTypeIndexUrl: privateTypeIndex?.object.value,
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
53
119
|
}
|
|
54
120
|
|
|
55
121
|
export async function fetchLoginUserProfile(loginUrl: string, fetch?: Fetch): Promise<SolidUserProfile | null> {
|
package/src/helpers/index.ts
CHANGED
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
|
-
|
|
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
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,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import md5 from 'md5';
|
|
2
|
+
import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
|
|
2
3
|
import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
|
|
4
|
+
import { fromRDF, toRDF } from 'jsonld';
|
|
3
5
|
import type { JsonLdDocument } from 'jsonld';
|
|
4
|
-
import { toRDF } from 'jsonld';
|
|
5
|
-
import md5 from 'md5';
|
|
6
6
|
import type { Quad } from 'rdf-js';
|
|
7
7
|
import type { Term as N3Term } from 'n3';
|
|
8
8
|
|
|
@@ -13,10 +13,10 @@ import NetworkRequestError from '@/errors/NetworkRequestError';
|
|
|
13
13
|
import NotFoundError from '@/errors/NotFoundError';
|
|
14
14
|
import UnauthorizedError from '@/errors/UnauthorizedError';
|
|
15
15
|
import { isJsonLDGraph } from '@/helpers/jsonld';
|
|
16
|
-
import type { JsonLD } from '@/helpers/jsonld';
|
|
16
|
+
import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
|
|
17
17
|
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
export declare type AnyFetch = (input: any, options?: any) => Promise<
|
|
19
|
+
export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
|
|
20
20
|
export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
|
|
21
21
|
export declare type Fetch = TypedFetch | AnyFetch;
|
|
22
22
|
|
|
@@ -44,7 +44,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
|
|
|
44
44
|
if (error instanceof UnauthorizedError)
|
|
45
45
|
throw error;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
if (error instanceof NotFoundError)
|
|
48
|
+
throw error;
|
|
49
|
+
|
|
50
|
+
throw new NetworkRequestError(url, { cause: error });
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -52,26 +55,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
52
55
|
const normalizedQuads = quads.slice(0);
|
|
53
56
|
const quadsIndexes: Record<string, Set<number>> = {};
|
|
54
57
|
const blankNodeIds = arr(quads)
|
|
55
|
-
.flatMap(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return ids;
|
|
67
|
-
})
|
|
58
|
+
.flatMap(
|
|
59
|
+
(quad, index) => tap(
|
|
60
|
+
arrayFilter([
|
|
61
|
+
quad.object.termType === 'BlankNode' ? quad.object.value : null,
|
|
62
|
+
quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
|
|
63
|
+
]),
|
|
64
|
+
ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
68
67
|
.filter()
|
|
69
68
|
.unique();
|
|
70
69
|
|
|
71
70
|
for (const originalId of blankNodeIds) {
|
|
71
|
+
const quadIndexes = quadsIndexes[originalId] as Set<number>;
|
|
72
72
|
const normalizedId = md5(
|
|
73
|
-
arr(
|
|
74
|
-
.map(index => quads[index])
|
|
73
|
+
arr(quadIndexes)
|
|
74
|
+
.map(index => quads[index] as Quad)
|
|
75
75
|
.filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
|
|
76
76
|
.map(
|
|
77
77
|
({ predicate, object }) => object.termType === 'BlankNode'
|
|
@@ -82,9 +82,12 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
82
82
|
.join(),
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
-
for (const index of
|
|
86
|
-
const quad = normalizedQuads[index];
|
|
87
|
-
const terms: Record<string, N3Term> = {
|
|
85
|
+
for (const index of quadIndexes) {
|
|
86
|
+
const quad = normalizedQuads[index] as Quad;
|
|
87
|
+
const terms: Record<string, N3Term> = {
|
|
88
|
+
subject: quad.subject as N3Term,
|
|
89
|
+
object: quad.object as N3Term,
|
|
90
|
+
};
|
|
88
91
|
|
|
89
92
|
for (const [termName, termValue] of Object.entries(terms)) {
|
|
90
93
|
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
|
|
@@ -93,7 +96,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
93
96
|
terms[termName] = new N3BlankNode(normalizedId);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
arrayReplace(
|
|
99
|
+
arrayReplace(
|
|
100
|
+
normalizedQuads,
|
|
101
|
+
quad,
|
|
102
|
+
new N3Quad(
|
|
103
|
+
terms.subject as N3Term,
|
|
104
|
+
quad.predicate as N3Term,
|
|
105
|
+
terms.object as N3Term,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
|
|
@@ -106,7 +117,7 @@ export interface ParsingOptions {
|
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
109
|
-
fetch = fetch ?? window.fetch;
|
|
120
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
110
121
|
|
|
111
122
|
const statements = await turtleToQuads(body);
|
|
112
123
|
|
|
@@ -126,6 +137,19 @@ export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<So
|
|
|
126
137
|
return new SolidDocument(url, statements, headers);
|
|
127
138
|
}
|
|
128
139
|
|
|
140
|
+
export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
|
|
141
|
+
try {
|
|
142
|
+
const document = await fetchSolidDocument(url, fetch);
|
|
143
|
+
|
|
144
|
+
return document;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!(error instanceof NotFoundError))
|
|
147
|
+
throw error;
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
|
|
130
154
|
if (isJsonLDGraph(jsonld)) {
|
|
131
155
|
const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
|
|
@@ -149,6 +173,14 @@ export function normalizeSparql(sparql: string): string {
|
|
|
149
173
|
.join(' ;\n');
|
|
150
174
|
}
|
|
151
175
|
|
|
176
|
+
export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
|
|
177
|
+
const graph = await fromRDF(quads);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
'@graph': graph as JsonLDResource[],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
export function quadsToTurtle(quads: Quad[]): string {
|
|
153
185
|
const writer = new TurtleWriter;
|
|
154
186
|
|
|
@@ -175,7 +207,7 @@ export async function sparqlToQuads(
|
|
|
175
207
|
sparql: string,
|
|
176
208
|
options: Partial<ParsingOptions> = {},
|
|
177
209
|
): Promise<Record<string, Quad[]>> {
|
|
178
|
-
const operations = sparql
|
|
210
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
179
211
|
const quads: Record<string, Quad[]> = {};
|
|
180
212
|
|
|
181
213
|
await Promise.all([...operations].map(async operation => {
|
|
@@ -189,7 +221,7 @@ export async function sparqlToQuads(
|
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
|
|
192
|
-
const operations = sparql
|
|
224
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
193
225
|
const quads: Record<string, Quad[]> = {};
|
|
194
226
|
|
|
195
227
|
for (const operation of operations) {
|
|
@@ -253,7 +285,7 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
|
|
|
253
285
|
}
|
|
254
286
|
|
|
255
287
|
export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
|
|
256
|
-
fetch = fetch ?? window.fetch;
|
|
288
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
257
289
|
|
|
258
290
|
await fetch(url, {
|
|
259
291
|
method: 'PATCH',
|
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)
|