feat: Use notification v0.2 features in discovery

This commit is contained in:
Joachim Van Herwegen 2023-01-24 13:42:28 +01:00
parent 23db528472
commit 10980e90a3
10 changed files with 92 additions and 67 deletions

View File

@ -9,7 +9,6 @@
"comment": "Handles the storage description triples used for discovery of a WebHookSubscription2021 endpoint.", "comment": "Handles the storage description triples used for discovery of a WebHookSubscription2021 endpoint.",
"@type": "WebHookDescriber", "@type": "WebHookDescriber",
"route": { "@id": "urn:solid-server:default:WebHookRoute" }, "route": { "@id": "urn:solid-server:default:WebHookRoute" },
"relative": "#webhookNotification",
"webIdRoute": { "@id": "urn:solid-server:default:WebHookWebIdRoute" } "webIdRoute": { "@id": "urn:solid-server:default:WebHookWebIdRoute" }
} }
] ]

View File

@ -9,7 +9,6 @@
"comment": "Handles the storage description triples used for discovery of a WebSocketSubscription2021 endpoint.", "comment": "Handles the storage description triples used for discovery of a WebSocketSubscription2021 endpoint.",
"@type": "NotificationDescriber", "@type": "NotificationDescriber",
"route": { "@id": "urn:solid-server:default:WebSocket2021Route" }, "route": { "@id": "urn:solid-server:default:WebSocket2021Route" },
"relative": "#websocketNotification",
"type": "http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021" "type": "http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021"
} }
] ]

View File

@ -2,52 +2,48 @@ import type { NamedNode, Quad } from '@rdfjs/types';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import type { InteractionRoute } from '../../identity/interaction/routing/InteractionRoute'; import type { InteractionRoute } from '../../identity/interaction/routing/InteractionRoute';
import { NOTIFY, RDF } from '../../util/Vocabularies'; import { NOTIFY } from '../../util/Vocabularies';
import { StorageDescriber } from '../description/StorageDescriber'; import { StorageDescriber } from '../description/StorageDescriber';
const { namedNode, quad } = DataFactory; const { namedNode, quad } = DataFactory;
export const DEFAULT_NOTIFICATION_FEATURES = [ export const DEFAULT_NOTIFICATION_FEATURES = [
NOTIFY.accept, NOTIFY.accept,
NOTIFY.expiration, NOTIFY.endAt,
NOTIFY.rate, NOTIFY.rate,
NOTIFY.startAt,
NOTIFY.state, NOTIFY.state,
]; ];
/** /**
* Outputs quads describing how to access a specific Notification Subscription type and its features, * Outputs quads describing a Notification Subscription Service,
* as described in https://solidproject.org/TR/notifications-protocol#discovery. * as described in https://solidproject.org/TR/2022/notifications-protocol-20221231#discovery and
* https://solidproject.org/TR/2022/notifications-protocol-20221231#description-resource-data-model.
*/ */
export class NotificationDescriber extends StorageDescriber { export class NotificationDescriber extends StorageDescriber {
private readonly path: NamedNode; private readonly path: NamedNode;
private readonly relative: string;
private readonly type: NamedNode; private readonly type: NamedNode;
private readonly features: NamedNode[]; private readonly features: NamedNode[];
/** /**
* @param route - The route describing where the subscription target is. * @param route - The route describing where the subscription target is.
* @param relative - Will be appended to the input path to generate a named node corresponding to the description.
* E.g., "#websocketNotification".
* @param type - The rdf:type of the subscription type. * @param type - The rdf:type of the subscription type.
* @param features - Which features are enabled for this subscription type. Defaults to accept/expiration/rate/state. * @param features - Which features are enabled for this subscription type. Defaults to accept/expiration/rate/state.
*/ */
public constructor(route: InteractionRoute, relative: string, type: string, public constructor(route: InteractionRoute, type: string,
features: string[] = DEFAULT_NOTIFICATION_FEATURES) { features: string[] = DEFAULT_NOTIFICATION_FEATURES) {
super(); super();
this.path = namedNode(route.getPath()); this.path = namedNode(route.getPath());
this.relative = relative;
this.type = namedNode(type); this.type = namedNode(type);
this.features = features.map(namedNode); this.features = features.map(namedNode);
} }
public async handle(input: ResourceIdentifier): Promise<Quad[]> { public async handle(input: ResourceIdentifier): Promise<Quad[]> {
const subject = namedNode(input.path); const subject = namedNode(input.path);
const subscription = namedNode(`${input.path}${this.relative}`);
return [ return [
quad(subject, NOTIFY.terms.notificationChannel, subscription), quad(subject, NOTIFY.terms.subscription, this.path),
quad(subscription, RDF.terms.type, this.type), quad(this.path, NOTIFY.terms.channelType, this.type),
quad(subscription, NOTIFY.terms.subscription, this.path), ...this.features.map((feature): Quad => quad(this.path, NOTIFY.terms.feature, feature)),
...this.features.map((feature): Quad => quad(subscription, NOTIFY.terms.feature, feature)),
]; ];
} }
} }

