Skip to content

Commit

Permalink
Merge pull request #1347 from assemblee-virtuelle/special-endpoint-mixin
Browse files Browse the repository at this point in the history
New SpecialEndpointMixin
  • Loading branch information
srosset81 authored Dec 20, 2024
2 parents 5a11c38 + b4ce4c2 commit f3be8b3
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 123 deletions.
1 change: 1 addition & 0 deletions src/middleware/packages/ldp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
ImageProcessorMixin: require('./mixins/image-processor'),
DocumentTaggerMixin: require('./mixins/document-tagger'),
DisassemblyMixin: require('./mixins/disassembly'),
SpecialEndpointMixin: require('./mixins/special-endpoint'),
// Other
defaultContainerOptions: require('./services/registry/defaultOptions'),
LdpAdapter: require('./adapter'),
Expand Down
87 changes: 87 additions & 0 deletions src/middleware/packages/ldp/mixins/special-endpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const urlJoin = require('url-join');
const { namedNode, triple } = require('@rdfjs/data-model');
const { MIME_TYPES } = require('@semapps/mime-types');
const { parseUrl, parseHeader, negotiateAccept, parseJson, parseTurtle } = require('@semapps/middlewares');

module.exports = {
settings: {
baseUrl: null,
settingsDataset: null,
endpoint: {
path: null, // Should start with a dot
initialData: {}
}
},
dependencies: ['api', 'ldp'],
async started() {
if (!this.settings.baseUrl) throw new Error(`The baseUrl must be specified for service ${this.name}`);
if (!this.settings.settingsDataset)
throw new Error(`The settingsDataset must be specified for service ${this.name}`);

await this.broker.call('api.addRoute', {
route: {
path: this.settings.endpoint.path,
bodyParsers: false,
authorization: false,
authentication: false,
aliases: {
'GET /': [parseUrl, parseHeader, negotiateAccept, parseJson, parseTurtle, `${this.name}.endpointGet`],
'POST /': this.actions.endpointPost
? [parseUrl, parseHeader, negotiateAccept, parseJson, parseTurtle, `${this.name}.endpointPost`]
: undefined
}
}
});

this.endpointUrl = urlJoin(this.settings.baseUrl, this.settings.endpoint.path);

const endpointExist = await this.broker.call(
'ldp.resource.exist',
{ resourceUri: this.endpointUrl, webId: 'system' },
{ meta: { dataset: this.settings.settingsDataset } }
);

if (!endpointExist) {
await this.broker.call(
'ldp.resource.create',
{
resource: {
id: this.endpointUrl,
...this.settings.endpoint.initialData
},
contentType: MIME_TYPES.JSON,
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset, skipEmitEvent: true, skipObjectsWatcher: true } }
);
}
},
actions: {
async endpointAdd(ctx) {
const { predicate, object } = ctx.params;

await ctx.call(
'ldp.resource.patch',
{
resourceUri: this.endpointUrl,
triplesToAdd: [triple(namedNode(this.endpointUrl), predicate, object)],
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset, skipEmitEvent: true, skipObjectsWatcher: true } }
);
},
async endpointGet(ctx) {
ctx.meta.$responseType = ctx.meta.headers?.accept;

return await ctx.call(
'ldp.resource.get',
{
resourceUri: this.endpointUrl,
accept: ctx.meta.headers?.accept,
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset } }
);
}
}
};
75 changes: 9 additions & 66 deletions src/middleware/packages/solid/services/endpoint.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,23 @@
const urlJoin = require('url-join');
const { namedNode, triple } = require('@rdfjs/data-model');
const { MIME_TYPES } = require('@semapps/mime-types');
const { parseUrl, parseHeader, negotiateAccept, parseJson, parseTurtle } = require('@semapps/middlewares');
const { SpecialEndpointMixin } = require('@semapps/ldp');

