trifid-core
Version:
Trifid Core
287 lines (286 loc) • 10.5 kB
JavaScript
import replaceStream from 'string-replace-stream';
import formats from '@rdfjs-elements/formats-pretty';
import ParsingClient from 'sparql-http-client/ParsingClient.js';
import SimpleClient from 'sparql-http-client/SimpleClient.js';
/**
* Supported types for the serialization.
*/
export const supportedTypes = [
'application/ld+json',
'application/n-triples',
'application/rdf+xml',
'text/turtle',
'application/trig',
'text/csv',
];
/**
* Convert a JSON-LD object to a CSV string.
* Works for simple JSON-LD objects got from a DESCRIBE query.
*
* @param jsonLD JSON-LD object to convert to CSV
* @returns CSV string
*/
const jsonLDToCSV = (jsonLD) => {
const rows = ['"key","value"'];
/**
* Process a value and add it to the rows array.
*
* @param key Key of the value.
* @param value Value to process.
*/
const processValue = (key, value) => {
const escapedKey = key.replace(/"/g, '""');
if (Array.isArray(value)) {
// For each array item, check if it's an object with '@id', else use the item directly
value.forEach((item) => {
const itemValue = (item && typeof item === 'object' && item['@id'])
? item['@id']
: item;
rows.push(`"${escapedKey}","${String(itemValue).replace(/"/g, '""')}"`);
});
}
else if (value && typeof value === 'object' && value['@id']) {
// Handle object with '@id'
rows.push(`"${escapedKey}","${String(value['@id']).replace(/"/g, '""')}"`);
}
else {
// Handle other values (null/undefined will become empty strings)
rows.push(`"${escapedKey}","${(value ? String(value) : '').replace(/"/g, '""')}"`);
}
};
// Process each entry of the JSON-LD object
for (const key in jsonLD) {
if (Object.prototype.hasOwnProperty.call(jsonLD, key)) {
processValue(key, jsonLD[key]);
}
}
// Add an empty row to make sure the CSV is ending with a blank line
rows.push('');
return rows.join('\n');
};
/**
* Serialize a formatted stream to a string.
*
* @param quadStream The formatted stream to serialize.
* @returns The serialized string.
*/
export const serializeFormattedStream = async (quadStream) => {
if (quadStream === null) {
throw new Error('No quad stream available');
}
let serialized = '';
// @ts-ignore -- the underlying stream is async-iterable at runtime
for await (const chunk of quadStream) {
serialized += chunk;
}
return serialized;
};
/**
* Serialize a quad stream to a string.
*
* @param quadStream The quad stream to serialize.
* @param mimeType The mime type to serialize to.
* @param _options Options (reserved for further use).
* @returns The serialized string.
*/
export const serializeQuadStream = async (quadStream, mimeType, _options = {}) => {
if (quadStream === null) {
throw new Error('No quad stream available');
}
const isCsv = mimeType === 'text/csv';
const serializerMimeType = isCsv ? 'application/ld+json' : mimeType;
const formatted = formats.serializers.import(serializerMimeType, quadStream);
let serialized = await serializeFormattedStream(formatted);
// Pretty print JSON-LD
if (serializerMimeType === 'application/ld+json') {
serialized = JSON.stringify(JSON.parse(serialized), null, 2);
}
if (isCsv) {
serialized = jsonLDToCSV(JSON.parse(serialized));
}
return serialized;
};
/**
* Compute the value for the `rewrite` option.
*
* @param value The value from the configuration (ideally: true, false or auto ; default="auto").
* @param datasetBaseUrl The dataset base URL to use in case rewriting is enabled.
* @returns The computed value of the `rewrite` option.
*/
const getRewriteOptionValue = (value, datasetBaseUrl) => {
const jsonValue = JSON.stringify(String(value).toLocaleLowerCase());
if (jsonValue === '"false"') {
return false;
}
if (jsonValue === '"true"') {
// Check if `datasetBaseUrl` is a valid URL if present
if (datasetBaseUrl) {
try {
new URL(datasetBaseUrl);
}
catch (error) {
throw new Error(`The current value you have for 'datasetBaseUrl' is '${datasetBaseUrl}', which is not a valid URL.`, { cause: error });
}
}
else {
throw new Error('Rewriting is enabled but no datasetBaseUrl is configured.');
}
return true;
}
// Let's assume that we are in "auto" mode.
// Check if `datasetBaseUrl` is a valid URL if present
if (datasetBaseUrl) {
try {
new URL(datasetBaseUrl);
return true;
}
catch (_e) {
// Don't throw in case of an invalid URL
return false;
}
}
return false;
};
/**
* Compute the value for the `rewrite` option and the `datasetBaseUrl`.
*
* @param value The value from the configuration (ideally: true, false or auto ; default="auto").
* @param datasetBaseUrl The dataset base URL to use in case rewriting is enabled.
* @returns The computed value of the `rewrite` option.
*/
export const getRewriteConfiguration = (value, datasetBaseUrl) => {
const iriOrigin = (iri) => {
const parts = new URL(iri);
parts.pathname = '/';
parts.search = '';
parts.username = '';
parts.password = '';
return parts.toString();
};
const rewriteValue = getRewriteOptionValue(value, datasetBaseUrl);
if (!rewriteValue) {
return {
rewrite: false,
datasetBaseUrl: null,
replaceIri: (iri) => iri,
iriOrigin,
};
}
if (datasetBaseUrl === undefined) {
throw new Error('Rewriting is enabled but no datasetBaseUrl is configured.');
}
const datasetBaseUrlValue = new URL(datasetBaseUrl);
datasetBaseUrlValue.search = '';
datasetBaseUrlValue.searchParams.forEach((_value, key) => datasetBaseUrlValue.searchParams.delete(key));
const datasetBaseUrlString = datasetBaseUrlValue.toString();
return {
rewrite: rewriteValue,
datasetBaseUrl: datasetBaseUrlString,
replaceIri: (iri) => iri.replace(iriOrigin(iri), datasetBaseUrlString),
iriOrigin,
};
};
/**
* Generate a SPARQL client.
*
* @param sparqlEndpoint The SPARQL endpoint URL.
* @param options Options.
* @returns The SPARQL client.
*/
export const generateClient = (sparqlEndpoint, options) => {
const clients = {
parsing: new ParsingClient({ endpointUrl: sparqlEndpoint, ...options }),
simple: new SimpleClient({ endpointUrl: sparqlEndpoint, ...options }),
};
/**
*
* @param query The SPARQL query to use.
* @param options Query options.
* @returns The quad stream or boolean for ASK queries.
*/
const query = async (query, options = {}) => {
const isAsk = options && options.ask;
const isSelect = options && options.select;
const headers = (options && options.headers) || {};
const rewriteResponse = (options && options.rewriteResponse) || [];
if (isAsk) {
return await clients.parsing.query.ask(query, { headers });
}
if (isSelect) {
const selectResults = await clients.parsing.query.select(query, { headers });
const replacedSelectResults = selectResults.map((row) => {
for (const key of Object.keys(row)) {
const term = row[key];
if (!term || typeof term.value !== 'string' || !term.value) {
continue;
}
let value = term.value;
for (const replacement of rewriteResponse) {
value = value.replace(replacement.find, replacement.replace);
}
// `term.value` is declared read-only on RDF/JS terms, but we
// intentionally rewrite it in place here.
;
term.value = value;
}
return row;
});
return replacedSelectResults;
}
const result = await clients.simple.query.construct(query, { headers });
const contentType = result.headers.get('Content-Type') || 'application/n-triples';
const body = result.body;
// Function to apply all replacements in sequence
const applyReplacements = (stream, replacements) => {
let pipeline = stream;
for (const replacement of replacements) {
pipeline = pipeline.pipe(replaceStream(replacement.find, replacement.replace));
}
return pipeline;
};
return {
response: applyReplacements(body, rewriteResponse),
contentType,
};
};
return {
clients,
query,
};
};
/**
* Create a SPARQL query function that can be used inside Trifid plugins.
*
* @param logger Logger instance.
* @param configuredEndpoints Configured endpoints.
* @param instanceHostname Instance hostname, used to resolve relative URLs.
* @returns Query function.
*/
export const initQuery = (logger, configuredEndpoints = {}, instanceHostname) => {
const endpoints = Object.fromEntries(Object.entries(configuredEndpoints).map(([name, options]) => {
logger.debug(`Configured following SPARQL endpoint: ${name}`);
const { url: endpointUrl, ...otherOptions } = options;
const url = new URL(endpointUrl, instanceHostname);
return [name, generateClient(url.toString(), otherOptions)];
}));
/**
* Execute a SPARQL query.
*
* @param pluginLogger Plugin logger instance.
* @returns Query result.
*/
const query = (pluginLogger) => async (query, options = {}) => {
pluginLogger.debug(`SPARQL query: \n${query}`);
const { endpoint: configuredEndpoint, ...otherOptions } = options;
const endpoint = configuredEndpoint || 'default';
const client = endpoints[endpoint];
if (!client) {
throw new Error(`Unknown SPARQL endpoint: ${endpoint}`);
}
return await client.query(query, otherOptions);
};
return {
endpoints,
query,
};
};