Skip to content

Commit

Permalink
entity-renderer: add support for redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
ludovicm67 committed Mar 4, 2024
1 parent 4e6b0ea commit deef1a8
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/spicy-needles-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zazuko/trifid-entity-renderer": minor
---

Add support for redirects.
31 changes: 28 additions & 3 deletions packages/core/lib/sparql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<RewriteResponseOptions>} [rewriteResponse] Replace strings in the response.
*/

/**
* @typedef {Object} SPARQLClient
* @property {{parsing: ParsingClient, simple: SimpleClient}} clients Supported clients.
* @property {(query: string, options?: QueryOptions) => Promise<QueryResult | boolean>} query Query function.
* @property {(query: string, options?: QueryOptions) => Promise<QueryResult | Array<import('sparql-http-client/ResultParser.js').ResultRow> | 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) => {
Expand All @@ -233,16 +234,40 @@ export const generateClient = (sparqlEndpoint, options) => {
*
* @param {string} query The SPARQL query to use.
* @param {QueryOptions?} [options] Query options.
* @returns {Promise<QueryResult | boolean>} The quad stream or boolean for ASK queries.
* @returns {Promise<QueryResult | Array<import('sparql-http-client/ResultParser.js').ResultRow> | 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
Expand Down
19 changes: 19 additions & 0 deletions packages/entity-renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/entity-renderer/examples/config/trifid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ globals:
url: /query

middlewares:
entity-host-web-page:
welcome-page:
module: trifid-core/middlewares/view.js
paths: /
config:
Expand Down
3 changes: 3 additions & 0 deletions packages/entity-renderer/examples/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const createTrifidInstance = async (configFilePath, logLevel = 'debug') =
}, {
entityRenderer: {
module: entityRendererTrifidPlugin,
config: {
followRedirects: true,
},
},
})
}
88 changes: 86 additions & 2 deletions packages/entity-renderer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,79 @@ 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 }',
resourceGraphQuery: 'DESCRIBE <{{iri}}>',
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: <http://www.w3.org/2011/http#>
PREFIX http2006: <http://www.w3.org/2006/http#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
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 <http://schema.org/validFrom> ?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 <http://schema.org/validFrom> ?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) => {
Expand Down Expand Up @@ -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), {
Expand All @@ -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)) {
Expand Down

0 comments on commit deef1a8

Please sign in to comment.