@noeldemartin/solid-utils 0.1.1-next.7b4ae3cff8eddb646fb583cde8416341461a0f08 → 0.1.1-next.8987fd38d01bd74fdde315a5f60c5f95f5183cd2

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/package.json CHANGED
@@ -1,26 +1,27 @@
1
1
  {
2
2
  "name": "@noeldemartin/solid-utils",
3
- "version": "0.1.1-next.7b4ae3cff8eddb646fb583cde8416341461a0f08",
3
+ "version": "0.1.1-next.8987fd38d01bd74fdde315a5f60c5f95f5183cd2",
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",
10
11
  "build:js": "noeldemartin-build-javascript",
11
12
  "build:types": "noeldemartin-build-types",
12
- "lint": "eslint src && tsc --noEmit",
13
+ "lint": "noeldemartin-lint src",
13
14
  "publish-next": "noeldemartin-publish-next",
14
15
  "test": "jest --verbose",
15
16
  "test:coverage": "jest --coverage"
16
17
  },
17
- "engines": {
18
- "node": ">=14.15.0 <15"
19
- },
20
18
  "repository": {
21
19
  "type": "git",
22
20
  "url": "git+https://github.com/noeldemartin/solid-utils.git"
23
21
  },
22
+ "engines": {
23
+ "node": ">=14.x"
24
+ },
24
25
  "author": "Noel De Martin",
25
26
  "license": "MIT",
