@vdwpsmt/node-red-contrib-sparql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # node-red-contrib-sparql
2
+
3
+ A [Node-RED](https://nodered.org/) node for executing SPARQL queries against SPARQL endpoints, implementing the [W3C SPARQL 1.1 Protocol](https://www.w3.org/TR/sparql11-protocol/). Based on [sparql-http-client](https://github.com/rdf-ext/sparql-http-client).
4
+
5
+ ## Features
6
+
7
+ - Execute **SELECT**, **CONSTRUCT**, **DESCRIBE**, **ASK**, and **UPDATE** SPARQL queries
8
+ - Supports all three W3C SPARQL Protocol HTTP methods: POST (direct), POST (form-encoded), and GET
9
+ - HTTP Basic Authentication support
10
+ - Configurable query timeout
11
+ - Multiple output formats: simplified bindings, flat values, or full SPARQL JSON response
12
+ - Dynamic query and endpoint injection via incoming messages
13
+
14
+ ## Installation
15
+
16
+ ### From npm
17
+
18
+ ```bash
19
+ cd ~/.node-red
20
+ npm install node-red-contrib-sparql
21
+ ```
22
+
23
+ ### Local development
24
+
25
+ ```bash
26
+ cd ~/.node-red
27
+ npm install /path/to/node-red-contrib-sparql
28
+ ```
29
+
30
+ ## Nodes
31
+
32
+ ### sparql-query
33
+
34
+ Executes a SPARQL query against a configured endpoint.
35
+
36
+ **Inputs:**
37
+
38
+ | Property | Type | Description |
39
+ |----------|------|-------------|
40
+ | `msg.query` | string | SPARQL query (highest priority, overrides node config) |
41
+ | `msg.payload` | string | Used as query if `msg.query` is not set |
42
+ | `msg.endpoint` | string | Endpoint URL (overrides configured endpoint) |
43
+
44
+ **Outputs:**
45
+
46
+ | Property | Type | Description |
47
+ |----------|------|-------------|
48
+ | `msg.payload` | array/boolean/string/object | Query results |
49
+ | `msg.queryType` | string | Detected query type (SELECT, CONSTRUCT, DESCRIBE, ASK, UPDATE) |
50
+ | `msg.sparqlQuery` | string | The executed query |
51
+
52
+ **Output formats (configurable):**
53
+
54
+ | Format | SELECT result | ASK result |
55
+ |--------|--------------|------------|
56
+ | JSON (simplified) | `results.bindings` array with type metadata | `boolean` |
57
+ | JSON (flat values) | Array of `{ var: "value" }` objects (no type info) | `boolean` |
58
+ | JSON (full response) | Complete SPARQL JSON Results object | Complete response |
59
+
60
+ CONSTRUCT/DESCRIBE always return RDF text. UPDATE always returns `{ success: true }`.
61
+
62
+ ### sparql-endpoint (config node)
63
+
64
+ Configures a reusable SPARQL endpoint connection.
65
+
66
+ | Property | Description |
67
+ |----------|-------------|
68
+ | Endpoint | SPARQL endpoint URL |
69
+ | HTTP Method | POST (direct), POST (form-encoded), or GET |
70
+ | Username / Password | Optional HTTP Basic Authentication credentials |
71
+
72
+ ## Example
73
+
74
+ Import the example flow from `examples/basic-select.json` via the Node-RED import menu.
75
+
76
+ ## Requirements
77
+
78
+ - Node.js >= 18.0.0
79
+ - Node-RED >= 2.0.0
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,107 @@
1
+ [
2
+ {
3
+ "id": "tab-sparql-example",
4
+ "type": "tab",
5
+ "label": "SPARQL Basic SELECT",
6
+ "disabled": false,
7
+ "info": "Example flow demonstrating a basic SPARQL SELECT query against the Wikidata endpoint."
8
+ },
9
+ {
10
+ "id": "endpoint1",
11
+ "type": "sparql-endpoint",
12
+ "name": "Wikidata",
13
+ "endpoint": "https://query.wikidata.org/sparql",
14
+ "httpMethod": "POST-direct"
15
+ },
16
+ {
17
+ "id": "inject1",
18
+ "type": "inject",
19
+ "z": "tab-sparql-example",
20
+ "name": "Run query",
21
+ "props": [],
22
+ "repeat": "",
23
+ "once": false,
24
+ "x": 150,
25
+ "y": 100,
26
+ "wires": [["sparql1"]]
27
+ },
28
+ {
29
+ "id": "sparql1",
30
+ "type": "sparql-query",
31
+ "z": "tab-sparql-example",
32
+ "name": "Wikidata SELECT",
33
+ "endpoint": "endpoint1",
34
+ "query": "SELECT ?item ?itemLabel WHERE {\n ?item wdt:P31 wd:Q146.\n SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\". }\n}\nLIMIT 10",
35
+ "outputFormat": "json",
36
+ "timeout": "60",
37
+ "x": 350,
38
+ "y": 100,
39
+ "wires": [["debug1"]]
40
+ },
41
+ {
42
+ "id": "debug1",
43
+ "type": "debug",
44
+ "z": "tab-sparql-example",
45
+ "name": "Results",
46
+ "active": true,
47
+ "tosidebar": true,
48
+ "complete": "payload",
49
+ "x": 550,
50
+ "y": 100,
51
+ "wires": []
52
+ },
53
+ {
54
+ "id": "inject2",
55
+ "type": "inject",
56
+ "z": "tab-sparql-example",
57
+ "name": "msg.query + msg.endpoint",
58
+ "props": [
59
+ { "p": "query", "v": "SELECT ?item ?itemLabel WHERE {\n ?item wdt:P31 wd:Q5398426.\n SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\". }\n}\nLIMIT 5", "vt": "str" },
60
+ { "p": "endpoint", "v": "https://query.wikidata.org/sparql", "vt": "str" }
61
+ ],
62
+ "repeat": "",
63
+ "once": false,
64
+ "x": 180,
65
+ "y": 240,
66
+ "wires": [["sparql2"]]
67
+ },
68
+ {
69
+ "id": "inject3",
70
+ "type": "inject",
71
+ "z": "tab-sparql-example",
72
+ "name": "msg.payload as query",
73
+ "props": [
74
+ { "p": "payload", "v": "SELECT ?item ?itemLabel WHERE {\n ?item wdt:P31 wd:Q523.\n SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\". }\n}\nLIMIT 5", "vt": "str" }
75
+ ],
76
+ "repeat": "",
77
+ "once": false,
78
+ "x": 180,
79
+ "y": 300,
80
+ "wires": [["sparql2"]]
81
+ },
82
+ {
83
+ "id": "sparql2",
84
+ "type": "sparql-query",
85
+ "z": "tab-sparql-example",
86
+ "name": "Dynamic inputs",
87
+ "endpoint": "endpoint1",
88
+ "query": "",
89
+ "outputFormat": "flat",
90
+ "timeout": "60",
91
+ "x": 400,
92
+ "y": 270,
93
+ "wires": [["debug2"]]
94
+ },
95
+ {
96
+ "id": "debug2",
97
+ "type": "debug",
98
+ "z": "tab-sparql-example",
99
+ "name": "Dynamic results",
100
+ "active": true,
101
+ "tosidebar": true,
102
+ "complete": "true",
103
+ "x": 600,
104
+ "y": 270,
105
+ "wires": []
106
+ }
107
+ ]
@@ -0,0 +1,71 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('sparql-endpoint', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: "" },
6
+ endpoint: { value: "", required: true },
7
+ httpMethod: { value: "POST-direct" }
8
+ },
9
+ credentials: {
10
+ username: { type: "text" },
11
+ password: { type: "password" }
12
+ },
13
+ label: function () {
14
+ return this.name || this.endpoint || "SPARQL Endpoint";
15
+ }
16
+ });
17
+ </script>
18
+
19
+ <script type="text/html" data-template-name="sparql-endpoint">
20
+ <div class="form-row">
21
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-config-input-name" placeholder="Name">
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-config-input-endpoint"><i class="fa fa-globe"></i> Endpoint</label>
26
+ <input type="text" id="node-config-input-endpoint" placeholder="https://example.org/sparql">
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-config-input-httpMethod"><i class="fa fa-exchange"></i> HTTP Method</label>
30
+ <select id="node-config-input-httpMethod">
31
+ <option value="POST-direct">POST (direct)</option>
32
+ <option value="POST-form">POST (form-encoded)</option>
33
+ <option value="GET">GET</option>
34
+ </select>
35
+ </div>
36
+ <div class="form-row">
37
+ <label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
38
+ <input type="text" id="node-config-input-username" placeholder="Username">
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label>
42
+ <input type="password" id="node-config-input-password" placeholder="Password">
43
+ </div>
44
+ </script>
45
+
46
+ <script type="text/html" data-help-name="sparql-endpoint">
47
+ <p>Configuration node for a SPARQL endpoint connection.</p>
48
+
49
+ <h3>Properties</h3>
50
+ <dl class="message-properties">
51
+ <dt>Endpoint</dt>
52
+ <dd>The SPARQL endpoint URL (e.g. <code>https://query.wikidata.org/sparql</code>).</dd>
53
+ <dt>HTTP Method</dt>
54
+ <dd>The HTTP method used to send SPARQL queries to the endpoint, as defined by the
55
+ <a href="https://www.w3.org/TR/sparql11-protocol/#query-operation" target="_blank">W3C SPARQL 1.1 Protocol</a>:
56
+ <ul>
57
+ <li><b>POST (direct)</b> &mdash; Sends the query in the request body with
58
+ <code>Content-Type: application/sparql-query</code>. This is the <b>recommended default</b>.
59
+ It has no URL-length limitation and is supported by virtually all SPARQL endpoints.</li>
60
+ <li><b>POST (form-encoded)</b> &mdash; Sends the query as a URL-encoded form parameter
61
+ (<code>query=...</code>) with <code>Content-Type: application/x-www-form-urlencoded</code>.
62
+ Some legacy endpoints may require this method.</li>
63
+ <li><b>GET</b> &mdash; Sends the query as a <code>?query=</code> URL parameter.
64
+ Simple but limited by maximum URL length (~2000 characters depending on server).</li>
65
+ </ul>
66
+ </dd>
67
+ <dt>Username / Password</dt>
68
+ <dd>Optional HTTP Basic Authentication credentials. These are stored encrypted in
69
+ Node-RED's credential store.</dd>
70
+ </dl>
71
+ </script>
@@ -0,0 +1,16 @@
1
+ module.exports = function (RED) {
2
+ function SparqlEndpointNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ this.name = config.name;
5
+ this.endpoint = config.endpoint || "";
6
+ this.httpMethod = config.httpMethod || "POST-direct";
7
+ this.credentials = this.credentials || {};
8
+ }
9
+
10
+ RED.nodes.registerType("sparql-endpoint", SparqlEndpointNode, {
11
+ credentials: {
12
+ username: { type: "text" },
13
+ password: { type: "password" }
14
+ }
15
+ });
16
+ };
@@ -0,0 +1,111 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('sparql-query', {
3
+ category: 'network',
4
+ color: '#4DB6AC',
5
+ defaults: {
6
+ name: { value: "" },
7
+ endpoint: { value: "", type: "sparql-endpoint" },
8
+ query: { value: "" },
9
+ outputFormat: { value: "json" },
10
+ timeout: { value: "60" }
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: "db.svg",
15
+ paletteLabel: "SPARQL query",
16
+ label: function () {
17
+ return this.name || "SPARQL query";
18
+ },
19
+ oneditprepare: function () {
20
+ var queryEditor = this.query;
21
+ $("#node-input-query").val(queryEditor);
22
+ }
23
+ });
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="sparql-query">
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" placeholder="Name">
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-endpoint"><i class="fa fa-globe"></i> Endpoint</label>
33
+ <input type="text" id="node-input-endpoint">
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-query"><i class="fa fa-search"></i> Query</label>
37
+ <textarea id="node-input-query" rows="10" cols="60" placeholder="SELECT ?S ?P ?O&#10;WHERE {&#10; ?S ?P ?O&#10;}" style="width:70%; font-family:monospace;"></textarea>
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout (s)</label>
41
+ <input type="number" id="node-input-timeout" placeholder="60" style="width:80px;">
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-outputFormat"><i class="fa fa-file-text-o"></i> Output</label>
45
+ <select id="node-input-outputFormat" style="width:70%;">
46
+ <option value="json">JSON (simplified)</option>
47
+ <option value="flat">JSON (flat values)</option>
48
+ <option value="full">JSON (full response)</option>
49
+ </select>
50
+ </div>
51
+ </script>
52
+
53
+ <script type="text/html" data-help-name="sparql-query">
54
+ <p>Executes a SPARQL query against a SPARQL endpoint using the
55
+ <a href="https://www.w3.org/TR/sparql11-protocol/" target="_blank">W3C SPARQL 1.1 Protocol</a>.</p>
56
+
57
+ <h3>Inputs</h3>
58
+ <dl class="message-properties">
59
+ <dt class="optional">query <span class="property-type">string</span></dt>
60
+ <dd>The SPARQL query to execute. Overrides the configured query.</dd>
61
+ <dt class="optional">payload <span class="property-type">string</span></dt>
62
+ <dd>Used as the SPARQL query if <code>msg.query</code> is not set.</dd>
63
+ <dt class="optional">endpoint <span class="property-type">string</span></dt>
64
+ <dd>SPARQL endpoint URL. Overrides the configured endpoint.</dd>
65
+ </dl>
66
+
67
+ <h3>Outputs</h3>
68
+ <dl class="message-properties">
69
+ <dt>payload <span class="property-type">array | boolean | string | object</span></dt>
70
+ <dd>The query results. For SELECT queries, an array of binding objects (SPARQL JSON format).
71
+ For CONSTRUCT/DESCRIBE, RDF serialization as text. For ASK, a boolean. For UPDATE, <code>{ success: true }</code>.</dd>
72
+ <dt>queryType <span class="property-type">string</span></dt>
73
+ <dd>The detected query type (SELECT, CONSTRUCT, DESCRIBE, ASK, UPDATE).</dd>
74
+ <dt>sparqlQuery <span class="property-type">string</span></dt>
75
+ <dd>The SPARQL query that was executed.</dd>
76
+ </dl>
77
+
78
+ <h3>Details</h3>
79
+ <p>This node sends SPARQL queries to an endpoint using the standard
80
+ <a href="https://www.w3.org/TR/sparql11-protocol/" target="_blank">SPARQL 1.1 Protocol</a>.
81
+ The HTTP method (POST direct, POST form-encoded, or GET) can be configured in the endpoint node.</p>
82
+
83
+ <p>The query can be supplied via the node configuration, <code>msg.query</code>,
84
+ or <code>msg.payload</code> (checked in that order of priority).</p>
85
+
86
+ <h4>Query input priority</h4>
87
+ <ol>
88
+ <li><code>msg.query</code> &mdash; highest priority</li>
89
+ <li><code>msg.payload</code> &mdash; used if <code>msg.query</code> is not set</li>
90
+ <li>Node configuration &mdash; used if neither message property is set</li>
91
+ </ol>
92
+
93
+ <h4>Note on multi-line queries</h4>
94
+ <p>SPARQL uses <code>#</code> for line comments: everything after <code>#</code> until the end of
95
+ the line is ignored. When sending a query via <code>msg.payload</code> from an inject node,
96
+ be aware that the inject node may collapse multi-line text into a single line. If your query
97
+ contains <code>#</code> comments, the comment will consume the rest of the query.</p>
98
+ <p>To avoid this, either:</p>
99
+ <ul>
100
+ <li>Use the <b>query field</b> in the sparql-query node configuration (preserves newlines)</li>
101
+ <li>Use a <b>template node</b> to set <code>msg.query</code> (preserves newlines)</li>
102
+ <li>Avoid <code>#</code> comments when passing queries via <code>msg.payload</code> from an inject node</li>
103
+ </ul>
104
+
105
+ <h3>References</h3>
106
+ <ul>
107
+ <li><a href="https://www.w3.org/TR/sparql11-query/" target="_blank">SPARQL 1.1 Query Language</a></li>
108
+ <li><a href="https://www.w3.org/TR/sparql11-protocol/" target="_blank">SPARQL 1.1 Protocol</a></li>
109
+ <li><a href="https://www.w3.org/TR/sparql11-results-json/" target="_blank">SPARQL 1.1 Query Results JSON Format</a></li>
110
+ </ul>
111
+ </script>
@@ -0,0 +1,187 @@
1
+ let SparqlClient;
2
+
3
+ module.exports = function (RED) {
4
+ function detectQueryType(query) {
5
+ // Strip comment lines before detection
6
+ const q = query.replace(/^\s*#[^\n]*\n?/gm, "");
7
+ if (/^\s*(PREFIX\s+[^\n]*\n\s*)*SELECT\b/i.test(q)) return "SELECT";
8
+ if (/^\s*(PREFIX\s+[^\n]*\n\s*)*CONSTRUCT\b/i.test(q)) return "CONSTRUCT";
9
+ if (/^\s*(PREFIX\s+[^\n]*\n\s*)*DESCRIBE\b/i.test(q)) return "DESCRIBE";
10
+ if (/^\s*(PREFIX\s+[^\n]*\n\s*)*ASK\b/i.test(q)) return "ASK";
11
+ if (/^\s*(PREFIX\s+[^\n]*\n\s*)*(INSERT|DELETE|LOAD|CLEAR|DROP|CREATE|COPY|MOVE|ADD)\b/i.test(q)) return "UPDATE";
12
+ return "UNKNOWN";
13
+ }
14
+
15
+ function mapOperation(httpMethod) {
16
+ switch (httpMethod) {
17
+ case "POST-direct": return "postDirect";
18
+ case "POST-form": return "postUrlencoded";
19
+ case "GET": return "get";
20
+ default: return "postDirect";
21
+ }
22
+ }
23
+
24
+ function SparqlQueryNode(config) {
25
+ RED.nodes.createNode(this, config);
26
+ const node = this;
27
+
28
+ this.query = config.query || "";
29
+ this.endpoint = RED.nodes.getNode(config.endpoint);
30
+ this.timeout = parseInt(config.timeout, 10) || 60;
31
+ this.outputFormat = config.outputFormat || "json";
32
+
33
+ node.on("input", async function (msg, send, done) {
34
+ send = send || function () { node.send.apply(node, arguments); };
35
+ done = done || function (err) { if (err) { node.error(err, msg); } };
36
+
37
+ const query = msg.query || msg.payload || node.query;
38
+
39
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
40
+ done(new Error("No SPARQL query provided. Set msg.query, msg.payload, or configure in the node."));
41
+ return;
42
+ }
43
+
44
+ // Determine endpoint URL
45
+ let endpointUrl;
46
+ if (msg.endpoint) {
47
+ endpointUrl = String(msg.endpoint);
48
+ } else if (node.endpoint) {
49
+ endpointUrl = node.endpoint.endpoint;
50
+ } else {
51
+ done(new Error("No SPARQL endpoint configured. Set msg.endpoint or configure an endpoint."));
52
+ return;
53
+ }
54
+
55
+ if (!endpointUrl) {
56
+ done(new Error("No valid endpoint URL provided."));
57
+ return;
58
+ }
59
+
60
+ node.status({ fill: "blue", shape: "dot", text: "querying..." });
61
+
62
+ try {
63
+ // Lazy-load ESM module
64
+ if (!SparqlClient) {
65
+ SparqlClient = (await import("sparql-http-client/SimpleClient.js")).default;
66
+ }
67
+
68
+ const creds = node.endpoint ? node.endpoint.credentials : {};
69
+ const httpMethod = node.endpoint ? node.endpoint.httpMethod : "POST-direct";
70
+ const operation = mapOperation(httpMethod);
71
+
72
+ const clientOpts = { endpointUrl };
73
+ if (creds && creds.username && creds.password) {
74
+ clientOpts.user = creds.username;
75
+ clientOpts.password = creds.password;
76
+ }
77
+
78
+ const timeoutMs = node.timeout * 1000;
79
+ const fetchOpts = { signal: AbortSignal.timeout(timeoutMs) };
80
+
81
+ const client = new SparqlClient(clientOpts);
82
+ const queryType = detectQueryType(query);
83
+
84
+ let result;
85
+
86
+ const outputFormat = node.outputFormat;
87
+
88
+ switch (queryType) {
89
+ case "SELECT": {
90
+ const response = await client.query.select(query, { operation, ...fetchOpts });
91
+ if (!response.ok) {
92
+ throw new Error(`SPARQL endpoint returned HTTP ${response.status}: ${await response.text()}`);
93
+ }
94
+ const json = await response.json();
95
+ if (outputFormat === "full") {
96
+ result = json;
97
+ } else if (outputFormat === "flat") {
98
+ result = json.results.bindings.map(row => {
99
+ const obj = {};
100
+ for (const key of Object.keys(row)) {
101
+ obj[key] = row[key].value;
102
+ }
103
+ return obj;
104
+ });
105
+ } else {
106
+ result = json.results.bindings;
107
+ }
108
+ break;
109
+ }
110
+
111
+ case "ASK": {
112
+ const response = await client.query.ask(query, { operation, ...fetchOpts });
113
+ if (!response.ok) {
114
+ throw new Error(`SPARQL endpoint returned HTTP ${response.status}: ${await response.text()}`);
115
+ }
116
+ const json = await response.json();
117
+ if (outputFormat === "full") {
118
+ result = json;
119
+ } else {
120
+ result = json.boolean;
121
+ }
122
+ break;
123
+ }
124
+
125
+ case "CONSTRUCT":
126
+ case "DESCRIBE": {
127
+ const response = await client.query.construct(query, { operation, ...fetchOpts });
128
+ if (!response.ok) {
129
+ throw new Error(`SPARQL endpoint returned HTTP ${response.status}: ${await response.text()}`);
130
+ }
131
+ result = await response.text();
132
+ break;
133
+ }
134
+
135
+ case "UPDATE": {
136
+ const response = await client.query.update(query, { operation, ...fetchOpts });
137
+ if (!response.ok) {
138
+ throw new Error(`SPARQL endpoint returned HTTP ${response.status}: ${await response.text()}`);
139
+ }
140
+ result = { success: true };
141
+ break;
142
+ }
143
+
144
+ default: {
145
+ // Fallback: send as SELECT
146
+ const response = await client.query.select(query, { operation, ...fetchOpts });
147
+ if (!response.ok) {
148
+ throw new Error(`SPARQL endpoint returned HTTP ${response.status}: ${await response.text()}`);
149
+ }
150
+ const json = await response.json();
151
+ if (outputFormat === "full") {
152
+ result = json;
153
+ } else if (outputFormat === "flat") {
154
+ result = json.results.bindings.map(row => {
155
+ const obj = {};
156
+ for (const key of Object.keys(row)) {
157
+ obj[key] = row[key].value;
158
+ }
159
+ return obj;
160
+ });
161
+ } else {
162
+ result = json.results.bindings;
163
+ }
164
+ }
165
+ }
166
+
167
+ msg.payload = result;
168
+ msg.queryType = queryType;
169
+ msg.sparqlQuery = query;
170
+
171
+ node.status({ fill: "green", shape: "dot", text: `${queryType} - ${Array.isArray(result) ? result.length + " results" : "done"}` });
172
+ send(msg);
173
+ done();
174
+ } catch (err) {
175
+ node.status({ fill: "red", shape: "ring", text: "error" });
176
+ done(err);
177
+ }
178
+ });
179
+
180
+ node.on("close", function (done) {
181
+ node.status({});
182
+ done();
183
+ });
184
+ }
185
+
186
+ RED.nodes.registerType("sparql-query", SparqlQueryNode);
187
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@vdwpsmt/node-red-contrib-sparql",
3
+ "version": "0.1.0",
4
+ "description": "A Node-RED node for executing SPARQL queries against SPARQL endpoints using the W3C SPARQL 1.1 Protocol",
5
+ "keywords": [
6
+ "node-red",
7
+ "sparql",
8
+ "rdf",
9
+ "linked-data",
10
+ "semantic-web",
11
+ "query"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "vdwpsmt",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/vdwpsmt/node-red-contrib-sparql.git"
18
+ },
19
+ "dependencies": {
20
+ "sparql-http-client": "^3.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "node-red": "^4.0.0",
24
+ "node-red-node-test-helper": "^0.3.0",
25
+ "mocha": "^10.0.0",
26
+ "should": "^13.2.3"
27
+ },
28
+ "scripts": {
29
+ "test": "mocha \"test/**/*_spec.js\" --timeout 10000"
30
+ },
31
+ "node-red": {
32
+ "version": ">=5.0.0",
33
+ "nodes": {
34
+ "sparql-query": "nodes/sparql-query.js",
35
+ "sparql-endpoint": "nodes/sparql-endpoint.js"
36
+ }
37
+ },
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ }
41
+ }
@@ -0,0 +1,54 @@
1
+ var helper = require("node-red-node-test-helper");
2
+ var sparqlQueryNode = require("../nodes/sparql-query.js");
3
+ var sparqlEndpointNode = require("../nodes/sparql-endpoint.js");
4
+
5
+ helper.init(require.resolve("node-red"));
6
+
7
+ describe("sparql-query Node", function () {
8
+ afterEach(function () {
9
+ helper.unload();
10
+ });
11
+
12
+ it("should be loaded", function (done) {
13
+ var flow = [{ id: "n1", type: "sparql-query", name: "test sparql" }];
14
+ helper.load([sparqlQueryNode, sparqlEndpointNode], flow, function () {
15
+ var n1 = helper.getNode("n1");
16
+ n1.should.have.property("name", "test sparql");
17
+ done();
18
+ });
19
+ });
20
+
21
+ it("should report error when no query is provided", function (done) {
22
+ var flow = [
23
+ { id: "n1", type: "sparql-query", name: "test sparql", endpoint: "e1", wires: [["n2"]] },
24
+ { id: "e1", type: "sparql-endpoint", endpoint: "https://dbpedia.org/sparql" },
25
+ { id: "n2", type: "helper" },
26
+ ];
27
+ helper.load([sparqlQueryNode, sparqlEndpointNode], flow, function () {
28
+ var n1 = helper.getNode("n1");
29
+ n1.receive({ payload: "" });
30
+ // Should produce an error, not crash
31
+ setTimeout(done, 500);
32
+ });
33
+ });
34
+
35
+ it("should execute a SELECT query against a SPARQL endpoint", function (done) {
36
+ this.timeout(30000);
37
+ var flow = [
38
+ { id: "n1", type: "sparql-query", name: "dbpedia", endpoint: "e1", wires: [["n2"]] },
39
+ { id: "e1", type: "sparql-endpoint", endpoint: "https://dbpedia.org/sparql" },
40
+ { id: "n2", type: "helper" },
41
+ ];
42
+ helper.load([sparqlQueryNode, sparqlEndpointNode], flow, function () {
43
+ var n2 = helper.getNode("n2");
44
+ var n1 = helper.getNode("n1");
45
+ n2.on("input", function (msg) {
46
+ msg.should.have.property("payload");
47
+ msg.payload.should.be.an.Array();
48
+ msg.should.have.property("queryType", "SELECT");
49
+ done();
50
+ });
51
+ n1.receive({ query: "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1" });
52
+ });
53
+ });
54
+ });