Skip to content

Commit 43e8cc6

Browse files
committed
[discovery] first draft of DiscoveryClient
1 parent eea36e0 commit 43e8cc6

File tree

9 files changed

+141
-34
lines changed

9 files changed

+141
-34
lines changed

packages/discovery/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
"url": "https://github.com/o-development/solid-notification-client/issues"
3232
},
3333
"devDependencies": {
34-
"@solid-notifications/types": "^0.0.0",
35-
"@solid/community-server": "^5.1.0",
36-
"@types/jest": "^29.5.0",
37-
"jest": "^29.5.0",
38-
"solid-test-utils": "^0.0.0",
39-
"ts-jest": "^29.1.0"
34+
"@solid-notifications/types": "^0.1.0",
35+
"solid-test-utils": "^0.0.0"
36+
},
37+
"dependencies": {
38+
"@janeirodigital/interop-utils": "^1.0.0-rc.21",
39+
"n3": "^1.17.0"
4040
}
4141
}

packages/discovery/src/client.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { DataFactory } from 'n3'
2+
import { parseTurtle, getDescriptionResource, getOneMatchingQuad, getAllMatchingQuads, NOTIFY, getStorageDescription } from '@janeirodigital/interop-utils'
3+
import type { DatasetCore } from '@rdfjs/types'
4+
import { ChannelType, SubscriptionService } from '@solid-notifications/types'
5+
6+
export class DiscoveryClient {
7+
8+
private rdf: {
9+
contentType: 'text/turtle' | 'application/ld+json',
10+
parse: typeof parseTurtle
11+
}
12+
constructor(private authnFetch: typeof fetch) {
13+
// TODO pass Turtle or JSON-LD parser and set accordignly
14+
this.rdf = {
15+
contentType: 'text/turtle',
16+
parse: parseTurtle
17+
}
18+
}
19+
20+
async findService(resourceUri: string, channelType: ChannelType): Promise<SubscriptionService | null> {
21+
22+
const storageDescription = await this.fetchStorageDescription(resourceUri)
23+
24+
// TODO handle multiple matching services
25+
const serviceNode = getOneMatchingQuad(storageDescription, null, NOTIFY.channelType, DataFactory.namedNode(channelType))?.subject
26+
if (!serviceNode) return null
27+
const features = getAllMatchingQuads(storageDescription, serviceNode, NOTIFY.feature).map(quad => quad.object.value)
28+
return {
29+
id: serviceNode.value,
30+
channelType,
31+
feature: features
32+
}
33+
}
34+
35+
36+
// TODO use some rdf-fetch util
37+
async fetchResource(resourceUri): Promise<DatasetCore> {
38+
const response = await this.authnFetch(resourceUri, {
39+
headers: {
40+
'Accept': this.rdf.contentType
41+
}
42+
})
43+
return this.rdf.parse(await response.text(), response.url)
44+
}
45+
46+
async discoverStorageDescription(resourceUri: string): Promise<string> {
47+
const response = await this.authnFetch(resourceUri, { method: 'head' })
48+
return getStorageDescription(response.headers.get('Link'))
49+
}
50+
51+
async fetchStorageDescription(resourceUri): Promise<DatasetCore> {
52+
const storageDescriptionUri = await this.discoverStorageDescription(resourceUri)
53+
return this.fetchResource(storageDescriptionUri)
54+
}
55+
56+
async discoverDescriptionResource(resourceUri: string): Promise<string> {
57+
const response = await this.authnFetch(resourceUri, { method: 'head' })
58+
return getDescriptionResource(response.headers.get('Link'))
59+
}
60+
61+
async fetchDescriptionResource(resourceUri): Promise<DatasetCore> {
62+
const resourceDescriptionUri = await this.discoverDescriptionResource(resourceUri)
63+
return this.fetchResource(resourceDescriptionUri)
64+
}
65+
}

