@noeldemartin/solid-utils 0.1.1-next.f279ff39536b39493ea8febae8d8052a9a3b1365 → 0.2.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,9 +1,10 @@
1
1
  {
2
2
  "name": "@noeldemartin/solid-utils",
3
- "version": "0.1.1-next.f279ff39536b39493ea8febae8d8052a9a3b1365",
3
+ "version": "0.2.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",
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",
@@ -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.23f6799530aeebef7d8efca4078fdcd960f8e441",
33
+ "@noeldemartin/solid-utils-external": "^0.1.0",
34
+ "@noeldemartin/utils": "^0.3.0",
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-next.e83e5d37837b9845675534ab9d3745820b1326b4",
48
- "@noeldemartin/scripts": "^0.1.1-next.40031a27875d800f17a8b8e7899e8304a16256c3",
47
+ "@noeldemartin/eslint-config-typescript": "^0.1.1",
48
+ "@noeldemartin/scripts": "^0.1.2",
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,11 @@
1
- import { Error } from '@noeldemartin/utils';
2
- import type { ErrorOptions } from '@noeldemartin/utils';
1
+ import { JSError } from '@noeldemartin/utils';
2
+ import type { JSErrorOptions } from '@noeldemartin/utils';
3
3
 
4
- export default class NetworkRequestError extends Error {
4
+ export default class NetworkRequestError extends JSError {
5
5
 
6
6
  public readonly url: string;
7
7
 
8
- constructor(url: string, options?: ErrorOptions) {
8
+ constructor(url: string, options?: JSErrorOptions) {
9
9
  super(`Request failed trying to fetch ${url}`, options);
10
10
 
11
11
  this.url = url;
@@ -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,4 +1,8 @@
1
- import { objectWithoutEmpty, silenced, urlParentDirectory, urlRoot, urlRoute } from '@noeldemartin/utils';
1
+ import { arrayUnique, objectWithoutEmpty, silenced, urlParentDirectory, urlRoot, urlRoute } from '@noeldemartin/utils';
2
+
3
+ import SolidStore from '../models/SolidStore';
4
+ import UnauthorizedError from '../errors/UnauthorizedError';
5
+ import type SolidDocument from '../models/SolidDocument';
2
6
 
3
7
  import { fetchSolidDocument } from './io';
4
8
  import type { Fetch } from './io';
@@ -6,6 +10,8 @@ import type { Fetch } from './io';
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,79 @@ 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) => {
30
+ document
31
+ .statements(undefined, 'foaf:isPrimaryTopicOf')
32
+ .map(quad => quad.object.value)
33
+ .forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
34
+ document
35
+ .statements(undefined, 'foaf:primaryTopic')
36
+ .map(quad => quad.subject.value)
37
+ .forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
38
+ };
39
+ const loadProfileDocuments = async (): Promise<void> => {
40
+ for (const [url, document] of Object.entries(documents)) {
41
+ if (document !== null) {
42
+ continue;
43
+ }
44
+
45
+ try {
46
+ const document = await fetchSolidDocument(url, fetch);
47
+
48
+ documents[url] = document;
49
+ store.addQuads(document.getQuads());
50
+
51
+ addReferencedDocumentUrls(document);
52
+ } catch (error) {
53
+ if (error instanceof UnauthorizedError) {
54
+ documents[url] = false;
55
+
56
+ continue;
57
+ }
58
+
59
+ throw error;
60
+ }
61
+ }
62
+ };
63
+
64
+ addReferencedDocumentUrls(webIdDocument);
65
+
66
+ do {
67
+ await loadProfileDocuments();
68
+ } while (Object.values(documents).some(document => document === null));
69
+
70
+ return {
71
+ store,
72
+ cloaked: Object.values(documents).some(document => document === false),
73
+ writableProfileUrl:
74
+ webIdDocument.isUserWritable()
75
+ ? webIdDocument.url
76
+ : Object
77
+ .values(documents)
78
+ .find((document): document is SolidDocument => !!document && document.isUserWritable())
79
+ ?.url ?? null,
80
+ };
81
+ }
82
+
16
83
  async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUserProfile> {
17
84
  const documentUrl = urlRoute(webId);
18
85
  const document = await fetchSolidDocument(documentUrl, fetch);
19
86
 
20
- if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer'))
87
+ if (!document.isPersonalProfile() && !document.contains(webId, 'solid:oidcIssuer')) {
21
88
  throw new Error(`${webId} is not a valid webId.`);
89
+ }
22
90
 
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');
91
+ const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, fetch);
92
+ const storageUrls = store.statements(webId, 'pim:storage').map(storage => storage.object.value);
93
+ const publicTypeIndex = store.statement(webId, 'solid:publicTypeIndex');
94
+ const privateTypeIndex = store.statement(webId, 'solid:privateTypeIndex');
26
95
 
27
96
  let parentUrl = urlParentDirectory(documentUrl);
28
97
  while (parentUrl && storageUrls.length === 0) {
@@ -37,19 +106,23 @@ async function fetchUserProfile(webId: string, fetch?: Fetch): Promise<SolidUser
37
106
  parentUrl = urlParentDirectory(parentUrl);
38
107
  }
39
108
 
40
- return objectWithoutEmpty({
109
+ return {
41
110
  webId,
42
- 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
- });
111
+ cloaked,
112
+ writableProfileUrl,
113
+ storageUrls: arrayUnique(storageUrls),
114
+ ...objectWithoutEmpty({
115
+ name:
116
+ store.statement(webId, 'vcard:fn')?.object.value ??
117
+ store.statement(webId, 'foaf:name')?.object.value,
118
+ avatarUrl:
119
+ store.statement(webId, 'vcard:hasPhoto')?.object.value ??
120
+ store.statement(webId, 'foaf:img')?.object.value,
121
+ oidcIssuerUrl: store.statement(webId, 'solid:oidcIssuer')?.object.value,
122
+ publicTypeIndexUrl: publicTypeIndex?.object.value,
123
+ privateTypeIndexUrl: privateTypeIndex?.object.value,
124
+ }),
125
+ };
53
126
  }
54
127
 
55
128
  export async function fetchLoginUserProfile(loginUrl: string, fetch?: Fetch): Promise<SolidUserProfile | null> {
@@ -1,59 +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> {
7
+ type TypeIndexType = 'public' | 'private';
8
+
9
+ async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch): Promise<string> {
9
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
+ 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
+ }
24
+
20
25
  fetch = fetch ?? window.fetch.bind(fetch);
21
26
 
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
- `;
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
41
  await Promise.all([
35
42
  createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
36
- updateSolidDocument(user.webId, profileUpdateBody, fetch),
43
+ updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
37
44
  ]);
38
45
 
46
+ if (type === 'public') {
47
+ // TODO Implement updating ACLs for the listing itself to public
48
+ }
49
+
39
50
  return typeIndexUrl;
40
51
  }
41
52
 
42
- export async function findContainerRegistration(
53
+ async function findRegistrations(
43
54
  typeIndexUrl: string,
44
- childrenType: string,
55
+ type: string | string[],
56
+ predicate: string,
45
57
  fetch?: Fetch,
46
- ): Promise<SolidThing | null> {
58
+ ): Promise<string[]> {
47
59
  const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
48
- const containerQuad = typeIndex
49
- .statements(undefined, 'rdfs:type', 'solid:TypeRegistration')
50
- .find(
51
- statement =>
52
- typeIndex.contains(statement.subject.value, 'solid:forClass', childrenType) &&
53
- typeIndex.contains(statement.subject.value, 'solid:instanceContainer'),
54
- );
55
-
56
- return containerQuad
57
- ? typeIndex.getThing(containerQuad.subject.value) ?? null
58
- : 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);
59
95
  }
package/src/helpers/io.ts CHANGED
@@ -1,10 +1,15 @@
1
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';
2
10
  import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
3
- import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
4
- import { fromRDF, toRDF } from 'jsonld';
5
- import type { JsonLdDocument } from 'jsonld';
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
 
@@ -84,25 +89,25 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
84
89
 
85
90
  for (const index of quadIndexes) {
86
91
  const quad = normalizedQuads[index] as Quad;
87
- const terms: Record<string, N3Term> = {
88
- subject: quad.subject as N3Term,
89
- object: quad.object as N3Term,
92
+ const terms: Record<string, Term> = {
93
+ subject: quad.subject as Term,
94
+ object: quad.object as Term,
90
95
  };
91
96
 
92
97
  for (const [termName, termValue] of Object.entries(terms)) {
93
98
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
94
99
  continue;
95
100
 
96
- terms[termName] = new N3BlankNode(normalizedId);
101
+ terms[termName] = createBlankNode(normalizedId) as Term;
97
102
  }
98
103
 
99
104
  arrayReplace(
100
105
  normalizedQuads,
101
106
  quad,
102
- new N3Quad(
103
- terms.subject as N3Term,
104
- quad.predicate as N3Term,
105
- terms.object as N3Term,
107
+ createQuad(
108
+ terms.subject as Term,
109
+ quad.predicate as Term,
110
+ terms.object as Term,
106
111
  ),
107
112
  );
108
113
  }
@@ -112,10 +117,15 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
112
117
  }
113
118
 
114
119
  export interface ParsingOptions {
115
- documentUrl: string;
120
+ baseIRI: string;
116
121
  normalizeBlankNodes: boolean;
117
122
  }
118
123
 
124
+ export interface RDFGraphData {
125
+ quads: Quad[];
126
+ containsRelativeIRIs: boolean;
127
+ }
128
+
119
129
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
120
130
  fetch = fetch ?? window.fetch.bind(window);
121
131
 
@@ -132,7 +142,7 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
132
142
 
133
143
  export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
134
144
  const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
135
- const statements = await turtleToQuads(data, { documentUrl: url });
145
+ const statements = await turtleToQuads(data, { baseIRI: url });
136
146
 
137
147
  return new SolidDocument(url, statements, headers);
138
148
  }
@@ -150,14 +160,14 @@ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Pro
150
160
  }
151
161
  }
152
162
 
153
- export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
163
+ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
154
164
  if (isJsonLDGraph(jsonld)) {
155
- const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
165
+ const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
156
166
 
157
167
  return graphQuads.flat();
158
168
  }
159
169
 
160
- return toRDF(jsonld as JsonLdDocument) as Promise<Quad[]>;
170
+ return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
161
171
  }
162
172
 
163
173
  export function normalizeSparql(sparql: string): string {
@@ -173,8 +183,54 @@ export function normalizeSparql(sparql: string): string {
173
183
  .join(' ;\n');
174
184
  }
175
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
+
176
232
  export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
177
- const graph = await fromRDF(quads);
233
+ const graph = await jsonLDFromRDF(quads);
178
234
 
179
235
  return {
180
236
  '@graph': graph as JsonLDResource[],
@@ -235,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
235
291
  }
236
292
 
237
293
  export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
238
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
239
- const parser = new TurtleParser(parserOptions);
240
- const quads: Quad[] = [];
241
-
242
- return new Promise((resolve, reject) => {
243
- parser.parse(turtle, (error, quad) => {
244
- if (error) {
245
- reject(
246
- new MalformedSolidDocumentError(
247
- options.documentUrl ?? null,
248
- SolidDocumentFormat.Turtle,
249
- error.message,
250
- ),
251
- );
252
- return;
253
- }
254
-
255
- if (!quad) {
256
- options.normalizeBlankNodes
257
- ? resolve(normalizeBlankNodes(quads))
258
- : resolve(quads);
294
+ const { quads } = await parseTurtle(turtle, options);
259
295
 
260
- return;
261
- }
262
-
263
- quads.push(quad);
264
- });
265
- });
296
+ return quads;
266
297
  }
267
298
 
268
299
  export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
269
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
300
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
270
301
  const parser = new TurtleParser(parserOptions);
271
302
 
272
303
  try {
@@ -277,7 +308,7 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
277
308
  : quads;
278
309
  } catch (error) {
279
310
  throw new MalformedSolidDocumentError(
280
- options.documentUrl ?? null,
311
+ options.baseIRI ?? null,
281
312
  SolidDocumentFormat.Turtle,
282
313
  (error as Error).message ?? '',
283
314
  );
@@ -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;
@@ -5,7 +8,24 @@ export type JsonLD = Partial<{
5
8
  }> & { [k: string]: unknown };
6
9
 
7
10
  export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
8
- export type JsonLDGraph = { '@graph': JsonLDResource[] };
11
+ export type JsonLDGraph = {
12
+ '@context'?: Record<string, unknown>;
13
+ '@graph': JsonLDResource[];
14
+ };
15
+
16
+ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
17
+ const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
18
+
19
+ if ('@graph' in compactedJsonLD) {
20
+ return compactedJsonLD as JsonLDGraph;
21
+ }
22
+
23
+ if ('@id' in compactedJsonLD) {
24
+ return { '@graph': [compactedJsonLD] } as JsonLDGraph;
25
+ }
26
+
27
+ return { '@graph': [] };
28
+ }
9
29
 
10
30
  export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
11
31
  return '@graph' in jsonld;
@@ -1,4 +1,4 @@
1
- import { Error, arrayRemove, pull, stringMatchAll } 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)}`);