module.exports = {
name: 'solid-endpoint',
mixins: [SpecialEndpointMixin],
settings: {
baseUrl: null,
settingsDataset: 'settings'
settingsDataset: null,
endpoint: {
path: '/.well-known/solid',
initialData: {
type: 'http://www.w3.org/ns/pim/space#Storage'
}
}
},
dependencies: ['api', 'ldp'],
async started() {
await this.broker.call('ldp.link-header.register', { actionName: 'solid-notifications.provider.getLink' });

await this.broker.call('api.addRoute', {
route: {
name: 'solid-endpoint',
path: '/.well-known/solid',
authorization: false,
authentication: false,
aliases: {
'GET /': [parseUrl, parseHeader, negotiateAccept, parseJson, parseTurtle, 'solid-endpoint.get']
}
}
});

this.endpointUrl = urlJoin(this.settings.baseUrl, '.well-known', 'solid');

const endpointExist = await this.broker.call(
'ldp.resource.exist',
{ resourceUri: this.endpointUrl, webId: 'system' },
{ meta: { dataset: this.settings.settingsDataset } }
);

if (!endpointExist) {
await this.broker.call(
'ldp.resource.create',
{
resource: {
id: this.endpointUrl,
type: 'http://www.w3.org/ns/pim/space#Storage'
},
contentType: MIME_TYPES.JSON,
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset, skipEmitEvent: true } }
);
}
},
actions: {
async add(ctx) {
const { predicate, object } = ctx.params;

await ctx.call(
'ldp.resource.patch',
{
resourceUri: this.endpointUrl,
triplesToAdd: [triple(namedNode(this.endpointUrl), predicate, object)],
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset, skipEmitEvent: true } }
);
},
async get(ctx) {
ctx.meta.$responseType = ctx.meta.headers?.accept;

return await ctx.call(
'ldp.resource.get',
{
resourceUri: this.endpointUrl,
accept: ctx.meta.headers?.accept,
webId: 'system'
},
{ meta: { dataset: this.settings.settingsDataset } }
);
},
getLink() {
return {
uri: this.endpointUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const path = require('path');
const urlJoin = require('url-join');
const { Errors: E } = require('moleculer-web');
const { parseHeader, negotiateContentType, parseJson } = require('@semapps/middlewares');
const { ControlledContainerMixin, getDatasetFromUri, arrayOf } = require('@semapps/ldp');
const { SpecialEndpointMixin, ControlledContainerMixin, getDatasetFromUri, arrayOf } = require('@semapps/ldp');
const { ACTIVITY_TYPES } = require('@semapps/activitypub');
const { MIME_TYPES } = require('@semapps/mime-types');
const { namedNode } = require('@rdfjs/data-model');
Expand All @@ -23,7 +21,7 @@ const moment = require('moment');
*/
module.exports = {
// name: 'solid-notifications.provider.<ChannelType>',
mixins: [ControlledContainerMixin],
mixins: [ControlledContainerMixin, SpecialEndpointMixin],
settings: {
// Channel properties (to be overridden)
channelType: null, // E.g. 'WebhookChannel2023',
Expand All @@ -42,6 +40,13 @@ module.exports = {
read: true,
write: true
}
},

// SpecialEndpointMixin (to be filled by channels services)
settingsDataset: null,
endpoint: {
path: null,
initialData: {}
}
},
dependencies: ['api', 'solid-endpoint'],
Expand All @@ -54,24 +59,9 @@ module.exports = {
if (this.settings.acceptedTypes?.length <= 0) this.settings.acceptedTypes = [this.settings.typePredicate];
},
async started() {
const { channelType, baseUrl } = this.settings;
const { pathname: basePath } = new URL(baseUrl);

await this.broker.call('api.addRoute', {
route: {
name: `notification-${channelType}`,
path: path.join(basePath, `/.notifications/${channelType}`),
bodyParsers: false,
authorization: false,
authentication: true,
aliases: {
'GET /': `${this.name}.discover`,
'POST /': [parseHeader, negotiateContentType, parseJson, `${this.name}.createChannel`]
}
}
});
const { channelType } = this.settings;

await this.broker.call('solid-endpoint.add', {
await this.broker.call('solid-endpoint.endpointAdd', {
predicate: namedNode('http://www.w3.org/ns/solid/notifications#subscription'),
object: namedNode(urlJoin(this.settings.baseUrl, '.notifications', channelType))
});
Expand All @@ -82,13 +72,8 @@ module.exports = {
this.loadChannelsFromDb({ removeOldChannels: true });
},
actions: {
async discover() {
throw new Error('Not implemented. Please provide this action in your service.');
// Cache for 1 day.
// ctx.meta.$responseHeaders = { 'Cache-Control': 'public, max-age=86400' };
},

async createChannel(ctx) {
// Action called by the SpecialEndpointMixin when POSTing to the endpoint
async endpointPost(ctx) {
// Expect format https://communitysolidserver.github.io/CommunitySolidServer/latest/usage/notifications/#webhooks
// Correct context: https://github.com/solid/vocab/blob/main/solid-notifications-context.jsonld
const type = ctx.params.type || ctx.params['@type'];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const urlJoin = require('url-join');
const fetch = require('node-fetch');
const NotificationChannelMixin = require('./notification-channel.mixin');

Expand Down Expand Up @@ -27,24 +26,19 @@ const WebhookChannel2023Service = {
en: 'Webhook Channel'
},
internal: true
},
endpoint: {
path: '/.notifications/WebhookChannel2023',
initialData: {
'notify:channelType': 'notify:WebhookChannel2023',
'notify:feature': ['notify:endAt', 'notify:rate', 'notify:startAt', 'notify:state']
}
}
},
created() {
if (!this.createJob) throw new Error('The QueueMixin must be configured with this service');
},
actions: {
async discover(ctx) {
// TODO Handle content negotiation
ctx.meta.$responseType = 'application/ld+json';
// Cache for 1 day.
ctx.meta.$responseHeaders = { 'Cache-Control': 'public, max-age=86400' };
return {
'@context': { notify: 'http://www.w3.org/ns/solid/notifications#' },
'@id': urlJoin(this.settings.baseUrl, '.notifications', 'WebhookChannel2023'),
'notify:channelType': 'notify:WebhookChannel2023',
'notify:feature': ['notify:endAt', 'notify:rate', 'notify:startAt', 'notify:state']
};
},
async getAppChannels(ctx) {
const { appUri, webId } = ctx.params;
const { origin: appOrigin } = new URL(appUri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const WebSocketChannel2023Service = {
en: 'WebSocket Channel'
},
internal: true
},
endpoint: {
path: '/.notifications/WebSocketChannel2023',
initialData: {
'notify:channelType': 'notify:WebSocketChannel2023',
'notify:feature': ['notify:endAt', 'notify:rate', 'notify:startAt', 'notify:state']
}
}
},
async started() {
Expand Down Expand Up @@ -51,19 +58,6 @@ const WebSocketChannel2023Service = {
}
});
},
actions: {
async discover(ctx) {
ctx.meta.$responseType = 'application/ld+json';
// Cache for 1 day.
ctx.meta.$responseHeaders = { 'Cache-Control': 'public, max-age=86400' };
return {
'@context': { notify: 'http://www.w3.org/ns/solid/notifications#' },
'@id': urlJoin(this.settings.baseUrl, '.notifications', 'WebSocketChannel2023'),
'notify:channelType': 'notify:WebSocketChannel2023',
'notify:feature': ['notify:endAt', 'notify:rate', 'notify:startAt', 'notify:state']
};
}
},
methods: {
onChannelDeleted(channel) {
// Close open connections (is removed from array on close event).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
name: 'solid-notifications.provider',
settings: {
baseUrl: null,
settingsDataset: null,
queueServiceUrl: null,
channels: {
webhook: true,
Expand All @@ -15,24 +16,25 @@ module.exports = {
},
dependencies: ['api', 'ontologies'],
async created() {
const { baseUrl, queueServiceUrl, channels } = this.settings;
const { baseUrl, settingsDataset, queueServiceUrl, channels } = this.settings;
if (!baseUrl) throw new Error(`The baseUrl setting is required`);
if (!settingsDataset) throw new Error(`The settingsDataset setting is required`);
if (channels.webhook && !queueServiceUrl)
throw new Error(`The queueServiceUrl setting is required if webhooks are activated`);

if (channels.webhook) {
this.broker.createService({
name: 'solid-notifications.provider.webhook',
mixins: [WebhookChannelService, QueueMixin(queueServiceUrl)],
settings: { baseUrl }
settings: { baseUrl, settingsDataset }
});
}

if (channels.websocket) {
this.broker.createService({
name: 'solid-notifications.provider.websocket',
mixins: [WebSocketChannelService],
settings: { baseUrl }
settings: { baseUrl, settingsDataset }
});
}
},
Expand Down

0 comments on commit f3be8b3

Please sign in to comment.