Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic prep #1792

Merged
merged 23 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
21b15be
feat: PREP Notifications
CxRes Aug 30, 2024
f4fb5a0
feat: Add Solid-PREP Notifications
CxRes Aug 30, 2024
109ce09
Bump CI and Docker to node v22
CxRes Aug 30, 2024
7b23e57
Replace nyc with c8
CxRes Aug 30, 2024
3d93e13
fix: Correct Parenting for Solid-PREP Notifications
CxRes Sep 8, 2024
fd99081
Step Down to Node 20
CxRes Sep 8, 2024
8021633
fix: Relax Content-Type Checks in Integration Tests
CxRes Sep 8, 2024
e0ebc31
fix: Repository for Surface Tests
CxRes Sep 9, 2024
beaaa03
chore: Bump Express PREP
CxRes Oct 7, 2024
bda034a
feat: Add Tests for PREP
CxRes Oct 8, 2024
b0e7eb0
feat: Add Flag to disable PREP
CxRes Oct 10, 2024
6a287e4
refactor: Remove Commented Code in `get.js`
CxRes Oct 13, 2024
a6067e3
chore: Bump PREP Dependencies
CxRes Oct 19, 2024
568b58a
fix: Send Parent Notifications on Root
CxRes Oct 23, 2024
3fa2be9
fix: Activity is `as:Add` on Container POST
CxRes Oct 23, 2024
6f4b22b
fix: Set `target` on Container POST
CxRes Oct 23, 2024
2a277ff
fix: Set `origin` when Contained Resource is Removed
CxRes Oct 23, 2024
d08a88f
test: Notification when Creating a Container with POST
CxRes Oct 23, 2024
33997ae
chore: Bump Express PREP
CxRes Oct 23, 2024
1f6c16f
fix: Do Not Overwrite `req.url` on Container PUT
CxRes Oct 23, 2024
c933a74
test: Notification when Creating/Deleting Container in Container
CxRes Oct 23, 2024
2da77b0
refactor: Add Debugging for Failed Notifications
CxRes Oct 23, 2024
44dc0bc
fix: Correctly Set Location on non-RDF Notifications
CxRes Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,30 @@ jobs:

strategy:
matrix:
node-version: [18.x]
node-version: [ '>=20.17.0' ]
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

# extract repository name
- if: github.event_name == 'pull_request'
run: echo "REPO_NAME=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV

- if: github.event_name != 'pull_request'
run: echo "REPO_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV

# extract branch name
- if: github.event_name == 'pull_request'
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV

- if: github.event_name != 'pull_request'
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV

# print repository name
- name: Get repository name
run: echo 'The repository name is' $REPO_NAME

# print branch name
- name: Get branch name
run: echo 'The branch name is' $BRANCH_NAME
Expand All @@ -40,12 +53,12 @@ jobs:
# test code
- run: npm run standard
- run: npm run validate
- run: npm run nyc
- run: npm run c8
# Test global install of the package
- run: npm pack .
- run: npm install -g solid-server-*.tgz
# Run the Solid test-suite
- run: bash test/surface/run-solid-test-suite.sh $BRANCH_NAME
- run: bash test/surface/run-solid-test-suite.sh $BRANCH_NAME $REPO_NAME

# TODO: The pipeline should automate publication to npm, so that the docker build gets the correct version
# This job will only dockerize solid-server@latest / solid-server@<tag-name> from npmjs.com!
Expand All @@ -56,7 +69,7 @@ jobs:
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: olegtarasov/[email protected]
id: tagName
Expand Down
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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed because the middlewares for PREP use ESM syntax and NSS is CJS. This magic flag allows importing ESM code in CJS code.

This will also prove generally useful, since it creates a path to migrate NSS to ESM one file at a time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the need to support your ESM middleware, but adding --experimental-require-module project-wide isn’t the best approach. Since it’s still experimental, it could introduce unpredictable behavior in different environments or future Node.js versions.

To ensure long-term stability and compatibility with the existing CJS modules, it’s better to bundle your libraries with a transpiler. This will let you use ESM without requiring the entire project to rely on an experimental flag

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()
2 changes: 1 addition & 1 deletion docker-image/src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:16-alpine
FROM node:20-alpine

