@noeldemartin/solid-utils 0.1.1-next.1f0cf6ccc237588ae655211348e94eba9ba16c8d → 0.1.1-next.2a9ce2db0a2467488ff7e61c66af13e0538c8904

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/src/helpers/io.ts CHANGED
@@ -1,10 +1,15 @@
1
- import { arr, arrayFilter, arrayReplace,objectWithoutEmpty } from '@noeldemartin/utils';
2
- import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
3
- import type { JsonLdDocument } from 'jsonld';
4
- import { toRDF } from 'jsonld';
5
1
  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
+ import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
6
11
  import type { Quad } from 'rdf-js';
7
- import type { Term as N3Term } from 'n3';
12
+ import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
8
13
 
9
14
  import SolidDocument from '@/models/SolidDocument';
10
15
 
@@ -13,10 +18,10 @@ import NetworkRequestError from '@/errors/NetworkRequestError';
13
18
  import NotFoundError from '@/errors/NotFoundError';
14
19
  import UnauthorizedError from '@/errors/UnauthorizedError';
15
20
  import { isJsonLDGraph } from '@/helpers/jsonld';
16
- import type { JsonLD } from '@/helpers/jsonld';
21
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
17
22
 
18
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- export declare type AnyFetch = (input: any, options?: any) => Promise<any>;
24
+ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
20
25
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
21
26
  export declare type Fetch = TypedFetch | AnyFetch;
22
27
 
@@ -44,7 +49,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
44
49
  if (error instanceof UnauthorizedError)
45
50
  throw error;
46
51
 
47
- throw new NetworkRequestError(url);
52
+ if (error instanceof NotFoundError)
53
+ throw error;
54
+
55
+ throw new NetworkRequestError(url, { cause: error });
48
56
  }
49
57
  }
50
58
 
@@ -52,26 +60,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
52
60
  const normalizedQuads = quads.slice(0);
53
61
  const quadsIndexes: Record<string, Set<number>> = {};
54
62
  const blankNodeIds = arr(quads)
55
- .flatMap((quad, index) => {
56
- const ids = arrayFilter([
57
- quad.object.termType === 'BlankNode' ? quad.object.value : null,
58
- quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
59
- ]);
60
-
61
- for (const id of ids) {
62
- quadsIndexes[id] = quadsIndexes[id] ?? new Set();
63
- quadsIndexes[id].add(index);
64
- }
65
-
66
- return ids;
67
- })
63
+ .flatMap(
64
+ (quad, index) => tap(
65
+ arrayFilter([
66
+ quad.object.termType === 'BlankNode' ? quad.object.value : null,
67
+ quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
68
+ ]),
69
+ ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
70
+ ),
71
+ )
68
72
  .filter()
69
73
  .unique();
70
74
 
