@noeldemartin/solid-utils 0.5.0 → 0.6.0-next.508449b33de64b0bcade86b642c9793381434231

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.
Files changed (66) hide show
  1. package/dist/chai.d.ts +31 -0
  2. package/dist/chai.js +23 -0
  3. package/dist/chai.js.map +1 -0
  4. package/dist/helpers-DGXMj9cx.js +107 -0
  5. package/dist/helpers-DGXMj9cx.js.map +1 -0
  6. package/dist/io-wCcrq4b9.js +401 -0
  7. package/dist/io-wCcrq4b9.js.map +1 -0
  8. package/dist/noeldemartin-solid-utils.d.ts +61 -65
  9. package/dist/noeldemartin-solid-utils.js +224 -0
  10. package/dist/noeldemartin-solid-utils.js.map +1 -0
  11. package/dist/testing.d.ts +40 -0
  12. package/dist/testing.js +7 -0
  13. package/dist/testing.js.map +1 -0
  14. package/dist/vitest.d.ts +50 -0
  15. package/dist/vitest.js +55 -0
  16. package/dist/vitest.js.map +1 -0
  17. package/package.json +67 -63
  18. package/src/chai/assertions.ts +35 -0
  19. package/src/chai/index.ts +19 -0
  20. package/src/errors/UnauthorizedError.ts +1 -3
  21. package/src/errors/UnsuccessfulNetworkRequestError.ts +2 -2
  22. package/src/helpers/auth.test.ts +221 -0
  23. package/src/helpers/auth.ts +28 -27
  24. package/src/helpers/identifiers.test.ts +76 -0
  25. package/src/helpers/identifiers.ts +14 -17
  26. package/src/helpers/index.ts +0 -1
  27. package/src/helpers/interop.ts +23 -16
  28. package/src/helpers/io.test.ts +228 -0
  29. package/src/helpers/io.ts +57 -77
  30. package/src/helpers/jsonld.ts +6 -6
  31. package/src/helpers/vocabs.ts +3 -6
  32. package/src/helpers/wac.test.ts +64 -0
  33. package/src/helpers/wac.ts +10 -6
  34. package/src/index.ts +4 -0
  35. package/src/models/SolidDocument.test.ts +77 -0
  36. package/src/models/SolidDocument.ts +14 -18
  37. package/src/models/SolidStore.ts +22 -12
  38. package/src/models/SolidThing.ts +5 -7
  39. package/src/models/index.ts +2 -0
  40. package/src/{helpers/testing.ts → testing/helpers.ts} +24 -27
  41. package/src/testing/hepers.test.ts +329 -0
  42. package/src/testing/index.ts +1 -2
  43. package/src/types/index.ts +2 -0
  44. package/src/types/n3.d.ts +0 -2
  45. package/src/vitest/index.ts +20 -0
  46. package/src/vitest/matchers.ts +68 -0
  47. package/.github/workflows/ci.yml +0 -16
  48. package/.nvmrc +0 -1
  49. package/CHANGELOG.md +0 -70
  50. package/dist/noeldemartin-solid-utils.cjs.js +0 -2
  51. package/dist/noeldemartin-solid-utils.cjs.js.map +0 -1
  52. package/dist/noeldemartin-solid-utils.esm.js +0 -2
  53. package/dist/noeldemartin-solid-utils.esm.js.map +0 -1
  54. package/dist/noeldemartin-solid-utils.umd.js +0 -90
  55. package/dist/noeldemartin-solid-utils.umd.js.map +0 -1
  56. package/noeldemartin.config.js +0 -9
  57. package/src/main.ts +0 -5
  58. package/src/plugins/chai/assertions.ts +0 -40
  59. package/src/plugins/chai/index.ts +0 -5
  60. package/src/plugins/cypress/types.d.ts +0 -15
  61. package/src/plugins/index.ts +0 -2
  62. package/src/plugins/jest/index.ts +0 -5
  63. package/src/plugins/jest/matchers.ts +0 -65
  64. package/src/plugins/jest/types.d.ts +0 -14
  65. package/src/testing/ResponseStub.ts +0 -46
  66. package/src/testing/mocking.ts +0 -33
