@noeldemartin/solid-utils 0.5.0 → 0.6.0-next.3e3ceb79b047f4ec87a416c2f920a13eda7a0df1
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/dist/io-CMHtz5bu.js +401 -0
- package/dist/io-CMHtz5bu.js.map +1 -0
- package/dist/noeldemartin-solid-utils.d.ts +13 -65
- package/dist/noeldemartin-solid-utils.js +224 -0
- package/dist/noeldemartin-solid-utils.js.map +1 -0
- package/dist/testing.d.ts +45 -0
- package/dist/testing.js +157 -0
- package/dist/testing.js.map +1 -0
- package/package.json +61 -63
- package/src/errors/UnauthorizedError.ts +1 -3
- package/src/errors/UnsuccessfulNetworkRequestError.ts +2 -2
- package/src/helpers/auth.test.ts +221 -0
- package/src/helpers/auth.ts +28 -27
- package/src/helpers/identifiers.test.ts +76 -0
- package/src/helpers/identifiers.ts +14 -17
- package/src/helpers/index.ts +0 -1
- package/src/helpers/interop.ts +23 -16
- package/src/helpers/io.test.ts +228 -0
- package/src/helpers/io.ts +57 -77
- package/src/helpers/jsonld.ts +6 -6
- package/src/helpers/vocabs.ts +3 -6
- package/src/helpers/wac.test.ts +64 -0
- package/src/helpers/wac.ts +10 -6
- package/src/index.ts +3 -0
- package/src/models/SolidDocument.test.ts +77 -0
- package/src/models/SolidDocument.ts +14 -18
- package/src/models/SolidStore.ts +22 -12
- package/src/models/SolidThing.ts +5 -7
- package/src/models/index.ts +2 -0
- package/src/{helpers/testing.ts → testing/helpers.ts} +24 -27
- package/src/testing/hepers.test.ts +329 -0
- package/src/testing/index.ts +2 -2
- package/src/testing/vitest/index.ts +13 -0
- package/src/testing/vitest/matchers.ts +68 -0
- package/src/types/n3.d.ts +0 -2
- package/.github/workflows/ci.yml +0 -16
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -70
- package/dist/noeldemartin-solid-utils.cjs.js +0 -2
- package/dist/noeldemartin-solid-utils.cjs.js.map +0 -1
- package/dist/noeldemartin-solid-utils.esm.js +0 -2
- package/dist/noeldemartin-solid-utils.esm.js.map +0 -1
- package/dist/noeldemartin-solid-utils.umd.js +0 -90
- package/dist/noeldemartin-solid-utils.umd.js.map +0 -1
- package/noeldemartin.config.js +0 -9
- package/src/main.ts +0 -5
- package/src/plugins/chai/assertions.ts +0 -40
- package/src/plugins/chai/index.ts +0 -5
- package/src/plugins/cypress/types.d.ts +0 -15
- package/src/plugins/index.ts +0 -2
- package/src/plugins/jest/index.ts +0 -5
- package/src/plugins/jest/matchers.ts +0 -65
- package/src/plugins/jest/types.d.ts +0 -14
- package/src/testing/ResponseStub.ts +0 -46
- package/src/testing/mocking.ts +0 -33
|
@@ -19,9 +19,7 @@ export default class UnauthorizedError extends JSError {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
public get forbidden(): boolean | undefined {
|
|
22
|
-
return typeof this.responseStatus !== 'undefined'
|
|
23
|
-
? this.responseStatus === 403
|
|
24
|
-
: undefined;
|
|
22
|
+
return typeof this.responseStatus !== 'undefined' ? this.responseStatus === 403 : undefined;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JSError } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
3
|
function getErrorMessage(messageOrResponse: string | Response, response?: Response): string {
|
|
4
|
-
response = response ?? messageOrResponse as Response;
|
|
4
|
+
response = response ?? (messageOrResponse as Response);
|
|
5
5
|
|
|
6
6
|
return typeof messageOrResponse === 'string'
|
|
7
7
|
? `${messageOrResponse} (returned ${response.status} status code)`
|
|
@@ -17,7 +17,7 @@ export default class UnsuccessfulRequestError extends JSError {
|
|
|
17
17
|
constructor(messageOrResponse: string | Response, response?: Response) {
|
|
18
18
|
super(getErrorMessage(messageOrResponse, response));
|
|
19
19
|
|
|
20
|
-
this.response = response ?? messageOrResponse as Response;
|
|
20
|
+
this.response = response ?? (messageOrResponse as Response);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { FakeResponse, FakeServer } from '@noeldemartin/testing';
|
|
4
|
+
|
|
5
|
+
import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils/errors';
|
|
6
|
+
|
|
7
|
+
import { fetchLoginUserProfile } from './auth';
|
|
8
|
+
|
|
9
|
+
describe('Auth helpers', () => {
|
|
10
|
+
|
|
11
|
+
it('reads NSS profiles', async () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const server = new FakeServer();
|
|
14
|
+
const webId = 'https://alice.solidcommunity.net/profile/card#me';
|
|
15
|
+
|
|
16
|
+
server.respondOnce(
|
|
17
|
+
'https://alice.solidcommunity.net/profile/card',
|
|
18
|
+
FakeResponse.success(
|
|
19
|
+
`
|
|
20
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
21
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
22
|
+
@prefix pim: <http://www.w3.org/ns/pim/space#>.
|
|
23
|
+
@prefix schema: <http://schema.org/>.
|
|
24
|
+
|
|
25
|
+
<>
|
|
26
|
+
a foaf:PersonalProfileDocument ;
|
|
27
|
+
foaf:maker <#me> ;
|
|
28
|
+
foaf:primaryTopic <#me> .
|
|
29
|
+
|
|
30
|
+
<#me>
|
|
31
|
+
a foaf:Person, schema:Person ;
|
|
32
|
+
foaf:name "Alice" ;
|
|
33
|
+
pim:preferencesFile </settings/prefs.ttl> ;
|
|
34
|
+
pim:storage </> ;
|
|
35
|
+
solid:oidcIssuer <https://solidcommunity.net> ;
|
|
36
|
+
solid:privateTypeIndex </settings/privateTypeIndex.ttl> ;
|
|
37
|
+
solid:publicTypeIndex </settings/publicTypeIndex.ttl> .
|
|
38
|
+
`,
|
|
39
|
+
{ 'WAC-Allow': 'user="read control write"' },
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
const profile = await fetchLoginUserProfile(webId, { fetch: server.fetch });
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(server.getRequests()).toHaveLength(1);
|
|
48
|
+
|
|
49
|
+
expect(profile).toEqual({
|
|
50
|
+
webId,
|
|
51
|
+
name: 'Alice',
|
|
52
|
+
cloaked: false,
|
|
53
|
+
oidcIssuerUrl: 'https://solidcommunity.net',
|
|
54
|
+
storageUrls: ['https://alice.solidcommunity.net/'],
|
|
55
|
+
privateTypeIndexUrl: 'https://alice.solidcommunity.net/settings/privateTypeIndex.ttl',
|
|
56
|
+
publicTypeIndexUrl: 'https://alice.solidcommunity.net/settings/publicTypeIndex.ttl',
|
|
57
|
+
writableProfileUrl: 'https://alice.solidcommunity.net/profile/card',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reads ESS profiles (public)', async () => {
|
|
62
|
+
// Arrange
|
|
63
|
+
const server = new FakeServer();
|
|
64
|
+
const webId = 'https://id.inrupt.com/alice';
|
|
65
|
+
|
|
66
|
+
// The first request returns a 303 to `${webId}?lookup`,
|
|
67
|
+
// but in order to simplify mocking requests we're assuming it doesn't.
|
|
68
|
+
server.respondOnce(
|
|
69
|
+
'https://id.inrupt.com/alice',
|
|
70
|
+
FakeResponse.success(`
|
|
71
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
72
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
73
|
+
@prefix pim: <http://www.w3.org/ns/pim/space#>.
|
|
74
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
75
|
+
|
|
76
|
+
<${webId}>
|
|
77
|
+
a foaf:Agent ;
|
|
78
|
+
rdfs:seeAlso <https://storage.inrupt.com/storage-hash/extendedProfile> ;
|
|
79
|
+
pim:storage <https://storage.inrupt.com/storage-hash/> ;
|
|
80
|
+
solid:oidcIssuer <https://login.inrupt.com> ;
|
|
81
|
+
foaf:isPrimaryTopicOf <https://storage.inrupt.com/storage-hash/extendedProfile> .
|
|
82
|
+
`),
|
|
83
|
+
);
|
|
84
|
+
server.respondOnce(
|
|
85
|
+
'https://storage.inrupt.com/storage-hash/extendedProfile',
|
|
86
|
+
new FakeResponse(undefined, undefined, 401),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
const profile = await fetchLoginUserProfile(webId, { fetch: server.fetch });
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(server.getRequests()).toHaveLength(2);
|
|
94
|
+
|
|
95
|
+
expect(profile).toEqual({
|
|
96
|
+
webId,
|
|
97
|
+
cloaked: true,
|
|
98
|
+
oidcIssuerUrl: 'https://login.inrupt.com',
|
|
99
|
+
storageUrls: ['https://storage.inrupt.com/storage-hash/'],
|
|
100
|
+
writableProfileUrl: null,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('reads ESS profiles (authenticated)', async () => {
|
|
105
|
+
// Arrange
|
|
106
|
+
const server = new FakeServer();
|
|
107
|
+
const webId = 'https://id.inrupt.com/alice';
|
|
108
|
+
|
|
109
|
+
// The first request returns a 303 to `${webId}?lookup`,
|
|
110
|
+
// but in order to simplify mocking requests we're assuming it doesn't.
|
|
111
|
+
server.respondOnce(
|
|
112
|
+
'https://id.inrupt.com/alice',
|
|
113
|
+
FakeResponse.success(`
|
|
114
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
115
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
116
|
+
@prefix pim: <http://www.w3.org/ns/pim/space#>.
|
|
117
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
118
|
+
|
|
119
|
+
<${webId}>
|
|
120
|
+
a foaf:Agent ;
|
|
121
|
+
rdfs:seeAlso <https://storage.inrupt.com/storage-hash/extendedProfile> ;
|
|
122
|
+
pim:storage <https://storage.inrupt.com/storage-hash/> ;
|
|
123
|
+
solid:oidcIssuer <https://login.inrupt.com> ;
|
|
124
|
+
foaf:isPrimaryTopicOf <https://storage.inrupt.com/storage-hash/extendedProfile> .
|
|
125
|
+
`),
|
|
126
|
+
);
|
|
127
|
+
server.respondOnce(
|
|
128
|
+
'https://storage.inrupt.com/storage-hash/extendedProfile',
|
|
129
|
+
FakeResponse.success(
|
|
130
|
+
`
|
|
131
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
|
132
|
+
@prefix schema: <http://schema.org/> .
|
|
133
|
+
|
|
134
|
+
<${webId}>
|
|
135
|
+
a foaf:Person, schema:Person ;
|
|
136
|
+
foaf:name "Alice" .
|
|
137
|
+
|
|
138
|
+
<https://storage.inrupt.com/storage-hash/extendedProfile>
|
|
139
|
+
a foaf:Document ;
|
|
140
|
+
foaf:maker <${webId}> ;
|
|
141
|
+
foaf:primaryTopic <${webId}> .
|
|
142
|
+
`,
|
|
143
|
+
{ 'WAC-Allow': 'user="read control write"' },
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Act
|
|
148
|
+
const profile = await fetchLoginUserProfile(webId, { fetch: server.fetch });
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
expect(server.getRequests()).toHaveLength(2);
|
|
152
|
+
|
|
153
|
+
expect(profile).toEqual({
|
|
154
|
+
webId,
|
|
155
|
+
name: 'Alice',
|
|
156
|
+
cloaked: false,
|
|
157
|
+
oidcIssuerUrl: 'https://login.inrupt.com',
|
|
158
|
+
storageUrls: ['https://storage.inrupt.com/storage-hash/'],
|
|
159
|
+
writableProfileUrl: 'https://storage.inrupt.com/storage-hash/extendedProfile',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('reads use.id profiles', async () => {
|
|
164
|
+
// Arrange
|
|
165
|
+
const server = new FakeServer();
|
|
166
|
+
const webId = 'https://use.id/alice';
|
|
167
|
+
const profileTurtle = `
|
|
168
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
169
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
170
|
+
@prefix pim: <http://www.w3.org/ns/pim/space#>.
|
|
171
|
+
|
|
172
|
+
<${webId}/profile>
|
|
173
|
+
a foaf:PersonalProfileDocument;
|
|
174
|
+
foaf:maker <${webId}>;
|
|
175
|
+
foaf:primaryTopic <${webId}>.
|
|
176
|
+
|
|
177
|
+
<${webId}>
|
|
178
|
+
solid:oidcIssuer <https://idp.use.id/>;
|
|
179
|
+
pim:storage <https://pods.use.id/storage-hash/>.
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
// The first request returns a 303 to `${webId}/profile`,
|
|
183
|
+
// but in order to simplify mocking requests we're assuming it doesn't.
|
|
184
|
+
server.respondOnce('https://use.id/alice', FakeResponse.success(profileTurtle, { 'WAC-Allow': 'user="read"' }));
|
|
185
|
+
server.respondOnce(
|
|
186
|
+
'https://use.id/alice/profile',
|
|
187
|
+
FakeResponse.success(profileTurtle, { 'WAC-Allow': 'user="read"' }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Act
|
|
191
|
+
const profile = await fetchLoginUserProfile(webId, { fetch: server.fetch });
|
|
192
|
+
|
|
193
|
+
// Assert
|
|
194
|
+
expect(server.getRequests()).toHaveLength(2);
|
|
195
|
+
|
|
196
|
+
expect(profile).toEqual({
|
|
197
|
+
webId,
|
|
198
|
+
cloaked: false,
|
|
199
|
+
oidcIssuerUrl: 'https://idp.use.id/',
|
|
200
|
+
storageUrls: ['https://pods.use.id/storage-hash/'],
|
|
201
|
+
writableProfileUrl: null,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('throws errors reading required profiles', async () => {
|
|
206
|
+
// Arrange
|
|
207
|
+
const server = new FakeServer();
|
|
208
|
+
const webId = 'https://pod.example.com/profile/card#me';
|
|
209
|
+
|
|
210
|
+
server.respondOnce('https://pod.example.com/profile/card', FakeResponse.success('invalid turtle'));
|
|
211
|
+
|
|
212
|
+
// Act
|
|
213
|
+
const fetchProfile = fetchLoginUserProfile(webId, { fetch: server.fetch, required: true });
|
|
214
|
+
|
|
215
|
+
// Assert
|
|
216
|
+
await expect(fetchProfile).rejects.toBeInstanceOf(MalformedSolidDocumentError);
|
|
217
|
+
|
|
218
|
+
expect(server.getRequests()).toHaveLength(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
});
|
package/src/helpers/auth.ts
CHANGED
|
@@ -19,7 +19,10 @@ export interface SolidUserProfile {
|
|
|
19
19
|
privateTypeIndexUrl?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function fetchExtendedUserProfile(
|
|
22
|
+
async function fetchExtendedUserProfile(
|
|
23
|
+
webIdDocument: SolidDocument,
|
|
24
|
+
options?: FetchSolidDocumentOptions,
|
|
25
|
+
): Promise<{
|
|
23
26
|
store: SolidStore;
|
|
24
27
|
cloaked: boolean;
|
|
25
28
|
writableProfileUrl: string | null;
|
|
@@ -29,12 +32,12 @@ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, options?:
|
|
|
29
32
|
const addReferencedDocumentUrls = (document: SolidDocument) => {
|
|
30
33
|
document
|
|
31
34
|
.statements(undefined, 'foaf:isPrimaryTopicOf')
|
|
32
|
-
.map(quad => quad.object.value)
|
|
33
|
-
.forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
|
|
35
|
+
.map((quad) => quad.object.value)
|
|
36
|
+
.forEach((profileDocumentUrl) => (documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null));
|
|
34
37
|
document
|
|
35
38
|
.statements(undefined, 'foaf:primaryTopic')
|
|
36
|
-
.map(quad => quad.subject.value)
|
|
37
|
-
.forEach(profileDocumentUrl => documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null);
|
|
39
|
+
.map((quad) => quad.subject.value)
|
|
40
|
+
.forEach((profileDocumentUrl) => (documents[profileDocumentUrl] = documents[profileDocumentUrl] ?? null));
|
|
38
41
|
};
|
|
39
42
|
const loadProfileDocuments = async (): Promise<void> => {
|
|
40
43
|
for (const [url, document] of Object.entries(documents)) {
|
|
@@ -43,12 +46,12 @@ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, options?:
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
try {
|
|
46
|
-
const
|
|
49
|
+
const _document = await fetchSolidDocument(url, options);
|
|
47
50
|
|
|
48
|
-
documents[url] =
|
|
49
|
-
store.addQuads(
|
|
51
|
+
documents[url] = _document;
|
|
52
|
+
store.addQuads(_document.getQuads());
|
|
50
53
|
|
|
51
|
-
addReferencedDocumentUrls(
|
|
54
|
+
addReferencedDocumentUrls(_document);
|
|
52
55
|
} catch (error) {
|
|
53
56
|
if (error instanceof UnauthorizedError) {
|
|
54
57
|
documents[url] = false;
|
|
@@ -65,18 +68,16 @@ async function fetchExtendedUserProfile(webIdDocument: SolidDocument, options?:
|
|
|
65
68
|
|
|
66
69
|
do {
|
|
67
70
|
await loadProfileDocuments();
|
|
68
|
-
} while (Object.values(documents).some(document => document === null));
|
|
71
|
+
} while (Object.values(documents).some((document) => document === null));
|
|
69
72
|
|
|
70
73
|
return {
|
|
71
74
|
store,
|
|
72
|
-
cloaked: Object.values(documents).some(document => document === false),
|
|
73
|
-
writableProfileUrl:
|
|
74
|
-
webIdDocument.
|
|
75
|
-
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
.find((document): document is SolidDocument => !!document && document.isUserWritable())
|
|
79
|
-
?.url ?? null,
|
|
75
|
+
cloaked: Object.values(documents).some((document) => document === false),
|
|
76
|
+
writableProfileUrl: webIdDocument.isUserWritable()
|
|
77
|
+
? webIdDocument.url
|
|
78
|
+
: (Object.values(documents).find(
|
|
79
|
+
(document): document is SolidDocument => !!document && document.isUserWritable(),
|
|
80
|
+
)?.url ?? null),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -97,7 +98,7 @@ async function fetchUserProfile(webId: string, options: FetchUserProfileOptions
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
const { store, writableProfileUrl, cloaked } = await fetchExtendedUserProfile(document, options);
|
|
100
|
-
const storageUrls = store.statements(webId, 'pim:storage').map(storage => storage.object.value);
|
|
101
|
+
const storageUrls = store.statements(webId, 'pim:storage').map((storage) => storage.object.value);
|
|
101
102
|
const publicTypeIndex = store.statement(webId, 'solid:publicTypeIndex');
|
|
102
103
|
const privateTypeIndex = store.statement(webId, 'solid:privateTypeIndex');
|
|
103
104
|
|
|
@@ -118,7 +119,7 @@ async function fetchUserProfile(webId: string, options: FetchUserProfileOptions
|
|
|
118
119
|
throw new Error(`Could not find any storage for ${webId}.`);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
await options.onLoaded?.(
|
|
122
|
+
await options.onLoaded?.(store);
|
|
122
123
|
|
|
123
124
|
return {
|
|
124
125
|
webId,
|
|
@@ -126,9 +127,7 @@ async function fetchUserProfile(webId: string, options: FetchUserProfileOptions
|
|
|
126
127
|
writableProfileUrl,
|
|
127
128
|
storageUrls: arrayUnique(storageUrls) as [string, ...string[]],
|
|
128
129
|
...objectWithoutEmpty({
|
|
129
|
-
name:
|
|
130
|
-
store.statement(webId, 'vcard:fn')?.object.value ??
|
|
131
|
-
store.statement(webId, 'foaf:name')?.object.value,
|
|
130
|
+
name: store.statement(webId, 'vcard:fn')?.object.value ?? store.statement(webId, 'foaf:name')?.object.value,
|
|
132
131
|
avatarUrl:
|
|
133
132
|
store.statement(webId, 'vcard:hasPhoto')?.object.value ??
|
|
134
133
|
store.statement(webId, 'foaf:img')?.object.value,
|
|
@@ -156,9 +155,11 @@ export async function fetchLoginUserProfile(
|
|
|
156
155
|
return fetchUserProfile(loginUrl, options);
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
const fetchProfile = silenced(url => fetchUserProfile(url, options));
|
|
158
|
+
const fetchProfile = silenced((url) => fetchUserProfile(url, options));
|
|
160
159
|
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
return (
|
|
161
|
+
(await fetchProfile(loginUrl)) ??
|
|
162
|
+
(await fetchProfile(loginUrl.replace(/\/$/, '').concat('/profile/card#me'))) ??
|
|
163
|
+
(await fetchProfile(urlRoot(loginUrl).concat('/profile/card#me')))
|
|
164
|
+
);
|
|
164
165
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mintJsonLDIdentifiers } from '@noeldemartin/solid-utils/helpers';
|
|
4
|
+
import { parseResourceSubject } from '@noeldemartin/solid-utils/helpers/identifiers';
|
|
5
|
+
import type { JsonLD } from '@noeldemartin/solid-utils/helpers';
|
|
6
|
+
|
|
7
|
+
describe('Identifiers helpers', () => {
|
|
8
|
+
|
|
9
|
+
it('mints JsonLD identifiers', () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const jsonld = {
|
|
12
|
+
'@context': { '@vocab': 'https://schema.org/' },
|
|
13
|
+
'@type': 'Recipe',
|
|
14
|
+
'name': 'Ramen',
|
|
15
|
+
'ingredients': ['Broth', 'Noodles'],
|
|
16
|
+
'instructions': [
|
|
17
|
+
{
|
|
18
|
+
'@type': 'HowToStep',
|
|
19
|
+
'text': 'Boil Noodles',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
'@type': 'HowToStep',
|
|
23
|
+
'text': 'Dip them into the broth',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
'http://purl.org/dc/terms/created': {
|
|
27
|
+
'@type': 'http://www.w3.org/2001/XMLSchema#dateTime',
|
|
28
|
+
'@value': '1997-07-21T23:42:00.000Z',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const jsonldWithIds = mintJsonLDIdentifiers(jsonld);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(jsonldWithIds['@id']).not.toBeUndefined();
|
|
37
|
+
|
|
38
|
+
const createdAt = jsonldWithIds['http://purl.org/dc/terms/created'] as Record<string, unknown>;
|
|
39
|
+
expect(createdAt['@id']).toBeUndefined();
|
|
40
|
+
|
|
41
|
+
const instructions = jsonldWithIds['instructions'] as [JsonLD, JsonLD];
|
|
42
|
+
expect(instructions[0]['@id']).not.toBeUndefined();
|
|
43
|
+
expect(instructions[1]['@id']).not.toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('parses subjects', () => {
|
|
47
|
+
expect(parseResourceSubject('https://my-pod.com/profile/card#me')).toEqual({
|
|
48
|
+
containerUrl: 'https://my-pod.com/profile/',
|
|
49
|
+
documentName: 'card',
|
|
50
|
+
resourceHash: 'me',
|
|
51
|
+
});
|
|
52
|
+
expect(parseResourceSubject('https://my-pod.com/about')).toEqual({
|
|
53
|
+
containerUrl: 'https://my-pod.com/',
|
|
54
|
+
documentName: 'about',
|
|
55
|
+
});
|
|
56
|
+
expect(parseResourceSubject('/profile/card#me')).toEqual({
|
|
57
|
+
containerUrl: '/profile/',
|
|
58
|
+
documentName: 'card',
|
|
59
|
+
resourceHash: 'me',
|
|
60
|
+
});
|
|
61
|
+
expect(parseResourceSubject('/about#sections')).toEqual({
|
|
62
|
+
containerUrl: '/',
|
|
63
|
+
documentName: 'about',
|
|
64
|
+
resourceHash: 'sections',
|
|
65
|
+
});
|
|
66
|
+
expect(parseResourceSubject('about#sections')).toEqual({
|
|
67
|
+
documentName: 'about',
|
|
68
|
+
resourceHash: 'sections',
|
|
69
|
+
});
|
|
70
|
+
expect(parseResourceSubject('about')).toEqual({
|
|
71
|
+
documentName: 'about',
|
|
72
|
+
});
|
|
73
|
+
expect(parseResourceSubject('')).toEqual({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { arr, isArray, isObject, objectDeepClone, objectWithoutEmpty, tap, urlParse, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import type { UrlParts } from '@noeldemartin/utils';
|
|
3
|
-
import type { JsonLD, JsonLDResource } from '
|
|
3
|
+
import type { JsonLD, JsonLDResource } from '@noeldemartin/solid-utils/helpers';
|
|
4
4
|
|
|
5
5
|
export interface SubjectParts {
|
|
6
6
|
containerUrl?: string;
|
|
@@ -9,11 +9,9 @@ export interface SubjectParts {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function getContainerPath(parts: UrlParts): string | null {
|
|
12
|
-
if (!parts.path || !parts.path.startsWith('/'))
|
|
13
|
-
return null;
|
|
12
|
+
if (!parts.path || !parts.path.startsWith('/')) return null;
|
|
14
13
|
|
|
15
|
-
if (parts.path.match(/^\/[^/]*$/))
|
|
16
|
-
return '/';
|
|
14
|
+
if (parts.path.match(/^\/[^/]*$/)) return '/';
|
|
17
15
|
|
|
18
16
|
return `/${arr(parts.path.split('/')).filter().slice(0, -1).join('/')}/`.replace('//', '/');
|
|
19
17
|
}
|
|
@@ -27,30 +25,29 @@ function getContainerUrl(parts: UrlParts): string | null {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
function __mintJsonLDIdentifiers(jsonld: JsonLD): void {
|
|
30
|
-
if (!('@type' in jsonld) || '@value' in jsonld)
|
|
31
|
-
return;
|
|
28
|
+
if (!('@type' in jsonld) || '@value' in jsonld) return;
|
|
32
29
|
|
|
33
30
|
jsonld['@id'] = jsonld['@id'] ?? uuid();
|
|
34
31
|
|
|
35
32
|
for (const propertyValue of Object.values(jsonld)) {
|
|
36
|
-
if (isObject(propertyValue))
|
|
37
|
-
__mintJsonLDIdentifiers(propertyValue);
|
|
33
|
+
if (isObject(propertyValue)) __mintJsonLDIdentifiers(propertyValue);
|
|
38
34
|
|
|
39
|
-
if (isArray(propertyValue))
|
|
40
|
-
propertyValue.forEach(value => isObject(value) && __mintJsonLDIdentifiers(value));
|
|
35
|
+
if (isArray(propertyValue)) propertyValue.forEach((value) => isObject(value) && __mintJsonLDIdentifiers(value));
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
export function mintJsonLDIdentifiers(jsonld: JsonLD): JsonLDResource {
|
|
45
|
-
return tap(objectDeepClone(jsonld) as JsonLDResource, clone => __mintJsonLDIdentifiers(clone));
|
|
40
|
+
return tap(objectDeepClone(jsonld) as JsonLDResource, (clone) => __mintJsonLDIdentifiers(clone));
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
export function parseResourceSubject(subject: string): SubjectParts {
|
|
49
44
|
const parts = urlParse(subject);
|
|
50
45
|
|
|
51
|
-
return !parts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
return !parts
|
|
47
|
+
? {}
|
|
48
|
+
: objectWithoutEmpty({
|
|
49
|
+
containerUrl: getContainerUrl(parts),
|
|
50
|
+
documentName: parts.path ? parts.path.split('/').pop() : null,
|
|
51
|
+
resourceHash: parts.fragment,
|
|
52
|
+
});
|
|
56
53
|
}
|
package/src/helpers/index.ts
CHANGED
package/src/helpers/interop.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { uuid } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
createSolidDocument,
|
|
5
|
+
fetchSolidDocument,
|
|
6
|
+
solidDocumentExists,
|
|
7
|
+
updateSolidDocument,
|
|
8
|
+
} from '@noeldemartin/solid-utils/helpers/io';
|
|
9
|
+
import type { Fetch } from '@noeldemartin/solid-utils/helpers/io';
|
|
10
|
+
import type { SolidUserProfile } from '@noeldemartin/solid-utils/helpers/auth';
|
|
6
11
|
|
|
7
12
|
type TypeIndexType = 'public' | 'private';
|
|
8
13
|
|
|
@@ -12,7 +17,7 @@ async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fet
|
|
|
12
17
|
const storageUrl = user.storageUrls[0];
|
|
13
18
|
const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
|
|
14
19
|
|
|
15
|
-
return await solidDocumentExists(typeIndexUrl, { fetch })
|
|
20
|
+
return (await solidDocumentExists(typeIndexUrl, { fetch }))
|
|
16
21
|
? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
|
|
17
22
|
: typeIndexUrl;
|
|
18
23
|
}
|
|
@@ -25,9 +30,10 @@ async function createTypeIndex(user: SolidUserProfile, type: TypeIndexType, fetc
|
|
|
25
30
|
fetch = fetch ?? window.fetch.bind(fetch);
|
|
26
31
|
|
|
27
32
|
const typeIndexUrl = await mintTypeIndexUrl(user, type, fetch);
|
|
28
|
-
const typeIndexBody =
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const typeIndexBody =
|
|
34
|
+
type === 'public'
|
|
35
|
+
? '<> a <http://www.w3.org/ns/solid/terms#TypeIndex> .'
|
|
36
|
+
: `
|
|
31
37
|
<> a
|
|
32
38
|
<http://www.w3.org/ns/solid/terms#TypeIndex>,
|
|
33
39
|
<http://www.w3.org/ns/solid/terms#UnlistedDocument> .
|
|
@@ -60,15 +66,16 @@ async function findRegistrations(
|
|
|
60
66
|
const typeIndex = await fetchSolidDocument(typeIndexUrl, { fetch });
|
|
61
67
|
const types = Array.isArray(type) ? type : [type];
|
|
62
68
|
|
|
63
|
-
return types
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
return types
|
|
70
|
+
.map((_type) =>
|
|
71
|
+
typeIndex
|
|
72
|
+
.statements(undefined, 'rdf:type', 'solid:TypeRegistration')
|
|
73
|
+
.filter((statement) => typeIndex.contains(statement.subject.value, 'solid:forClass', _type))
|
|
74
|
+
.map((statement) => typeIndex.statements(statement.subject.value, predicate))
|
|
75
|
+
.flat()
|
|
76
|
+
.map((statement) => statement.object.value)
|
|
77
|
+
.filter((url) => !!url))
|
|
78
|
+
.flat();
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|