# hadolint ignore=DL3018
RUN apk add --no-cache openssl
Expand Down
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-negotiate-events').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
2 changes: 2 additions & 0 deletions lib/handlers/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ async function handler (req, res, next) {
try {
await ldp.delete(req)
debug('DELETE -- Ok.')
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(200)
next()
} catch (err) {
Expand Down
49 changes: 42 additions & 7 deletions lib/handlers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const translate = require('../utils.js').translate
const error = require('../http-error')

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

const prepConfig = 'accept=("message/rfc822" "application/ld+json" "text/turtle")'

async function handler (req, res, next) {
const ldp = req.app.locals.ldp
Expand Down Expand Up @@ -103,22 +106,46 @@ async function handler (req, res, next) {
debug(' sending data browser file: ' + dataBrowserPath)
res.sendFile(dataBrowserPath)
return
} else if (stream) {
} else if (stream) { // EXIT text/html
res.setHeader('Content-Type', contentType)
return stream.pipe(res)
}
}

// 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)
// }
// }

CxRes marked this conversation as resolved.
Show resolved Hide resolved
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: prepConfig },
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 +157,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: prepConfig },
body: data,
headers
})) return
res.setHeader('Content-Type', possibleRDFType)
res.send(data)
return next()
Expand Down
115 changes: 115 additions & 0 deletions lib/handlers/notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
module.exports = handler

const libPath = require('path/posix')

const headerTemplate = require('express-prep/templates').header
const solidRDFTemplate = require('../rdf-notification-template')

const ALLOWED_RDF_MIME_TYPES = [
'application/ld+json',
'application/activity+json',
'text/turtle'
]

function getParent (path) {
if (path === '' || path === '/') return
const parent = libPath.dirname(path)
if (parent === '/') return
return `${parent}/`
}

function getActivity (method) {
if (method === 'DELETE') {
return 'Delete'
}
return 'Update'
}

function getParentActivity (method, status) {
if (method === 'DELETE') {
return 'Remove'
}
if (status === 201) {
return 'Add'
}
return 'Update'
}

function handler (req, res, next) {
const { trigger, defaultNotification } = res.events.prep

const { method } = req
const { statusCode } = res
const eventID = res.getHeader('event-id')
const fullUrl = new URL(req.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()

// If the resource itself newly created,
// it could not have been subscribed for notifications already
if (!((method === 'PUT' || method === 'PATCH') && statusCode === 201)) {
try {
trigger({
generateNotification (
negotiatedFields
) {
const mediaType = negotiatedFields['content-type']

if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
activity: getActivity(method),
eventID,
object: String(fullUrl),
date: eventDate,
// We use eTag as a proxy for state for now
state: res.getHeader('ETag'),
mediaType
})}`
} else {
return defaultNotification({
...(res.method === 'POST') && { location: res.getHeader('Content-Location') }
})
}
}
})
} catch (error) {
// Failed notification message
CxRes marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Write a notification to parent container
// POST in Solid creates a child resource
const parent = getParent(req.path)
if (parent && method !== 'POST') {
try {
const parentID = res.setEventID(parent)
const parentUrl = new URL(parent, fullUrl)
trigger({
path: parent,
generateNotification (
negotiatedFields
) {
const mediaType = negotiatedFields['content-type']
if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
activity: getParentActivity(method, statusCode),
eventID: parentID,
date: eventDate,
object: String(parentUrl),
target: statusCode === 201 ? String(fullUrl) : undefined,
eTag: undefined,
mediaType
})}`
}
}
})
} catch (error) {
// Failed notification message
}
}

next()
}
2 changes: 2 additions & 0 deletions lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ async function patchHandler (req, res, next) {
return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
})

// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
// Send the status and result to the client
res.status(resourceExists ? 200 : 201)
res.send(result)
Expand Down
4 changes: 4 additions & 0 deletions lib/handlers/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ async function handler (req, res, next) {
// Handled by backpressure of streams!
busboy.on('finish', function () {
debug('Done storing files')
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(200)
next()
})
Expand All @@ -91,6 +93,8 @@ async function handler (req, res, next) {
debug('File stored in ' + resourcePath)
header.addLinks(res, links)
res.set('Location', resourcePath)
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(201)
next()
},
Expand Down
2 changes: 2 additions & 0 deletions lib/handlers/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ 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
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(resourceExists ? 204 : 201)
return next()
} catch (err) {
Expand Down
9 changes: 5 additions & 4 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 @@ -23,10 +24,10 @@ 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.put('/*', allow('Append'), put)
router.delete('/*', allow('Write'), del)
router.post('/*', allow('Append'), post, notify)
router.patch('/*', allow('Append'), patch, notify)
router.put('/*', allow('Append'), put, notify)
router.delete('/*', allow('Write'), del, notify)

return router
}
Loading
Loading