package/src/helpers/io.ts CHANGED
@@ -1,24 +1,20 @@
1
+ import jsonld from 'jsonld';
1
2
  import md5 from 'md5';
2
- import {
3
- TurtleParser,
4
- TurtleWriter,
5
- createBlankNode,
6
- createQuad,
7
- jsonLDFromRDF,
8
- jsonLDToRDF,
9
- } from '@noeldemartin/solid-utils-external';
10
3
  import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
11
- import type { Quad } from 'rdf-js';
12
- import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
4
+ import { BlankNode as N3BlankNode, Quad as N3Quad, Parser, Writer } from 'n3';
5
+ import type { JsonLdDocument } from 'jsonld';
6
+ import type { Quad } from '@rdfjs/types';
7
+ import type { Term } from 'n3';
13
8
 
14
- import SolidDocument from '@/models/SolidDocument';
9
+ import SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
15
10
 
16
- import MalformedSolidDocumentError, { SolidDocumentFormat } from '@/errors/MalformedSolidDocumentError';
17
- import NetworkRequestError from '@/errors/NetworkRequestError';
18
- import NotFoundError from '@/errors/NotFoundError';
19
- import UnauthorizedError from '@/errors/UnauthorizedError';
20
- import { isJsonLDGraph } from '@/helpers/jsonld';
21
- import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
11
+ // eslint-disable-next-line max-len
12
+ import MalformedSolidDocumentError, { SolidDocumentFormat } from '@noeldemartin/solid-utils/errors/MalformedSolidDocumentError';
13
+ import NetworkRequestError from '@noeldemartin/solid-utils/errors/NetworkRequestError';
14
+ import NotFoundError from '@noeldemartin/solid-utils/errors/NotFoundError';
15
+ import UnauthorizedError from '@noeldemartin/solid-utils/errors/UnauthorizedError';
16
+ import { isJsonLDGraph } from '@noeldemartin/solid-utils/helpers/jsonld';
17
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@noeldemartin/solid-utils/helpers/jsonld';
22
18
 
23
19
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
20
  export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
@@ -44,11 +40,9 @@ async function fetchRawSolidDocument(
44
40
  const fetch = options?.fetch ?? window.fetch;
45
41
  const response = await fetch(url, requestOptions);
46
42
 
47
- if (response.status === 404)
48
- throw new NotFoundError(url);
43
+ if (response.status === 404) throw new NotFoundError(url);
49
44
 
50
- if ([401, 403].includes(response.status))
51
- throw new UnauthorizedError(url, response.status);
45
+ if ([401, 403].includes(response.status)) throw new UnauthorizedError(url, response.status);
52
46
 
53
47
  const body = await response.text();
54
48
 
@@ -57,11 +51,9 @@ async function fetchRawSolidDocument(
57
51
  headers: response.headers,
58
52
  };
59
53
  } catch (error) {
60
- if (error instanceof UnauthorizedError)
61
- throw error;
54
+ if (error instanceof UnauthorizedError) throw error;
62
55
 
63
- if (error instanceof NotFoundError)
64
- throw error;
56
+ if (error instanceof NotFoundError) throw error;
65
57
 
66
58
  throw new NetworkRequestError(url, { cause: error });
67
59
  }
@@ -71,15 +63,14 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
71
63
  const normalizedQuads = quads.slice(0);
72
64
  const quadsIndexes: Record<string, Set<number>> = {};
73
65
  const blankNodeIds = arr(quads)
74
- .flatMap(
75
- (quad, index) => tap(
66
+ .flatMap((quad, index) =>
67
+ tap(
76
68
  arrayFilter([
77
69
  quad.object.termType === 'BlankNode' ? quad.object.value : null,
78
70
  quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
79
71
  ]),
80
- ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
81
- ),
82
- )
72
+ (ids) => ids.forEach((id) => (quadsIndexes[id] ??= new Set()).add(index)),
73
+ ))
83
74
  .filter()