View File

@ -3,19 +3,18 @@ import { DataFactory } from 'n3';
import type { Quad } from 'rdf-js'; import type { Quad } from 'rdf-js';
import type { ResourceIdentifier } from '../../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../http/representation/ResourceIdentifier';
import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute'; import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute';
import { NOTIFY, RDF } from '../../../util/Vocabularies'; import { NOTIFY } from '../../../util/Vocabularies';
import { DEFAULT_NOTIFICATION_FEATURES, NotificationDescriber } from '../NotificationDescriber'; import { DEFAULT_NOTIFICATION_FEATURES, NotificationDescriber } from '../NotificationDescriber';
const { namedNode, quad } = DataFactory; const { namedNode, quad } = DataFactory;
export interface WebHookStorageDescriberArgs { export interface WebHookStorageDescriberArgs {
route: InteractionRoute; route: InteractionRoute;
relative: string;
webIdRoute: InteractionRoute; webIdRoute: InteractionRoute;
features?: string[]; features?: string[];
} }
/** /**
* Handles the necessary triples for describing a WebHookSubcription2021 notification channel. * Handles the necessary triples for describing a WebHookSubscription2021 subscription service.
* *
* Extends {@link NotificationDescriber} by adding the necessary `notify:webid` and `notify:webhookAuth` triples. * Extends {@link NotificationDescriber} by adding the necessary `notify:webid` and `notify:webhookAuth` triples.
*/ */
@ -25,7 +24,7 @@ export class WebHookDescriber extends NotificationDescriber {
public constructor(args: WebHookStorageDescriberArgs) { public constructor(args: WebHookStorageDescriberArgs) {
const features = args.features ?? [ ...DEFAULT_NOTIFICATION_FEATURES ]; const features = args.features ?? [ ...DEFAULT_NOTIFICATION_FEATURES ];
features.push(NOTIFY.webhookAuth); features.push(NOTIFY.webhookAuth);
super(args.route, args.relative, NOTIFY.WebHookSubscription2021, features); super(args.route, NOTIFY.WebHookSubscription2021, features);
this.webId = namedNode(args.webIdRoute.getPath()); this.webId = namedNode(args.webIdRoute.getPath());
} }
@ -34,7 +33,7 @@ export class WebHookDescriber extends NotificationDescriber {
const quads = await super.handle(input); const quads = await super.handle(input);
// Find the notification channel subject // Find the notification channel subject
const typeQuad = quads.find((entry): boolean => entry.predicate.equals(RDF.terms.type) && const typeQuad = quads.find((entry): boolean => entry.predicate.equals(NOTIFY.terms.channelType) &&
entry.object.equals(NOTIFY.terms.WebHookSubscription2021)); entry.object.equals(NOTIFY.terms.WebHookSubscription2021));
quads.push(quad(typeQuad!.subject, NOTIFY.terms.webid, this.webId)); quads.push(quad(typeQuad!.subject, NOTIFY.terms.webid, this.webId));

View File

@ -193,10 +193,11 @@ export const MA = createVocabulary('http://www.w3.org/ns/ma-ont#',
export const NOTIFY = createVocabulary('http://www.w3.org/ns/solid/notifications#', export const NOTIFY = createVocabulary('http://www.w3.org/ns/solid/notifications#',
'accept', 'accept',
'expiration', 'channelType',
'endAt',
'feature', 'feature',
'notificationChannel',
'rate', 'rate',
'startAt',
'state', 'state',
'subscription', 'subscription',
'webhookAuth', 'webhookAuth',

View File

@ -1,21 +1,58 @@
{ {
"@context": { "@context": {
"@version": 1.1,
"@protected": true,
"id": "@id", "id": "@id",
"type": "@type", "type": "@type",
"notify": "http://www.w3.org/ns/solid/notifications#", "notify": "http://www.w3.org/ns/solid/notifications#",
"WebSocketSubscription2021": "notify:WebSocketSubscription2021", "xsd": "http://www.w3.org/2001/XMLSchema#",
"features": {
"@id": "notify:features", "accept": "notify:accept",
"@type": "@id"
}, "channel": {
"notificationChannel": { "@id": "notify:channel",
"@id": "notify:notificationChannel", "@type": "@id" },
"@type": "@id"
}, "channelType": {
"@id": "notify:channelType",
"@type": "@vocab" },
"endAt": {
"@id": "notify:endAt",
"@type": "xsd:dateTime" },
"feature": {
"@id": "notify:feature",
"@type": "@vocab" },
"rate": {
"@id": "notify:rate",
"@type": "xsd:duration" },
"receiveFrom": {
"@id": "notify:receiveFrom",
"@type": "@id" },
"sender": {
"@id": "notify:sender",
"@type": "@id" },
"sendTo": {
"@id": "notify:sendTo",
"@type": "@id" },
"state": "notify:state", "state": "notify:state",
"startAt": {
"@id": "notify:startAt",
"@type": "xsd:dateTime" },
"subscription": { "subscription": {
"@id": "notify:subscription", "@id": "notify:subscription",
"@type": "@id" "@type": "@id" },
}
"topic": {
"@id": "notify:topic",
"@type": "@id" }
} }
} }

View File

@ -8,7 +8,7 @@ import type { App } from '../../src/init/App';
import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil'; import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil';
import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil'; import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil';
import { readJsonStream } from '../../src/util/StreamUtil'; import { readJsonStream } from '../../src/util/StreamUtil';
import { NOTIFY, RDF } from '../../src/util/Vocabularies'; import { NOTIFY } from '../../src/util/Vocabularies';
import { expectNotification, subscribe } from '../util/NotificationUtil'; import { expectNotification, subscribe } from '../util/NotificationUtil';
import { getPort } from '../util/Util'; import { getPort } from '../util/Util';
import { import {
@ -95,17 +95,15 @@ describe.each(stores)('A server supporting WebHookSubscription2021 using %s', (n
const quads = new Store(new Parser().parse(await response.text())); const quads = new Store(new Parser().parse(await response.text()));
// Find the notification channel for websockets // Find the notification channel for websockets
const channels = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.notificationChannel, null); const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null);
const webHookChannels = channels.filter((channel): boolean => quads.has( const webhookSubscriptions = subscriptions.filter((channel): boolean => quads.has(
quad(channel as NamedNode, RDF.terms.type, namedNode(`${NOTIFY.namespace}WebHookSubscription2021`)), quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebHookSubscription2021`)),
)); ));
expect(webHookChannels).toHaveLength(1); expect(webhookSubscriptions).toHaveLength(1);
const subscriptionUrls = quads.getObjects(webHookChannels[0], NOTIFY.terms.subscription, null); subscriptionUrl = webhookSubscriptions[0].value;
expect(subscriptionUrls).toHaveLength(1);
subscriptionUrl = subscriptionUrls[0].value;
// It should also link to the server WebID // It should also link to the server WebID
const webIds = quads.getObjects(webHookChannels[0], NOTIFY.terms.webid, null); const webIds = quads.getObjects(webhookSubscriptions[0], NOTIFY.terms.webid, null);
expect(webIds).toHaveLength(1); expect(webIds).toHaveLength(1);
serverWebId = webIds[0].value; serverWebId = webIds[0].value;
}); });

View File

@ -6,7 +6,7 @@ import { BasicRepresentation } from '../../src/http/representation/BasicRepresen
import type { App } from '../../src/init/App'; import type { App } from '../../src/init/App';
import type { ResourceStore } from '../../src/storage/ResourceStore'; import type { ResourceStore } from '../../src/storage/ResourceStore';
import { joinUrl } from '../../src/util/PathUtil'; import { joinUrl } from '../../src/util/PathUtil';
import { NOTIFY, RDF } from '../../src/util/Vocabularies'; import { NOTIFY } from '../../src/util/Vocabularies';
import { expectNotification, subscribe } from '../util/NotificationUtil'; import { expectNotification, subscribe } from '../util/NotificationUtil';
import { getPort } from '../util/Util'; import { getPort } from '../util/Util';
import { import {
@ -86,14 +86,12 @@ describe.each(stores)('A server supporting WebSocketSubscription2021 using %s',
const quads = new Store(new Parser().parse(await response.text())); const quads = new Store(new Parser().parse(await response.text()));
// Find the notification channel for websockets // Find the notification channel for websockets
const channels = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.notificationChannel, null); const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null);
const websocketChannels = channels.filter((channel): boolean => quads.has( const websocketSubscriptions = subscriptions.filter((channel): boolean => quads.has(
quad(channel as NamedNode, RDF.terms.type, namedNode(`${NOTIFY.namespace}WebSocketSubscription2021`)), quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebSocketSubscription2021`)),
)); ));
expect(websocketChannels).toHaveLength(1); expect(websocketSubscriptions).toHaveLength(1);
const subscriptionUrls = quads.getObjects(websocketChannels[0], NOTIFY.terms.subscription, null); subscriptionUrl = websocketSubscriptions[0].value;
expect(subscriptionUrls).toHaveLength(1);
subscriptionUrl = subscriptionUrls[0].value;
}); });
it('supports subscribing.', async(): Promise<void> => { it('supports subscribing.', async(): Promise<void> => {

View File

@ -5,29 +5,28 @@ import {
AbsolutePathInteractionRoute, AbsolutePathInteractionRoute,
} from '../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute'; } from '../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
import { NotificationDescriber } from '../../../../src/server/notifications/NotificationDescriber'; import { NotificationDescriber } from '../../../../src/server/notifications/NotificationDescriber';
import { NOTIFY, RDF } from '../../../../src/util/Vocabularies'; import { NOTIFY } from '../../../../src/util/Vocabularies';
const { namedNode, quad } = DataFactory; const { namedNode, quad } = DataFactory;
describe('A NotificationDescriber', (): void => { describe('A NotificationDescriber', (): void => {
const identifier: ResourceIdentifier = { path: 'http://example.com/foo' }; const identifier: ResourceIdentifier = { path: 'http://example.com/foo' };
const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/websockets/'); const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/websockets/');
const relative = '#websocketNotification';
const type = 'http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021'; const type = 'http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021';
let describer: NotificationDescriber; let describer: NotificationDescriber;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
describer = new NotificationDescriber(route, relative, type); describer = new NotificationDescriber(route, type);
}); });
it('outputs the expected quads.', async(): Promise<void> => { it('outputs the expected quads.', async(): Promise<void> => {
const subscription = namedNode('http://example.com/foo#websocketNotification'); const subscription = namedNode('http://example.com/.notifications/websockets/');
const quads = await describer.handle(identifier); const quads = await describer.handle(identifier);
expect(quads).toBeRdfIsomorphic([ expect(quads).toBeRdfIsomorphic([
quad(namedNode(identifier.path), NOTIFY.terms.notificationChannel, subscription), quad(namedNode(identifier.path), NOTIFY.terms.subscription, subscription),
quad(subscription, RDF.terms.type, namedNode(type)), quad(subscription, NOTIFY.terms.channelType, namedNode(type)),
quad(subscription, NOTIFY.terms.subscription, namedNode('http://example.com/.notifications/websockets/')),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.accept), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.accept),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.expiration), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.endAt),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.startAt),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.rate), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.rate),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.state), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.state),
]); ]);