71
75
  for (const originalId of blankNodeIds) {
76
+ const quadIndexes = quadsIndexes[originalId] as Set<number>;
72
77
  const normalizedId = md5(
73
- arr(quadsIndexes[originalId])
74
- .map(index => quads[index])
78
+ arr(quadIndexes)
79
+ .map(index => quads[index] as Quad)
75
80
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
76
81
  .map(
77
82
  ({ predicate, object }) => object.termType === 'BlankNode'
@@ -82,18 +87,29 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
82
87
  .join(),
83
88
  );
84
89
 
85
- for (const index of quadsIndexes[originalId]) {
86
- const quad = normalizedQuads[index];
87
- const terms: Record<string, N3Term> = { subject: quad.subject as N3Term, object: quad.object as N3Term };
90
+ for (const index of quadIndexes) {
91
+ const quad = normalizedQuads[index] as Quad;
92
+ const terms: Record<string, Term> = {
93
+ subject: quad.subject as Term,
94
+ object: quad.object as Term,
95
+ };
88
96
 
89
97
  for (const [termName, termValue] of Object.entries(terms)) {
90
98
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
91
99
  continue;
92
100
 
93
- terms[termName] = new N3BlankNode(normalizedId);
101
+ terms[termName] = createBlankNode(normalizedId) as Term;
94
102
  }
95
103
 
96
- arrayReplace(normalizedQuads, quad, new N3Quad(terms.subject, quad.predicate as N3Term, terms.object));
104
+ arrayReplace(
105
+ normalizedQuads,
106
+ quad,
107
+ createQuad(
108
+ terms.subject as Term,
109
+ quad.predicate as Term,
110
+ terms.object as Term,
111
+ ),
112
+ );
97
113
  }
98
114
  }
99
115
 
@@ -101,12 +117,17 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
101
117
  }
102
118
 
103
119
  export interface ParsingOptions {
104
- documentUrl: string;
120
+ baseIRI: string;
105
121
  normalizeBlankNodes: boolean;
106
122
  }
107
123
 
124
+ export interface RDFGraphData {
125
+ quads: Quad[];
126
+ containsRelativeIRIs: boolean;
127
+ }
128
+
108
129
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
109
- fetch = fetch ?? window.fetch;
130
+ fetch = fetch ?? window.fetch.bind(window);
110
131
 
111
132
  const statements = await turtleToQuads(body);
112
133
 
@@ -121,19 +142,32 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
121
142
 
122
143
  export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
123
144
  const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
124
- const statements = await turtleToQuads(data, { documentUrl: url });
145
+ const statements = await turtleToQuads(data, { baseIRI: url });
125
146
 
126
147
  return new SolidDocument(url, statements, headers);
127
148
  }
128
149
 
129
- export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
150
+ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
151
+ try {
152
+ const document = await fetchSolidDocument(url, fetch);
153
+
154
+ return document;
155
+ } catch (error) {
156
+ if (!(error instanceof NotFoundError))
157
+ throw error;
158
+
159
+ return null;
160
+ }
161
+ }
162
+
163
+ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
130
164
  if (isJsonLDGraph(jsonld)) {
131
- const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
165
+ const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
132
166
 
133
167
  return graphQuads.flat();
134
168
  }
135
169
 
136
- return toRDF(jsonld as JsonLdDocument) as Promise<Quad[]>;
170
+ return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
137
171
  }
138
172
 