84
75
  .unique();
85
76
 
@@ -87,13 +78,10 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
87
78
  const quadIndexes = quadsIndexes[originalId] as Set<number>;
88
79
  const normalizedId = md5(
89
80
  arr(quadIndexes)
90
- .map(index => quads[index] as Quad)
81
+ .map((index) => quads[index] as Quad)
91
82
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
92
- .map(
93
- ({ predicate, object }) => object.termType === 'BlankNode'
94
- ? predicate.value
95
- : predicate.value + object.value,
96
- )
83
+ .map(({ predicate, object }) =>
84
+ object.termType === 'BlankNode' ? predicate.value : predicate.value + object.value)
97
85
  .sorted()
98
86
  .join(),
99
87
  );
@@ -106,20 +94,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
106
94
  };
107
95
 
108
96
  for (const [termName, termValue] of Object.entries(terms)) {
109
- if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
110
- continue;
97
+ if (termValue.termType !== 'BlankNode' || termValue.value !== originalId) continue;
111
98
 
112
- terms[termName] = createBlankNode(normalizedId) as Term;
99
+ terms[termName] = new N3BlankNode(normalizedId) as Term;
113
100
  }
114
101
 
115
102
  arrayReplace(
116
103
  normalizedQuads,
117
104
  quad,
118
- createQuad(
119
- terms.subject as Term,
120
- quad.predicate as Term,
121
- terms.object as Term,
122
- ),
105
+ new N3Quad(terms.subject as Term, quad.predicate as Term, terms.object as Term),
123
106
  );
124
107
  }
125
108
  }
@@ -128,15 +111,18 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
128
111
  }
129
112
 
130
113
  function normalizeQuads(quads: Quad[]): string {
131
- return quads.map(quad => ' ' + quadToTurtle(quad)).sort().join('\n');
114
+ return quads
115
+ .map((quad) => ' ' + quadToTurtle(quad))
116
+ .sort()
117
+ .join('\n');
132
118
  }
133
119
 