packages/discovery/src/getDescribedByUri.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/discovery/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from "./getDescribedByUri";
1+
export * from "./client";
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import SolidTestUtils from "solid-test-utils";
2+
import { DiscoveryClient } from "../src/client";
3+
import { DC, NOTIFY } from "@janeirodigital/interop-utils";
4+
5+
describe("discovery", () => {
6+
const stu = new SolidTestUtils();
7+
beforeAll(async () => stu.beforeAll());
8+
afterAll(async () => stu.afterAll());
9+
10+
const cardUri = "http://localhost:3001/example/profile/card"
11+
12+
test("discoverStorageDescription", async () => {
13+
const client = new DiscoveryClient(stu.authFetch)
14+
const storageDescriptionUri = await client.discoverStorageDescription(cardUri)
15+
expect(storageDescriptionUri).toBe('http://localhost:3001/example/.well-known/solid');
16+
});
17+
18+
test("fetchStorageDescription", async () => {
19+
const client = new DiscoveryClient(stu.authFetch)
20+
const dataset = await client.fetchStorageDescription(cardUri)
21+
expect(dataset.match(null, NOTIFY.subscription, NOTIFY.WebhookChannel2023)).toBeTruthy()
22+
});
23+
24+
test("discoverDescriptionResource", async () => {
25+
const client = new DiscoveryClient(stu.authFetch)
26+
const resourceDescriptionUri = await client.discoverDescriptionResource(cardUri)
27+
expect(resourceDescriptionUri).toBe('http://localhost:3001/example/profile/card.meta');
28+
});
29+
30+
test("fetchDescriptionResource", async () => {
31+
const client = new DiscoveryClient(stu.authFetch)
32+
const dataset = await client.fetchDescriptionResource(cardUri)
33+
expect(dataset.match(null, DC.modified)).toBeTruthy()
34+
});
35+
36+
test("find Webhook service", async () => {
37+
const client = new DiscoveryClient(stu.authFetch)
38+
const service = await client.findService(cardUri, NOTIFY.WebhookChannel2023.value)
39+
expect(service).toEqual(expect.objectContaining({
40+
id: 'http://localhost:3001/.notifications/WebhookChannel2023/',
41+
channelType: NOTIFY.WebhookChannel2023.value
42+
}))
43+
});
44+
45+
test("find Web Socket service", async () => {
46+
const client = new DiscoveryClient(stu.authFetch)
47+
const service = await client.findService(cardUri, NOTIFY.WebSocketChannel2023.value)
48+
expect(service).toEqual(expect.objectContaining({
49+
id: 'http://localhost:3001/.notifications/WebSocketChannel2023/',
50+
channelType: NOTIFY.WebSocketChannel2023.value
51+
}))
52+
});
53+
54+
test("find non existing service", async () => {
55+
const client = new DiscoveryClient(stu.authFetch)
56+
//@ts-ignore
57+
const service = await client.findService(cardUri, 'https://fake.example/SomeChannel')
58+
expect(service).toBeNull()
59+
});
60+
});

packages/discovery/test/getDescribedByUri.test.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/solid-test-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
},
2929
"devDependencies": {
3030
"@inrupt/solid-client-authn-core": "^1.14.0",
31-
"@solid/community-server": "^5.1.0",
31+
"@solid/community-server": "^6.0.1",
3232
"node-fetch": "^3.3.1"
3333
}
3434
}

packages/types/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@solid-notifications/types",
3-
"version": "0.0.0",
3+
"version": "0.1.0",
44
"description": "Shared typescript types for solid notification libraries",
55
"keywords": [
66
"solid",
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
export enum ChannelType {
2+
WebhookChannel2023 = 'http://www.w3.org/ns/solid/notifications#WebhookChannel2023',
3+
WebSocketChannel2023 = 'http://www.w3.org/ns/solid/notifications#WebSocketChannel2023',
4+
}
5+
16
export interface NotificationChannel {
27
id: string;
3-
type: string;
8+
type: ChannelType; // TODO channel types should be extendible
49
topic: string | string[];
5-
receiveFrom: string;
10+
receiveFrom?: string;
611
sendTo?: string;
712
sender?: string;
813
}

0 commit comments

Comments
 (0)