@noeldemartin/solid-utils 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noeldemartin/solid-utils",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -31,10 +31,9 @@
31
31
  "homepage": "https://github.com/noeldemartin/solid-utils",
32
32
  "dependencies": {
33
33
  "@babel/runtime": "^7.14.0",
34
- "@noeldemartin/faker": "^7.6.0",
35
34
  "@noeldemartin/solid-utils-external": "^0.1.1",
36
- "@noeldemartin/utils": "^0.5.1",
37
- "@types/rdf-js": "^4.0.1",
35
+ "@noeldemartin/utils": "^0.6.0",
36
+ "@rdfjs/types": "^1.1.0",
38
37
  "core-js": "^3.12.1",
39
38
  "md5": "^2.3.0"
40
39
  },
@@ -5,7 +5,7 @@ import UnauthorizedError from '../errors/UnauthorizedError';
5
5
  import type SolidDocument from '../models/SolidDocument';
6
6
 
7
7
  import { fetchSolidDocument } from './io';
8
- import type { Fetch } from './io';
8
+ import type { Fetch, FetchSolidDocumentOptions } from './io';
9
9
 
10
10
  export interface SolidUserProfile {
11
11
  webId: string;
@@ -19,7 +19,7 @@ export interface SolidUserProfile {
19
19
  privateTypeIndexUrl?: string;
20
20
  }
21
21
 
22
- async function fetchExtendedUserProfile(webIdDocument: SolidDocument, fetch?: Fetch): Promise<{
22
+ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, options?: FetchSolidDocumentOptions): Promise<{
23
23
  store: SolidStore;
24
24
  cloaked: boolean;
25
25
  writableProfileUrl: string | null;
@@ -43,7 +43,7 @@ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, fetch?: Fe
43
43
  }
44
44
 
45
45
  try {
46
- const document = await fetchSolidDocument(url, fetch);
46
+ const document = await fetchSolidDocument(url, options);
47
47
 
48
48
  documents[url] = document;
49
49
  store.addQuads(document.getQuads());
@@ -80,22 +80,30 @@ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, fetch?: Fe
80
80
  };
81
81
  }
82
82
 
83
- async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUserProfile> {
83
+ async function fetchUserProfile(webId: string, options: FetchUserProfileOptions = {}): Promise<SolidUserProfile> {
84
+ const requestOptions: FetchSolidDocumentOptions = {
85
+ fetch: options.fetch,
86
+
87
+ // Needed for CSS v7.1.3.
88
+ // See https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1972
89
+ cache: 'no-store',
90
+ };
91
+
84
92
  const documentUrl = urlRoute(webId);
85
- const document = await fetchSolidDocument(documentUrl, fetch);
93
+ const document = await fetchSolidDocument(documentUrl, requestOptions);
86
94
 
87
95
  if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer')) {
88
96
  throw new Error(`${webId} is not a valid webId.`);
89
97
  }
90
98
 
91
- const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, fetch);
99
+ const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, options);
92
100
  const storageUrls = store.statements(webId, 'pim:storage').map(storage => storage.object.value);
93
101
  const publicTypeIndex = store.statement(webId, 'solid:publicTypeIndex');
94
102
  const privateTypeIndex = store.statement(webId, 'solid:privateTypeIndex');
95
103
 
96
104
  let parentUrl = urlParentDirectory(documentUrl);
97
105
  while (parentUrl && storageUrls.length === 0) {
98
- const parentDocument = await silenced(fetchSolidDocument(parentUrl, fetch));
106
+ const parentDocument = await silenced(fetchSolidDocument(parentUrl, requestOptions));
99
107
 
100
108
  if (parentDocument?.isStorage()) {
101
109
  storageUrls.push(parentUrl);
@@ -110,6 +118,8 @@ async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUser
110
118
  throw new Error(`Could not find any storage for ${webId}.`);
111
119
  }
112
120
 
121
+ await options.onLoaded?.(new SolidStore(store.statements(webId)));
122
+
113
123
  return {
114
124
  webId,
115
125
  cloaked,
@@ -129,9 +139,13 @@ async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUser
129
139
  };
130
140
  }
131
141
 
