Skip to content

Commit

Permalink
feat: PREP Notifications
Browse files Browse the repository at this point in the history
Implements Per Resource Events notifications in Node Solid Server.

Implementation Notes:
+ Uses `--experimental-require-module` to load esm packages natively. Requires node > 22.0.0. Start scripts and test invocations have been appropriately modified.
+ NSS converts strings into an older streaming format which was not being detected by Express-PREP. Express-PREP was modified to ask the user if the body provided is a stream, thus circumventing this issue.
+ Notifications are triggered from a common middleware, which is invoked after the response has been succesfully sent.
+ Uses Express PREP supplied default template for notifications.
  • Loading branch information
CxRes committed Aug 28, 2024
1 parent f5b2b5c commit 91a074d
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 31 deletions.
2 changes: 1 addition & 1 deletion bin/solid
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --experimental-require-module
const startCli = require('./lib/cli')
startCli()
2 changes: 1 addition & 1 deletion bin/solid.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --experimental-require-module
const startCli = require('./lib/cli')
startCli()
8 changes: 8 additions & 0 deletions lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const ResourceMapper = require('./resource-mapper')
const aclCheck = require('@solid/acl-check')
const { version } = require('../package.json')

const acceptEvents = require('express-accept-events').default
const events = require('express-events-negotiate').default
const eventID = require('express-prep/event-id').default
const prep = require('express-prep').default

const corsSettings = cors({
methods: [
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
Expand Down Expand Up @@ -61,6 +66,9 @@ function createApp (argv = {}) {

const app = express()

// Add PREP support
app.use(acceptEvents, events, eventID, prep)

initAppLocals(app, argv, ldp)
initHeaders(app)
initViews(app, configPath)
Expand Down
45 changes: 39 additions & 6 deletions lib/handlers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const translate = require('../utils.js').translate
const error = require('../http-error')

const RDFs = require('../ldp').mimeTypesAsArray()
const isRdf = require('../ldp').mimeTypeIsRdf

async function handler (req, res, next) {
const ldp = req.app.locals.ldp
Expand Down Expand Up @@ -110,15 +111,39 @@ async function handler (req, res, next) {
}

// If request accepts the content-type we found
// if (stream && negotiator.mediaType([contentType])) {
// res.setHeader('Content-Type', contentType)
// if (contentRange) {
// const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize }
// res.writeHead(206, headers)
// return stream.pipe(res)
// } else {
// return stream.pipe(res)
// }
// }

if (stream && negotiator.mediaType([contentType])) {
res.setHeader('Content-Type', contentType)
let headers = {
'Content-Type': contentType
}
if (contentRange) {
const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize }
res.writeHead(206, headers)
return stream.pipe(res)
} else {
return stream.pipe(res)
headers = {
...headers,
'Content-Range': contentRange,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize
}
res.statusCode = 206
}

if (isRdf(contentType) && !res.sendEvents({
config: { prep: '' },
body: stream,
isBodyStream: true,
headers
})) return
res.set(headers)
return stream.pipe(res)
}

// If it is not in our RDFs we can't even translate,
Expand All @@ -130,6 +155,14 @@ async function handler (req, res, next) {
// Translate from the contentType found to the possibleRDFType desired
const data = await translate(stream, baseUri, contentType, possibleRDFType)
debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType)
const headers = {
'Content-Type': possibleRDFType
}
if (isRdf(contentType) && !res.sendEvents({
config: { prep: '' },
body: data,
headers
})) return
res.setHeader('Content-Type', possibleRDFType)
res.send(data)
return next()
Expand Down
12 changes: 12 additions & 0 deletions lib/handlers/notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = handler

function handler (req, res, next) {
res.events.prep.trigger({
generateNotifications () {
return res.events.prep.defaultNotification({
...(res.method === 'POST') && { location: res.getHeader('Content-Location') }
})
}
})
next()
}
5 changes: 3 additions & 2 deletions lib/ldp-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const del = require('./handlers/delete')
const patch = require('./handlers/patch')
const index = require('./handlers/index')
const copy = require('./handlers/copy')
const notify = require('./handlers/notify')

function LdpMiddleware (corsSettings) {
const router = express.Router('/')
Expand All @@ -24,9 +25,9 @@ function LdpMiddleware (corsSettings) {
router.copy('/*', allow('Write'), copy)
router.get('/*', index, allow('Read'), header.addPermissions, get)
router.post('/*', allow('Append'), post)
router.patch('/*', allow('Append'), patch)
router.patch('/*', allow('Append'), patch, notify)
router.put('/*', allow('Append'), put)
router.delete('/*', allow('Write'), del)
router.delete('/*', allow('Write'), del, notify)

return router
}
Loading

0 comments on commit 91a074d

Please sign in to comment.