@noeldemartin/solid-utils 0.1.1-next.0138d472d679413be54bc014f8cf21f03a1e1c3c → 0.1.1-next.03a73b36b7695e277f0298e6355fc1c99c8d0b2b

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.
@@ -1,7 +1,9 @@
1
- module.exports = {
1
+ const { defineConfig } = require('@noeldemartin/scripts');
2
+
3
+ module.exports = defineConfig({
2
4
  name: 'NoelDeMartinSolidUtils',
3
5
  declarations: [
4
6
  // 'src/plugins/cypress/types.d.ts',
5
7
  'src/plugins/jest/types.d.ts',
6
8
  ],
7
- };
9
+ });
package/package.json CHANGED
@@ -1,26 +1,27 @@
1
1
  {
2
2
  "name": "@noeldemartin/solid-utils",
3
- "version": "0.1.1-next.0138d472d679413be54bc014f8cf21f03a1e1c3c",
3
+ "version": "0.1.1-next.03a73b36b7695e277f0298e6355fc1c99c8d0b2b",
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.f9c86762b10628346fab4d9d8f543b7f9e3c997f",
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
+ }
@@ -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
+ }
@@ -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';
@@ -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
  }