26
27
  "bugs": {
@@ -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.2.1-next.c3e1bb57127c6f5eab27c6fbb99463760b5aa02c",
33
+ "@noeldemartin/solid-utils-external": "^0.1.0",
34
+ "@noeldemartin/utils": "0.2.1-next.6b17f2fc333f9389f0f4ebca96f962cd2172ee26",
33
35
  "@types/rdf-js": "^4.0.1",
34
36
  "core-js": "^3.12.1",
35
37
  "jest-diff": "^26.6.2",
36
- "jsonld": "^5.2.0",
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.0",
48
- "@noeldemartin/scripts": "^0.1.1",
47
+ "@noeldemartin/eslint-config-typescript": "^0.1.0-next.e83e5d37837b9845675534ab9d3745820b1326b4",
48
+ "@noeldemartin/scripts": "^0.1.1-next.7c3f1d44c5b0fe2a75160df634ca8cae4486d378",
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 { Error } from '@noeldemartin/utils';
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 Error {
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 { Error } from '@noeldemartin/utils';
1
+ import { JSError } from '@noeldemartin/utils';
2
+ import type { JSErrorOptions } from '@noeldemartin/utils';
2
3
 
3
- export default class NetworkRequestError extends Error {
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,6 +1,6 @@
1
- import { Error } from '@noeldemartin/utils';
1
+ import { JSError } from '@noeldemartin/utils';
2
2
 
3
- export default class NotFoundError extends Error {
3
+ export default class NotFoundError extends JSError {
4
4
 
5
5
  public readonly url: string;
6
6
 
@@ -1,4 +1,4 @@
1
- import { Error } from '@noeldemartin/utils';
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 Error {
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
+ }
@@ -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 UnsuccessfulNetworkRequestError } from './UnsuccessfulNetworkRequestError';
@@ -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(`Document at ${documentUrl} is not a profile.`);
81
+ if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer'))
82
+ throw new Error(`${webId} is not a valid webId.`);
22
83
 
23
- const storageUrls = document.statements(webId, 'pim:storage').map(storage => storage.object.value);
24
- const publicTypeIndex = document.statement(webId, 'solid:publicTypeIndex');
25
- const privateTypeIndex = document.statement(webId, 'solid:privateTypeIndex');
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 objectWithoutEmpty({
102
+ return {
41
103
  webId,
42
104
  storageUrls,
43
- name:
44
- document.statement(webId, 'vcard:fn')?.object.value ??
45
- document.statement(webId, 'foaf:name')?.object.value,
46
- avatarUrl:
47
- document.statement(webId, 'vcard:hasPhoto')?.object.value ??
48
- document.statement(webId, 'foaf:img')?.object.value,
49
- oidcIssuerUrl: document.statement(webId, 'solid:oidcIssuer')?.object.value,
50
- publicTypeIndexUrl: publicTypeIndex?.object.value,
51
- privateTypeIndexUrl: privateTypeIndex?.object.value,
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> {
@@ -0,0 +1,56 @@
1
+ import { arr, isArray, isObject, objectDeepClone, objectWithoutEmpty, tap, urlParse, uuid } from '@noeldemartin/utils';
2
+ import type { UrlParts } from '@noeldemartin/utils';
3
+ import type { JsonLD, JsonLDResource } from '@/helpers';
4
+
5
+ export interface SubjectParts {
6
+ containerUrl?: string;
7
+ documentName?: string;
8
+ resourceHash?: string;
9
+ }
10
+
11
+ function getContainerPath(parts: UrlParts): string | null {
12
+ if (!parts.path || !parts.path.startsWith('/'))
13
+ return null;
14
+
15
+ if (parts.path.match(/^\/[^/]*$/))
16
+ return '/';
17
+
18
+ return `/${arr(parts.path.split('/')).filter().slice(0, -1).join('/')}/`.replace('//', '/');
19
+ }
20
+
21
+ function getContainerUrl(parts: UrlParts): string | null {
22
+ const containerPath = getContainerPath(parts);
23
+
24
+ return parts.protocol && parts.domain
25
+ ? `${parts.protocol}://${parts.domain}${containerPath ?? '/'}`
26
+ : containerPath;
27
+ }
28
+
29
+ function __mintJsonLDIdentifiers(jsonld: JsonLD): void {
30
+ if (!('@type' in jsonld) || '@value' in jsonld)
31
+ return;
32
+
33
+ jsonld['@id'] = jsonld['@id'] ?? uuid();
34
+
35
+ for (const propertyValue of Object.values(jsonld)) {
36
+ if (isObject(propertyValue))
37
+ __mintJsonLDIdentifiers(propertyValue);
38
+
39
+ if (isArray(propertyValue))
40
+ propertyValue.forEach(value => isObject(value) && __mintJsonLDIdentifiers(value));
41
+ }
42
+ }
43
+
44
+ export function mintJsonLDIdentifiers(jsonld: JsonLD): JsonLDResource {
45
+ return tap(objectDeepClone(jsonld) as JsonLDResource, clone => __mintJsonLDIdentifiers(clone));
46
+ }
47
+
48
+ export function parseResourceSubject(subject: string): SubjectParts {
49
+ const parts = urlParse(subject);
50
+
51
+ return !parts ? {} : objectWithoutEmpty({
52
+ containerUrl: getContainerUrl(parts),
53
+ documentName: parts.path ? parts.path.split('/').pop() : null,
54
+ resourceHash: parts.fragment,
55
+ });
56
+ }
@@ -1,6 +1,8 @@
1
1
  export * from './auth';
2
+ export * from './identifiers';
2
3
  export * from './interop';
3
4
  export * from './io';
4
5
  export * from './jsonld';
5
6
  export * from './testing';
6
7
  export * from './vocabs';
8
+ export * from './wac';
@@ -1,57 +1,95 @@
1
1
  import { uuid } from '@noeldemartin/utils';
2
2
 
3
3
  import { createSolidDocument, fetchSolidDocument, solidDocumentExists, updateSolidDocument } from '@/helpers/io';
4
- import type SolidThing from '@/models/SolidThing';
5
4
  import type { Fetch } from '@/helpers/io';
6
5
  import type { SolidUserProfile } from '@/helpers/auth';
7
6
 
8
- async function mintPrivateTypeIndexUrl(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
9
- fetch = fetch ?? window.fetch;
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/privateTypeIndex`;
13
+ const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
13
14
 
14
15
  return await solidDocumentExists(typeIndexUrl, fetch)
15
- ? `${storageUrl}settings/privateTypeIndex-${uuid()}`
16
+ ? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
16
17
  : typeIndexUrl;
17
18
  }
18
19
 
19
- export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
20
- fetch = fetch ?? window.fetch;
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
- const typeIndexUrl = await mintPrivateTypeIndexUrl(user, fetch);
23
- const typeIndexBody = `
24
- <> a
25
- <http://www.w3.org/ns/solid/terms#TypeIndex>,
26
- <http://www.w3.org/ns/solid/terms#UnlistedDocument> .
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#privateTypeIndex> <${typeIndexUrl}> .
37
+ <${user.webId}> <http://www.w3.org/ns/solid/terms#${type}TypeIndex> <${typeIndexUrl}> .
31
38
  }
32
39
  `;
33
40
 
34
- createSolidDocument(typeIndexUrl, typeIndexBody, fetch);
35
- updateSolidDocument(user.webId, profileUpdateBody, fetch);
41
+ await Promise.all([
42
+ createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
43
+ updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
44
+ ]);
45
+
46
+ if (type === 'public') {
47
+ // TODO Implement updating ACLs for the listing itself to public
48
+ }
36
49
 
37
50
  return typeIndexUrl;
38
51
  }
39
52
 
40
- export async function findContainerRegistration(
53
+ async function findRegistrations(
41
54
  typeIndexUrl: string,
42
- childrenType: string,
55
+ type: string | string[],
56
+ predicate: string,
43
57
  fetch?: Fetch,
44
- ): Promise<SolidThing | null> {
58
+ ): Promise<string[]> {
45
59
  const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
46
- const containerQuad = typeIndex
47
- .statements(undefined, 'rdfs:type', 'solid:TypeRegistration')
48
- .find(
49
- statement =>
50
- typeIndex.contains(statement.subject.value, 'solid:forClass', childrenType) &&
51
- typeIndex.contains(statement.subject.value, 'solid:instanceContainer'),
52
- );
53
-
54
- return containerQuad
55
- ? typeIndex.getThing(containerQuad.subject.value) ?? null
56
- : null;
60
+ const types = Array.isArray(type) ? type : [type];
61
+
62
+ return types.map(
63
+ type => typeIndex
64
+ .statements(undefined, 'rdf:type', 'solid:TypeRegistration')
65
+ .filter(statement => typeIndex.contains(statement.subject.value, 'solid:forClass', type))
66
+ .map(statement => typeIndex.statements(statement.subject.value, predicate))
67
+ .flat()
68
+ .map(statement => statement.object.value)
69
+ .filter(url => !!url),
70
+ ).flat();
71
+ }
72
+
73
+ export async function createPublicTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
74
+ return createTypeIndex(user, 'public', fetch);
75
+ }
76
+
77
+ export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
78
+ return createTypeIndex(user, 'private', fetch);
79
+ }
80
+
81
+ export async function findContainerRegistrations(
82
+ typeIndexUrl: string,
83
+ type: string | string[],
84
+ fetch?: Fetch,
85
+ ): Promise<string[]> {
86
+ return findRegistrations(typeIndexUrl, type, 'solid:instanceContainer', fetch);
87
+ }
88
+
89
+ export async function findInstanceRegistrations(
90
+ typeIndexUrl: string,
91
+ type: string | string[],
92
+ fetch?: Fetch,
93
+ ): Promise<string[]> {
94
+ return findRegistrations(typeIndexUrl, type, 'solid:instance', fetch);
57
95
  }