132
- export interface FetchLoginUserProfileOptions {
133
- required?: boolean;
142
+ export interface FetchUserProfileOptions {
134
143
  fetch?: Fetch;
144
+ onLoaded?(store: SolidStore): Promise<unknown> | unknown;
145
+ }
146
+
147
+ export interface FetchLoginUserProfileOptions extends FetchUserProfileOptions {
148
+ required?: boolean;
135
149
  }
136
150
 
137
151
  export async function fetchLoginUserProfile(
@@ -139,10 +153,10 @@ export async function fetchLoginUserProfile(
139
153
  options: FetchLoginUserProfileOptions = {},
140
154
  ): Promise<SolidUserProfile | null> {
141
155
  if (options.required) {
142
- return fetchUserProfile(loginUrl, options.fetch);
156
+ return fetchUserProfile(loginUrl, options);
143
157
  }
144
158
 
145
- const fetchProfile = silenced(url => fetchUserProfile(url, options.fetch));
159
+ const fetchProfile = silenced(url => fetchUserProfile(url, options));
146
160
 
147
161
  return await fetchProfile(loginUrl)
148
162
  ?? await fetchProfile(loginUrl.replace(/\/$/, '').concat('/profile/card#me'))
@@ -12,7 +12,7 @@ async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fet
12
12
  const storageUrl = user.storageUrls[0];
13
13
  const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
14
14
 
15
- return await solidDocumentExists(typeIndexUrl, fetch)
15
+ return await solidDocumentExists(typeIndexUrl, { fetch })
16
16
  ? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
17
17
  : typeIndexUrl;
18
18
  }
@@ -38,10 +38,8 @@ async function createTypeIndex(user: SolidUserProfile, type: TypeIndexType, fetc
38
38
  }
39
39
  `;
40
40
 
41
- await Promise.all([
42
- createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
43
- updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
44
- ]);
41
+ await createSolidDocument(typeIndexUrl, typeIndexBody, fetch);
42
+ await updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch);
45
43
 
46
44
  if (type === 'public') {
47
45
  // TODO This is currently implemented in soukai-solid.
@@ -59,7 +57,7 @@ async function findRegistrations(
59
57
  predicate: string,
60
58
  fetch?: Fetch,
61
59
  ): Promise<string[]> {
62
- const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
60
+ const typeIndex = await fetchSolidDocument(typeIndexUrl, { fetch });
63
61
  const types = Array.isArray(type) ? type : [type];
64
62
 
65
63
  return types.map(
package/src/helpers/io.ts CHANGED
@@ -25,13 +25,24 @@ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
25
25
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
26
26
  export declare type Fetch = TypedFetch | AnyFetch;
27
27
 
28
- async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body: string; headers: Headers }> {
29
- const options = {
28
+ const ANONYMOUS_PREFIX = 'anonymous://';
29
+ const ANONYMOUS_PREFIX_LENGTH = ANONYMOUS_PREFIX.length;
30
+
31
+ async function fetchRawSolidDocument(
32
+ url: string,
33
+ options?: FetchSolidDocumentOptions,
34
+ ): Promise<{ body: string; headers: Headers }> {
35
+ const requestOptions: RequestInit = {
30
36
  headers: { Accept: 'text/turtle' },
31
37
  };
32
38
 
39
+ if (options?.cache) {
40
+ requestOptions.cache = options.cache;
41
+ }
42
+
33
43
  try {
34
- const response = await fetch(url, options);
44
+ const fetch = options?.fetch ?? window.fetch;
45
+ const response = await fetch(url, requestOptions);
35
46
 
36
47
  if (response.status === 404)
37
48
  throw new NotFoundError(url);
@@ -116,6 +127,33 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
116
127
  return normalizedQuads;
117
128
  }
118
129
 
130
+ function normalizeQuads(quads: Quad[]): string {
131
+ return quads.map(quad => ' ' + quadToTurtle(quad)).sort().join('\n');
132
+ }
133
+
134
+ function preprocessSubjects(jsonld: JsonLD): void {
135
+ if (!jsonld['@id']?.startsWith('#')) {
136
+ return;
137
+ }
138
+
139
+ jsonld['@id'] = ANONYMOUS_PREFIX + jsonld['@id'];
140
+ }
141
+
142
+ function postprocessSubjects(quads: Quad[]): void {
143
+ for (const quad of quads) {
144
+ if (!quad.subject.value.startsWith(ANONYMOUS_PREFIX)) {
145
+ continue;
146
+ }
147
+
148
+ quad.subject.value = quad.subject.value.slice(ANONYMOUS_PREFIX_LENGTH);
149
+ }
150
+ }
151
+
152
+ export interface FetchSolidDocumentOptions {
153
+ fetch?: Fetch;
154
+ cache?: RequestCache;
155
+ }
156
+
119
157
  export interface ParsingOptions {
120
158
  baseIRI: string;
121
159
  normalizeBlankNodes: boolean;
@@ -140,16 +178,19 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
140
178
  return new SolidDocument(url, statements, new Headers({}));
141
179
  }
142
180
 
143
- export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
144
- const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
181
+ export async function fetchSolidDocument(url: string, options?: FetchSolidDocumentOptions): Promise<SolidDocument> {
182
+ const { body: data, headers } = await fetchRawSolidDocument(url, options);
145
183
  const statements = await turtleToQuads(data, { baseIRI: url });
146
184
 
147
185
  return new SolidDocument(url, statements, headers);
148
186
  }
149
187
 
150
- export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
188
+ export async function fetchSolidDocumentIfFound(
189
+ url: string,
190
+ options?: FetchSolidDocumentOptions,
191
+ ): Promise<SolidDocument | null> {
151
192
  try {
152
- const document = await fetchSolidDocument(url, fetch);
193
+ const document = await fetchSolidDocument(url, options);
153
194
 
154
195
  return document;
155
196
  } catch (error) {
@@ -167,7 +208,13 @@ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Q
167
208
  return graphQuads.flat();
168
209
  }
169
210
 
170
- return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
211
+ preprocessSubjects(jsonld);
212
+
213
+ const quads = await (jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
214
+
215
+ postprocessSubjects(quads);
216
+
217
+ return quads;
171
218
  }
172
219
 
173
220
  export function normalizeSparql(sparql: string): string {
@@ -176,13 +223,19 @@ export function normalizeSparql(sparql: string): string {
176
223
  return Object
177
224
  .entries(quads)
178
225
  .reduce((normalizedOperations, [operation, quads]) => {
179
- const normalizedQuads = quads.map(quad => ' ' + quadToTurtle(quad)).sort().join('\n');
226
+ const normalizedQuads = normalizeQuads(quads);
180
227
 
181
228
  return normalizedOperations.concat(`${operation.toUpperCase()} DATA {\n${normalizedQuads}\n}`);
182
229
  }, [] as string[])
183
230
  .join(' ;\n');
184
231
  }
185
232
 
233
+ export function normalizeTurtle(sparql: string): string {
234
+ const quads = turtleToQuadsSync(sparql);
235
+
236
+ return normalizeQuads(quads);
237
+ }
238
+
186
239
  export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
187
240
  const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
188
241
  const parser = new TurtleParser(parserOptions);
@@ -249,9 +302,9 @@ export function quadToTurtle(quad: Quad): string {
249
302
  return writer.quadsToString([quad]).slice(0, -1);
250
303
  }
251
304
 
252
- export async function solidDocumentExists(url: string, fetch?: Fetch): Promise<boolean> {
305
+ export async function solidDocumentExists(url: string, options?: FetchSolidDocumentOptions): Promise<boolean> {
253
306
  try {
254
- const document = await fetchSolidDocument(url, fetch);
307
+ const document = await fetchSolidDocument(url, options);
255
308
 
256
309
  return !document.isEmpty();
257
310
  } catch (error) {
@@ -8,6 +8,7 @@ export interface ExpandIRIOptions {
8
8
  const knownPrefixes: RDFContext = {
9
9
  acl: 'http://www.w3.org/ns/auth/acl#',
10
10
  foaf: 'http://xmlns.com/foaf/0.1/',
11
+ ldp: 'http://www.w3.org/ns/ldp#',
11
12
  pim: 'http://www.w3.org/ns/pim/space#',
12
13
  purl: 'http://purl.org/dc/terms/',
13
14
  rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
@@ -26,7 +26,7 @@ async function fetchEffectiveACL(
26
26
  ): Promise<SolidDocument> {
27
27
  aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
28
28
 
29
- const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch);
29
+ const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', { fetch });
30
30
 
31
31
  if (!aclDocument) {
32
32
  return fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
@@ -1,5 +1,5 @@
1
- import { normalizeSparql } from '@/helpers/io';
2
- import { jsonldEquals, sparqlEquals } from '@/helpers/testing';
1
+ import { normalizeSparql, normalizeTurtle } from '@/helpers/io';
2
+ import { jsonldEquals, sparqlEquals, turtleEquals } from '@/helpers/testing';
3
3
  import type { EqualityResult } from '@/helpers/testing';
4
4
 
5
5
  interface FormatResultOptions {
@@ -50,6 +50,16 @@ const matchers: jest.ExpectExtendMap = {
50
50
  received: normalizeSparql(received),
51
51
  });
52
52
  },
53
+ toEqualTurtle(received, expected) {
54
+ const result = turtleEquals(expected, received);
55
+
56
+ return formatResult(result, {
57
+ context: this,
58
+ hint: 'toEqualTurtle',
59
+ expected: normalizeTurtle(expected),
60
+ received: normalizeTurtle(received),
61
+ });
62
+ },
53
63
  };
54
64
 
55
65
  export default matchers;
@@ -5,6 +5,7 @@ declare global {
5
5
  // TODO generate automatically
6
6
  interface Matchers<R> {
7
7
  toEqualSparql(sparql: string): R;
8
+ toEqualTurtle(turtle: string): R;
8
9
  toEqualJsonLD(jsonld: JsonLD): Promise<R>;
9
10
  }
10
11
 
@@ -1,3 +1,2 @@
1
- export * from './faking';
2
1
  export * from './mocking';
3
2
  export { default as ResponseStub } from './ResponseStub';
@@ -1,26 +0,0 @@
1
- version: v1.0
2
- name: Solid Utils
3
- agent:
4
- machine:
5
- type: e1-standard-2
6
- os_image: ubuntu1804
7
- blocks:
8
- - name: CI
9
- task:
10
- prologue:
11
- commands:
12
- - checkout
13
- - nvm install
14
- - cache restore
15
- - npm ci
16
- - cache store
17
- jobs:
18
- - name: Linting
19
- commands:
20
- - npm run lint
21
- - name: Tests
22
- commands:
23
- - npm test
24
- - name: Build
25
- commands:
26
- - npm run build
@@ -1,36 +0,0 @@
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
- }