139
173
  export function normalizeSparql(sparql: string): string {
@@ -149,6 +183,60 @@ export function normalizeSparql(sparql: string): string {
149
183
  .join(' ;\n');
150
184
  }
151
185
 
186
+ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
187
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
188
+ const parser = new TurtleParser(parserOptions);
189
+ const data: RDFGraphData = {
190
+ quads: [],
191
+ containsRelativeIRIs: false,
192
+ };
193
+
194
+ return new Promise((resolve, reject) => {
195
+ const resolveRelativeIRI = parser._resolveRelativeIRI;
196
+
197
+ parser._resolveRelativeIRI = (...args) => {
198
+ data.containsRelativeIRIs = true;
199
+ parser._resolveRelativeIRI = resolveRelativeIRI;
200
+
201
+ return parser._resolveRelativeIRI(...args);
202
+ };
203
+
204
+ parser.parse(turtle, (error, quad) => {
205
+ if (error) {
206
+ reject(
207
+ new MalformedSolidDocumentError(
208
+ options.baseIRI ?? null,
209
+ SolidDocumentFormat.Turtle,
210
+ error.message,
211
+ ),
212
+ );
213
+
214
+ return;
215
+ }
216
+
217
+ if (!quad) {
218
+ // data.quads = options.normalizeBlankNodes
219
+ // ? normalizeBlankNodes(data.quads)
220
+ // : data.quads;
221
+
222
+ resolve(data);
223
+
224
+ return;
225
+ }
226
+
227
+ data.quads.push(quad);
228
+ });
229
+ });
230
+ }
231
+
232
+ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
233
+ const graph = await jsonLDFromRDF(quads);
234
+
235
+ return {
236
+ '@graph': graph as JsonLDResource[],
237
+ };
238
+ }
239
+
152
240
  export function quadsToTurtle(quads: Quad[]): string {
153
241
  const writer = new TurtleWriter;
154
242
 
@@ -175,7 +263,7 @@ export async function sparqlToQuads(
175
263
  sparql: string,
176
264
  options: Partial<ParsingOptions> = {},
177
265
  ): Promise<Record<string, Quad[]>> {
178
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
266
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
179
267
  const quads: Record<string, Quad[]> = {};
180
268
 
181
269
  await Promise.all([...operations].map(async operation => {
@@ -189,7 +277,7 @@ export async function sparqlToQuads(
189
277
  }
190
278
 
191
279
  export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
192
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
280
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
193
281
  const quads: Record<string, Quad[]> = {};
194
282
 
195
283
  for (const operation of operations) {
@@ -203,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
203
291
  }
204
292
 
205
293
  export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
206
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
207
- const parser = new TurtleParser(parserOptions);
208
- const quads: Quad[] = [];
294
+ const { quads } = await parseTurtle(turtle, options);
209
295
 
210
- return new Promise((resolve, reject) => {
211
- parser.parse(turtle, (error, quad) => {
212
- if (error) {
213
- reject(
214
- new MalformedSolidDocumentError(
215
- options.documentUrl ?? null,
216
- SolidDocumentFormat.Turtle,
217
- error.message,
218
- ),
219
- );
220
- return;
221
- }
222
-
223
- if (!quad) {
224
- options.normalizeBlankNodes
225
- ? resolve(normalizeBlankNodes(quads))
226
- : resolve(quads);
227
-
228
- return;
229
- }
230
-
231
- quads.push(quad);
232
- });
233
- });
296
+ return quads;
234
297
  }
235
298
 
236
299
  export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
237
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
300
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
238
301
  const parser = new TurtleParser(parserOptions);
239
302
 
240
303
  try {
@@ -245,7 +308,7 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
245
308
  : quads;
246
309
  } catch (error) {
247
310
  throw new MalformedSolidDocumentError(
248
- options.documentUrl ?? null,
311
+ options.baseIRI ?? null,
249
312
  SolidDocumentFormat.Turtle,
250
313
  (error as Error).message ?? '',
251
314
  );
@@ -253,7 +316,7 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
253
316
  }
254
317
 
255
318
  export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
256
- fetch = fetch ?? window.fetch;
319
+ fetch = fetch ?? window.fetch.bind(window);
257
320
 
258
321
  await fetch(url, {
259
322
  method: 'PATCH',
@@ -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;
@@ -7,6 +10,20 @@ export type JsonLD = Partial<{
7
10
  export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
8
11
  export type JsonLDGraph = { '@graph': JsonLDResource[] };
9
12
 
13
+ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
14
+ const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
15
+
16
+ if ('@graph' in compactedJsonLD) {
17
+ return compactedJsonLD as JsonLDGraph;
18
+ }
19
+
20
+ if ('@id' in compactedJsonLD) {
21
+ return { '@graph': [compactedJsonLD] } as JsonLDGraph;
22
+ }
23
+
24
+ return { '@graph': [] };
25
+ }
26
+
10
27
  export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
11
28
  return '@graph' in jsonld;
12
29
  }
@@ -1,4 +1,4 @@
1
- import { Error, arrayRemove, pull } from '@noeldemartin/utils';
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 Error {
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)}`);
@@ -32,37 +32,36 @@ function containsPatterns(value: string): boolean {
32
32
  return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
33
33
  }
34
34
 
35
- function quadValueEquals(expected: string, actual: string): boolean {
36
- if (!containsPatterns(expected))
37
- return expected === actual;
38
-
35
+ function createPatternRegexp(expected: string): RegExp {
39
36
  const patternAliases = [];
37
+ const patternMatches = stringMatchAll<4, 1 | 2>(
38
+ expected,
39
+ /\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
40
+ );
41
+ const patterns: string[] = [];
42
+ let expectedRegExp = expected;
40
43
 
41
- if (!(expected in patternsRegExpsIndex)) {
42
- const patternMatches = expected.matchAll(/\[\[((.*?)\]\[)?([^\]]+)\]\]/g);
43
- const patterns: string[] = [];
44
- let expectedRegExp = expected;
45
-
46
- for (const patternMatch of patternMatches) {
47
- if (patternMatch[2]) {
48
- patternAliases.push(patternMatch[2]);
49
- }
44
+ for (const patternMatch of patternMatches) {
45
+ patternMatch[2] && patternAliases.push(patternMatch[2]);
50
46
 
51
- patterns.push(patternMatch[3]);
52
-
53
- expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
54
- }
47
+ patterns.push(patternMatch[3]);
55
48
 
56
- expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
49
+ expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
50
+ }
57
51
 
58
- for (const [patternIndex, pattern] of Object.entries(patterns)) {
59
- expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
60
- }
52
+ expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
61
53
 
62
- patternsRegExpsIndex[expected] = new RegExp(expectedRegExp);
54
+ for (const [patternIndex, pattern] of Object.entries(patterns)) {
55
+ expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
63
56
  }
64
57
 
65
- return patternsRegExpsIndex[expected].test(actual);
58
+ return new RegExp(expectedRegExp);
59
+ }
60
+
61
+ function quadValueEquals(expected: string, actual: string): boolean {
62
+ return containsPatterns(expected)
63
+ ? (patternsRegExpsIndex[expected] ??= createPatternRegexp(expected)).test(actual)
64
+ : expected === actual;
66
65
  }
67
66
 
68
67
  function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
@@ -6,10 +6,12 @@ export interface ExpandIRIOptions {
6
6
  }
7
7
 
8
8
  const knownPrefixes: RDFContext = {
9
+ acl: 'http://www.w3.org/ns/auth/acl#',
9
10
  foaf: 'http://xmlns.com/foaf/0.1/',
10
11
  pim: 'http://www.w3.org/ns/pim/space#',
11
12
  purl: 'http://purl.org/dc/terms/',
12
- rdfs: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
14
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
13
15
  schema: 'https://schema.org/',
14
16
  solid: 'http://www.w3.org/ns/solid/terms#',
15
17
  vcard: 'http://www.w3.org/2006/vcard/ns#',
@@ -25,7 +27,7 @@ export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}):
25
27
 
26
28
  const [prefix, name] = iri.split(':');
27
29
 
28
- if (name) {
30
+ if (prefix && name) {
29
31
  const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
30
32
 
31
33
  if (!expandedPrefix)
@@ -0,0 +1,45 @@
1
+ import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
2
+
3
+ import { fetchSolidDocumentIfFound } from '@/helpers/io';
4
+ import type SolidDocument from '@/models/SolidDocument';
5
+ import type { Fetch } from '@/helpers/io';
6
+
7
+ async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
8
+ fetch = fetch ?? window.fetch.bind(window);
9
+
10
+ const resourceHead = await fetch(resourceUrl, { method: 'HEAD' });
11
+ const linkHeader = resourceHead.headers.get('Link') ?? '';
12
+ const url = linkHeader.match(/<([^>]+)>;\s*rel="acl"/)?.[1] ?? null;
13
+
14
+ if (!url) {
15
+ throw new Error(`Could not find ACL Resource for '${resourceUrl}'`);
16
+ }
17
+
18
+ return urlResolve(requireUrlParentDirectory(resourceUrl), url);
19
+ }
20
+
21
+ async function fetchEffectiveACL(
22
+ resourceUrl: string,
23
+ fetch: Fetch,
24
+ aclResourceUrl?: string | null,
25
+ ): Promise<SolidDocument> {
26
+ aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
27
+
28
+ return await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch)
29
+ ?? await fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
30
+ }
31
+
32
+ export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
33
+ url: string;
34
+ effectiveUrl: string;
35
+ document: SolidDocument;
36
+ }> {
37
+ const url = await fetchACLResourceUrl(documentUrl, fetch);
38
+ const document = await fetchEffectiveACL(documentUrl, fetch, url);
39
+
40
+ return objectWithoutEmpty({
41
+ url,
42
+ effectiveUrl: document.url,
43
+ document,
44
+ });
45
+ }
@@ -1,30 +1,33 @@
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 SolidThing from './SolidThing';
6
+ import SolidStore from './SolidStore';
7
7
 
8
- export default class SolidDocument {
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 isEmpty(): boolean {
21
- return this.statements.length === 0;
22
- }
23
-
24
27
  public isPersonalProfile(): boolean {
25
28
  return !!this.statement(
26
29
  this.url,
27
- expandIRI('rdfs:type'),
30
+ expandIRI('rdf:type'),
28
31
  expandIRI('foaf:PersonalProfileDocument'),
29
32
  );
30
33
  }
@@ -33,41 +36,27 @@ export default class SolidDocument {
33
36
  return !!this.headers.get('Link')?.match(/<http:\/\/www\.w3\.org\/ns\/pim\/space#Storage>;[^,]+rel="type"/);
34
37
  }
35
38
 
36
- public getLastModified(): Date | null {
37
- return parseDate(this.headers.get('last-modified'))
38
- ?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
39
- ?? this.getLatestDocumentDate()
40
- ?? null;
39
+ public isUserWritable(): boolean {
40
+ return this.getUserPermissions().includes(SolidDocumentPermission.Write);
41
41
  }
42
42
 
43
- public statements(subject?: string, predicate?: string, object?: string): Quad[] {
44
- return this.quads.filter(
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
- );
43
+ public getUserPermissions(): SolidDocumentPermission[] {
44
+ return this.getPermissionsFromWAC('user');
50
45
  }
51
46
 
52
- public statement(subject?: string, predicate?: string, object?: string): Quad | null {
53
- const statement = this.quads.find(
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;
47
+ public getPublicPermissions(): SolidDocumentPermission[] {
48
+ return this.getPermissionsFromWAC('public');
61
49
  }
62
50
 
63
- public contains(subject: string, predicate?: string, object?: string): boolean {
64
- return this.statement(subject, predicate, object) !== null;
51
+ public getLastModified(): Date | null {
52
+ return parseDate(this.headers.get('last-modified'))
53
+ ?? parseDate(this.statement(this.url, 'purl:modified')?.object.value)
54
+ ?? this.getLatestDocumentDate()
55
+ ?? null;
65
56
  }
66
57
 
67
- public getThing(subject: string): SolidThing {
68
- const statements = this.statements(subject);
69
-
70
- return new SolidThing(subject, statements);
58
+ protected expandIRI(iri: string): string {
59
+ return expandIRI(iri, { defaultPrefix: this.url });
71
60
  }
72
61
 
73
62
  private getLatestDocumentDate(): Date | null {
@@ -81,4 +70,16 @@ export default class SolidDocument {
81
70
  return dates.length > 0 ? dates.reduce((a, b) => a > b ? a : b) : null;
82
71
  }
83
72
 
73
+ private getPermissionsFromWAC(type: string): SolidDocumentPermission[] {
74
+ const wacAllow = this.headers.get('WAC-Allow') ?? '';
75
+ const publicModes = stringMatch<2>(wacAllow, new RegExp(`${type}="([^"]+)"`))?.[1] ?? '';
76
+
77
+ return arrayFilter([
78
+ publicModes.includes('read') && SolidDocumentPermission.Read,
79
+ publicModes.includes('write') && SolidDocumentPermission.Write,
80
+ publicModes.includes('append') && SolidDocumentPermission.Append,
81
+ publicModes.includes('control') && SolidDocumentPermission.Control,
82
+ ]);
83
+ }
84
+
84
85
  }
@@ -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
+ }
@@ -1,2 +1,3 @@
1
- export { default as SolidDocument } from './SolidDocument';
1
+ export { default as SolidDocument, SolidDocumentPermission } from './SolidDocument';
2
+ export { default as SolidStore } from './SolidStore';
2
3
  export { default as SolidThing } from './SolidThing';