diff --git a/config/http/notifications/webhooks/description.json b/config/http/notifications/webhooks/description.json index 60f9ea0b2..4f015b548 100644 --- a/config/http/notifications/webhooks/description.json +++ b/config/http/notifications/webhooks/description.json @@ -9,7 +9,6 @@ "comment": "Handles the storage description triples used for discovery of a WebHookSubscription2021 endpoint.", "@type": "WebHookDescriber", "route": { "@id": "urn:solid-server:default:WebHookRoute" }, - "relative": "#webhookNotification", "webIdRoute": { "@id": "urn:solid-server:default:WebHookWebIdRoute" } } ] diff --git a/config/http/notifications/websockets/description.json b/config/http/notifications/websockets/description.json index 973d6dabf..b9666ed13 100644 --- a/config/http/notifications/websockets/description.json +++ b/config/http/notifications/websockets/description.json @@ -9,7 +9,6 @@ "comment": "Handles the storage description triples used for discovery of a WebSocketSubscription2021 endpoint.", "@type": "NotificationDescriber", "route": { "@id": "urn:solid-server:default:WebSocket2021Route" }, - "relative": "#websocketNotification", "type": "http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021" } ] diff --git a/src/server/notifications/NotificationDescriber.ts b/src/server/notifications/NotificationDescriber.ts index 92e94243b..9506341d6 100644 --- a/src/server/notifications/NotificationDescriber.ts +++ b/src/server/notifications/NotificationDescriber.ts @@ -2,52 +2,48 @@ import type { NamedNode, Quad } from '@rdfjs/types'; import { DataFactory } from 'n3'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { InteractionRoute } from '../../identity/interaction/routing/InteractionRoute'; -import { NOTIFY, RDF } from '../../util/Vocabularies'; +import { NOTIFY } from '../../util/Vocabularies'; import { StorageDescriber } from '../description/StorageDescriber'; const { namedNode, quad } = DataFactory; export const DEFAULT_NOTIFICATION_FEATURES = [ NOTIFY.accept, - NOTIFY.expiration, + NOTIFY.endAt, NOTIFY.rate, + NOTIFY.startAt, NOTIFY.state, ]; /** - * Outputs quads describing how to access a specific Notification Subscription type and its features, - * as described in https://solidproject.org/TR/notifications-protocol#discovery. + * Outputs quads describing a Notification Subscription Service, + * 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 { private readonly path: NamedNode; - private readonly relative: string; private readonly type: NamedNode; private readonly features: NamedNode[]; /** * @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 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) { super(); this.path = namedNode(route.getPath()); - this.relative = relative; this.type = namedNode(type); this.features = features.map(namedNode); } public async handle(input: ResourceIdentifier): Promise { const subject = namedNode(input.path); - const subscription = namedNode(`${input.path}${this.relative}`); return [ - quad(subject, NOTIFY.terms.notificationChannel, subscription), - quad(subscription, RDF.terms.type, this.type), - quad(subscription, NOTIFY.terms.subscription, this.path), - ...this.features.map((feature): Quad => quad(subscription, NOTIFY.terms.feature, feature)), + quad(subject, NOTIFY.terms.subscription, this.path), + quad(this.path, NOTIFY.terms.channelType, this.type), + ...this.features.map((feature): Quad => quad(this.path, NOTIFY.terms.feature, feature)), ]; } } diff --git a/src/server/notifications/WebHookSubscription2021/WebHookDescriber.ts b/src/server/notifications/WebHookSubscription2021/WebHookDescriber.ts index bb8b99de8..ee19d9688 100644 --- a/src/server/notifications/WebHookSubscription2021/WebHookDescriber.ts +++ b/src/server/notifications/WebHookSubscription2021/WebHookDescriber.ts @@ -3,19 +3,18 @@ import { DataFactory } from 'n3'; import type { Quad } from 'rdf-js'; import type { ResourceIdentifier } from '../../../http/representation/ResourceIdentifier'; 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'; const { namedNode, quad } = DataFactory; export interface WebHookStorageDescriberArgs { route: InteractionRoute; - relative: string; webIdRoute: InteractionRoute; 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. */ @@ -25,7 +24,7 @@ export class WebHookDescriber extends NotificationDescriber { public constructor(args: WebHookStorageDescriberArgs) { const features = args.features ?? [ ...DEFAULT_NOTIFICATION_FEATURES ]; features.push(NOTIFY.webhookAuth); - super(args.route, args.relative, NOTIFY.WebHookSubscription2021, features); + super(args.route, NOTIFY.WebHookSubscription2021, features); this.webId = namedNode(args.webIdRoute.getPath()); } @@ -34,7 +33,7 @@ export class WebHookDescriber extends NotificationDescriber { const quads = await super.handle(input); // 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)); quads.push(quad(typeQuad!.subject, NOTIFY.terms.webid, this.webId)); diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 2a418445a..e6b342f9c 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -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#', 'accept', - 'expiration', + 'channelType', + 'endAt', 'feature', - 'notificationChannel', 'rate', + 'startAt', 'state', 'subscription', 'webhookAuth', diff --git a/templates/contexts/notification.jsonld b/templates/contexts/notification.jsonld index 9efaa06d6..b63e5d1c6 100644 --- a/templates/contexts/notification.jsonld +++ b/templates/contexts/notification.jsonld @@ -1,21 +1,58 @@ { "@context": { + "@version": 1.1, + "@protected": true, "id": "@id", "type": "@type", "notify": "http://www.w3.org/ns/solid/notifications#", - "WebSocketSubscription2021": "notify:WebSocketSubscription2021", - "features": { - "@id": "notify:features", - "@type": "@id" - }, - "notificationChannel": { - "@id": "notify:notificationChannel", - "@type": "@id" - }, + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "accept": "notify:accept", + + "channel": { + "@id": "notify:channel", + "@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", + + "startAt": { + "@id": "notify:startAt", + "@type": "xsd:dateTime" }, + "subscription": { "@id": "notify:subscription", - "@type": "@id" - } + "@type": "@id" }, + + "topic": { + "@id": "notify:topic", + "@type": "@id" } } } diff --git a/test/integration/WebHookSubscription2021.test.ts b/test/integration/WebHookSubscription2021.test.ts index f70ba26fa..6c802bb9b 100644 --- a/test/integration/WebHookSubscription2021.test.ts +++ b/test/integration/WebHookSubscription2021.test.ts @@ -8,7 +8,7 @@ import type { App } from '../../src/init/App'; import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil'; import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil'; 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 { getPort } from '../util/Util'; 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())); // Find the notification channel for websockets - const channels = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.notificationChannel, null); - const webHookChannels = channels.filter((channel): boolean => quads.has( - quad(channel as NamedNode, RDF.terms.type, namedNode(`${NOTIFY.namespace}WebHookSubscription2021`)), + const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null); + const webhookSubscriptions = subscriptions.filter((channel): boolean => quads.has( + quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebHookSubscription2021`)), )); - expect(webHookChannels).toHaveLength(1); - const subscriptionUrls = quads.getObjects(webHookChannels[0], NOTIFY.terms.subscription, null); - expect(subscriptionUrls).toHaveLength(1); - subscriptionUrl = subscriptionUrls[0].value; + expect(webhookSubscriptions).toHaveLength(1); + subscriptionUrl = webhookSubscriptions[0].value; // 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); serverWebId = webIds[0].value; }); diff --git a/test/integration/WebSocketSubscription2021.test.ts b/test/integration/WebSocketSubscription2021.test.ts index 555591a1c..5424eca91 100644 --- a/test/integration/WebSocketSubscription2021.test.ts +++ b/test/integration/WebSocketSubscription2021.test.ts @@ -6,7 +6,7 @@ import { BasicRepresentation } from '../../src/http/representation/BasicRepresen import type { App } from '../../src/init/App'; import type { ResourceStore } from '../../src/storage/ResourceStore'; 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 { getPort } from '../util/Util'; import { @@ -86,14 +86,12 @@ describe.each(stores)('A server supporting WebSocketSubscription2021 using %s', const quads = new Store(new Parser().parse(await response.text())); // Find the notification channel for websockets - const channels = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.notificationChannel, null); - const websocketChannels = channels.filter((channel): boolean => quads.has( - quad(channel as NamedNode, RDF.terms.type, namedNode(`${NOTIFY.namespace}WebSocketSubscription2021`)), + const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null); + const websocketSubscriptions = subscriptions.filter((channel): boolean => quads.has( + quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebSocketSubscription2021`)), )); - expect(websocketChannels).toHaveLength(1); - const subscriptionUrls = quads.getObjects(websocketChannels[0], NOTIFY.terms.subscription, null); - expect(subscriptionUrls).toHaveLength(1); - subscriptionUrl = subscriptionUrls[0].value; + expect(websocketSubscriptions).toHaveLength(1); + subscriptionUrl = websocketSubscriptions[0].value; }); it('supports subscribing.', async(): Promise => { diff --git a/test/unit/server/notifications/NotificationDescriber.test.ts b/test/unit/server/notifications/NotificationDescriber.test.ts index 347a4ee96..c9f62cffc 100644 --- a/test/unit/server/notifications/NotificationDescriber.test.ts +++ b/test/unit/server/notifications/NotificationDescriber.test.ts @@ -5,29 +5,28 @@ import { AbsolutePathInteractionRoute, } from '../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute'; import { NotificationDescriber } from '../../../../src/server/notifications/NotificationDescriber'; -import { NOTIFY, RDF } from '../../../../src/util/Vocabularies'; +import { NOTIFY } from '../../../../src/util/Vocabularies'; const { namedNode, quad } = DataFactory; describe('A NotificationDescriber', (): void => { const identifier: ResourceIdentifier = { path: 'http://example.com/foo' }; const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/websockets/'); - const relative = '#websocketNotification'; const type = 'http://www.w3.org/ns/solid/notifications#WebSocketSubscription2021'; let describer: NotificationDescriber; beforeEach(async(): Promise => { - describer = new NotificationDescriber(route, relative, type); + describer = new NotificationDescriber(route, type); }); it('outputs the expected quads.', async(): Promise => { - const subscription = namedNode('http://example.com/foo#websocketNotification'); + const subscription = namedNode('http://example.com/.notifications/websockets/'); const quads = await describer.handle(identifier); expect(quads).toBeRdfIsomorphic([ - quad(namedNode(identifier.path), NOTIFY.terms.notificationChannel, subscription), - quad(subscription, RDF.terms.type, namedNode(type)), - quad(subscription, NOTIFY.terms.subscription, namedNode('http://example.com/.notifications/websockets/')), + quad(namedNode(identifier.path), NOTIFY.terms.subscription, subscription), + quad(subscription, NOTIFY.terms.channelType, namedNode(type)), 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.state), ]); diff --git a/test/unit/server/notifications/WebHookSubscription2021/WebHookDescriber.test.ts b/test/unit/server/notifications/WebHookSubscription2021/WebHookDescriber.test.ts index febfbf34e..b44028734 100644 --- a/test/unit/server/notifications/WebHookSubscription2021/WebHookDescriber.test.ts +++ b/test/unit/server/notifications/WebHookSubscription2021/WebHookDescriber.test.ts @@ -5,30 +5,29 @@ import { AbsolutePathInteractionRoute, } from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute'; 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; describe('A WebHookDescriber', (): void => { const identifier: ResourceIdentifier = { path: 'http://example.com/foo' }; const route = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/'); const webIdRoute = new AbsolutePathInteractionRoute('http://example.com/.notifications/webhooks/webId'); - const relative = '#webhookNotification'; const type = 'http://www.w3.org/ns/solid/notifications#WebHookSubscription2021'; let describer: WebHookDescriber; beforeEach(async(): Promise => { - describer = new WebHookDescriber({ route, webIdRoute, relative }); + describer = new WebHookDescriber({ route, webIdRoute }); }); it('outputs the expected quads.', async(): Promise => { - const subscription = namedNode('http://example.com/foo#webhookNotification'); + const subscription = namedNode('http://example.com/.notifications/webhooks/'); const quads = await describer.handle(identifier); expect(quads).toBeRdfIsomorphic([ - quad(namedNode(identifier.path), NOTIFY.terms.notificationChannel, subscription), - quad(subscription, RDF.terms.type, namedNode(type)), - quad(subscription, NOTIFY.terms.subscription, namedNode('http://example.com/.notifications/webhooks/')), + quad(namedNode(identifier.path), NOTIFY.terms.subscription, subscription), + quad(subscription, NOTIFY.terms.channelType, namedNode(type)), 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.state), quad(subscription, NOTIFY.terms.feature, NOTIFY.terms.webhookAuth),