View File

@ -5,30 +5,29 @@ import {
AbsolutePathInteractionRoute, AbsolutePathInteractionRoute,
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute'; } from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
import { WebHookDescriber } from '../../../../../src/server/notifications/WebHookSubscription2021/WebHookDescriber'; import { WebHookDescriber } from '../../../../../src/server/notifications/WebHookSubscription2021/WebHookDescriber';
import { NOTIFY, RDF } from '../../../../../src/util/Vocabularies'; import { NOTIFY } from '../../../../../src/util/Vocabularies';
const { namedNode, quad } = DataFactory; const { namedNode, quad } = DataFactory;
describe('A WebHookDescriber', (): void => { describe('A WebHookDescriber', (): void => {
const identifier: ResourceIdentifier = { path: 'http://example.com/foo' }; const identifier: ResourceIdentifier = { path: 'http://example.com/foo' };
const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/'); const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/');
const webIdRoute = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/webId'); const webIdRoute = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/webId');
const relative = '#webhookNotification';
const type = 'http://www.w3.org/ns/solid/notifications#WebHookSubscription2021'; const type = 'http://www.w3.org/ns/solid/notifications#WebHookSubscription2021';
let describer: WebHookDescriber; let describer: WebHookDescriber;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
describer = new WebHookDescriber({ route, webIdRoute, relative }); describer = new WebHookDescriber({ route, webIdRoute });
}); });
it('outputs the expected quads.', async(): Promise<void> => { it('outputs the expected quads.', async(): Promise<void> => {
const subscription = namedNode('http://example.com/foo#webhookNotification'); const subscription = namedNode('http://example.com/.notifications/webhooks/');
const quads = await describer.handle(identifier); const quads = await describer.handle(identifier);
expect(quads).toBeRdfIsomorphic([ expect(quads).toBeRdfIsomorphic([
quad(namedNode(identifier.path), NOTIFY.terms.notificationChannel, subscription), quad(namedNode(identifier.path), NOTIFY.terms.subscription, subscription),
quad(subscription, RDF.terms.type, namedNode(type)), quad(subscription, NOTIFY.terms.channelType, namedNode(type)),
quad(subscription, NOTIFY.terms.subscription, namedNode('http://example.com/.notifications/webhooks/')),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.accept), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.accept),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.expiration), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.endAt),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.startAt),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.rate), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.rate),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.state), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.state),
quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.webhookAuth), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.webhookAuth),