diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7df9e137..2c89132d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - node-version: [ '>=20.17.0' ] + node-version: [ '^20.17.0' ] os: [ubuntu-latest] steps: diff --git a/lib/handlers/delete.js b/lib/handlers/delete.js index 438bfe65..77eb7f05 100644 --- a/lib/handlers/delete.js +++ b/lib/handlers/delete.js @@ -6,12 +6,9 @@ async function handler (req, res, next) { debug('DELETE -- Request on' + req.originalUrl) const ldp = req.app.locals.ldp - const prep = req.app.locals.prep try { await ldp.delete(req) debug('DELETE -- Ok.') - // Add event-id for notifications - prep && res.setHeader('Event-ID', res.setEventID()) res.sendStatus(200) next() } catch (err) { diff --git a/lib/handlers/notify.js b/lib/handlers/notify.js index 42090cb3..2883daf0 100644 --- a/lib/handlers/notify.js +++ b/lib/handlers/notify.js @@ -38,18 +38,32 @@ function getParentActivity (method, status) { return 'Update' } +function filterMillseconds (isoDate) { + return `${isoDate.substring(0, 19)}${isoDate.substring(23)}` +} + +function getDate (date) { + if (date) { + const eventDate = new Date(date) + if (!isNaN(eventDate.valueOf())) { + return filterMillseconds(eventDate.toISOString()) + } + } + const now = new Date() + return filterMillseconds(now.toISOString()) +} + function handler (req, res, next) { const { trigger, defaultNotification } = res.events.prep const { method, path } = req const { statusCode } = res - const eventID = res.getHeader('event-id') + const eventID = res.setEventID() const fullUrl = new URL(path, `${req.protocol}://${req.hostname}/`) // Date is a hack since node does not seem to provide access to send date. // Date needs to be shared with parent notification - const eventDate = res._header.match(/^Date: (.*?)$/m)?.[1] || - new Date().toUTCString() + const eventDate = getDate(res._header.match(/^Date: (.*?)$/m)?.[1]) // If the resource itself newly created, // it could not have been subscribed for notifications already @@ -61,18 +75,19 @@ function handler (req, res, next) { ) { const mediaType = negotiatedFields['content-type'] const activity = getActivity(method, path) - const target = activity === 'Add' + const object = activity === 'Add' ? res.getHeader('location') + : String(fullUrl) + const target = activity === 'Add' + ? String(fullUrl) : undefined if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) { return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({ activity, eventID, - object: String(fullUrl), + object, target, date: eventDate, - // We use eTag as a proxy for state for now - state: res.getHeader('ETag'), mediaType })}` } else { @@ -93,7 +108,10 @@ function handler (req, res, next) { // POST in Solid creates a child resource const parent = getParent(path) if (parent && method !== 'POST') { - const parentID = res.setEventID(parent) + res.setEventID({ + path: parent, + id: eventID + }) const parentUrl = new URL(parent, fullUrl) try { trigger({ @@ -103,15 +121,15 @@ function handler (req, res, next) { ) { const mediaType = negotiatedFields['content-type'] const activity = getParentActivity(method, statusCode) - const target = activity !== 'Update' ? String(fullUrl) : undefined + const object = activity === 'Update' ? String(parentUrl) : String(fullUrl) + const target = activity === 'Update' ? undefined : String(parentUrl) if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) { return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({ activity, - eventID: parentID, + eventID, date: eventDate, - object: String(parentUrl), + object, target, - eTag: undefined, mediaType })}` } diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index d9469e75..53f75ec9 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -39,7 +39,6 @@ function contentForNew (contentType) { // Handles a PATCH request async function patchHandler (req, res, next) { debug(`PATCH -- ${req.originalUrl}`) - const prep = req.app.locals.prep try { // Obtain details of the target resource const ldp = req.app.locals.ldp @@ -91,9 +90,6 @@ async function patchHandler (req, res, next) { await applyPatch(patchObject, graph, url) return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri) }) - - // Add event-id for notifications - prep && res.setHeader('Event-ID', res.setEventID()) // Send the status and result to the client res.status(resourceExists ? 200 : 201) res.send(result) diff --git a/lib/handlers/post.js b/lib/handlers/post.js index dd16700d..5942519f 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -11,7 +11,6 @@ const getContentType = require('../utils').getContentType async function handler (req, res, next) { const ldp = req.app.locals.ldp - const prep = req.app.locals.prep const contentType = getContentType(req.headers) debug('content-type is ', contentType) // Handle SPARQL(-update?) query @@ -73,8 +72,6 @@ async function handler (req, res, next) { // Handled by backpressure of streams! busboy.on('finish', function () { debug('Done storing files') - // Add event-id for notifications - prep && res.setHeader('Event-ID', res.setEventID()) res.sendStatus(200) next() }) @@ -94,8 +91,6 @@ async function handler (req, res, next) { debug('File stored in ' + resourcePath) header.addLinks(res, links) res.set('Location', resourcePath) - // Add event-id for notifications - prep && res.setHeader('Event-ID', res.setEventID()) res.sendStatus(201) next() }, diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 5c2e1f67..ba698ff9 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -59,7 +59,6 @@ async function checkPermission (request, resourceExists) { // TODO could be renamed as putResource (it now covers container and non-container) async function putStream (req, res, next, stream = req) { const ldp = req.app.locals.ldp - const prep = req.app.locals.prep // try { // Obtain details of the target resource let resourceExists = true @@ -78,8 +77,6 @@ async function putStream (req, res, next, stream = req) { // Fails with Append on existing resource if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists) await ldp.put(req, stream, getContentType(req.headers)) - // Add event-id for notifications - prep && res.setHeader('Event-ID', res.setEventID()) res.sendStatus(resourceExists ? 204 : 201) return next() } catch (err) { diff --git a/lib/rdf-notification-template.js b/lib/rdf-notification-template.js index 8eeee2c7..77fb4332 100644 --- a/lib/rdf-notification-template.js +++ b/lib/rdf-notification-template.js @@ -1,10 +1,12 @@ +const uuid = require('uuid') + const CONTEXT_ACTIVITYSTREAMS = 'https://www.w3.org/ns/activitystreams' const CONTEXT_NOTIFICATION = 'https://www.w3.org/ns/solid/notification/v1' const CONTEXT_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema' function generateJSONNotification ({ activity: type, - eventId: id, + eventID, date: published, object, target, @@ -13,30 +15,40 @@ function generateJSONNotification ({ return { published, type, - id, + id: `urn:uuid:${uuid.v4()}`, + ...(eventID) && { state: eventID }, object, ...(type === 'Add') && { target }, - ...(type === 'Remove') && { origin: target }, - ...(state) && { state } + ...(type === 'Remove') && { origin: target } } } function generateTurtleNotification ({ activity, - eventId, + eventID, date, object, - target, - state = undefined + target }) { - const stateLine = `\n notify:state "${state}" ;` + let targetLine = '' + let stateLine = '' + + if (activity === 'Add') { + targetLine = `\n as:target <${target}> ;` + } + if (activity === 'Remove') { + targetLine = `\n as:origin <${target}> ;` + } + if (eventID) { + stateLine = `\n notify:state "${eventID}" ;` + } return `@prefix as: <${CONTEXT_ACTIVITYSTREAMS}#> . @prefix notify: <${CONTEXT_NOTIFICATION}#> . @prefix xsd: <${CONTEXT_XML_SCHEMA}#> . -<${eventId}> a as:${activity} ;${state && stateLine} - as:object ${object} ; + a as:${activity} ; + as:object <${object}> ;${targetLine}${stateLine} as:published "${date}"^^xsd:dateTime .`.replaceAll('\n', '\r\n') } diff --git a/package-lock.json b/package-lock.json index 67eb63e3..e3f8f37a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "express-accept-events": "^0.3.0", "express-handlebars": "^5.3.5", "express-negotiate-events": "^0.3.0", - "express-prep": "^0.6.3", + "express-prep": "^0.6.4", "express-session": "^1.18.0", "extend": "^3.0.2", "from2": "^2.3.0", @@ -4549,6 +4549,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -4566,6 +4567,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -4578,6 +4580,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -4590,12 +4593,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "devOptional": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "devOptional": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -4613,6 +4618,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -4628,6 +4634,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -5180,6 +5187,8 @@ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -5230,6 +5239,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6720,6 +6730,8 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -7951,6 +7963,8 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -7974,6 +7988,8 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -7983,6 +7999,8 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -8002,13 +8020,17 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/cacache/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8305,6 +8327,8 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -8381,6 +8405,8 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -11024,9 +11050,9 @@ } }, "node_modules/express-prep": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/express-prep/-/express-prep-0.6.3.tgz", - "integrity": "sha512-RIdkQHCi9VqE/vC1AHTMV1aggd408WLlG0z9uPqv5Tj+AzZuHj0ycEgGTgFnvafYzJCbhs208+wV2MrbFlCajQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/express-prep/-/express-prep-0.6.4.tgz", + "integrity": "sha512-ZLN4AngjZ5ANxPvgFzrCso/j1VuwfBp3PtaXywgq2U+rhb82xg9E49083pKM6US585isCq74sGZ9Nu3dzQYDGw==", "license": "MPL-2.0", "dependencies": { "crypto-random-string": "^5.0.0", @@ -11536,6 +11562,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -11659,6 +11686,8 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^7.0.3" }, @@ -12345,6 +12374,8 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -12357,6 +12388,8 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12368,7 +12401,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/html-escaper": { "version": "2.0.2", @@ -12600,6 +12635,8 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -13500,6 +13537,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -16414,6 +16452,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -16424,6 +16463,8 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^7.0.3" }, @@ -16436,6 +16477,8 @@ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -16448,6 +16491,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16459,13 +16504,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -16478,6 +16527,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16489,13 +16540,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -16509,6 +16564,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16520,13 +16577,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -17343,6 +17404,8 @@ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "hosted-git-info": "^3.0.2", "osenv": "^0.1.5", @@ -17355,6 +17418,8 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", + "optional": true, + "peer": true, "bin": { "semver": "bin/semver" } @@ -19887,6 +19952,8 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -19915,6 +19982,8 @@ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "deprecated": "This package is no longer supported.", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -19981,6 +20050,8 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -20004,6 +20075,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "devOptional": true, "license": "BlueOak-1.0.0" }, "node_modules/pane-registry": { @@ -20150,6 +20222,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -20166,6 +20239,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "devOptional": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -20986,6 +21060,8 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "optional": true, + "peer": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" } @@ -22483,6 +22559,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -23418,6 +23495,8 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^7.0.3" }, @@ -23769,6 +23848,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -23783,6 +23863,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -23899,6 +23980,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -24212,6 +24294,8 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -24229,6 +24313,8 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -24241,6 +24327,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -24253,6 +24341,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -24262,6 +24352,8 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", + "optional": true, + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -24273,7 +24365,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/temp": { "version": "0.8.4", @@ -25076,6 +25170,8 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "unique-slug": "^4.0.0" }, @@ -25088,6 +25184,8 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "imurmurhash": "^0.1.4" }, @@ -25278,6 +25376,8 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "builtins": "^1.0.3" } @@ -25286,7 +25386,9 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/validator": { "version": "13.12.0", @@ -25645,6 +25747,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -25669,6 +25772,8 @@ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -25679,7 +25784,9 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/ws": { "version": "8.18.0", diff --git a/package.json b/package.json index 34ead272..94aa4a86 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "express-accept-events": "^0.3.0", "express-handlebars": "^5.3.5", "express-negotiate-events": "^0.3.0", - "express-prep": "^0.6.3", + "express-prep": "^0.6.4", "express-session": "^1.18.0", "extend": "^3.0.2", "from2": "^2.3.0", diff --git a/test/integration/prep-test.js b/test/integration/prep-test.js index 7dea73ed..54260c60 100644 --- a/test/integration/prep-test.js +++ b/test/integration/prep-test.js @@ -1,10 +1,13 @@ const fs = require('fs') const path = require('path') +const uuid = require('uuid') const { expect } = require('chai') const { parseDictionary } = require('structured-headers') const prepFetch = require('prep-fetch').default const { createServer } = require('../utils') +const dateTimeRegex = /^-?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|(?:\+|-)\d{2}:\d{2})$/ + const samplePath = path.join(__dirname, '../resources', 'sampleContainer') const sampleFile = fs.readFileSync(path.join(samplePath, 'example1.ttl')) @@ -92,10 +95,13 @@ describe('Per Resource Events Protocol', function () { const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Add') - expect(notification.target).to.match(/sampleContainer\/example-prep\.ttl$/) - expect(notification.object).to.match(/sampleContainer\/$/) + expect(notification.target).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/example-prep\.ttl$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when contained resource is modified', async function () { @@ -111,9 +117,12 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Update') expect(notification.object).to.match(/sampleContainer\/$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when contained resource is deleted', @@ -124,10 +133,13 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Remove') - expect(notification.object).to.match(/sampleContainer\/$/) - expect(notification.origin).to.match(/sampleContainer\/.*example-prep.ttl$/) + expect(notification.origin).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/.*example-prep.ttl$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when a contained container is created', async function () { @@ -140,10 +152,13 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Add') - expect(notification.target).to.match(/sampleContainer\/example-prep\/$/) - expect(notification.object).to.match(/sampleContainer\/$/) + expect(notification.target).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/example-prep\/$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when a contained container is deleted', async function () { @@ -153,10 +168,13 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Remove') - expect(notification.origin).to.match(/sampleContainer\/example-prep\/$/) - expect(notification.object).to.match(/sampleContainer\/$/) + expect(notification.origin).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/example-prep\/$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when a container is created by POST', @@ -172,10 +190,13 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Add') - expect(notification.object).to.match(/sampleContainer\/$/) - expect(notification.target).to.match(/sampleContainer\/.*example-post\/$/) + expect(notification.target).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/.*example-post\/$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when resource is created by POST', @@ -191,10 +212,13 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Add') - expect(notification.object).to.match(/sampleContainer\/$/) - expect(notification.target).to.match(/sampleContainer\/.*example-prep.ttl$/) + expect(notification.target).to.match(/sampleContainer\/$/) + expect(notification.object).to.match(/sampleContainer\/.*example-prep.ttl$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) controller.abort() }) }) @@ -254,10 +278,12 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') - expect(notification).to.haveOwnProperty('state') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Update') expect(notification.object).to.match(/sampleContainer\/example-prep\.ttl$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) }) it('when removed with DELETE, it should also close the connection', @@ -268,10 +294,12 @@ solid:inserts { . }.` const { value } = await notificationsIterator.next() expect(value.headers.get('content-type')).to.match(/application\/ld\+json/) const notification = await value.json() - expect(notification).to.haveOwnProperty('published') - expect(notification).to.haveOwnProperty('state') + expect(notification.published).to.match(dateTimeRegex) + expect(isNaN((new Date(notification.published)).valueOf())).to.equal(false) expect(notification.type).to.equal('Delete') expect(notification.object).to.match(/sampleContainer\/example-prep\.ttl$/) + expect(uuid.validate(notification.id.substring(9))).to.equal(true) + expect(notification.state).to.match(/\w{6}/) const { done } = await notificationsIterator.next() expect(done).to.equal(true) })