Skip to content

Commit c9ea983

Browse files
author
Niels V
committed
Replace sparql-client-2 with native fetch
Node 18 ships fetch built-in, so sparql-client-2 is no longer needed. The replacement uses the same public API (query, update, sparqlEscape*, SPARQL tagged template) and forwards the same mu-auth headers as before. sudo and scope can now be passed as options to query/update instead of constructing a dedicated client with newSparqlClient.
1 parent 360ed03 commit c9ea983

2 files changed

Lines changed: 158 additions & 120 deletions

File tree

helpers/mu/sparql.js

Lines changed: 158 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,32 @@
11
import httpContext from 'express-http-context';
2-
import SC2 from 'sparql-client-2';
32
import env from 'env-var';
4-
5-
const { SparqlClient, SPARQL } = SC2;
3+
import SPARQL from './sparql-tag.js';
64

75
const LOG_SPARQL_QUERIES = process.env.LOG_SPARQL_QUERIES != undefined ? env.get('LOG_SPARQL_QUERIES').asBool() : env.get('LOG_SPARQL_ALL').asBool();
86
const LOG_SPARQL_UPDATES = process.env.LOG_SPARQL_UPDATES != undefined ? env.get('LOG_SPARQL_UPDATES').asBool() : env.get('LOG_SPARQL_ALL').asBool();
97
const DEBUG_AUTH_HEADERS = env.get('DEBUG_AUTH_HEADERS').asBool();
8+
const MU_SPARQL_ENDPOINT = process.env.MU_SPARQL_ENDPOINT || 'http://database:8890/sparql';
109

1110
//==-- logic --==//
1211

13-
// builds a new sparqlClient
14-
function newSparqlClient(userOptions) {
15-
let options = { requestDefaults: { headers: { } } };
16-
17-
if (userOptions.sudo === true) {
18-
if (env.get("ALLOW_MU_AUTH_SUDO").asBool()) {
19-
options.requestDefaults.headers['mu-auth-sudo'] = "true";
20-
} else {
21-
throw "Error, sudo request but service lacks ALLOW_MU_AUTH_SUDO header";
22-
}
23-
}
24-
25-
if (userOptions.scope) {
26-
options.requestDefaults.headers['mu-auth-scope'] = userOptions.scope;
27-
} else if (process.env.DEFAULT_MU_AUTH_SCOPE) {
28-
options.requestDefaults.headers['mu-auth-scope'] = process.env.DEFAULT_MU_AUTH_SCOPE;
29-
}
30-
31-
if (httpContext.get('request')) {
32-
options.requestDefaults.headers['mu-session-id'] = httpContext.get('request').get('mu-session-id');
33-
options.requestDefaults.headers['mu-call-id'] = httpContext.get('request').get('mu-call-id');
34-
options.requestDefaults.headers['mu-auth-allowed-groups'] = httpContext.get('request').get('mu-auth-allowed-groups'); // groups of incoming request
35-
}
36-
37-
if (httpContext.get('response')) {
38-
const allowedGroups = httpContext.get('response').get('mu-auth-allowed-groups'); // groups returned by a previous SPARQL query
39-
if (allowedGroups)
40-
options.requestDefaults.headers['mu-auth-allowed-groups'] = allowedGroups;
41-
}
42-
43-
if (DEBUG_AUTH_HEADERS) {
44-
console.log(`Headers set on SPARQL client: ${JSON.stringify(options)}`);
45-
}
46-
47-
return new SparqlClient(process.env.MU_SPARQL_ENDPOINT, options);
48-
}
49-
5012
/**
51-
* @typedef {Object} QueryOptions
52-
* @property {boolean?} sudo Execute the query as sudo
53-
* @property {string?} scope URI of the scope with whith the query is executed. Use the environment variable `DEFAULT_MU_AUTH_SCOPE` if possible.
54-
*/
55-
56-
/**
57-
* Execute a sparql QUERY. Intended for use with QUERY and ASK.
13+
* Execute a sparql QUERY. Intended for use with SELECT and ASK.
5814
*
5915
* See environment variables for logging: `LOG_SPARQL_ALL`, `LOG_SPARQL_QUERIES`, `DEBUG_AUTH_HEADERS`
6016
*
6117
* @param { string } queryString SPARQL query as a string.
6218
* @param { QueryOptions? } options Operational changes to the SPARQL query.
6319
* @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
6420
*/
65-
function query( queryString, options ) {
21+
function query(queryString, options = {}) {
6622
if (LOG_SPARQL_QUERIES) {
6723
console.log(queryString);
6824
}
6925
return executeQuery(queryString, options);
70-
};
26+
}
7127

