From deef1a8bfc319485a610365b85e0831e1e19a47a Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Mon, 4 Mar 2024 16:49:18 +0100 Subject: [PATCH] entity-renderer: add support for redirects --- .changeset/spicy-needles-smash.md | 5 ++ packages/core/lib/sparql.js | 31 ++++++- packages/entity-renderer/README.md | 19 ++++ .../examples/config/trifid.yaml | 2 +- packages/entity-renderer/examples/instance.js | 3 + packages/entity-renderer/index.js | 88 ++++++++++++++++++- 6 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 .changeset/spicy-needles-smash.md diff --git a/.changeset/spicy-needles-smash.md b/.changeset/spicy-needles-smash.md new file mode 100644 index 00000000..142670f8 --- /dev/null +++ b/.changeset/spicy-needles-smash.md @@ -0,0 +1,5 @@ +--- +"@zazuko/trifid-entity-renderer": minor +--- + +Add support for redirects. diff --git a/packages/core/lib/sparql.js b/packages/core/lib/sparql.js index b117a791..efe465f3 100644 --- a/packages/core/lib/sparql.js +++ b/packages/core/lib/sparql.js @@ -207,20 +207,21 @@ export const getRewriteConfiguration = (value, datasetBaseUrl) => { /** * @typedef {Object} QueryOptions * @property {boolean} [ask] Is it a ASK query? + * @property {boolean} [select] Is it a SELECT query? * @property {Array} [rewriteResponse] Replace strings in the response. */ /** * @typedef {Object} SPARQLClient * @property {{parsing: ParsingClient, simple: SimpleClient}} clients Supported clients. - * @property {(query: string, options?: QueryOptions) => Promise} query Query function. + * @property {(query: string, options?: QueryOptions) => Promise | boolean>} query Query function. */ /** * Generate a SPARQL client. * * @param {string} sparqlEndpoint The SPARQL endpoint URL. - * @param {Object} options Options. + * @param {QueryOptions} options Options. * @returns {SPARQLClient} The SPARQL client. */ export const generateClient = (sparqlEndpoint, options) => { @@ -233,16 +234,40 @@ export const generateClient = (sparqlEndpoint, options) => { * * @param {string} query The SPARQL query to use. * @param {QueryOptions?} [options] Query options. - * @returns {Promise} The quad stream or boolean for ASK queries. + * @returns {Promise | boolean>} The quad stream or boolean for ASK queries. */ const query = async (query, options = {}) => { const isAsk = options && options.ask + const isSelect = options && options.select const rewriteResponse = (options && options.rewriteResponse) || [] if (isAsk) { return await clients.parsing.query.ask(query) } + if (isSelect) { + const selectResults = await clients.parsing.query.select(query) + const replacedSelectResults = selectResults.map((row) => { + for (const key in row) { + if (!Object.prototype.hasOwnProperty.call(row, key) || !row[key].value) { + continue + } + + let value = row[key].value + if (typeof value !== 'string') { + continue + } + + for (const replacement of rewriteResponse) { + value = value.replace(replacement.find, replacement.replace) + } + row[key].value = value + } + return row + }) + return replacedSelectResults + } + const result = await clients.simple.query.construct(query) const contentType = result.headers.get('Content-Type') || 'application/n-triples' const body = result.body diff --git a/packages/entity-renderer/README.md b/packages/entity-renderer/README.md index 8578f8db..c08fce75 100644 --- a/packages/entity-renderer/README.md +++ b/packages/entity-renderer/README.md @@ -61,6 +61,25 @@ You can use the following configuration option `rewrite` and set it to one of th - `true`: rewrite the result of the SPARQL queries by replacing the `datasetBaseUrl` value with the current domain. - `false`: this will disable the rewriting mechanism. This is useful if your triples are already matching the domain name where your Trifid instance is deployed. +## Follow redirects + +Using SPARQL it is possible to define some redirects. +This plugin can follow those redirects and render the final resource, if the `followRedirects` configuration option is set to `true`. + +The default value is `false`. + +```yaml +middlewares: + # […] + entity-renderer: + module: "@zazuko/trifid-entity-renderer" + config: + followRedirects: true + redirectQuery: "…" # Select query used to get the redirect target ; needs to return a row with `?responseCode` and `?location` bindings. +``` + +The default redirect query supports `http://www.w3.org/2011/http#` and `http://www.w3.org/2006/http#` prefixes. + ## Other configuration options - `resourceExistsQuery`: The `ASK` query to check whether the resources exists or not diff --git a/packages/entity-renderer/examples/config/trifid.yaml b/packages/entity-renderer/examples/config/trifid.yaml index 2d1587f4..ac34d0c3 100644 --- a/packages/entity-renderer/examples/config/trifid.yaml +++ b/packages/entity-renderer/examples/config/trifid.yaml @@ -8,7 +8,7 @@ globals: url: /query middlewares: - entity-host-web-page: + welcome-page: module: trifid-core/middlewares/view.js paths: / config: diff --git a/packages/entity-renderer/examples/instance.js b/packages/entity-renderer/examples/instance.js index d2a14b2d..82ca3455 100644 --- a/packages/entity-renderer/examples/instance.js +++ b/packages/entity-renderer/examples/instance.js @@ -18,6 +18,9 @@ export const createTrifidInstance = async (configFilePath, logLevel = 'debug') = }, { entityRenderer: { module: entityRendererTrifidPlugin, + config: { + followRedirects: true, + }, }, }) } diff --git a/packages/entity-renderer/index.js b/packages/entity-renderer/index.js index 3588face..fda58240 100644 --- a/packages/entity-renderer/index.js +++ b/packages/entity-renderer/index.js @@ -34,6 +34,15 @@ const replaceIriInQuery = (query, iri) => { return query.split('{{iri}}').join(iri) } +const streamToString = (stream) => { + const chunks = [] + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + stream.on('error', (err) => reject(err)) + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + }) +} + const defaultConfiguration = { resourceNoSlash: true, resourceExistsQuery: 'ASK { <{{iri}}> ?p ?o }', @@ -41,6 +50,63 @@ const defaultConfiguration = { containerExistsQuery: 'ASK { ?s a ?o. FILTER REGEX(STR(?s), "^{{iri}}") }', containerGraphQuery: 'CONSTRUCT { ?s a ?o. } WHERE { ?s a ?o. FILTER REGEX(STR(?s), "^{{iri}}") }', + redirectQuery: ` + PREFIX http2011: + PREFIX http2006: + PREFIX rdf: + + SELECT ?req ?res ?location ?responseCode ?validFrom + WHERE { + GRAPH ?g { + + # Handle 2011 version + { + ?req2011 rdf:type http2011:GetRequest. + ?req2011 http2011:requestURI <{{iri}}>. + ?req2011 http2011:response ?res2011. + + ?res2011 rdf:type http2011:Response. + ?res2011 http2011:location ?location2011. + ?res2011 http2011:responseCode ?responseCode2011. + + OPTIONAL { + ?res2011 ?validFrom2011. + } + } + + UNION + + # Handle 2006 version + { + ?req2006 rdf:type http2006:GetRequest. + ?req2006 http2006:requestURI <{{iri}}>. + ?req2006 http2006:response ?res2006. + + ?res2006 rdf:type http2006:Response. + ?res2006 http2006:location ?location2006. + ?res2006 http2006:responseCode ?responseCode2006. + + OPTIONAL { + ?res2006 ?validFrom2006. + } + } + + # Combine results, using priority for 2011 version over 2006 version + BIND(COALESCE(?req2011, ?req2006) AS ?req) + BIND(COALESCE(?res2011, ?res2006) AS ?res) + BIND(COALESCE(?location2011, ?location2006) AS ?location) + BIND(COALESCE(?validFrom2011, ?validFrom2006) AS ?validFrom) + # Just get the response code as a string instead of the full IRI + BIND(STRAFTER(STR(COALESCE(?responseCode2011, ?responseCode2006)), "#") AS ?responseCode) + } + } + LIMIT 1 + `, + followRedirects: false, +} + +const fixContentTypeHeader = (contentType) => { + return contentType.split(';')[0].trim().toLocaleLowerCase() } const factory = async (trifid) => { @@ -114,6 +180,25 @@ const factory = async (trifid) => { } try { + // Check if there is a redirect for the IRI + if (mergedConfig.followRedirects) { + const redirect = await query(replaceIriInQuery(mergedConfig.redirectQuery, iri), { + ask: false, + select: true, // Force the parsing of the response + rewriteResponse, + }) + if (redirect.length > 0) { + const entityRedirect = redirect[0] + const { responseCode, location } = entityRedirect + if (responseCode && location && responseCode.value && location.value) { + logger.debug(`Redirecting <${iri}> to <${location.value}> (HTTP ${responseCode.value})`) + return reply.status(parseInt(responseCode.value, 10)).redirect(location.value) + } else { + logger.warn('Redirect query did not return the expected results') + } + } + } + // Get the entity from the dataset const describeQuery = isContainer ? mergedConfig.containerGraphQuery : mergedConfig.resourceGraphQuery const entity = await query(replaceIriInQuery(describeQuery, iri), { @@ -127,8 +212,7 @@ const factory = async (trifid) => { } // Make sure the Content-Type is lower case and without parameters (e.g. charset) - const fixedContentType = entityContentType.split(';')[0].trim().toLocaleLowerCase() - + const fixedContentType = fixContentTypeHeader(entityContentType) const quadStream = parsers.import(fixedContentType, entityStream) if (sparqlSupportedTypes.includes(acceptHeader)) {