From c4e36e1ca2e6e0683c414887c49d642721150fbd Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Tue, 16 Jan 2024 18:11:16 +0100 Subject: [PATCH] entity-renderer: remove the use of hijackresponse --- .changeset/pretty-days-whisper.md | 5 + package-lock.json | 18 ++-- .../examples/config/trifid.yaml | 14 +-- packages/entity-renderer/index.js | 91 ++++++++++--------- packages/entity-renderer/package.json | 2 +- 5 files changed, 68 insertions(+), 62 deletions(-) create mode 100644 .changeset/pretty-days-whisper.md diff --git a/.changeset/pretty-days-whisper.md b/.changeset/pretty-days-whisper.md new file mode 100644 index 00000000..5fa11d1a --- /dev/null +++ b/.changeset/pretty-days-whisper.md @@ -0,0 +1,5 @@ +--- +"@zazuko/trifid-entity-renderer": minor +--- + +Remove the use of hijackresponse diff --git a/package-lock.json b/package-lock.json index ac8c6915..b7134016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17093,9 +17093,9 @@ } }, "node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -20721,8 +20721,8 @@ "@zazuko/env": "^1.10.1", "@zazuko/prefixes": "^2.1.0", "@zazuko/rdf-entity-webcomponent": "^0.7.7", + "absolute-url": "^2.0.0", "express": "^4.18.2", - "hijackresponse": "^5.0.0", "lit": "^3.0.2", "p-queue": "^8.0.1", "sparql-http-client": "^2.4.2", @@ -20736,12 +20736,10 @@ "trifid-plugin-yasgui": "^2.2.5" } }, - "packages/entity-renderer/node_modules/hijackresponse": { - "version": "5.0.0", - "license": "ISC", - "engines": { - "node": ">=8.0.0" - } + "packages/entity-renderer/node_modules/absolute-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/absolute-url/-/absolute-url-2.0.0.tgz", + "integrity": "sha512-igt5tP2e5gKgoHNHualmvIcPfUY+sVqfihs/eKGB8mfgNSX+FtAC1Zi19LVd8cmcpmfGIVXxAGCSG0YxnWeUIg==" }, "packages/graph-explorer": { "name": "trifid-plugin-graph-explorer", diff --git a/packages/entity-renderer/examples/config/trifid.yaml b/packages/entity-renderer/examples/config/trifid.yaml index 1b54e7da..6b8c75fc 100644 --- a/packages/entity-renderer/examples/config/trifid.yaml +++ b/packages/entity-renderer/examples/config/trifid.yaml @@ -23,10 +23,10 @@ middlewares: graphName: http://example.com/graph unionDefaultGraph: true - sparql-handler: - module: trifid-handler-sparql - config: - resourceExistsQuery: "ASK { <${iri}> ?p ?o }" - resourceGraphQuery: "DESCRIBE <${iri}>" - containerExistsQuery: "ASK { <${iri}> ?p ?o }" - containerGraphQuery: "DESCRIBE <${iri}>" + # sparql-handler: + # module: trifid-handler-sparql + # config: + # resourceExistsQuery: "ASK { <${iri}> ?p ?o }" + # resourceGraphQuery: "DESCRIBE <${iri}>" + # containerExistsQuery: "ASK { <${iri}> ?p ?o }" + # containerGraphQuery: "DESCRIBE <${iri}>" diff --git a/packages/entity-renderer/index.js b/packages/entity-renderer/index.js index 380f79a7..bcb1a4f3 100644 --- a/packages/entity-renderer/index.js +++ b/packages/entity-renderer/index.js @@ -1,7 +1,10 @@ +/* eslint-disable no-template-curly-in-string */ import { dirname } from 'path' import { fileURLToPath } from 'url' import { parsers } from '@rdfjs/formats-common' -import hijackResponse from 'hijackresponse' +import absoluteUrl from 'absolute-url' +import ParsingClient from 'sparql-http-client/ParsingClient.js' +import SimpleClient from 'sparql-http-client/SimpleClient.js' import rdf from '@zazuko/env' import { createEntityRenderer } from './renderer/entity.js' @@ -28,6 +31,10 @@ const getAcceptHeader = (req) => { return req.headers.accept } +const replaceIriInQuery = (query, iri) => { + return query.split('{{iri}}').join(iri) +} + const factory = async (trifid) => { const { render, logger, config } = trifid const entityRenderer = createEntityRenderer({ options: config, logger }) @@ -48,70 +55,66 @@ const factory = async (trifid) => { return next() } - // update "Accept" HTTP header depending on the requested type - req.headers.accept = getAcceptHeader(req) - - // only take care of the rendering if HTML is requested - const accepts = req.accepts(['text/plain', 'json', 'html']) - if (accepts !== 'html') { + // @TODO: make sure the results is from the specified type + // eslint-disable-next-line no-unused-vars + const acceptHeader = getAcceptHeader(req) + + // Generate the IRI we expect + const iriUrl = new URL(encodeURI(absoluteUrl(req))) + iriUrl.search = '' + iriUrl.searchParams.forEach((_value, key) => iriUrl.searchParams.delete(key)) + const iri = iriUrl.toString() + logger.debug(`IRI value: ${iri}`) + + // @TODO: allow the user to configure the endpoint URL + const endpointUrl = new URL('/query', absoluteUrl(req)) + const endpointUrlAsString = endpointUrl.toString() + + const sparqlClientAsk = new ParsingClient({ endpointUrl: endpointUrlAsString }) + const sparqlClient = new SimpleClient({ endpointUrl: endpointUrlAsString }) + + // Check if the IRI exists in the dataset + // @TODO: allow the user to configure the query + const askQuery = 'ASK { <{{iri}}> ?p ?o }' + const exists = await sparqlClientAsk.query.ask(replaceIriInQuery(askQuery, iri)) + if (!exists) { return next() } - req.headers.accept = 'application/n-quads' - - const { readable, writable } = await hijackResponse(res, next) - - const contentType = res.getHeader('Content-Type') - if (!contentType) { - return readable.pipe(writable) - } + try { + // Get the entity from the dataset + // @TODO: allow the user to configure the query + const describeQuery = 'DESCRIBE <{{iri}}>' + const entity = await sparqlClient.query.construct(replaceIriInQuery(describeQuery, iri)) + const entityContentType = entity.headers.get('Content-Type') || 'application/n-triples' + const entityStream = entity.body - const mimeType = contentType.toLowerCase().split(';')[0].trim() - const hijackFormats = [ - 'application/ld+json', - 'application/trig', - 'application/n-quads', - 'application/n-triples', - 'text/n3', - 'text/turtle', - 'application/rdf+xml', - ] - - if (!hijackFormats.includes(mimeType)) { - return readable.pipe(writable) - } + // Make sure the Content-Type is lower case and without parameters (e.g. charset) + const fixedContentType = entityContentType.split(';')[0].trim().toLocaleLowerCase() - const quadStream = parsers.import(mimeType, readable) - const dataset = await rdf.dataset().import(quadStream) + const quadStream = parsers.import(fixedContentType, entityStream) + const dataset = await rdf.dataset().import(quadStream) - let contentToForward - try { const { entityHtml, entityLabel, entityUrl } = await entityRenderer( req, res, { dataset }, ) - const metadata = await metadataProvider(req, { dataset }) - contentToForward = await render(entityTemplatePath, { + + res.setHeader('Content-Type', 'text/html') + res.send(await render(entityTemplatePath, { dataset: entityHtml, locals: res.locals, entityLabel, entityUrl, metadata, config, - }) - res.setHeader('Content-Type', 'text/html') - - // Without this, the browser will try to download the HTML file if the `Content-Disposition` header is set by the SPARQL endpoint - res.removeHeader('Content-Disposition') + })) } catch (e) { logger.error(e) - return readable.pipe(writable) + return next() } - - writable.write(contentToForward) - writable.end() } } diff --git a/packages/entity-renderer/package.json b/packages/entity-renderer/package.json index 83e933e4..6f99eef6 100644 --- a/packages/entity-renderer/package.json +++ b/packages/entity-renderer/package.json @@ -26,8 +26,8 @@ "@zazuko/env": "^1.10.1", "@zazuko/prefixes": "^2.1.0", "@zazuko/rdf-entity-webcomponent": "^0.7.7", + "absolute-url": "^2.0.0", "express": "^4.18.2", - "hijackresponse": "^5.0.0", "lit": "^3.0.2", "p-queue": "^8.0.1", "sparql-http-client": "^2.4.2",