7228
/**
73-
* Execute a sparql QUERY.
29+
* Execute a sparql UPDATE.
7430
* Intended for use with `DELETE {} INSERT {} WHERE {}`, `INSERT DATA` and `DELETE DATA`.
7531
*
7632
* See environment variables for logging: `LOG_SPARQL_ALL`, `LOG_SPARQL_UPDATES`, `DEBUG_AUTH_HEADERS`
@@ -79,58 +35,150 @@ function query( queryString, options ) {
7935
* @param { QueryOptions? } options Operational changes to the SPARQL query.
8036
* @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
8137
*/
82-
function update( queryString, options ) {
38+
function update(queryString, options = {}) {
8339
if (LOG_SPARQL_UPDATES) {
8440
console.log(queryString);
8541
}
8642
return executeQuery(queryString, options);
87-
};
43+
}
44+
45+
/**
46+
* Build the default headers for a SPARQL request from the current HTTP
47+
* context, forwarding mu-auth headers so mu-authorization can apply the
48+
* correct access rules.
49+
*/
50+
function defaultHeaders() {
51+
const headers = new Headers();
52+
headers.set('content-type', 'application/x-www-form-urlencoded');
53+
headers.set('Accept', 'application/sparql-results+json');
54+
55+
const req = httpContext.get('request');
56+
if (req) {
57+
const muSessionId = req.get('mu-session-id');
58+
if (muSessionId) headers.set('mu-session-id', muSessionId);
59+
60+
const muCallId = req.get('mu-call-id');
61+
if (muCallId) headers.set('mu-call-id', muCallId);
62+
63+
// Forward allowed-groups from the incoming request so mu-authorization
64+
// does not have to recompute them on every SPARQL call.
65+
const allowedGroups = req.get('mu-auth-allowed-groups');
66+
if (allowedGroups) headers.set('mu-auth-allowed-groups', allowedGroups);
67+
}
68+
69+
const res = httpContext.get('response');
70+
if (res) {
71+
// If a previous SPARQL query within this request already resolved the
72+
// allowed groups, forward them to avoid redundant lookups.
73+
const allowedGroups = res.get('mu-auth-allowed-groups');
74+
if (allowedGroups) headers.set('mu-auth-allowed-groups', allowedGroups);
75+
}
76+
77+
return headers;
78+
}
8879

