@noeldemartin/solid-utils 0.1.1-next.f279ff39536b39493ea8febae8d8052a9a3b1365 → 0.2.0-next.1b9830994a89d900e969b890901e13f7f04df993
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 +125 -18
- 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 +9 -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 +104 -19
- 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/main.ts +1 -0
- package/src/models/SolidDocument.ts +41 -35
- package/src/models/SolidStore.ts +61 -0
- package/src/models/index.ts +2 -1
- package/src/testing/ResponseStub.ts +46 -0
- package/src/testing/faking.ts +36 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/mocking.ts +34 -0
- package/src/types/n3.d.ts +9 -0
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)}`);
|
package/src/helpers/vocabs.ts
CHANGED
|
@@ -10,7 +10,8 @@ const knownPrefixes: RDFContext = {
|
|
|
10
10
|
foaf: 'http://xmlns.com/foaf/0.1/',
|
|
11
11
|
pim: 'http://www.w3.org/ns/pim/space#',
|
|
12
12
|
purl: 'http://purl.org/dc/terms/',
|
|
13
|
-
|
|
13
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
14
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
14
15
|
schema: 'https://schema.org/',
|
|
15
16
|
solid: 'http://www.w3.org/ns/solid/terms#',
|
|
16
17
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
package/src/helpers/wac.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
+
import UnsupportedAuthorizationProtocolError from '@/errors/UnsupportedAuthorizationProtocolError';
|
|
3
4
|
import { fetchSolidDocumentIfFound } from '@/helpers/io';
|
|
4
5
|
import type SolidDocument from '@/models/SolidDocument';
|
|
5
6
|
import type { Fetch } from '@/helpers/io';
|
|
@@ -25,8 +26,17 @@ async function fetchEffectiveACL(
|
|
|
25
26
|
): Promise<SolidDocument> {
|
|
26
27
|
aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch);
|
|
30
|
+
|
|
31
|
+
if (!aclDocument) {
|
|
32
|
+
return fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (aclDocument.isACPResource()) {
|
|
36
|
+
throw new UnsupportedAuthorizationProtocolError(resourceUrl, 'ACP');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return aclDocument;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
|
package/src/main.ts
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
|
-
import { parseDate } from '@noeldemartin/utils';
|
|
1
|
+
import { arrayFilter, parseDate, stringMatch } from '@noeldemartin/utils';
|
|
2
2
|
import type { Quad } from 'rdf-js';
|
|
3
3
|
|
|
4
4
|
import { expandIRI } from '@/helpers/vocabs';
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import SolidStore from './SolidStore';
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export enum SolidDocumentPermission {
|
|
9
|
+
Read = 'read',
|
|
10
|
+
Write = 'write',
|
|
11
|
+
Append = 'append',
|
|
12
|
+
Control = 'control',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class SolidDocument extends SolidStore {
|
|
9
16
|
|
|
10
17
|
public readonly url: string;
|
|
11
18
|
public readonly headers: Headers;
|
|
12
|
-
private quads: Quad[];
|
|
13
19
|
|
|
14
20
|
public constructor(url: string, quads: Quad[], headers: Headers) {
|
|
21
|
+
super(quads);
|
|
22
|
+
|
|
15
23
|
this.url = url;
|
|
16
|
-
this.quads = quads;
|
|
17
24
|
this.headers = headers;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
public
|
|
21
|
-
return this.
|
|
27
|
+
public isACPResource(): boolean {
|
|
28
|
+
return !!this.headers.get('Link')
|
|
29
|
+
?.match(/<http:\/\/www\.w3\.org\/ns\/solid\/acp#AccessControlResource>;[^,]+rel="type"/);
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
public isPersonalProfile(): boolean {
|
|
25
33
|
return !!this.statement(
|
|
26
34
|
this.url,
|
|
27
|
-
expandIRI('
|
|
35
|
+
expandIRI('rdf:type'),
|
|
28
36
|
expandIRI('foaf:PersonalProfileDocument'),
|
|
29
37
|
);
|
|
30
38
|
}
|
|
@@ -33,41 +41,27 @@ export default class SolidDocument {
|
|
|
33
41
|
return !!this.headers.get('Link')?.match(/<http:\/\/www\.w3\.org\/ns\/pim\/space#Storage>;[^,]+rel="type"/);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
public
|
|
37
|
-
return
|
|
38
|
-
?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
|
|
39
|
-
?? this.getLatestDocumentDate()
|
|
40
|
-
?? null;
|
|
44
|
+
public isUserWritable(): boolean {
|
|
45
|
+
return this.getUserPermissions().includes(SolidDocumentPermission.Write);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
public
|
|
44
|
-
return this.
|
|
45
|
-
statement =>
|
|
46
|
-
(!object || statement.object.value === expandIRI(object, { defaultPrefix: this.url })) &&
|
|
47
|
-
(!subject || statement.subject.value === expandIRI(subject, { defaultPrefix: this.url })) &&
|
|
48
|
-
(!predicate || statement.predicate.value === expandIRI(predicate, { defaultPrefix: this.url })),
|
|
49
|
-
);
|
|
48
|
+
public getUserPermissions(): SolidDocumentPermission[] {
|
|
49
|
+
return this.getPermissionsFromWAC('user');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
public
|
|
53
|
-
|
|
54
|
-
statement =>
|
|
55
|
-
(!object || statement.object.value === expandIRI(object, { defaultPrefix: this.url })) &&
|
|
56
|
-
(!subject || statement.subject.value === expandIRI(subject, { defaultPrefix: this.url })) &&
|
|
57
|
-
(!predicate || statement.predicate.value === expandIRI(predicate, { defaultPrefix: this.url })),
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
return statement ?? null;
|
|
52
|
+
public getPublicPermissions(): SolidDocumentPermission[] {
|
|
53
|
+
return this.getPermissionsFromWAC('public');
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
public
|
|
64
|
-
return this.
|
|
56
|
+
public getLastModified(): Date | null {
|
|
57
|
+
return parseDate(this.headers.get('last-modified'))
|
|
58
|
+
?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
|
|
59
|
+
?? this.getLatestDocumentDate()
|
|
60
|
+
?? null;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return new SolidThing(subject, statements);
|
|
63
|
+
protected expandIRI(iri: string): string {
|
|
64
|
+
return expandIRI(iri, { defaultPrefix: this.url });
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
private getLatestDocumentDate(): Date | null {
|
|
@@ -81,4 +75,16 @@ export default class SolidDocument {
|
|
|
81
75
|
return dates.length > 0 ? dates.reduce((a, b) => a > b ? a : b) : null;
|
|
82
76
|
}
|
|
83
77
|
|
|
78
|
+
private getPermissionsFromWAC(type: string): SolidDocumentPermission[] {
|
|
79
|
+
const wacAllow = this.headers.get('WAC-Allow') ?? '';
|
|
80
|
+
const publicModes = stringMatch<2>(wacAllow, new RegExp(`${type}="([^"]+)"`))?.[1] ?? '';
|
|
81
|
+
|
|
82
|
+
return arrayFilter([
|
|
83
|
+
publicModes.includes('read') && SolidDocumentPermission.Read,
|
|
84
|
+
publicModes.includes('write') && SolidDocumentPermission.Write,
|
|
85
|
+
publicModes.includes('append') && SolidDocumentPermission.Append,
|
|
86
|
+
publicModes.includes('control') && SolidDocumentPermission.Control,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Quad } from 'rdf-js';
|
|
2
|
+
|
|
3
|
+
import { expandIRI } from '@/helpers/vocabs';
|
|
4
|
+
|
|
5
|
+
import SolidThing from './SolidThing';
|
|
6
|
+
|
|
7
|
+
export default class SolidStore {
|
|
8
|
+
|
|
9
|
+
private quads: Quad[];
|
|
10
|
+
|
|
11
|
+
public constructor(quads: Quad[] = []) {
|
|
12
|
+
this.quads = quads;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public isEmpty(): boolean {
|
|
16
|
+
return this.statements.length === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public getQuads(): Quad[] {
|
|
20
|
+
return this.quads.slice(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public addQuads(quads: Quad[]): void {
|
|
24
|
+
this.quads.push(...quads);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public statements(subject?: string, predicate?: string, object?: string): Quad[] {
|
|
28
|
+
return this.quads.filter(
|
|
29
|
+
statement =>
|
|
30
|
+
(!object || statement.object.value === this.expandIRI(object)) &&
|
|
31
|
+
(!subject || statement.subject.value === this.expandIRI(subject)) &&
|
|
32
|
+
(!predicate || statement.predicate.value === this.expandIRI(predicate)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public statement(subject?: string, predicate?: string, object?: string): Quad | null {
|
|
37
|
+
const statement = this.quads.find(
|
|
38
|
+
statement =>
|
|
39
|
+
(!object || statement.object.value === this.expandIRI(object)) &&
|
|
40
|
+
(!subject || statement.subject.value === this.expandIRI(subject)) &&
|
|
41
|
+
(!predicate || statement.predicate.value === this.expandIRI(predicate)),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return statement ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public contains(subject: string, predicate?: string, object?: string): boolean {
|
|
48
|
+
return this.statement(subject, predicate, object) !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getThing(subject: string): SolidThing {
|
|
52
|
+
const statements = this.statements(subject);
|
|
53
|
+
|
|
54
|
+
return new SolidThing(subject, statements);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected expandIRI(iri: string): string {
|
|
58
|
+
return expandIRI(iri);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}
|
package/src/models/index.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export default class ResponseStub implements Response {
|
|
2
|
+
|
|
3
|
+
private rawBody: string;
|
|
4
|
+
|
|
5
|
+
public readonly body!: ReadableStream<Uint8Array> | null;
|
|
6
|
+
public readonly bodyUsed!: boolean;
|
|
7
|
+
public readonly headers: Headers;
|
|
8
|
+
public readonly ok!: boolean;
|
|
9
|
+
public readonly redirected!: boolean;
|
|
10
|
+
public readonly status: number;
|
|
11
|
+
public readonly statusText!: string;
|
|
12
|
+
public readonly trailer!: Promise<Headers>;
|
|
13
|
+
public readonly type!: ResponseType;
|
|
14
|
+
public readonly url!: string;
|
|
15
|
+
|
|
16
|
+
public constructor(rawBody: string = '', headers: Record<string, string> = {}, status: number = 200) {
|
|
17
|
+
this.rawBody = rawBody;
|
|
18
|
+
this.headers = new Headers(headers);
|
|
19
|
+
this.status = status;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public async arrayBuffer(): Promise<ArrayBuffer> {
|
|
23
|
+
throw new Error('ResponseStub.arrayBuffer is not implemented');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async blob(): Promise<Blob> {
|
|
27
|
+
throw new Error('ResponseStub.blob is not implemented');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async formData(): Promise<FormData> {
|
|
31
|
+
throw new Error('ResponseStub.formData is not implemented');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async json(): Promise<unknown> {
|
|
35
|
+
return JSON.parse(this.rawBody);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async text(): Promise<string> {
|
|
39
|
+
return this.rawBody;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public clone(): Response {
|
|
43
|
+
return { ...this };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { faker } from '@noeldemartin/faker';
|
|
2
|
+
import { stringToSlug } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
export interface ContainerOptions {
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DocumentOptions extends ContainerOptions {
|
|
9
|
+
containerUrl: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResourceOptions extends DocumentOptions {
|
|
14
|
+
documentUrl: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function fakeContainerUrl(options: Partial<ContainerOptions> = {}): string {
|
|
19
|
+
const baseUrl = options.baseUrl ?? faker.internet.url();
|
|
20
|
+
|
|
21
|
+
return baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fakeDocumentUrl(options: Partial<DocumentOptions> = {}): string {
|
|
25
|
+
const containerUrl = options.containerUrl ?? fakeContainerUrl(options);
|
|
26
|
+
const name = options.name ?? faker.random.word();
|
|
27
|
+
|
|
28
|
+
return containerUrl + stringToSlug(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fakeResourceUrl(options: Partial<ResourceOptions> = {}): string {
|
|
32
|
+
const documentUrl = options.documentUrl ?? fakeDocumentUrl(options);
|
|
33
|
+
const hash = options.hash ?? 'it';
|
|
34
|
+
|
|
35
|
+
return documentUrl + '#' + hash;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fail } from '@noeldemartin/utils';
|
|
2
|
+
import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
import type { Fetch } from '@/helpers/io';
|
|
5
|
+
|
|
6
|
+
import ResponseStub from './ResponseStub';
|
|
7
|
+
|
|
8
|
+
export interface FetchMockMethods {
|
|
9
|
+
mockResponse(body?: string, headers?: Record<string, string>, status?: number): void;
|
|
10
|
+
mockNotFoundResponse(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FetchMock = jest.Mock<GetClosureResult<Fetch>, GetClosureArgs<Fetch>> & Fetch & FetchMockMethods;
|
|
14
|
+
|
|
15
|
+
export function mockFetch(): FetchMock {
|
|
16
|
+
const responses: ResponseStub[] = [];
|
|
17
|
+
const methods: FetchMockMethods = {
|
|
18
|
+
mockResponse(body, headers, status) {
|
|
19
|
+
responses.push(new ResponseStub(body, headers, status));
|
|
20
|
+
},
|
|
21
|
+
mockNotFoundResponse() {
|
|
22
|
+
responses.push(new ResponseStub('', {}, 404));
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
27
|
+
const fetchMock = jest.fn(async (...args: GetClosureArgs<Fetch>) => {
|
|
28
|
+
return responses.shift() ?? fail<Response>('fetch mock called without response');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
Object.assign(fetchMock, methods);
|
|
32
|
+
|
|
33
|
+
return fetchMock as FetchMock;
|
|
34
|
+
}
|