From d052d3331b77277a363bd84104a0f8dcc7cc0f4c Mon Sep 17 00:00:00 2001 From: karel kremer Date: Mon, 30 Dec 2024 13:18:54 +0100 Subject: [PATCH 1/3] add sparql-tag code from sparql-client-2 --- helpers/mu/sparql-tag.js | 19 +++ helpers/mu/sparql.js | 3 +- helpers/mu/term/index.js | 170 ++++++++++++++++++++++++++ helpers/mu/term/iri.js | 5 + helpers/mu/term/literal.js | 240 +++++++++++++++++++++++++++++++++++++ 5 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 helpers/mu/sparql-tag.js create mode 100644 helpers/mu/term/index.js create mode 100644 helpers/mu/term/iri.js create mode 100644 helpers/mu/term/literal.js diff --git a/helpers/mu/sparql-tag.js b/helpers/mu/sparql-tag.js new file mode 100644 index 0000000..0abf0d4 --- /dev/null +++ b/helpers/mu/sparql-tag.js @@ -0,0 +1,19 @@ +/** + * Implements an ECMAScript 2015 template tag in ECMAScript 5... + */ +var Term = require('./term'); + +module.exports = SPARQL; + +function SPARQL(template) { + // This would be easier in ES6: + // function SPARQL(template, ...subsitutions) { ... } + var substitutions = [].slice.call(arguments, 1); + var result = template[0]; + + substitutions.forEach(function (value, i) { + result += Term.create(value).format() + template[i + 1]; + }); + + return result; +} diff --git a/helpers/mu/sparql.js b/helpers/mu/sparql.js index 4a23432..f350e56 100644 --- a/helpers/mu/sparql.js +++ b/helpers/mu/sparql.js @@ -1,8 +1,9 @@ import httpContext from 'express-http-context'; import SC2 from 'sparql-client-2'; import env from 'env-var'; +import SPARQL from './sparql-tag'; -const { SparqlClient, SPARQL } = SC2; +const { SparqlClient } = SC2; const LOG_SPARQL_QUERIES = process.env.LOG_SPARQL_QUERIES != undefined ? env.get('LOG_SPARQL_QUERIES').asBool() : env.get('LOG_SPARQL_ALL').asBool(); const LOG_SPARQL_UPDATES = process.env.LOG_SPARQL_UPDATES != undefined ? env.get('LOG_SPARQL_UPDATES').asBool() : env.get('LOG_SPARQL_ALL').asBool(); diff --git a/helpers/mu/term/index.js b/helpers/mu/term/index.js new file mode 100644 index 0000000..525be95 --- /dev/null +++ b/helpers/mu/term/index.js @@ -0,0 +1,170 @@ +/** + * Base class for representing RDF Terms. + * This file was taken from the `node-sparql-client` project as is except for + * the removal of the unimplemented blank node term type. + * + * Terms are required to provide a #format() method. + * + * http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#sparqlBasicTerms + */ + +var assign = require("lodash/assign"); +var assert = require("assert"); + +module.exports = Term; +/* Ensure the following are required *after* module.exports assignment due to + * circular dependency to Term.prototype. */ +var IRI = require("./iri"); +var Literal = require("./literal"); + +/** + * XSD datatypes. SPARQL has literals for these. + */ +var KNOWN_DATATYPES = { + boolean: 1, + decimal: 1, + double: 1, + integer: 1, +}; + +/** + * An RDF Term. IRIs and literals are all RDF terms. + */ +function Term() {} + +/** + * Returns a term suitable for replacement in a SPARQL query. + */ +Term.prototype.format = function dummyFormat() { + assert(false, "term MUST implement a #format method!"); +}; + +Term.prototype.toString = function () { + return JSON.stringify(this, "type value datatype xml:lang".split(/\s+/)); +}; + +/** + * Creates a term from an arbitrary value, and options, if any. + * Valid options: + * + * - lang: Sets the language tag for the value. + * Relevant only if value is a string. + * - xml:lang: Same as lang. + * - datatype: Sets the datatype for a literal. + * - type: Can be an SPARQL term type 'literal', 'uri', 'bnode'; + * the value will be interpreted as in the SPARQL spec. + * Additionally, can be 'integer', 'double', 'decimal'. + */ +Term.create = function create(value, options) { + if (options) { + return createTerm(assign({}, options, { value: value })); + } + return createTerm(value); +}; + +/* Helpers. */ + +function createTerm(value) { + var type = determineType(value); + + switch (type) { + case "string": + return Literal.create(value); + case "number": + /* + 'e0' to look like a SPARQL double literal. */ + return Literal.createWithDataType(value, { xsd: "double" }); + case "boolean": + return Literal.createWithDataType(value, { xsd: "boolean" }); + case "object": + return createTermFromObject(value); + } + + throw new TypeError("Cannot bind " + type + " value: " + value); +} + +function createTermFromObject(object) { + var value, type; + + /* Check if it's a short URI object. */ + if (Object.keys(object).length === 1) { + return IRI.createFromObject(object); + } + + value = object.value; + + if (value === undefined) { + throw new Error( + "Binding must contain property called `value`. " + + "If you're trying to bind a URI, do so explicitly by " + + "writing {value: {prefix: name}, type: 'uri', ...} " + + "rather than {prefix: name, ...}" + ); + } + + resolveDataTypeShortcuts(object); + + type = determineType(value); + switch (true) { + case object.type === "uri": + return IRI.create(value); + case object.lang !== undefined: + return Literal.createWithLangaugeTag(value, object.lang); + case object["xml:lang"] !== undefined: + return Literal.createWithLangaugeTag(value, object["xml:lang"]); + case object.datatype !== undefined: + return Literal.createWithDataType(value, object.datatype); + } + throw new Error("Could not bind object: " + require("util").inspect(object)); +} + +/** + * The value `type` can be one of the XSD types, but this is just a shortcut + * for {type: 'literal', datatype: givenType}. + * + * This patches the object, such that type is moved to + */ +function resolveDataTypeShortcuts(object) { + var TYPES = { + bnode: 1, + literal: 1, + uri: 1, + }; + var datatype, + type = object.type; + + if (type === undefined || type in TYPES) { + /* Nothing to resolve. */ + return object; + } + + if (type in KNOWN_DATATYPES) { + datatype = { xsd: type }; + } else { + datatype = type; + } + + object.type = "literal"; + object.datatype = datatype; + + return object; +} + +/** + * Returns a string of: + * * 'null' = With type: 'bnode' is a blank node + * * 'undefined' + * * 'number' => An xsd:double; can be coreced with 'type' to + * xsd:integer or xsd:decimal. + * * 'boolean' => An xsd:boolean + * * 'string' => A plain literal; can add an xml:lang property + * with type 'uri', is considered a fully-qualified IRI. + * * 'object' => If length 1, a URI. Else, must contain 'value' and pass + * rest of the properties as options. + * * 'function' + */ +function determineType(unknown) { + var value = + unknown === null || unknown === undefined ? unknown : unknown.valueOf(); + + return value === null ? "null" : typeof value; +} diff --git a/helpers/mu/term/iri.js b/helpers/mu/term/iri.js new file mode 100644 index 0000000..bf0769b --- /dev/null +++ b/helpers/mu/term/iri.js @@ -0,0 +1,5 @@ +/** + * "Symlink" to the real IRI module, but uses Node's require mechanism + * instead. + */ +module.exports = require('../iri'); diff --git a/helpers/mu/term/literal.js b/helpers/mu/term/literal.js new file mode 100644 index 0000000..58e5420 --- /dev/null +++ b/helpers/mu/term/literal.js @@ -0,0 +1,240 @@ +/** + * Literal RDF terms. Strings and other primitive datatypes. + */ + +module.exports = Literal; + +var assert = require("assert"); + +var Term = require("."); +var IRI = require("./iri"); + +var SPARQL_LITERAL_PATTERNS = { + boolean: /true|false/, + integer: /^[-+]?[0-9]+$/, + double: /^[-+]?(?:[0-9]+.[0-9]*|.[0-9]+|[0-9]+)[eE][+-]?[0-9]+$/, + decimal: /^[-+]?[0-9]*.[0-9]+$/, +}; + +function Literal(value, datatype) { + this.value = assertSafeString("" + value); + if (datatype !== undefined) { + try { + this.datatype = IRI.create(datatype); + } catch (e) { + // TODO: Ensure we're getting the right error. + throw new Error( + "Datatype must be string or single-valued " + + "object. Got " + + datatype + + " instead" + ); + } + } +} + +Literal.prototype = Object.create(Term.prototype, { + type: { value: "literal", enumerable: true }, +}); + +Literal.prototype.format = function () { + var term; + + if (knownDatatype(this.datatype)) { + term = tryFormatType(this.value, this.datatype.id); + if (term !== undefined) { + return term.asString + ? formatStringWithDataType(term.literal, this.datatype) + : term.literal; + } + } + + return formatStringWithDataType(this.value, this.datatype); +}; + +/** + * Creates a literal with no datatype. + */ +Literal.create = function (value) { + return new StringLiteral(value); +}; + +/** + * Creates a literal with an explicit language tag. + */ +Literal.createWithLangaugeTag = function (value, languageTag) { + if (typeof languageTag !== "string") { + throw new TypeError("Term as written must specify a language tag."); + } + return new StringLiteral(value, languageTag); +}; + +/** + * Creates a literal with an explicit datatype. + */ +Literal.createWithDataType = function (value, datatype) { + if (datatype === undefined) { + throw new TypeError("Undefined datatype provided."); + } + return new Literal(value, datatype); +}; + +/** + * Ensures U+0000 is not in the string. + */ +function assertSafeString(value) { + if (/\u0000/.test(value)) { + throw new Error("Refusing to encode string with null-character"); + } + return value; +} + +/** + * Escapes all special characters in a string, except for linefeeds (U+000A). + */ +function escapeString(str) { + /* From: http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#grammarEscapes */ + /* This omits newline. */ + var escapableCodePoints = /[\\\u0009\u000D\u0008\u000C\u0022\u0027]/g; + return str.replace(escapableCodePoints, function (character) { + return "\\" + character; + }); +} + +/** + * Format the string part of a string. + */ +function formatString(value) { + var stringified = "" + value; + var escaped = escapeString(stringified); + var hasSingleQuote = /'/.test(stringified); + var hasDoubleQuote = /"/.test(stringified); + var hasNewline = /"/.test(stringified); + + var delimiter; + + if (hasNewline || (hasSingleQuote && hasDoubleQuote)) { + delimiter = '"""'; + } else if (hasSingleQuote) { + delimiter = '"'; + } else { + delimiter = "'"; + } + + assert( + !new RegExp("(?!\\\\)" + delimiter).test(escaped), + "found `" + delimiter + "` in `" + escaped + "`" + ); + return delimiter + escaped + delimiter; +} + +/** + * + */ +function formatStringWithDataType(value, datatype) { + var term = formatString(value); + + if (datatype !== undefined) { + return term + "^^" + datatype.format(); + } + return term; +} + +function knownDatatype(iri) { + if (!iri || iri.namespace !== "xsd") { + return false; + } + + return true; +} + +/** + * Returns formatted value of built in xsd types. Returns undefined if the + * given value does not match the pattern. + */ +function tryFormatType(value, type) { + var stringifiedValue = "" + value; + assert(SPARQL_LITERAL_PATTERNS[type] !== undefined); + + if (type === "double") { + return tryFormatDouble(value); + } + + if (SPARQL_LITERAL_PATTERNS[type].test(stringifiedValue)) { + return { literal: stringifiedValue }; + } +} + +/** + * Tries to coerce the given value into looking like a SPARQL double literal. + * Returns the original value if it fails. + * + * Although not SPARQL string literals, the special values are converted into + * their XSD equivalents[1]: + * + * JS xsd + * ======================== + * NaN => NaN + * Infinity => INF + * -Infinity => -INF + * + * [1]: http://www.w3.org/TR/xmlschema-2/#double-lexical-representation + */ +function tryFormatDouble(value) { + var pattern = SPARQL_LITERAL_PATTERNS.double; + var stringified = "" + value; + + /* Special cases for +/-Infinity: */ + if (Math.abs(+value) === Infinity) { + stringified = (value < 0 ? "-" : "") + "INF"; + return { literal: stringified, asString: true }; + } + + /* Try to make the given double look like a SPARQL double literal. */ + if (pattern.test(stringified)) { + return { literal: stringified }; + } + + stringified += "e0"; + + if (pattern.test(stringified)) { + return { literal: stringified }; + } +} + +function StringLiteral(value, languageTag) { + Literal.call(this, value); + + if (languageTag !== undefined) { + this["xml:lang"] = assertSafeLanguageTag(languageTag); + } +} + +StringLiteral.prototype = Object.create(Literal.prototype, { + languageTag: { + get: function () { + return this["xml:lang"]; + }, + }, +}); + +StringLiteral.prototype.format = function () { + var term = formatString(this.value); + + if (this.languageTag !== undefined) { + term += "@" + this.languageTag; + } + return term; +}; + +/** + * Raises an error if the language tag seems malformed. + */ +function assertSafeLanguageTag(tag) { + /* See: http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#rLANGTAG */ + if (/^[a-zA-Z]+(?:-[a-zA-Z0-9]+)*$/.test(tag)) { + return tag; + } + + throw new Error("Invalid langauge tag: " + tag); +} From deab1a9d7c8b3a2f720164147d709ec04a75086a Mon Sep 17 00:00:00 2001 From: karel kremer Date: Mon, 30 Dec 2024 15:08:18 +0100 Subject: [PATCH 2/3] replace sparql-client-2 library with digest-fetch, as was done in mu-auth-sudo, bump nodemon. currently no security warnings according to npm audit --- TUTORIALS.md | 2 +- helpers/mu/sparql-tag.js | 18 ++-- helpers/mu/sparql.js | 200 ++++++++++++++++++++++++--------------- helpers/mu/term/iri.js | 113 +++++++++++++++++++++- package.json | 4 +- 5 files changed, 247 insertions(+), 90 deletions(-) diff --git a/TUTORIALS.md b/TUTORIALS.md index 0ec49cb..82f039d 100644 --- a/TUTORIALS.md +++ b/TUTORIALS.md @@ -56,7 +56,7 @@ app.get('/users', function(req, res) { ‘app’ is a reference to the Express app while ‘query’ is a helper function to send a SPARQL query to the triplestore. There are functions available to generate a UUID, to escape characters in a SPARQL query, etc. A complete list can be found in [the template’s README](README.md#helpers). -The mu-javascript-template uses the [sparql-client-2](https://www.npmjs.com/package/sparql-client-2) library to interact with the triplestore. Check this library for the response structure of the `query` helper. +The mu-javascript-template uses http requests to query the sparql endpoint according to the SPARQL protocol via POST with URL-encoded parameters. The response will be formatted according to the [SPARQL Query results JSON format](https://www.w3.org/TR/2013/REC-sparql11-results-json-20130321/). ### Using additional libraries If you need additional libraries, just list them in a package.json file next to your app.js. It works as you would expect: just define the packages in the dependencies section of the package.json. They will be installed automatically at build time. While developing in a container as described above, you will have to restart the container for the new included packages to be installed. diff --git a/helpers/mu/sparql-tag.js b/helpers/mu/sparql-tag.js index 0abf0d4..84adb29 100644 --- a/helpers/mu/sparql-tag.js +++ b/helpers/mu/sparql-tag.js @@ -1,19 +1,19 @@ /** * Implements an ECMAScript 2015 template tag in ECMAScript 5... */ -var Term = require('./term'); +var Term = require("./term"); module.exports = SPARQL; function SPARQL(template) { - // This would be easier in ES6: - // function SPARQL(template, ...subsitutions) { ... } - var substitutions = [].slice.call(arguments, 1); - var result = template[0]; + // This would be easier in ES6: + // function SPARQL(template, ...subsitutions) { ... } + var substitutions = [].slice.call(arguments, 1); + var result = template[0]; - substitutions.forEach(function (value, i) { - result += Term.create(value).format() + template[i + 1]; - }); + substitutions.forEach(function (value, i) { + result += Term.create(value).format() + template[i + 1]; + }); - return result; + return result; } diff --git a/helpers/mu/sparql.js b/helpers/mu/sparql.js index f350e56..ffffeb8 100644 --- a/helpers/mu/sparql.js +++ b/helpers/mu/sparql.js @@ -1,103 +1,155 @@ import httpContext from 'express-http-context'; -import SC2 from 'sparql-client-2'; import env from 'env-var'; import SPARQL from './sparql-tag'; - -const { SparqlClient } = SC2; +import DigestFetch from "digest-fetch"; const LOG_SPARQL_QUERIES = process.env.LOG_SPARQL_QUERIES != undefined ? env.get('LOG_SPARQL_QUERIES').asBool() : env.get('LOG_SPARQL_ALL').asBool(); const LOG_SPARQL_UPDATES = process.env.LOG_SPARQL_UPDATES != undefined ? env.get('LOG_SPARQL_UPDATES').asBool() : env.get('LOG_SPARQL_ALL').asBool(); const DEBUG_AUTH_HEADERS = env.get('DEBUG_AUTH_HEADERS').asBool(); +const MU_SPARQL_ENDPOINT = env.get('MU_SPARQL_ENDPOINT').default('http://database:8890/sparql').asString(); +const RETRY = env.get('MU_QUERY_RETRY').default('false').asBool(); +const RETRY_MAX_ATTEMPTS = env.get('MU_QUERY_RETRY_MAX_ATTEMPTS').default('5').asInt(); +const RETRY_FOR_HTTP_STATUS_CODES = env.get('MU_QUERY_RETRY_FOR_HTTP_STATUS_CODES').default('').asArray(); +const RETRY_FOR_CONNECTION_ERRORS = env.get('MU_QUERY_RETRY_FOR_CONNECTION_ERRORS').default('ECONNRESET,ETIMEDOUT,EAI_AGAIN').asArray(); +const RETRY_TIMEOUT_INCREMENT_FACTOR = env.get('MU_QUERY_RETRY_TIMEOUT_INCREMENT_FACTOR').default('0.1').asFloat(); //==-- logic --==// -// builds a new sparqlClient -function newSparqlClient() { - let options = { requestDefaults: { headers: { } } }; - - if (httpContext.get('request')) { - options.requestDefaults.headers['mu-session-id'] = httpContext.get('request').get('mu-session-id'); - options.requestDefaults.headers['mu-call-id'] = httpContext.get('request').get('mu-call-id'); - options.requestDefaults.headers['mu-auth-allowed-groups'] = httpContext.get('request').get('mu-auth-allowed-groups'); // groups of incoming request - } - - if (httpContext.get('response')) { - const allowedGroups = httpContext.get('response').get('mu-auth-allowed-groups'); // groups returned by a previous SPARQL query - if (allowedGroups) - options.requestDefaults.headers['mu-auth-allowed-groups'] = allowedGroups; - } - - if (DEBUG_AUTH_HEADERS) { - console.log(`Headers set on SPARQL client: ${JSON.stringify(options)}`); - } - - return new SparqlClient(process.env.MU_SPARQL_ENDPOINT, options).register({ - mu: 'http://mu.semte.ch/vocabularies/', - muCore: 'http://mu.semte.ch/vocabularies/core/', - muExt: 'http://mu.semte.ch/vocabularies/ext/' - }); -} - // executes a query (you can use the template syntax) -function query( queryString ) { +function query( queryString, extraHeaders = {}, connectionOptions = {} ) { if (LOG_SPARQL_QUERIES) { console.log(queryString); } - return executeQuery(queryString); + return executeQuery(queryString, extraHeaders, connectionOptions); }; // executes an update query -function update( queryString ) { +function update(queryString, extraHeaders = {}, connectionOptions = {}) { if (LOG_SPARQL_UPDATES) { console.log(queryString); } return executeQuery(queryString); }; -function executeQuery( queryString ) { - return newSparqlClient().query(queryString).executeRaw().then(response => { - const temp = httpContext; - if (httpContext.get('response') && !httpContext.get('response').headersSent) { - // set mu-auth-allowed-groups on outgoing response - const allowedGroups = response.headers['mu-auth-allowed-groups']; - if (allowedGroups) { - httpContext.get('response').setHeader('mu-auth-allowed-groups', allowedGroups); - if (DEBUG_AUTH_HEADERS) { - console.log(`Update mu-auth-allowed-groups to ${allowedGroups}`); - } - } else { - httpContext.get('response').removeHeader('mu-auth-allowed-groups'); - if (DEBUG_AUTH_HEADERS) { - console.log('Remove mu-auth-allowed-groups'); - } - } - - // set mu-auth-used-groups on outgoing response - const usedGroups = response.headers['mu-auth-used-groups']; - if (usedGroups) { - httpContext.get('response').setHeader('mu-auth-used-groups', usedGroups); - if (DEBUG_AUTH_HEADERS) { - console.log(`Update mu-auth-used-groups to ${usedGroups}`); - } - } else { - httpContext.get('response').removeHeader('mu-auth-used-groups'); - if (DEBUG_AUTH_HEADERS) { - console.log('Remove mu-auth-used-groups'); - } - } + +function defaultHeaders() { + const headers = new Headers(); + headers.set("content-type", "application/x-www-form-urlencoded"); + headers.set("Accept", "application/sparql-results+json"); + if (httpContext.get("request")) { + headers.set( + "mu-session-id", + httpContext.get("request").get("mu-session-id") + ); + headers.set("mu-call-id", httpContext.get("request").get("mu-call-id")); + } + return headers; +} + +async function executeQuery(queryString, extraHeaders = {}, connectionOptions = {}, attempt = 0) +{ + const sparqlEndpoint = connectionOptions.sparqlEndpoint ?? MU_SPARQL_ENDPOINT; + const headers = defaultHeaders(); + for (const key of Object.keys(extraHeaders)) { + headers.append(key, extraHeaders[key]); + } + if (DEBUG_AUTH_HEADERS) { + const stringifiedHeaders = Array.from(headers.entries()) + .filter(([key]) => key.startsWith("mu-")) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); + console.log(`Headers set on SPARQL client: ${stringifiedHeaders}`); + } + + try { + // note that URLSearchParams is used because it correctly encodes for form-urlencoded + const formData = new URLSearchParams(); + formData.set("query", queryString); + headers.append("Content-Length", formData.toString().length.toString()); + + let response; + if (connectionOptions.authUser && connectionOptions.authPassword) { + const client = new DigestFetch( + connectionOptions.authUser, + connectionOptions.authPassword, + { basic: connectionOptions.authType === "basic" } + ); + response = await client.fetch(sparqlEndpoint, { + method: "POST", + body: formData.toString(), + headers, + }); + } else { + response = await fetch(sparqlEndpoint, { + method: "POST", + body: formData.toString(), + headers, + }); } + if (response.ok) { + return await maybeJSON(response); + } else { + throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`); + } + } catch (ex) { + if (mayRetry(ex, attempt, connectionOptions)) { + attempt += 1; + + const sleepTime = nextAttemptTimeout(attempt); + console.log(`Sleeping ${sleepTime} ms before next attempt`); + await new Promise((r) => setTimeout(r, sleepTime)); + + return await executeRawQuery( + queryString, + extraHeaders, + connectionOptions, + attempt + ); + } else { + console.log(`Failed Query: + ${queryString}`); + throw ex; + } + } +} - function maybeParseJSON(body) { - // Catch invalid JSON - try { - return JSON.parse(body); - } catch (ex) { - return null; - } +async function maybeJSON(response) { + try { + return await response.json(); + } catch (e) { + return null; + } +} + +function mayRetry( + error, + attempt, + connectionOptions = {} +) { + console.log( + `Checking retry allowed for error: ${error} and attempt: ${attempt}` + ); + + let mayRetry = false; + + if (!(RETRY || connectionOptions.mayRetry)) { + mayRetry = false; + } else if (attempt < RETRY_MAX_ATTEMPTS) { + if (error.code && RETRY_FOR_CONNECTION_ERRORS.includes(error.code)) { + mayRetry = true; + } else if ( error.httpStatus && RETRY_FOR_HTTP_STATUS_CODES.includes(`${error.httpStatus}`) ) { + mayRetry = true; } + } + + console.log(`Retry allowed? ${mayRetry}`); + + return mayRetry; +} - return maybeParseJSON(response.body); - }); +function nextAttemptTimeout(attempt) { + // expected to be milliseconds + return Math.round(RETRY_TIMEOUT_INCREMENT_FACTOR * Math.exp(attempt + 10)); } function sparqlEscapeString( value ){ @@ -158,7 +210,6 @@ function sparqlEscape( value, type ){ //==-- exports --==// const exports = { - newSparqlClient: newSparqlClient, SPARQL: SPARQL, sparql: SPARQL, query: query, @@ -175,7 +226,6 @@ const exports = { export default exports; export { - newSparqlClient, SPARQL as SPARQL, SPARQL as sparql, query, diff --git a/helpers/mu/term/iri.js b/helpers/mu/term/iri.js index bf0769b..b9549ae 100644 --- a/helpers/mu/term/iri.js +++ b/helpers/mu/term/iri.js @@ -1,5 +1,112 @@ /** - * "Symlink" to the real IRI module, but uses Node's require mechanism - * instead. + * An IRI is like a URI but forbids spaces. */ -module.exports = require('../iri'); + +var Term = require("../term"); + +module.exports = IRI; + +/** + * Base IRI. + */ +function IRI() { + Term.call(this); +} + +IRI.prototype = Object.create(Term.prototype, { + type: { value: "uri", enumerable: true }, +}); + +/** + * Returns an IRI for whatever is passed in. + */ +IRI.create = function (value) { + if (typeof value === "object") { + return IRI.createFromObject(value); + } else if (typeof value === "string") { + return new IRIReference(value); + } else { + throw new TypeError("Invalid IRI"); + } +}; + +/** + * Returns an IRI object or null if none can be created. + */ +IRI.createFromObject = function (object) { + var namespace; + var value; + var keys = Object.keys(object); + if (keys.length !== 1) { + throw new Error("Invalid prefixed IRI."); + } + + namespace = keys[0]; + value = object[namespace]; + + if (typeof value !== "string") { + throw new TypeError("Invalid prefixed IRI."); + } + + /* TODO: This is NOT a sufficient regex! */ + if (!/^[^\s;.,<|$]+$/.test(value)) { + throw new Error("Invalid IRI identifier"); + } + + return new PrefixedNameIRI(namespace, value); +}; + +/** + * A Prefixed Name like: + * book:book1 + * or + * :book1 + */ +function PrefixedNameIRI(namespace, identifier) { + IRI.call(this); + this.namespace = namespace; + this.id = identifier; +} + +PrefixedNameIRI.prototype = Object.create(IRI.prototype, { + value: { + get: function () { + return "INVALID!" + this.format(); + }, + enumerable: true, + }, +}); + +PrefixedNameIRI.prototype.format = function () { + return this.namespace + ":" + this.id; +}; + +/** + * An IRI reference like: + * + * or . + */ +function IRIReference(iri) { + IRI.call(this); + /* + * IRIREF is defined here: + * http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#rIRIREF + */ + if (!/^[^<>"{}|^`\\\u0000-\u0020]*$/.test(iri)) { + throw new Error("Invalid IRI: " + iri); + } + this.iri = iri; +} + +IRIReference.prototype = Object.create(IRI.prototype, { + value: { + get: function () { + return this.iri; + }, + enumerable: true, + }, +}); + +IRIReference.prototype.format = function () { + return "<" + this.iri + ">"; +}; diff --git a/package.json b/package.json index b16c39e..f8740e2 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "babel-plugin-module-resolver": "5.0.0", "body-parser": "~1.20.1", "coffeescript": "^2.6.1", + "digest-fetch": "^2.0.3", "env-var": "^7.0.0", "express": "^4.17.1", "express-http-context": "~1.2.4", - "nodemon": "^2.0.20", - "sparql-client-2": "https://github.com/erikap/node-sparql-client.git", + "nodemon": "^3.1.9", "typescript": "^4.6.2", "uuid": "^9.0.0" }, From 42b88b80006ae675e84116d8346b4f58df8b5b1f Mon Sep 17 00:00:00 2001 From: karel kremer Date: Mon, 30 Dec 2024 15:30:05 +0100 Subject: [PATCH 3/3] keep behaviour of updating response headers --- helpers/mu/sparql.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/helpers/mu/sparql.js b/helpers/mu/sparql.js index ffffeb8..3d009b1 100644 --- a/helpers/mu/sparql.js +++ b/helpers/mu/sparql.js @@ -86,6 +86,7 @@ async function executeQuery(queryString, extraHeaders = {}, connectionOptions = headers, }); } + updateResponseHeaders(response); if (response.ok) { return await maybeJSON(response); } else { @@ -113,6 +114,39 @@ async function executeQuery(queryString, extraHeaders = {}, connectionOptions = } } +function updateResponseHeaders(response){ + // update the outgoing response headers with the headers received from the SPARQL endpoint + if (httpContext.get('response') && !httpContext.get('response').headersSent) { + // set mu-auth-allowed-groups on outgoing response + const allowedGroups = response.headers.get('mu-auth-allowed-groups'); + if (allowedGroups) { + httpContext.get('response').setHeader('mu-auth-allowed-groups', allowedGroups); + if (DEBUG_AUTH_HEADERS) { + console.log(`Update mu-auth-allowed-groups to ${allowedGroups}`); + } + } else { + httpContext.get('response').removeHeader('mu-auth-allowed-groups'); + if (DEBUG_AUTH_HEADERS) { + console.log('Remove mu-auth-allowed-groups'); + } + } + + // set mu-auth-used-groups on outgoing response + const usedGroups = response.headers.get('mu-auth-used-groups'); + if (usedGroups) { + httpContext.get('response').setHeader('mu-auth-used-groups', usedGroups); + if (DEBUG_AUTH_HEADERS) { + console.log(`Update mu-auth-used-groups to ${usedGroups}`); + } + } else { + httpContext.get('response').removeHeader('mu-auth-used-groups'); + if (DEBUG_AUTH_HEADERS) { + console.log('Remove mu-auth-used-groups'); + } + } + } +} + async function maybeJSON(response) { try { return await response.json();