89-
function executeQuery( queryString, options ) {
90-
return newSparqlClient(options || {}).query(queryString).executeRaw().then(response => {
91-
const temp = httpContext;
92-
93-
if (httpContext.get('response') && !httpContext.get('response').headersSent) {
94-
// set mu-auth-allowed-groups on outgoing response
95-
const allowedGroups = response.headers['mu-auth-allowed-groups'];
96-
if (allowedGroups) {
97-
httpContext.get('response').setHeader('mu-auth-allowed-groups', allowedGroups);
98-
if (DEBUG_AUTH_HEADERS) {
99-
console.log(`Update mu-auth-allowed-groups to ${allowedGroups}`);
100-
}
101-
} else {
102-
httpContext.get('response').removeHeader('mu-auth-allowed-groups');
103-
if (DEBUG_AUTH_HEADERS) {
104-
console.log('Remove mu-auth-allowed-groups');
105-
}
106-
}
107-
108-
// set mu-auth-used-groups on outgoing response
109-
const usedGroups = response.headers['mu-auth-used-groups'];
110-
if (usedGroups) {
111-
httpContext.get('response').setHeader('mu-auth-used-groups', usedGroups);
112-
if (DEBUG_AUTH_HEADERS) {
113-
console.log(`Update mu-auth-used-groups to ${usedGroups}`);
114-
}
115-
} else {
116-
httpContext.get('response').removeHeader('mu-auth-used-groups');
117-
if (DEBUG_AUTH_HEADERS) {
118-
console.log('Remove mu-auth-used-groups');
119-
}
120-
}
80+
/**
81+
* @typedef {Object} QueryOptions
82+
* @property {boolean?} sudo Execute the query with mu-auth-sudo privileges.
83+
* @property {string?} scope URI of the scope to use. Falls back to the DEFAULT_MU_AUTH_SCOPE environment variable.
84+
* @property {object?} extraHeaders Additional headers to include in the request.
85+
*/
86+
87+
/**
88+
* Send a SPARQL query to the configured endpoint and return the parsed JSON
89+
* response.
90+
*
91+
* @param { string } queryString SPARQL query as a string.
92+
* @param { QueryOptions? } options Operational changes to the SPARQL query.
93+
* @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
94+
*/
95+
async function executeQuery(queryString, options = {}) {
96+
const headers = defaultHeaders();
97+
98+
const extraHeaders = options.extraHeaders ?? {};
99+
for (const key of Object.keys(extraHeaders)) {
100+
headers.append(key, extraHeaders[key]);
101+
}
102+
103+
if (options.sudo === true) {
104+
if (env.get('ALLOW_MU_AUTH_SUDO').asBool()) {
105+
headers.set('mu-auth-sudo', 'true');
106+
} else {
107+
throw new Error('sudo query requested but ALLOW_MU_AUTH_SUDO is not set');
121108
}
109+
}
110+
111+
if (options.scope) {
112+
headers.set('mu-auth-scope', options.scope);
113+
} else if (process.env.DEFAULT_MU_AUTH_SCOPE) {
114+
headers.set('mu-auth-scope', process.env.DEFAULT_MU_AUTH_SCOPE);
115+
}
116+
117+
if (DEBUG_AUTH_HEADERS) {
118+
const muHeaders = Array.from(headers.entries())
119+
.filter(([key]) => key.startsWith('mu-'))
120+
.map(([key, value]) => `${key}: ${value}`)
121+
.join(', ');
122+
console.log(`SPARQL request mu-headers: ${muHeaders}`);
123+
}
124+
125+
const formData = new URLSearchParams();
126+
formData.set('query', queryString);
122127

123-
function maybeParseJSON(body) {
124-
// Catch invalid JSON
125-
try {
126-
return JSON.parse(body);
127-
} catch (ex) {
128-
return null;
129-
}
128+
try {
129+
const response = await fetch(MU_SPARQL_ENDPOINT, {
130+
method: 'POST',
131+
body: formData.toString(),
132+
headers,
133+
});
134+
135+
updateResponseHeaders(response);
136+
137+
if (!response.ok) {
138+
throw new Error(`SPARQL endpoint returned HTTP ${response.status} ${response.statusText}`);
130139
}
131140

132-
return maybeParseJSON(response.body);
133-
});
141+
return await maybeJSON(response);
142+
} catch (ex) {
143+
console.log(`Failed Query:
144+
${queryString}`);
145+
throw ex;
146+
}
147+
}
148+
149+
/**
150+
* Copy mu-auth group headers from the SPARQL response back onto the outgoing
151+
* HTTP response so the client receives up-to-date group information.
152+
*/
153+
function updateResponseHeaders(response) {
154+
const res = httpContext.get('response');
155+
if (!res || res.headersSent) return;
156+
157+
const allowedGroups = response.headers.get('mu-auth-allowed-groups');
158+
if (allowedGroups) {
159+
res.setHeader('mu-auth-allowed-groups', allowedGroups);
160+
if (DEBUG_AUTH_HEADERS) console.log(`Forwarded mu-auth-allowed-groups: ${allowedGroups}`);
161+
} else {
162+
res.removeHeader('mu-auth-allowed-groups');
163+
if (DEBUG_AUTH_HEADERS) console.log('Removed mu-auth-allowed-groups from response');
164+
}
165+
166+
const usedGroups = response.headers.get('mu-auth-used-groups');
167+
if (usedGroups) {
168+
res.setHeader('mu-auth-used-groups', usedGroups);
169+
if (DEBUG_AUTH_HEADERS) console.log(`Forwarded mu-auth-used-groups: ${usedGroups}`);
170+
} else {
171+
res.removeHeader('mu-auth-used-groups');
172+
if (DEBUG_AUTH_HEADERS) console.log('Removed mu-auth-used-groups from response');
173+
}
174+
}
175+
176+
async function maybeJSON(response) {
177+
try {
178+
return await response.json();
179+
} catch (_) {
180+
return null;
181+
}
134182
}
135183

136184
/**
@@ -198,9 +246,10 @@ function sparqlEscapeDate( value ){
198246
};
199247

200248
/**
201-
* Escapes a date string or date object into an xsd:dateTime for use in a SPARQL.
249+
* Escape date string or date object into an xsd:dateTime for use in a SPARQL string.
202250
*
203-
* @param { Date | string | number } value Date representation (understood by `new Date`) to convert.
251+
* @param { Date | string | number } value Date representation
252+
* (understood by `new Date`) to convert.
204253
* @return { string } Date representation for SPARQL query.
205254
*/
206255
function sparqlEscapeDateTime( value ){
@@ -217,14 +266,6 @@ function sparqlEscapeBool( value ){
217266
return value ? '"true"^^xsd:boolean' : '"false"^^xsd:boolean';
218267
};
219268

220-
/**
221-
* Escapes a value based on the supplide type rather than the separately published functions. Prefer to use the
222-
* functions.
223-
*
224-
* @param { "string"|"uri"|"bool"|"decimal"|"int"|"float"|"date"|"dateTime"} type The value to be escaped.
225-
* @param {*} value The value to be escaped.
226-
* @return { string } Boolean representation for SPARQL query.
227-
*/
228269
function sparqlEscape( value, type ){
229270
switch(type) {
230271
case 'string':
@@ -251,25 +292,23 @@ function sparqlEscape( value, type ){
251292

252293
//==-- exports --==//
253294
const exports = {
254-
newSparqlClient: newSparqlClient,
255-
SPARQL: SPARQL,
295+
SPARQL,
256296
sparql: SPARQL,
257-
query: query,
258-
update: update,
259-
sparqlEscape: sparqlEscape,
260-
sparqlEscapeString: sparqlEscapeString,
261-
sparqlEscapeUri: sparqlEscapeUri,
262-
sparqlEscapeDecimal: sparqlEscapeDecimal,
263-
sparqlEscapeInt: sparqlEscapeInt,
264-
sparqlEscapeFloat: sparqlEscapeFloat,
265-
sparqlEscapeDate: sparqlEscapeDate,
266-
sparqlEscapeDateTime: sparqlEscapeDateTime,
267-
sparqlEscapeBool: sparqlEscapeBool
268-
}
297+
query,
298+
update,
299+
sparqlEscape,
300+
sparqlEscapeString,
301+
sparqlEscapeUri,
302+
sparqlEscapeDecimal,
303+
sparqlEscapeInt,
304+
sparqlEscapeFloat,
305+
sparqlEscapeDate,
306+
sparqlEscapeDateTime,
307+
sparqlEscapeBool,
308+
};
269309
export default exports;
270310

271311
export {
272-
newSparqlClient,
273312
SPARQL as SPARQL,
274313
SPARQL as sparql,
275314
query,
@@ -282,5 +321,5 @@ export {
282321
sparqlEscapeFloat,
283322
sparqlEscapeDate,
284323
sparqlEscapeDateTime,
285-
sparqlEscapeBool
324+
sparqlEscapeBool,
286325
};

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"env-var": "^7.0.0",
2424
"express": "^4.17.1",
2525
"express-http-context": "~1.2.4",
26-
"sparql-client-2": "https://github.com/erikap/node-sparql-client.git",
2726
"typescript": "^4.6.2",
2827
"uuid": "^9.0.0"
2928
},

0 commit comments

Comments
 (0)