134
- function preprocessSubjects(jsonld: JsonLD): void {
135
- if (!jsonld['@id']?.startsWith('#')) {
120
+ function preprocessSubjects(json: JsonLD): void {
121
+ if (!json['@id']?.startsWith('#')) {
136
122
  return;
137
123
  }
138
124
 
139
- jsonld['@id'] = ANONYMOUS_PREFIX + jsonld['@id'];
125
+ json['@id'] = ANONYMOUS_PREFIX + json['@id'];
140
126
  }
141
127
 
142
128
  function postprocessSubjects(quads: Quad[]): void {
@@ -194,23 +180,22 @@ export async function fetchSolidDocumentIfFound(
194
180
 
195
181
  return document;
196
182
  } catch (error) {
197
- if (!(error instanceof NotFoundError))
198
- throw error;
183
+ if (!(error instanceof NotFoundError)) throw error;
199
184
 
200
185
  return null;
201
186
  }
202
187
  }
203
188
 
204
- export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
205
- if (isJsonLDGraph(jsonld)) {
206
- const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
189
+ export async function jsonldToQuads(json: JsonLD, baseIRI?: string): Promise<Quad[]> {
190
+ if (isJsonLDGraph(json)) {
191
+ const graphQuads = await Promise.all(json['@graph'].map((resource) => jsonldToQuads(resource, baseIRI)));
207
192
 
208
193
  return graphQuads.flat();
209
194
  }
210
195
 
211
- preprocessSubjects(jsonld);
196
+ preprocessSubjects(json);
212
197
 
213
- const quads = await (jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
198
+ const quads = await (jsonld.toRDF(json as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
214
199
 
215
200
  postprocessSubjects(quads);
216
201
 
@@ -220,10 +205,9 @@ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Q
220
205
  export function normalizeSparql(sparql: string): string {
221
206
  const quads = sparqlToQuadsSync(sparql);
222
207
 
223
- return Object
224
- .entries(quads)
225
- .reduce((normalizedOperations, [operation, quads]) => {
226
- const normalizedQuads = normalizeQuads(quads);
208
+ return Object.entries(quads)
209
+ .reduce((normalizedOperations, [operation, _quads]) => {
210
+ const normalizedQuads = normalizeQuads(_quads);
227
211
 
228
212
  return normalizedOperations.concat(`${operation.toUpperCase()} DATA {\n${normalizedQuads}\n}`);
229
213
  }, [] as string[])
@@ -238,7 +222,7 @@ export function normalizeTurtle(sparql: string): string {
238
222
 
239
223
  export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
240
224
  const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
241
- const parser = new TurtleParser(parserOptions);
225
+ const parser = new Parser(parserOptions);
242
226
  const data: RDFGraphData = {
243
227
  quads: [],
244
228
  containsRelativeIRIs: false,
@@ -257,11 +241,7 @@ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {
257
241
  parser.parse(turtle, (error, quad) => {
258
242
  if (error) {
259
243
  reject(
260
- new MalformedSolidDocumentError(
261
- options.baseIRI ?? null,
262
- SolidDocumentFormat.Turtle,
263
- error.message,
264
- ),
244
+ new MalformedSolidDocumentError(options.baseIRI ?? null, SolidDocumentFormat.Turtle, error.message),
265
245
  );
266
246
 
267
247
  return;
@@ -283,7 +263,7 @@ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {
283
263
  }
284
264
 
285
265
  export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
286
- const graph = await jsonLDFromRDF(quads);
266
+ const graph = await jsonld.fromRDF(quads);
287
267
 
288
268
  return {
289
269
  '@graph': graph as JsonLDResource[],
@@ -291,13 +271,13 @@ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
291
271
  }
292
272
 
293
273
  export function quadsToTurtle(quads: Quad[]): string {
294
- const writer = new TurtleWriter;
274
+ const writer = new Writer();
295
275
 
296
276
  return writer.quadsToString(quads);
297
277
  }
298
278
 
299
279
  export function quadToTurtle(quad: Quad): string {
300
- const writer = new TurtleWriter;
280
+ const writer = new Writer();
301
281
 
302
282
  return writer.quadsToString([quad]).slice(0, -1);
303
283
  }
@@ -319,12 +299,14 @@ export async function sparqlToQuads(
319
299
  const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
320
300
  const quads: Record<string, Quad[]> = {};
321
301
 
322
- await Promise.all([...operations].map(async operation => {
323
- const operationName = operation[1].toLowerCase();
324
- const operationBody = operation[2];
302
+ await Promise.all(
303
+ [...operations].map(async (operation) => {
304
+ const operationName = operation[1].toLowerCase();
305
+ const operationBody = operation[2];
325
306
 
326
- quads[operationName] = await turtleToQuads(operationBody, options);
327
- }));
307
+ quads[operationName] = await turtleToQuads(operationBody, options);
308
+ }),
309
+ );
328
310
 
329
311
  return quads;
330
312
  }
@@ -351,14 +333,12 @@ export async function turtleToQuads(turtle: string, options: Partial<ParsingOpti
351
333
 
352
334
  export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
353
335
  const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
354
- const parser = new TurtleParser(parserOptions);
336
+ const parser = new Parser(parserOptions);
355
337
 
356
338
  try {
357
339
  const quads = parser.parse(turtle);
358
340
 
359
- return options.normalizeBlankNodes
360
- ? normalizeBlankNodes(quads)
361
- : quads;
341
+ return options.normalizeBlankNodes ? normalizeBlankNodes(quads) : quads;
362
342
  } catch (error) {
363
343
  throw new MalformedSolidDocumentError(
364
344
  options.baseIRI ?? null,
@@ -1,5 +1,5 @@
1
- import { compactJsonLD } from '@noeldemartin/solid-utils-external';
2
- import type { JsonLdDocument } from '@noeldemartin/solid-utils-external';
1
+ import jsonld from 'jsonld';
2
+ import type { JsonLdDocument } from 'jsonld';
3
3
 
4
4
  export type JsonLD = Partial<{
5
5
  '@context': Record<string, unknown>;
@@ -13,8 +13,8 @@ export type JsonLDGraph = {
13
13
  '@graph': JsonLDResource[];
14
14
  };
15
15
 
16
- export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
17
- const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
16
+ export async function compactJsonLDGraph(json: JsonLDGraph): Promise<JsonLDGraph> {
17
+ const compactedJsonLD = await jsonld.compact(json as JsonLdDocument, {});
18
18
 
19
19
  if ('@graph' in compactedJsonLD) {
20
20
  return compactedJsonLD as JsonLDGraph;
@@ -27,6 +27,6 @@ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGra
27
27
  return { '@graph': [] };
28
28
  }
29
29
 
30
- export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
31
- return '@graph' in jsonld;
30
+ export function isJsonLDGraph(json: JsonLD): json is JsonLDGraph {
31
+ return '@graph' in json;
32
32
  }
@@ -23,22 +23,19 @@ export function defineIRIPrefix(name: string, value: string): void {
23
23
  }
24
24
 
25
25
  export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}): string {
26
- if (iri.startsWith('http'))
27
- return iri;
26
+ if (iri.startsWith('http')) return iri;
28
27
 
29
28
  const [prefix, name] = iri.split(':');
30
29
 
31
30
  if (prefix && name) {
32
31
  const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
33
32
 
34
- if (!expandedPrefix)
35
- throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
33
+ if (!expandedPrefix) throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
36
34
 
37
35
  return expandedPrefix + name;
38
36
  }
39
37
 
40
- if (!options.defaultPrefix)
41
- throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
38
+ if (!options.defaultPrefix) throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
42
39
 
43
40
  return options.defaultPrefix + prefix;
44
41
  }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { FakeResponse, FakeServer } from '@noeldemartin/testing';
3
+
4
+ // eslint-disable-next-line max-len
5
+ import UnsupportedAuthorizationProtocolError from '@noeldemartin/solid-utils/errors/UnsupportedAuthorizationProtocolError';
6
+
7
+ import { fetchSolidDocumentACL } from './wac';
8
+
9
+ describe('WAC helpers', () => {
10
+
11
+ it('resolves relative ACL urls', async () => {
12
+ // Arrange
13
+ const server = new FakeServer();
14
+ const documentUrl = 'https://example.com/alice/movies/my-favorite-movie';
15
+
16
+ server.respondOnce(documentUrl, FakeResponse.success(undefined, { Link: '<my-favorite-movie.acl>;rel="acl"' }));
17
+ server.respondOnce(
18
+ `${documentUrl}.acl`,
19
+ FakeResponse.success(`
20
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
21
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
22
+
23
+ <#owner>
24
+ a acl:Authorization;
25
+ acl:agent <https://example.com/alice/profile/card#me>;
26
+ acl:accessTo <./my-favorite-movie> ;
27
+ acl:mode acl:Read, acl:Write, acl:Control .
28
+ `),
29
+ );
30
+
31
+ // Act
32
+ const { url, effectiveUrl, document } = await fetchSolidDocumentACL(documentUrl, server.fetch);
33
+
34
+ // Assert
35
+ expect(url).toEqual(`${documentUrl}.acl`);
36
+ expect(effectiveUrl).toEqual(url);
37
+ expect(document.contains(`${documentUrl}.acl#owner`, 'rdf:type', 'acl:Authorization')).toBe(true);
38
+
39
+ expect(server.getRequests()).toHaveLength(2);
40
+ expect(server.getRequest(documentUrl)?.method).toEqual('HEAD');
41
+ expect(server.getRequest(url)).not.toBeNull();
42
+ });
43
+
44
+ it('fails with ACP resources', async () => {
45
+ // Arrange
46
+ const server = new FakeServer();
47
+ const documentUrl = 'https://example.com/alice/movies/my-favorite-movie';
48
+
49
+ server.respondOnce(documentUrl, FakeResponse.success(undefined, { Link: '<my-favorite-movie.acl>;rel="acl"' }));
50
+ server.respondOnce(
51
+ `${documentUrl}.acl`,
52
+ FakeResponse.success(undefined, {
53
+ Link: '<http://www.w3.org/ns/solid/acp#AccessControlResource>; rel="type"',
54
+ }),
55
+ );
56
+
57
+ // Act
58
+ const promisedDocument = fetchSolidDocumentACL(documentUrl, server.fetch);
59
+
60
+ // Assert
61
+ await expect(promisedDocument).rejects.toBeInstanceOf(UnsupportedAuthorizationProtocolError);
62
+ });
63
+
64
+ });
@@ -1,9 +1,10 @@
1
1
  import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
2
2
 
3
- import UnsupportedAuthorizationProtocolError from '@/errors/UnsupportedAuthorizationProtocolError';
4
- import { fetchSolidDocumentIfFound } from '@/helpers/io';
5
- import type SolidDocument from '@/models/SolidDocument';
6
- import type { Fetch } from '@/helpers/io';
3
+ // eslint-disable-next-line max-len
4
+ import UnsupportedAuthorizationProtocolError from '@noeldemartin/solid-utils/errors/UnsupportedAuthorizationProtocolError';
5
+ import { fetchSolidDocumentIfFound } from '@noeldemartin/solid-utils/helpers/io';
6
+ import type SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
7
+ import type { Fetch } from '@noeldemartin/solid-utils/helpers/io';
7
8
 
8
9
  async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
9
10
  fetch = fetch ?? window.fetch.bind(window);
@@ -24,7 +25,7 @@ async function fetchEffectiveACL(
24
25
  fetch: Fetch,
25
26
  aclResourceUrl?: string | null,
26
27
  ): Promise<SolidDocument> {
27
- aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
28
+ aclResourceUrl = aclResourceUrl ?? (await fetchACLResourceUrl(resourceUrl, fetch));
28
29
 
29
30
  const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', { fetch });
30
31
 
@@ -39,7 +40,10 @@ async function fetchEffectiveACL(
39
40
  return aclDocument;
40
41
  }
41
42
 
42
- export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
43
+ export async function fetchSolidDocumentACL(
44
+ documentUrl: string,
45
+ fetch: Fetch,
46
+ ): Promise<{
43
47
  url: string;
44
48
  effectiveUrl: string;
45
49
  document: SolidDocument;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './errors';
2
+ export * from './helpers';
3
+ export * from './models';
4
+ export * from './types';
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import SolidDocument from '@noeldemartin/solid-utils/models/SolidDocument';
4
+ import { turtleToQuadsSync } from '@noeldemartin/solid-utils/helpers';
5
+
6
+ describe('SolidDocument', () => {
7
+
8
+ it('Identifies storage documents', () => {
9
+ const hasStorageHeader = (link: string) => {
10
+ const document = new SolidDocument('', [], new Headers({ Link: link }));
11
+
12
+ return document.isStorage();
13
+ };
14
+
15
+ /* eslint-disable max-len */
16
+ expect(hasStorageHeader('')).toBe(false);
17
+ expect(hasStorageHeader('<http://www.w3.org/ns/pim/space#Storage>; rel="type"')).toBe(true);
18
+ expect(hasStorageHeader('<http://www.w3.org/ns/pim/space#Storage>; rel="something-else"; rel="type"')).toBe(
19
+ true,
20
+ );
21
+ expect(
22
+ hasStorageHeader(
23
+ '<http://www.w3.org/ns/pim/space#Storage>; rel="something-else", <http://example.com>; rel="type"',
24
+ ),
25
+ ).toBe(false);
26
+ /* eslint-enable max-len */
27
+ });
28
+
29
+ it('Parses last modified from header', () => {
30
+ const document = new SolidDocument('', [], new Headers({ 'Last-Modified': 'Fri, 03 Sept 2021 16:09:12 GMT' }));
31
+
32
+ expect(document.getLastModified()).toEqual(new Date(1630685352000));
33
+ });
34
+
35
+ it('Parses last modified from document purl:modified', () => {
36
+ const document = new SolidDocument(
37
+ 'https://pod.example.org/my-document',
38
+ turtleToQuadsSync(
39
+ `
40
+ <./fallback>
41
+ <http://purl.org/dc/terms/modified>
42
+ "2021-09-03T16:23:25.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
43
+
44
+ <>
45
+ <http://purl.org/dc/terms/modified>
46
+ "2021-09-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
47
+ `,
48
+ { baseIRI: 'https://pod.example.org/my-document' },
49
+ ),
50
+ new Headers({ 'Last-Modified': 'invalid date' }),
51
+ );
52
+
53
+ expect(document.getLastModified()).toEqual(new Date(1630685352000));
54
+ });
55
+
56
+ it('Parses last modified from any purl date', () => {
57
+ const document = new SolidDocument(
58
+ 'https://pod.example.org/my-document',
59
+ turtleToQuadsSync(
60
+ `
61
+ <./fallback-one>
62
+ <http://purl.org/dc/terms/modified>
63
+ "2021-05-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
64
+
65
+ <./fallback-two>
66
+ <http://purl.org/dc/terms/created>
67
+ "2021-09-03T16:09:12.000Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
68
+ `,
69
+ { baseIRI: 'https://pod.example.org/my-document' },
70
+ ),
71
+ new Headers({ 'Last-Modified': 'invalid date' }),
72
+ );
73
+
74
+ expect(document.getLastModified()).toEqual(new Date(1630685352000));
75
+ });
76
+
77
+ });
@@ -1,7 +1,7 @@
1
1
  import { arrayFilter, parseDate, stringMatch } from '@noeldemartin/utils';
2
- import type { Quad } from 'rdf-js';
2
+ import type { Quad } from '@rdfjs/types';
3
3
 
4
- import { expandIRI } from '@/helpers/vocabs';
4
+ import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
5
5
 
6
6
  import SolidStore from './SolidStore';
7
7
 
@@ -25,16 +25,13 @@ export default class SolidDocument extends SolidStore {
25
25
  }
26
26
 
27
27
  public isACPResource(): boolean {
28
- return !!this.headers.get('Link')
28
+ return !!this.headers
29
+ .get('Link')
29
30
  ?.match(/<http:\/\/www\.w3\.org\/ns\/solid\/acp#AccessControlResource>;[^,]+rel="type"/);
30
31
  }
31
32
 
32
33
  public isPersonalProfile(): boolean {
33
- return !!this.statement(
34
- this.url,
35
- expandIRI('rdf:type'),
36
- expandIRI('foaf:PersonalProfileDocument'),
37
- );
34
+ return !!this.statement(this.url, expandIRI('rdf:type'), expandIRI('foaf:PersonalProfileDocument'));
38
35
  }
39
36
 
40
37
  public isStorage(): boolean {
@@ -54,10 +51,12 @@ export default class SolidDocument extends SolidStore {
54
51
  }
55
52
 
56
53
  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;
54
+ return (
55
+ parseDate(this.headers.get('last-modified')) ??
56
+ parseDate(this.statement(this.url, 'purl:modified')?.object.value) ??
57
+ this.getLatestDocumentDate() ??
58
+ null
59
+ );
61
60
  }
62
61
 
63
62
  protected expandIRI(iri: string): string {
@@ -65,14 +64,11 @@ export default class SolidDocument extends SolidStore {
65
64
  }
66
65
 
67
66
  private getLatestDocumentDate(): Date | null {
68
- const dates = [
69
- ...this.statements(undefined, 'purl:modified'),
70
- ...this.statements(undefined, 'purl:created'),
71
- ]
72
- .map(statement => parseDate(statement.object.value))
67
+ const dates = [...this.statements(undefined, 'purl:modified'), ...this.statements(undefined, 'purl:created')]
68
+ .map((statement) => parseDate(statement.object.value))
73
69
  .filter((date): date is Date => date !== null);
74
70
 
75
- return dates.length > 0 ? dates.reduce((a, b) => a > b ? a : b) : null;
71
+ return dates.length > 0 ? dates.reduce((a, b) => (a > b ? a : b)) : null;
76
72
  }
77
73
 
78
74
  private getPermissionsFromWAC(type: string): SolidDocumentPermission[] {
@@ -1,9 +1,11 @@
1
- import type { Quad } from 'rdf-js';
1
+ import type { BlankNode, Literal, NamedNode, Quad, Variable } from '@rdfjs/types';
2
2
 
3
- import { expandIRI } from '@/helpers/vocabs';
3
+ import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
4
4
 
5
5
  import SolidThing from './SolidThing';
6
6
 
7
+ export type Term = NamedNode | Literal | BlankNode | Quad | Variable;
8
+
7
9
  export default class SolidStore {
8
10
 
9
11
  private quads: Quad[];
@@ -24,21 +26,21 @@ export default class SolidStore {
24
26
  this.quads.push(...quads);
25
27
  }
26
28
 
27
- public statements(subject?: string, predicate?: string, object?: string): Quad[] {
29
+ public statements(subject?: Term | string, predicate?: Term | string, object?: Term | string): Quad[] {
28
30
  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)),
31
+ (statement) =>
32
+ (!object || this.termMatches(statement.object, object)) &&
33
+ (!subject || this.termMatches(statement.subject, subject)) &&
34
+ (!predicate || this.termMatches(statement.predicate, predicate)),
33
35
  );
34
36
  }
35
37
 
36
- public statement(subject?: string, predicate?: string, object?: string): Quad | null {
38
+ public statement(subject?: Term | string, predicate?: Term | string, object?: Term | string): Quad | null {
37
39
  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)),
40
+ (_statement) =>
41
+ (!object || this.termMatches(_statement.object, object)) &&
42
+ (!subject || this.termMatches(_statement.subject, subject)) &&
43
+ (!predicate || this.termMatches(_statement.predicate, predicate)),
42
44
  );
43
45
 
44
46
  return statement ?? null;
@@ -58,4 +60,12 @@ export default class SolidStore {
58
60
  return expandIRI(iri);
59
61
  }
60
62
 
63
+ protected termMatches(term: Term, value: Term | string): boolean {
64
+ if (typeof value === 'string') {
65
+ return this.expandIRI(value) === term.value;
66
+ }
67
+
68
+ return term.termType === term.termType && term.value === value.value;
69
+ }
70
+
61
71
  }
@@ -1,6 +1,6 @@
1
- import type { Quad } from 'rdf-js';
1
+ import type { Quad } from '@rdfjs/types';
2
2
 
3
- import { expandIRI } from '@/helpers/vocabs';
3
+ import { expandIRI } from '@noeldemartin/solid-utils/helpers/vocabs';
4
4
 
5
5
  export default class SolidThing {
6
6
 
@@ -13,15 +13,13 @@ export default class SolidThing {
13
13
  }
14
14
 
15
15
  public value(property: string): string | undefined {
16
- return this.quads
17
- .find(quad => quad.predicate.value === expandIRI(property))
18
- ?.object.value;
16
+ return this.quads.find((quad) => quad.predicate.value === expandIRI(property))?.object.value;
19
17
  }
20
18
 
21
19
  public values(property: string): string[] {
22
20
  return this.quads
23
- .filter(quad => quad.predicate.value === expandIRI(property))
24
- .map(quad => quad.object.value);
21
+ .filter((quad) => quad.predicate.value === expandIRI(property))
22
+ .map((quad) => quad.object.value);
25
23
  }
26
24
 
27
25
  }