├── .github ├── dependabot.yaml └── workflows │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── ParsingClient.js ├── ParsingQuery.js ├── README.md ├── RawQuery.js ├── ResultParser.js ├── SimpleClient.js ├── StreamClient.js ├── StreamQuery.js ├── StreamStore.js ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── api.md └── index.html ├── examples ├── graph-store.js ├── query-to-graph-store.js ├── select-json.js ├── select-parsing.js ├── select-raw-post-urlencoded.js ├── select-raw.js └── select-stream.js ├── index.js ├── lib ├── asyncToReadabe.js ├── checkResponse.js ├── isDataFactory.js ├── isDatasetCoreFactory.js └── mergeHeaders.js ├── package.json └── test ├── ParsingClient.test.js ├── ParsingQuery.test.js ├── RawQuery.test.js ├── ResultParser.test.js ├── SimpleClient.test.js ├── StreamClient.test.js ├── StreamQuery.test.js ├── StreamStore.test.js ├── support ├── examples.js ├── isServerError.js ├── isSocketError.js ├── namespaces.js └── testFactory.js └── test.js /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: npm 8 | directory: / 9 | schedule: 10 | interval: daily 11 | versioning-strategy: increase-if-necessary 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | test: 7 | runs-on: ubuntu-24.04 8 | strategy: 9 | matrix: 10 | node: 11 | - '18' 12 | - '20' 13 | - '22' 14 | - '23' 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - run: npm install 21 | - run: npm test 22 | - uses: coverallsapp/github-action@v2 23 | with: 24 | flag-name: run Node v${{ matrix.node }} 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | parallel: true 27 | finally: 28 | needs: test 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - uses: coverallsapp/github-action@v2 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | parallel-finished: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.0.0] - 2024-02-18 9 | 10 | ### Added 11 | 12 | - ESM support 13 | - exports for all classes in the `index.js` package entrypoint 14 | 15 | ### Changed 16 | 17 | - options like `endpointUrl`, `user`, and `password` are attached to the client object, allowing creating new client 18 | instances from existing instances 19 | - methods that return a `Readable` stream objects are sync 20 | - updated dependencies 21 | 22 | ### Removed 23 | 24 | - CommonJS support 25 | - `BaseClient` and `Endpoint` class 26 | - automatic request splitting for Graph Store uploads 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thomas Bergwinkl 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. 22 | -------------------------------------------------------------------------------- /ParsingClient.js: -------------------------------------------------------------------------------- 1 | import DataModelFactory from '@rdfjs/data-model/Factory.js' 2 | import DatasetFactory from '@rdfjs/dataset/Factory.js' 3 | import Environment from '@rdfjs/environment' 4 | import isDatasetCoreFactory from './lib/isDatasetCoreFactory.js' 5 | import ParsingQuery from './ParsingQuery.js' 6 | import SimpleClient from './SimpleClient.js' 7 | 8 | const defaultFactory = new Environment([DataModelFactory, DatasetFactory]) 9 | 10 | /** 11 | * A client implementation based on {@link ParsingQuery} that parses SPARQL results into RDF/JS DatasetCore objects 12 | * (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). It does not provide a store interface. 13 | * 14 | * @extends SimpleClient 15 | * @property {ParsingQuery} query 16 | * 17 | * @example 18 | * // read the height of the Eiffel Tower from Wikidata with a SELECT query 19 | * 20 | * import ParsingClient from 'sparql-http-client/ParsingClient.js' 21 | * 22 | * const endpointUrl = 'https://query.wikidata.org/sparql' 23 | * const query = ` 24 | * PREFIX wd: 25 | * PREFIX p: 26 | * PREFIX ps: 27 | * PREFIX pq: 28 | * 29 | * SELECT ?value WHERE { 30 | * wd:Q243 p:P2048 ?height. 31 | * 32 | * ?height pq:P518 wd:Q24192182; 33 | * ps:P2048 ?value . 34 | * }` 35 | * 36 | * const client = new ParsingClient({ endpointUrl }) 37 | * const result = await client.query.select(query) 38 | * 39 | * for (const row of result) { 40 | * for (const [key, value] of Object.entries(row)) { 41 | * console.log(`${key}: ${value.value} (${value.termType})`) 42 | * } 43 | * } 44 | */ 45 | class ParsingClient extends SimpleClient { 46 | /** 47 | * @param {Object} options 48 | * @param {string} [options.endpointUrl] SPARQL query endpoint URL 49 | * @param {factory} [options.factory] RDF/JS factory 50 | * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation 51 | * @param {Headers} [options.headers] headers sent with every request 52 | * @param {string} [options.password] password used for basic authentication 53 | * @param {string} [options.storeUrl] SPARQL Graph Store URL 54 | * @param {string} [options.updateUrl] SPARQL update endpoint URL 55 | * @param {string} [options.user] user used for basic authentication 56 | */ 57 | constructor ({ 58 | endpointUrl, 59 | factory = defaultFactory, 60 | fetch, 61 | headers, 62 | password, 63 | storeUrl, 64 | updateUrl, 65 | user 66 | }) { 67 | super({ 68 | endpointUrl, 69 | factory, 70 | fetch, 71 | headers, 72 | password, 73 | storeUrl, 74 | updateUrl, 75 | user, 76 | Query: ParsingQuery 77 | }) 78 | 79 | if (!isDatasetCoreFactory(this.factory)) { 80 | throw new Error('the given factory doesn\'t implement the DatasetCoreFactory interface') 81 | } 82 | } 83 | } 84 | 85 | export default ParsingClient 86 | -------------------------------------------------------------------------------- /ParsingQuery.js: -------------------------------------------------------------------------------- 1 | import chunks from 'stream-chunks/chunks.js' 2 | import StreamQuery from './StreamQuery.js' 3 | 4 | /** 5 | * A query implementation that wraps the results of the {@link StreamQuery} into RDF/JS DatasetCore objects 6 | * (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). 7 | * 8 | * @extends StreamQuery 9 | */ 10 | class ParsingQuery extends StreamQuery { 11 | /** 12 | * Sends a request for a CONSTRUCT or DESCRIBE query 13 | * 14 | * @param {string} query CONSTRUCT or DESCRIBE query 15 | * @param {Object} options 16 | * @param {Headers} [options.headers] additional request headers 17 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 18 | * @return {Promise} 19 | */ 20 | async construct (query, { headers, operation } = {}) { 21 | const quads = await chunks(await super.construct(query, { headers, operation })) 22 | 23 | return this.client.factory.dataset(quads) 24 | } 25 | 26 | /** 27 | * Sends a request for a SELECT query 28 | * 29 | * @param {string} query SELECT query 30 | * @param {Object} [options] 31 | * @param {Headers} [options.headers] additional request headers 32 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 33 | * @return {Promise>>} 34 | */ 35 | async select (query, { headers, operation } = {}) { 36 | return chunks(await super.select(query, { headers, operation })) 37 | } 38 | } 39 | 40 | export default ParsingQuery 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sparql-http-client 2 | 3 | [![build status](https://img.shields.io/github/actions/workflow/status/rdf-ext/sparql-http-client/test.yaml?branch=master)](https://github.com/rdf-ext/sparql-http-client/actions/workflows/test.yaml) 4 | [![npm version](https://img.shields.io/npm/v/sparql-http-client.svg)](https://www.npmjs.com/package/sparql-http-client) 5 | 6 | SPARQL client for easier handling of SPARQL Queries and Graph Store requests. 7 | The [SPARQL Protocol](https://www.w3.org/TR/sparql11-protocol/) is used for [SPARQL Queries](https://www.w3.org/TR/sparql11-query/) and [SPARQL Updates](https://www.w3.org/TR/sparql11-update/). 8 | The [SPARQL Graph Store Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/) is used to manage Named Graphs. 9 | 10 | It provides client implementations in different flavors. 11 | The default client comes with an interface for [streams](https://github.com/nodejs/readable-stream), the simple client is closer to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), and the parsing client wraps the results directly into [RDF/JS DatasetCore](https://rdf.js.org/dataset-spec/#datasetcore-interface) objects or arrays. 12 | 13 | ## Usage 14 | 15 | The example below shows how to use the client to run a `SELECT` query against the Wikidata endpoint. 16 | Check the [documentation](https://rdf-ext.github.io/sparql-http-client/) for more details. 17 | 18 | ```javascript 19 | import SparqlClient from 'sparql-http-client' 20 | 21 | const endpointUrl = 'https://query.wikidata.org/sparql' 22 | const query = ` 23 | PREFIX wd: 24 | PREFIX p: 25 | PREFIX ps: 26 | PREFIX pq: 27 | 28 | SELECT ?value WHERE { 29 | wd:Q243 p:P2048 ?height. 30 | 31 | ?height pq:P518 wd:Q24192182; 32 | ps:P2048 ?value . 33 | }` 34 | 35 | const client = new SparqlClient({ endpointUrl }) 36 | const stream = client.query.select(query) 37 | 38 | stream.on('data', row => { 39 | for (const [key, value] of Object.entries(row)) { 40 | console.log(`${key}: ${value.value} (${value.termType})`) 41 | } 42 | }) 43 | 44 | stream.on('error', err => { 45 | console.error(err) 46 | }) 47 | ``` 48 | -------------------------------------------------------------------------------- /RawQuery.js: -------------------------------------------------------------------------------- 1 | import mergeHeaders from './lib/mergeHeaders.js' 2 | 3 | /** 4 | * A query implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. 5 | */ 6 | class RawQuery { 7 | /** 8 | * @param {Object} options 9 | * @param {SimpleClient} options.client client that provides the HTTP I/O 10 | */ 11 | constructor ({ client }) { 12 | this.client = client 13 | } 14 | 15 | /** 16 | * Sends a request for a ASK query 17 | * 18 | * @param {string} query ASK query 19 | * @param {Object} [options] 20 | * @param {Headers} [options.headers] additional request headers 21 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 22 | * @return {Promise} 23 | */ 24 | async ask (query, { headers, operation = 'get' } = {}) { 25 | headers = mergeHeaders(headers) 26 | 27 | if (!headers.has('accept')) { 28 | headers.set('accept', 'application/sparql-results+json') 29 | } 30 | 31 | return this.client[operation](query, { headers }) 32 | } 33 | 34 | /** 35 | * Sends a request for a CONSTRUCT or DESCRIBE query 36 | * 37 | * @param {string} query CONSTRUCT or DESCRIBE query 38 | * @param {Object} [options] 39 | * @param {Headers} [options.headers] additional request headers 40 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 41 | * @return {Promise} 42 | */ 43 | async construct (query, { headers, operation = 'get' } = {}) { 44 | headers = mergeHeaders(headers) 45 | 46 | if (!headers.has('accept')) { 47 | headers.set('accept', 'application/n-triples') 48 | } 49 | 50 | return this.client[operation](query, { headers }) 51 | } 52 | 53 | /** 54 | * Sends a request for a SELECT query 55 | * 56 | * @param {string} query SELECT query 57 | * @param {Object} [options] 58 | * @param {Headers} [options.headers] additional request headers 59 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 60 | * @return {Promise} 61 | */ 62 | async select (query, { headers, operation = 'get' } = {}) { 63 | headers = mergeHeaders(headers) 64 | 65 | if (!headers.has('accept')) { 66 | headers.set('accept', 'application/sparql-results+json') 67 | } 68 | 69 | return this.client[operation](query, { headers }) 70 | } 71 | 72 | /** 73 | * Sends a request for an update query 74 | * 75 | * @param {string} query update query 76 | * @param {Object} [options] 77 | * @param {Headers} [options.headers] additional request headers 78 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='postUrlencoded'] SPARQL Protocol operation 79 | * @return {Promise} 80 | */ 81 | async update (query, { headers, operation = 'postUrlencoded' } = {}) { 82 | headers = mergeHeaders(headers) 83 | 84 | if (!headers.has('accept')) { 85 | headers.set('accept', '*/*') 86 | } 87 | 88 | return this.client[operation](query, { headers, update: true }) 89 | } 90 | } 91 | 92 | export default RawQuery 93 | -------------------------------------------------------------------------------- /ResultParser.js: -------------------------------------------------------------------------------- 1 | import JsonParser from '@bergos/jsonparse' 2 | import { Transform } from 'readable-stream' 3 | 4 | /** 5 | * A Transform stream that parses JSON SPARQL results and emits one object per row with the variable names as keys and 6 | * RDF/JS terms as values. 7 | */ 8 | class ResultParser extends Transform { 9 | /** 10 | * @param {Object} options 11 | * @param {DataFactory} options.factory RDF/JS DataFactory used to create the quads and terms 12 | */ 13 | constructor ({ factory }) { 14 | super({ 15 | readableObjectMode: true 16 | }) 17 | 18 | this.factory = factory 19 | this.jsonParser = new JsonParser() 20 | this.jsonParser.onError = err => this.destroy(err) 21 | this.jsonParser.onValue = value => this.onValue(value) 22 | } 23 | 24 | _write (chunk, encoding, callback) { 25 | this.jsonParser.write(chunk) 26 | 27 | callback() 28 | } 29 | 30 | onValue (raw) { 31 | if (this.jsonParser.stack.length !== 3) { 32 | return 33 | } 34 | 35 | if (this.jsonParser.stack[1].key !== 'results' || this.jsonParser.stack[2].key !== 'bindings') { 36 | return 37 | } 38 | 39 | if (Object.keys(raw).length === 0) { 40 | return 41 | } 42 | 43 | const row = {} 44 | 45 | for (const [key, value] of Object.entries(raw)) { 46 | row[key] = this.valueToTerm(value) 47 | } 48 | 49 | this.push(row) 50 | } 51 | 52 | valueToTerm (value) { 53 | if (value.type === 'uri') { 54 | return this.factory.namedNode(value.value) 55 | } 56 | 57 | if (value.type === 'bnode') { 58 | return this.factory.blankNode(value.value) 59 | } 60 | 61 | if (value.type === 'literal' || value.type === 'typed-literal') { 62 | const datatype = (value.datatype && this.factory.namedNode(value.datatype)) 63 | 64 | return this.factory.literal(value.value, datatype || value['xml:lang']) 65 | } 66 | 67 | return null 68 | } 69 | } 70 | 71 | export default ResultParser 72 | -------------------------------------------------------------------------------- /SimpleClient.js: -------------------------------------------------------------------------------- 1 | import defaultFetch from 'nodeify-fetch' 2 | import mergeHeaders from './lib/mergeHeaders.js' 3 | import RawQuery from './RawQuery.js' 4 | 5 | /** 6 | * A client implementation based on {@link RawQuery} that prepares URLs and headers for SPARQL queries and returns the 7 | * raw fetch response. It does not provide a store interface. 8 | * 9 | * @property {RawQuery} query 10 | * @property {string} endpointUrl 11 | * @property {RawQuery} factory 12 | * @property {factory} fetch 13 | * @property {Headers} headers 14 | * @property {string} password 15 | * @property {string} storeUrl 16 | * @property {string} updateUrl 17 | * @property {string} user 18 | * @property {string} updateUrl 19 | * 20 | * @example 21 | * // read the height of the Eiffel Tower from Wikidata with a SELECT query 22 | * 23 | * import SparqlClient from 'sparql-http-client/SimpleClient.js' 24 | * 25 | * const endpointUrl = 'https://query.wikidata.org/sparql' 26 | * const query = ` 27 | * PREFIX wd: 28 | * PREFIX p: 29 | * PREFIX ps: 30 | * PREFIX pq: 31 | * 32 | * SELECT ?value WHERE { 33 | * wd:Q243 p:P2048 ?height. 34 | * 35 | * ?height pq:P518 wd:Q24192182; 36 | * ps:P2048 ?value . 37 | * }` 38 | * 39 | * const client = new SparqlClient({ endpointUrl }) 40 | * const res = await client.query.select(query) 41 | * 42 | * if (!res.ok) { 43 | * return console.error(res.statusText) 44 | * } 45 | * 46 | * const content = await res.json() 47 | * 48 | * console.log(JSON.stringify(content, null, 2)) 49 | */ 50 | class SimpleClient { 51 | /** 52 | * @param {Object} options 53 | * @param {string} [options.endpointUrl] SPARQL query endpoint URL 54 | * @param {factory} [options.factory] RDF/JS factory 55 | * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation 56 | * @param {Headers} [options.headers] headers sent with every request 57 | * @param {string} [options.password] password used for basic authentication 58 | * @param {string} [options.storeUrl] SPARQL Graph Store URL 59 | * @param {string} [options.updateUrl] SPARQL update endpoint URL 60 | * @param {string} [options.user] user used for basic authentication 61 | * @param {Query} [options.Query] Constructor of a query implementation 62 | * @param {Store} [options.Store] Constructor of a store implementation 63 | */ 64 | constructor ({ 65 | endpointUrl, 66 | factory, 67 | fetch = defaultFetch, 68 | headers, 69 | password, 70 | storeUrl, 71 | updateUrl, 72 | user, 73 | Query = RawQuery, 74 | Store 75 | }) { 76 | if (!endpointUrl && !storeUrl && !updateUrl) { 77 | throw new Error('no endpointUrl, storeUrl, or updateUrl given') 78 | } 79 | 80 | this.endpointUrl = endpointUrl 81 | this.factory = factory 82 | this.fetch = fetch 83 | this.headers = new Headers(headers) 84 | this.password = password 85 | this.storeUrl = storeUrl 86 | this.updateUrl = updateUrl 87 | this.user = user 88 | this.query = Query ? new Query({ client: this }) : null 89 | this.store = Store ? new Store({ client: this }) : null 90 | 91 | if (typeof user === 'string' && typeof password === 'string') { 92 | this.headers.set('authorization', `Basic ${btoa(`${user}:${password}`)}`) 93 | } 94 | } 95 | 96 | /** 97 | * Sends a GET request as defined in the 98 | * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get SPARQL Protocol specification}. 99 | * 100 | * @param {string} query SPARQL query 101 | * @param {Object} options 102 | * @param {Headers} [options.headers] additional request headers 103 | * @param {boolean} [options.update=false] send the request to the updateUrl 104 | * @return {Promise} 105 | */ 106 | async get (query, { headers, update = false } = {}) { 107 | let url = null 108 | 109 | if (!update) { 110 | url = new URL(this.endpointUrl) 111 | url.searchParams.append('query', query) 112 | } else { 113 | url = new URL(this.updateUrl) 114 | url.searchParams.append('update', query) 115 | } 116 | 117 | return this.fetch(url.toString().replace(/\+/g, '%20'), { 118 | method: 'GET', 119 | headers: mergeHeaders(this.headers, headers) 120 | }) 121 | } 122 | 123 | /** 124 | * Sends a POST directly request as defined in the 125 | * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct SPARQL Protocol specification}. 126 | * 127 | * 128 | * @param {string} query SPARQL query 129 | * @param {Object} options 130 | * @param {Headers} [options.headers] additional request headers 131 | * @param {boolean} [options.update=false] send the request to the updateUrl 132 | * @return {Promise} 133 | */ 134 | async postDirect (query, { headers, update = false } = {}) { 135 | let url = null 136 | 137 | if (!update) { 138 | url = new URL(this.endpointUrl) 139 | } else { 140 | url = new URL(this.updateUrl) 141 | } 142 | 143 | headers = mergeHeaders(this.headers, headers) 144 | 145 | if (!headers.has('content-type')) { 146 | headers.set('content-type', 'application/sparql-query; charset=utf-8') 147 | } 148 | 149 | return this.fetch(url, { 150 | method: 'POST', 151 | headers, 152 | body: query 153 | }) 154 | } 155 | 156 | /** 157 | * Sends a POST URL-encoded request as defined in the 158 | * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded SPARQL Protocol specification}. 159 | * 160 | * @param {string} query SPARQL query 161 | * @param {Object} options 162 | * @param {Headers} [options.headers] additional request headers 163 | * @param {boolean} [options.update=false] send the request to the updateUrl 164 | * @return {Promise} 165 | */ 166 | async postUrlencoded (query, { headers, update = false } = {}) { 167 | let url = null 168 | let body = null 169 | 170 | if (!update) { 171 | url = new URL(this.endpointUrl) 172 | body = `query=${encodeURIComponent(query)}` 173 | } else { 174 | url = new URL(this.updateUrl) 175 | body = `update=${encodeURIComponent(query)}` 176 | } 177 | 178 | headers = mergeHeaders(this.headers, headers) 179 | 180 | if (!headers.has('content-type')) { 181 | headers.set('content-type', 'application/x-www-form-urlencoded') 182 | } 183 | 184 | return this.fetch(url, { 185 | method: 'POST', 186 | headers, 187 | body 188 | }) 189 | } 190 | } 191 | 192 | export default SimpleClient 193 | -------------------------------------------------------------------------------- /StreamClient.js: -------------------------------------------------------------------------------- 1 | import defaultFactory from '@rdfjs/data-model' 2 | import isDataFactory from './lib/isDataFactory.js' 3 | import SimpleClient from './SimpleClient.js' 4 | import StreamQuery from './StreamQuery.js' 5 | import StreamStore from './StreamStore.js' 6 | 7 | /** 8 | * The default client implementation based on {@link StreamQuery} and {@link StreamStore} parses SPARQL results into 9 | * Readable streams of RDF/JS Quad objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). Graph Store 10 | * read and write operations are handled using Readable streams. 11 | * 12 | * @extends SimpleClient 13 | * @property {StreamQuery} query 14 | * @property {StreamStore} store 15 | * 16 | * @example 17 | * // read the height of the Eiffel Tower from Wikidata with a SELECT query 18 | * 19 | * import SparqlClient from 'sparql-http-client' 20 | * 21 | * const endpointUrl = 'https://query.wikidata.org/sparql' 22 | * const query = ` 23 | * PREFIX wd: 24 | * PREFIX p: 25 | * PREFIX ps: 26 | * PREFIX pq: 27 | * 28 | * SELECT ?value WHERE { 29 | * wd:Q243 p:P2048 ?height. 30 | * 31 | * ?height pq:P518 wd:Q24192182; 32 | * ps:P2048 ?value . 33 | * }` 34 | * 35 | * const client = new SparqlClient({ endpointUrl }) 36 | * const stream = client.query.select(query) 37 | * 38 | * stream.on('data', row => { 39 | * for (const [key, value] of Object.entries(row)) { 40 | * console.log(`${key}: ${value.value} (${value.termType})`) 41 | * } 42 | * }) 43 | * 44 | * stream.on('error', err => { 45 | * console.error(err) 46 | * }) 47 | * 48 | * @example 49 | * // read all quads from a local triplestore using the Graph Store protocol 50 | * 51 | * import rdf from 'rdf-ext' 52 | * import SparqlClient from 'sparql-http-client' 53 | * 54 | * const client = new SparqlClient({ 55 | * storeUrl: 'http://localhost:3030/test/data', 56 | * factory: rdf 57 | * }) 58 | * 59 | * const stream = local.store.get(rdf.defaultGraph()) 60 | * 61 | * stream.on('data', quad => { 62 | * console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) 63 | * }) 64 | */ 65 | class StreamClient extends SimpleClient { 66 | /** 67 | * @param {Object} options 68 | * @param {string} [options.endpointUrl] SPARQL query endpoint URL 69 | * @param {factory} [options.factory] RDF/JS factory 70 | * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation 71 | * @param {Headers} [options.headers] headers sent with every request 72 | * @param {string} [options.password] password used for basic authentication 73 | * @param {string} [options.storeUrl] SPARQL Graph Store URL 74 | * @param {string} [options.updateUrl] SPARQL update endpoint URL 75 | * @param {string} [options.user] user used for basic authentication 76 | */ 77 | constructor ({ 78 | endpointUrl, 79 | factory = defaultFactory, 80 | fetch, 81 | headers, 82 | password, 83 | storeUrl, 84 | updateUrl, 85 | user 86 | }) { 87 | super({ 88 | endpointUrl, 89 | factory, 90 | fetch, 91 | headers, 92 | password, 93 | storeUrl, 94 | updateUrl, 95 | user, 96 | Query: StreamQuery, 97 | Store: StreamStore 98 | }) 99 | 100 | if (!isDataFactory(this.factory)) { 101 | throw new Error('the given factory doesn\'t implement the DataFactory interface') 102 | } 103 | } 104 | } 105 | 106 | export default StreamClient 107 | -------------------------------------------------------------------------------- /StreamQuery.js: -------------------------------------------------------------------------------- 1 | import N3Parser from '@rdfjs/parser-n3' 2 | import asyncToReadabe from './lib/asyncToReadabe.js' 3 | import checkResponse from './lib/checkResponse.js' 4 | import mergeHeaders from './lib/mergeHeaders.js' 5 | import RawQuery from './RawQuery.js' 6 | import ResultParser from './ResultParser.js' 7 | 8 | /** 9 | * A query implementation based on {@link RawQuery} that parses SPARQL results into Readable streams of RDF/JS Quad 10 | * objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). 11 | * 12 | * @extends RawQuery 13 | */ 14 | class StreamQuery extends RawQuery { 15 | /** 16 | * Sends a request for a ASK query 17 | * 18 | * @param {string} query ASK query 19 | * @param {Object} [options] 20 | * @param {Headers} [options.headers] additional request headers 21 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 22 | * @return {Promise} 23 | */ 24 | async ask (query, { headers, operation } = {}) { 25 | const res = await super.ask(query, { headers, operation }) 26 | 27 | await checkResponse(res) 28 | 29 | const json = await res.json() 30 | 31 | return json.boolean 32 | } 33 | 34 | /** 35 | * Sends a request for a CONSTRUCT or DESCRIBE query 36 | * 37 | * @param {string} query CONSTRUCT or DESCRIBE query 38 | * @param {Object} [options] 39 | * @param {Headers} [options.headers] additional request headers 40 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 41 | * @return {Readable} 42 | */ 43 | construct (query, { headers, operation } = {}) { 44 | return asyncToReadabe(async () => { 45 | headers = mergeHeaders(headers) 46 | 47 | if (!headers.has('accept')) { 48 | headers.set('accept', 'application/n-triples, text/turtle') 49 | } 50 | 51 | const res = await super.construct(query, { headers, operation }) 52 | 53 | await checkResponse(res) 54 | 55 | const parser = new N3Parser({ factory: this.client.factory }) 56 | 57 | return parser.import(res.body) 58 | }) 59 | } 60 | 61 | /** 62 | * Sends a request for a SELECT query 63 | * 64 | * @param {string} query SELECT query 65 | * @param {Object} [options] 66 | * @param {Headers} [options.headers] additional request headers 67 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation 68 | * @return {Readable} 69 | */ 70 | select (query, { headers, operation } = {}) { 71 | return asyncToReadabe(async () => { 72 | const res = await super.select(query, { headers, operation }) 73 | 74 | await checkResponse(res) 75 | 76 | const parser = new ResultParser({ factory: this.client.factory }) 77 | 78 | return res.body.pipe(parser) 79 | }) 80 | } 81 | 82 | /** 83 | * Sends a request for an update query 84 | * 85 | * @param {string} query update query 86 | * @param {Object} [options] 87 | * @param {Headers} [options.headers] additional request headers 88 | * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='postUrlencoded'] SPARQL Protocol operation 89 | * @return {Promise} 90 | */ 91 | async update (query, { headers, operation } = {}) { 92 | const res = await super.update(query, { headers, operation }) 93 | 94 | await checkResponse(res) 95 | } 96 | } 97 | 98 | export default StreamQuery 99 | -------------------------------------------------------------------------------- /StreamStore.js: -------------------------------------------------------------------------------- 1 | import N3Parser from '@rdfjs/parser-n3' 2 | import toNT from '@rdfjs/to-ntriples' 3 | import TripleToQuadTransform from 'rdf-transform-triple-to-quad' 4 | import { Transform } from 'readable-stream' 5 | import asyncToReadabe from './lib/asyncToReadabe.js' 6 | import checkResponse from './lib/checkResponse.js' 7 | import mergeHeaders from './lib/mergeHeaders.js' 8 | 9 | /** 10 | * A store implementation that parses and serializes SPARQL Graph Store responses and requests into/from Readable 11 | * streams. 12 | */ 13 | class StreamStore { 14 | /** 15 | * @param {Object} options 16 | * @param {SimpleClient} options.client client that provides the HTTP I/O 17 | */ 18 | constructor ({ client }) { 19 | this.client = client 20 | } 21 | 22 | /** 23 | * Sends a GET request to the Graph Store 24 | * 25 | * @param {NamedNode} [graph] source graph 26 | * @return {Promise} 27 | */ 28 | get (graph) { 29 | return this.read({ method: 'GET', graph }) 30 | } 31 | 32 | /** 33 | * Sends a POST request to the Graph Store 34 | * 35 | * @param {Readable} stream triples/quads to write 36 | * @param {Object} [options] 37 | * @param {Term} [options.graph] target graph 38 | * @return {Promise} 39 | */ 40 | async post (stream, { graph } = {}) { 41 | return this.write({ graph, method: 'POST', stream }) 42 | } 43 | 44 | /** 45 | * Sends a PUT request to the Graph Store 46 | * 47 | * @param {Readable} stream triples/quads to write 48 | * @param {Object} [options] 49 | * @param {Term} [options.graph] target graph 50 | * @return {Promise} 51 | */ 52 | async put (stream, { graph } = {}) { 53 | return this.write({ graph, method: 'PUT', stream }) 54 | } 55 | 56 | /** 57 | * Generic read request to the Graph Store 58 | * 59 | * @param {Object} [options] 60 | * @param {Term} [options.graph] source graph 61 | * @param {string} options.method HTTP method 62 | * @returns {Readable} 63 | */ 64 | read ({ graph, method }) { 65 | return asyncToReadabe(async () => { 66 | const url = new URL(this.client.storeUrl) 67 | 68 | if (graph && graph.termType !== 'DefaultGraph') { 69 | url.searchParams.append('graph', graph.value) 70 | } else { 71 | url.searchParams.append('default', '') 72 | } 73 | 74 | const res = await this.client.fetch(url, { 75 | method, 76 | headers: mergeHeaders(this.client.headers, { accept: 'application/n-triples' }) 77 | }) 78 | 79 | await checkResponse(res) 80 | 81 | const parser = new N3Parser({ factory: this.client.factory }) 82 | const tripleToQuad = new TripleToQuadTransform(graph, { factory: this.client.factory }) 83 | 84 | return parser.import(res.body).pipe(tripleToQuad) 85 | }) 86 | } 87 | 88 | /** 89 | * Generic write request to the Graph Store 90 | * 91 | * @param {Object} [options] 92 | * @param {Term} [graph] target graph 93 | * @param {string} method HTTP method 94 | * @param {Readable} stream triples/quads to write 95 | * @returns {Promise} 96 | */ 97 | async write ({ graph, method, stream }) { 98 | const url = new URL(this.client.storeUrl) 99 | 100 | if (graph && graph.termType !== 'DefaultGraph') { 101 | url.searchParams.append('graph', graph.value) 102 | } else { 103 | url.searchParams.append('default', '') 104 | } 105 | 106 | const serialize = new Transform({ 107 | writableObjectMode: true, 108 | transform (quad, encoding, callback) { 109 | const triple = { 110 | subject: quad.subject, 111 | predicate: quad.predicate, 112 | object: quad.object, 113 | graph: { termType: 'DefaultGraph' } 114 | } 115 | 116 | callback(null, `${toNT(triple)}\n`) 117 | } 118 | }) 119 | 120 | const res = await this.client.fetch(url, { 121 | method, 122 | headers: mergeHeaders(this.client.headers, { 'content-type': 'application/n-triples' }), 123 | body: stream.pipe(serialize), 124 | duplex: 'half' 125 | }) 126 | 127 | await checkResponse(res) 128 | } 129 | } 130 | 131 | export default StreamStore 132 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdf-ext/sparql-http-client/8064ce84f88b3b8f6d5c16a84ba1acd8c1d569f4/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # sparql-http-client 2 | 3 | [![build status](https://img.shields.io/github/actions/workflow/status/rdf-ext/sparql-http-client/test.yaml?branch=master)](https://github.com/rdf-ext/sparql-http-client/actions/workflows/test.yaml) 4 | [![npm version](https://img.shields.io/npm/v/sparql-http-client.svg)](https://www.npmjs.com/package/sparql-http-client) 5 | 6 | This package provides a handful of clients that can talk to SPARQL endpoints using Query, Update, and Graph Store Protocols. 7 | 8 | - [`StreamClient`](stream-client.md): The default client implementation parses SPARQL results into Readable streams of RDF/JS Quad objects (`CONSTRUCT`/`DESCRIBE`) or Readable streams of objects (`SELECT`). 9 | Graph Store read and write operations are handled using Readable streams. 10 | - [`ParsingClient`](parsing-client.md): A client implementation that parses SPARQL results into RDF/JS DatasetCore objects (`CONSTRUCT`/`DESCRIBE`) or an array of objects (`SELECT`). 11 | It does not provide a store interface. 12 | - [`SimpleClient`](simple-client.md): A client implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. 13 | It does not provide a store interface. 14 | 15 | [`StreamClient`](stream-client.md) is the default export of the package. 16 | 17 | ## Usage 18 | 19 | ### Creating a client 20 | 21 | The constructor of all clients accepts a single object argument with the following properties: 22 | 23 | | Param | Type | Default | Description | 24 | |-------------|-----------------------|----------------------------------------|---------------------------------------------| 25 | | endpointUrl | string | | SPARQL query endpoint URL | 26 | | updateUrl | string | | SPARQL update endpoint URL | 27 | | storeUrl | string | | SPARQL Graph Store URL | 28 | | user | string | | user used for basic authentication | 29 | | password | string | | password used for basic authentication | 30 | | headers | Headers | | headers sent with every request | 31 | | factory | factory | `@rdfjs/data-model` & `@rdfjs/dataset` | RDF/JS factory | 32 | | fetch | fetch | `nodeify-fetch` | fetch implementation | 33 | 34 | At least one URL argument must be given. 35 | Besides that, all properties are optional, but omitting some of them will disable certain capabilities. 36 | Also, not all properties are supported by all implementations. Check their respective pages. 37 | 38 | A client object has all properties attached required to create a new instance. 39 | It is, therefore, possible to create a new client object of a different type based on an existing client object: 40 | 41 | ```javascript 42 | const simpleClient = new SimpleClient({ endpointUrl }) 43 | const parsingClient = new ParsingClient(simpleClient) 44 | ``` 45 | 46 | ### Examples 47 | 48 | The [API](api.md) section contains examples for all clients. 49 | 50 | ### Queries 51 | 52 | All methods for SPARQL Queries and Updates are attached to the instance property `query`. 53 | Their return types are implementation-specific. 54 | See the [API](api.md) section for more details on the individual methods. 55 | 56 | ### Graph Store 57 | 58 | All methods for SPARQL Graph Store are attached to the instance property `store`. 59 | See the [API](api.md) section for more details on the individual methods. 60 | 61 | ### Advanced Topics 62 | 63 | #### Headers 64 | 65 | HTTP requests to the SPARQL endpoint can have additional headers added to them. 66 | For example, to pass authorization information. 67 | 68 | One method for doing so is to set headers on the method call: 69 | 70 | ```javascript 71 | const client = new SparqlClient({ endpointUrl: 'https://query.wikidata.org/sparql' }) 72 | 73 | client.query.select(query, { 74 | headers: { 75 | Authorization: 'Bearer token' 76 | } 77 | }) 78 | ``` 79 | 80 | It is also possible to set headers in the constructor of the client. 81 | 82 | The headers will be sent on all requests originating from the instance of the client: 83 | 84 | ```javascript 85 | const client = new SparqlClient({ 86 | endpointUrl: 'https://query.wikidata.org/sparql', 87 | headers: { 88 | Authorization: 'Bearer token' 89 | } 90 | }) 91 | ``` 92 | 93 | #### Operation 94 | 95 | SPARQL queries and updates over the SPARQL Protocol can be done with different [operations](https://www.w3.org/TR/sparql11-protocol/#protocol). 96 | By default, all read queries use `get`, and updates use `postUrlencoded`. 97 | Very long queries may exceed the maximum request header length. 98 | For those cases, it's useful to switch to operations that use a `POST` request. 99 | This can be done by the optional `operation` argument. 100 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * sparql-http-client 2 | * [About](/) 3 | * [API](api.md) 4 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 |
4 |
ParsingClientSimpleClient
5 |

A client implementation based on ParsingQuery that parses SPARQL results into RDF/JS DatasetCore objects 6 | (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). It does not provide a store interface.

7 |
8 |
ParsingQueryStreamQuery
9 |

A query implementation that wraps the results of the StreamQuery into RDF/JS DatasetCore objects 10 | (CONSTRUCT/DESCRIBE) or an array of objects (SELECT).

11 |
12 |
RawQuery
13 |

A query implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response.

14 |
15 |
ResultParser
16 |

A Transform stream that parses JSON SPARQL results and emits one object per row with the variable names as keys and 17 | RDF/JS terms as values.

18 |
19 |
SimpleClient
20 |

A client implementation based on RawQuery that prepares URLs and headers for SPARQL queries and returns the 21 | raw fetch response. It does not provide a store interface.

22 |
23 |
StreamClientSimpleClient
24 |

The default client implementation based on StreamQuery and StreamStore parses SPARQL results into 25 | Readable streams of RDF/JS Quad objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). Graph Store 26 | read and write operations are handled using Readable streams.

27 |
28 |
StreamQueryRawQuery
29 |

A query implementation based on RawQuery that parses SPARQL results into Readable streams of RDF/JS Quad 30 | objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT).

31 |
32 |
StreamStore
33 |

A store implementation that parses and serializes SPARQL Graph Store responses and requests into/from Readable 34 | streams.

35 |
36 |
37 | 38 | 39 | 40 | ## ParsingClient ⇐ [SimpleClient](#SimpleClient) 41 | A client implementation based on [ParsingQuery](#ParsingQuery) that parses SPARQL results into RDF/JS DatasetCore objects 42 | (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). It does not provide a store interface. 43 | 44 | **Kind**: global class 45 | **Extends**: [SimpleClient](#SimpleClient) 46 | **Properties** 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
NameType
queryParsingQuery
59 | 60 | 61 | * [ParsingClient](#ParsingClient) ⇐ [SimpleClient](#SimpleClient) 62 | * [new ParsingClient(options)](#new_ParsingClient_new) 63 | * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> 64 | * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> 65 | * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> 66 | 67 | 68 | 69 | ### new ParsingClient(options) 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 94 | 95 | 97 | 98 | 100 | 101 | 103 | 104 |
ParamTypeDefaultDescription
optionsObject
[options.endpointUrl]string

SPARQL query endpoint URL

81 |
[options.factory]factory

RDF/JS factory

84 |
[options.fetch]fetchnodeify-fetch

fetch implementation

87 |
[options.headers]Headers

headers sent with every request

90 |
[options.password]string

password used for basic authentication

93 |
[options.storeUrl]string

SPARQL Graph Store URL

96 |
[options.updateUrl]string

SPARQL update endpoint URL

99 |
[options.user]string

user used for basic authentication

102 |
105 | 106 | **Example** 107 | ```js 108 | // read the height of the Eiffel Tower from Wikidata with a SELECT query 109 | 110 | import ParsingClient from 'sparql-http-client/ParsingClient.js' 111 | 112 | const endpointUrl = 'https://query.wikidata.org/sparql' 113 | const query = ` 114 | PREFIX wd: 115 | PREFIX p: 116 | PREFIX ps: 117 | PREFIX pq: 118 | 119 | SELECT ?value WHERE { 120 | wd:Q243 p:P2048 ?height. 121 | 122 | ?height pq:P518 wd:Q24192182; 123 | ps:P2048 ?value . 124 | }` 125 | 126 | const client = new ParsingClient({ endpointUrl }) 127 | const result = await client.query.select(query) 128 | 129 | for (const row of result) { 130 | for (const [key, value] of Object.entries(row)) { 131 | console.log(`${key}: ${value.value} (${value.termType})`) 132 | } 133 | } 134 | ``` 135 | 136 | 137 | ### parsingClient.get(query, options) ⇒ Promise.<Response> 138 | Sends a GET request as defined in the 139 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). 140 | 141 | **Kind**: instance method of [ParsingClient](#ParsingClient) 142 | **Overrides**: [get](#SimpleClient+get) 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 153 | 154 | 155 | 156 | 158 | 159 | 161 | 162 |
ParamTypeDefaultDescription
querystring

SPARQL query

152 |
optionsObject
[options.headers]Headers

additional request headers

157 |
[options.update]booleanfalse

send the request to the updateUrl

160 |
163 | 164 | 165 | 166 | ### parsingClient.postDirect(query, options) ⇒ Promise.<Response> 167 | Sends a POST directly request as defined in the 168 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). 169 | 170 | **Kind**: instance method of [ParsingClient](#ParsingClient) 171 | **Overrides**: [postDirect](#SimpleClient+postDirect) 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 182 | 183 | 184 | 185 | 187 | 188 | 190 | 191 |
ParamTypeDefaultDescription
querystring

SPARQL query

181 |
optionsObject
[options.headers]Headers

additional request headers

186 |
[options.update]booleanfalse

send the request to the updateUrl

189 |
192 | 193 | 194 | 195 | ### parsingClient.postUrlencoded(query, options) ⇒ Promise.<Response> 196 | Sends a POST URL-encoded request as defined in the 197 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). 198 | 199 | **Kind**: instance method of [ParsingClient](#ParsingClient) 200 | **Overrides**: [postUrlencoded](#SimpleClient+postUrlencoded) 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 216 | 217 | 219 | 220 |
ParamTypeDefaultDescription
querystring

SPARQL query

210 |
optionsObject
[options.headers]Headers

additional request headers

215 |
[options.update]booleanfalse

send the request to the updateUrl

218 |
221 | 222 | 223 | 224 | ## ParsingQuery ⇐ [StreamQuery](#StreamQuery) 225 | A query implementation that wraps the results of the [StreamQuery](#StreamQuery) into RDF/JS DatasetCore objects 226 | (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). 227 | 228 | **Kind**: global class 229 | **Extends**: [StreamQuery](#StreamQuery) 230 | 231 | * [ParsingQuery](#ParsingQuery) ⇐ [StreamQuery](#StreamQuery) 232 | * [.construct(query, options)](#ParsingQuery+construct) ⇒ Promise.<DatasetCore> 233 | * [.select(query, [options])](#ParsingQuery+select) ⇒ Promise.<Array.<Object.<string, Term>>> 234 | * [.ask(query, [options])](#StreamQuery+ask) ⇒ Promise.<boolean> 235 | * [.update(query, [options])](#StreamQuery+update) ⇒ Promise.<void> 236 | 237 | 238 | 239 | ### parsingQuery.construct(query, options) ⇒ Promise.<DatasetCore> 240 | Sends a request for a CONSTRUCT or DESCRIBE query 241 | 242 | **Kind**: instance method of [ParsingQuery](#ParsingQuery) 243 | **Overrides**: [construct](#StreamQuery+construct) 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 254 | 255 | 256 | 257 | 259 | 260 | 262 | 263 |
ParamTypeDefaultDescription
querystring

CONSTRUCT or DESCRIBE query

253 |
optionsObject
[options.headers]Headers

additional request headers

258 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

261 |
264 | 265 | 266 | 267 | ### parsingQuery.select(query, [options]) ⇒ Promise.<Array.<Object.<string, Term>>> 268 | Sends a request for a SELECT query 269 | 270 | **Kind**: instance method of [ParsingQuery](#ParsingQuery) 271 | **Overrides**: [select](#StreamQuery+select) 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 282 | 283 | 284 | 285 | 287 | 288 | 290 | 291 |
ParamTypeDefaultDescription
querystring

SELECT query

281 |
[options]Object
[options.headers]Headers

additional request headers

286 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

289 |
292 | 293 | 294 | 295 | ### parsingQuery.ask(query, [options]) ⇒ Promise.<boolean> 296 | Sends a request for a ASK query 297 | 298 | **Kind**: instance method of [ParsingQuery](#ParsingQuery) 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 309 | 310 | 311 | 312 | 314 | 315 | 317 | 318 |
ParamTypeDefaultDescription
querystring

ASK query

308 |
[options]Object
[options.headers]Headers

additional request headers

313 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

316 |
319 | 320 | 321 | 322 | ### parsingQuery.update(query, [options]) ⇒ Promise.<void> 323 | Sends a request for an update query 324 | 325 | **Kind**: instance method of [ParsingQuery](#ParsingQuery) 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 336 | 337 | 338 | 339 | 341 | 342 | 344 | 345 |
ParamTypeDefaultDescription
querystring

update query

335 |
[options]Object
[options.headers]Headers

additional request headers

340 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''postUrlencoded'

SPARQL Protocol operation

343 |
346 | 347 | 348 | 349 | ## RawQuery 350 | A query implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. 351 | 352 | **Kind**: global class 353 | 354 | * [RawQuery](#RawQuery) 355 | * [new RawQuery(options)](#new_RawQuery_new) 356 | * [.ask(query, [options])](#RawQuery+ask) ⇒ Promise.<Response> 357 | * [.construct(query, [options])](#RawQuery+construct) ⇒ Promise.<Response> 358 | * [.select(query, [options])](#RawQuery+select) ⇒ Promise.<Response> 359 | * [.update(query, [options])](#RawQuery+update) ⇒ Promise.<Response> 360 | 361 | 362 | 363 | ### new RawQuery(options) 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 376 | 377 |
ParamTypeDescription
optionsObject
options.clientSimpleClient

client that provides the HTTP I/O

375 |
378 | 379 | 380 | 381 | ### rawQuery.ask(query, [options]) ⇒ Promise.<Response> 382 | Sends a request for a ASK query 383 | 384 | **Kind**: instance method of [RawQuery](#RawQuery) 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 395 | 396 | 397 | 398 | 400 | 401 | 403 | 404 |
ParamTypeDefaultDescription
querystring

ASK query

394 |
[options]Object
[options.headers]Headers

additional request headers

399 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

402 |
405 | 406 | 407 | 408 | ### rawQuery.construct(query, [options]) ⇒ Promise.<Response> 409 | Sends a request for a CONSTRUCT or DESCRIBE query 410 | 411 | **Kind**: instance method of [RawQuery](#RawQuery) 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 422 | 423 | 424 | 425 | 427 | 428 | 430 | 431 |
ParamTypeDefaultDescription
querystring

CONSTRUCT or DESCRIBE query

421 |
[options]Object
[options.headers]Headers

additional request headers

426 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

429 |
432 | 433 | 434 | 435 | ### rawQuery.select(query, [options]) ⇒ Promise.<Response> 436 | Sends a request for a SELECT query 437 | 438 | **Kind**: instance method of [RawQuery](#RawQuery) 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 449 | 450 | 451 | 452 | 454 | 455 | 457 | 458 |
ParamTypeDefaultDescription
querystring

SELECT query

448 |
[options]Object
[options.headers]Headers

additional request headers

453 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

456 |
459 | 460 | 461 | 462 | ### rawQuery.update(query, [options]) ⇒ Promise.<Response> 463 | Sends a request for an update query 464 | 465 | **Kind**: instance method of [RawQuery](#RawQuery) 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 476 | 477 | 478 | 479 | 481 | 482 | 484 | 485 |
ParamTypeDefaultDescription
querystring

update query

475 |
[options]Object
[options.headers]Headers

additional request headers

480 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''postUrlencoded'

SPARQL Protocol operation

483 |
486 | 487 | 488 | 489 | ## ResultParser 490 | A Transform stream that parses JSON SPARQL results and emits one object per row with the variable names as keys and 491 | RDF/JS terms as values. 492 | 493 | **Kind**: global class 494 | 495 | 496 | ### new ResultParser(options) 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 509 | 510 |
ParamTypeDescription
optionsObject
options.factoryDataFactory

RDF/JS DataFactory used to create the quads and terms

508 |
511 | 512 | 513 | 514 | ## SimpleClient 515 | A client implementation based on [RawQuery](#RawQuery) that prepares URLs and headers for SPARQL queries and returns the 516 | raw fetch response. It does not provide a store interface. 517 | 518 | **Kind**: global class 519 | **Properties** 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 |
NameType
queryRawQuery
endpointUrlstring
factoryRawQuery
fetchfactory
headersHeaders
passwordstring
storeUrlstring
updateUrlstring
userstring
updateUrlstring
550 | 551 | 552 | * [SimpleClient](#SimpleClient) 553 | * [new SimpleClient(options)](#new_SimpleClient_new) 554 | * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> 555 | * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> 556 | * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> 557 | 558 | 559 | 560 | ### new SimpleClient(options) 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 573 | 574 | 576 | 577 | 579 | 580 | 582 | 583 | 585 | 586 | 588 | 589 | 591 | 592 | 594 | 595 | 597 | 598 | 600 | 601 |
ParamTypeDefaultDescription
optionsObject
[options.endpointUrl]string

SPARQL query endpoint URL

572 |
[options.factory]factory

RDF/JS factory

575 |
[options.fetch]fetchnodeify-fetch

fetch implementation

578 |
[options.headers]Headers

headers sent with every request

581 |
[options.password]string

password used for basic authentication

584 |
[options.storeUrl]string

SPARQL Graph Store URL

587 |
[options.updateUrl]string

SPARQL update endpoint URL

590 |
[options.user]string

user used for basic authentication

593 |
[options.Query]Query

Constructor of a query implementation

596 |
[options.Store]Store

Constructor of a store implementation

599 |
602 | 603 | **Example** 604 | ```js 605 | // read the height of the Eiffel Tower from Wikidata with a SELECT query 606 | 607 | import SparqlClient from 'sparql-http-client/SimpleClient.js' 608 | 609 | const endpointUrl = 'https://query.wikidata.org/sparql' 610 | const query = ` 611 | PREFIX wd: 612 | PREFIX p: 613 | PREFIX ps: 614 | PREFIX pq: 615 | 616 | SELECT ?value WHERE { 617 | wd:Q243 p:P2048 ?height. 618 | 619 | ?height pq:P518 wd:Q24192182; 620 | ps:P2048 ?value . 621 | }` 622 | 623 | const client = new SparqlClient({ endpointUrl }) 624 | const res = await client.query.select(query) 625 | 626 | if (!res.ok) { 627 | return console.error(res.statusText) 628 | } 629 | 630 | const content = await res.json() 631 | 632 | console.log(JSON.stringify(content, null, 2)) 633 | ``` 634 | 635 | 636 | ### simpleClient.get(query, options) ⇒ Promise.<Response> 637 | Sends a GET request as defined in the 638 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). 639 | 640 | **Kind**: instance method of [SimpleClient](#SimpleClient) 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 651 | 652 | 653 | 654 | 656 | 657 | 659 | 660 |
ParamTypeDefaultDescription
querystring

SPARQL query

650 |
optionsObject
[options.headers]Headers

additional request headers

655 |
[options.update]booleanfalse

send the request to the updateUrl

658 |
661 | 662 | 663 | 664 | ### simpleClient.postDirect(query, options) ⇒ Promise.<Response> 665 | Sends a POST directly request as defined in the 666 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). 667 | 668 | **Kind**: instance method of [SimpleClient](#SimpleClient) 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 679 | 680 | 681 | 682 | 684 | 685 | 687 | 688 |
ParamTypeDefaultDescription
querystring

SPARQL query

678 |
optionsObject
[options.headers]Headers

additional request headers

683 |
[options.update]booleanfalse

send the request to the updateUrl

686 |
689 | 690 | 691 | 692 | ### simpleClient.postUrlencoded(query, options) ⇒ Promise.<Response> 693 | Sends a POST URL-encoded request as defined in the 694 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). 695 | 696 | **Kind**: instance method of [SimpleClient](#SimpleClient) 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 707 | 708 | 709 | 710 | 712 | 713 | 715 | 716 |
ParamTypeDefaultDescription
querystring

SPARQL query

706 |
optionsObject
[options.headers]Headers

additional request headers

711 |
[options.update]booleanfalse

send the request to the updateUrl

714 |
717 | 718 | 719 | 720 | ## StreamClient ⇐ [SimpleClient](#SimpleClient) 721 | The default client implementation based on [StreamQuery](#StreamQuery) and [StreamStore](#StreamStore) parses SPARQL results into 722 | Readable streams of RDF/JS Quad objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). Graph Store 723 | read and write operations are handled using Readable streams. 724 | 725 | **Kind**: global class 726 | **Extends**: [SimpleClient](#SimpleClient) 727 | **Properties** 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 |
NameType
queryStreamQuery
storeStreamStore
742 | 743 | 744 | * [StreamClient](#StreamClient) ⇐ [SimpleClient](#SimpleClient) 745 | * [new StreamClient(options)](#new_StreamClient_new) 746 | * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> 747 | * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> 748 | * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> 749 | 750 | 751 | 752 | ### new StreamClient(options) 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 765 | 766 | 768 | 769 | 771 | 772 | 774 | 775 | 777 | 778 | 780 | 781 | 783 | 784 | 786 | 787 |
ParamTypeDefaultDescription
optionsObject
[options.endpointUrl]string

SPARQL query endpoint URL

764 |
[options.factory]factory

RDF/JS factory

767 |
[options.fetch]fetchnodeify-fetch

fetch implementation

770 |
[options.headers]Headers

headers sent with every request

773 |
[options.password]string

password used for basic authentication

776 |
[options.storeUrl]string

SPARQL Graph Store URL

779 |
[options.updateUrl]string

SPARQL update endpoint URL

782 |
[options.user]string

user used for basic authentication

785 |
788 | 789 | **Example** 790 | ```js 791 | // read the height of the Eiffel Tower from Wikidata with a SELECT query 792 | 793 | import SparqlClient from 'sparql-http-client' 794 | 795 | const endpointUrl = 'https://query.wikidata.org/sparql' 796 | const query = ` 797 | PREFIX wd: 798 | PREFIX p: 799 | PREFIX ps: 800 | PREFIX pq: 801 | 802 | SELECT ?value WHERE { 803 | wd:Q243 p:P2048 ?height. 804 | 805 | ?height pq:P518 wd:Q24192182; 806 | ps:P2048 ?value . 807 | }` 808 | 809 | const client = new SparqlClient({ endpointUrl }) 810 | const stream = client.query.select(query) 811 | 812 | stream.on('data', row => { 813 | for (const [key, value] of Object.entries(row)) { 814 | console.log(`${key}: ${value.value} (${value.termType})`) 815 | } 816 | }) 817 | 818 | stream.on('error', err => { 819 | console.error(err) 820 | }) 821 | ``` 822 | **Example** 823 | ```js 824 | // read all quads from a local triplestore using the Graph Store protocol 825 | 826 | import rdf from 'rdf-ext' 827 | import SparqlClient from 'sparql-http-client' 828 | 829 | const client = new SparqlClient({ 830 | storeUrl: 'http://localhost:3030/test/data', 831 | factory: rdf 832 | }) 833 | 834 | const stream = local.store.get(rdf.defaultGraph()) 835 | 836 | stream.on('data', quad => { 837 | console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) 838 | }) 839 | ``` 840 | 841 | 842 | ### streamClient.get(query, options) ⇒ Promise.<Response> 843 | Sends a GET request as defined in the 844 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). 845 | 846 | **Kind**: instance method of [StreamClient](#StreamClient) 847 | **Overrides**: [get](#SimpleClient+get) 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 858 | 859 | 860 | 861 | 863 | 864 | 866 | 867 |
ParamTypeDefaultDescription
querystring

SPARQL query

857 |
optionsObject
[options.headers]Headers

additional request headers

862 |
[options.update]booleanfalse

send the request to the updateUrl

865 |
868 | 869 | 870 | 871 | ### streamClient.postDirect(query, options) ⇒ Promise.<Response> 872 | Sends a POST directly request as defined in the 873 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). 874 | 875 | **Kind**: instance method of [StreamClient](#StreamClient) 876 | **Overrides**: [postDirect](#SimpleClient+postDirect) 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 887 | 888 | 889 | 890 | 892 | 893 | 895 | 896 |
ParamTypeDefaultDescription
querystring

SPARQL query

886 |
optionsObject
[options.headers]Headers

additional request headers

891 |
[options.update]booleanfalse

send the request to the updateUrl

894 |
897 | 898 | 899 | 900 | ### streamClient.postUrlencoded(query, options) ⇒ Promise.<Response> 901 | Sends a POST URL-encoded request as defined in the 902 | [SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). 903 | 904 | **Kind**: instance method of [StreamClient](#StreamClient) 905 | **Overrides**: [postUrlencoded](#SimpleClient+postUrlencoded) 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 916 | 917 | 918 | 919 | 921 | 922 | 924 | 925 |
ParamTypeDefaultDescription
querystring

SPARQL query

915 |
optionsObject
[options.headers]Headers

additional request headers

920 |
[options.update]booleanfalse

send the request to the updateUrl

923 |
926 | 927 | 928 | 929 | ## StreamQuery ⇐ [RawQuery](#RawQuery) 930 | A query implementation based on [RawQuery](#RawQuery) that parses SPARQL results into Readable streams of RDF/JS Quad 931 | objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). 932 | 933 | **Kind**: global class 934 | **Extends**: [RawQuery](#RawQuery) 935 | 936 | * [StreamQuery](#StreamQuery) ⇐ [RawQuery](#RawQuery) 937 | * [.ask(query, [options])](#StreamQuery+ask) ⇒ Promise.<boolean> 938 | * [.construct(query, [options])](#StreamQuery+construct) ⇒ Readable 939 | * [.select(query, [options])](#StreamQuery+select) ⇒ Readable 940 | * [.update(query, [options])](#StreamQuery+update) ⇒ Promise.<void> 941 | 942 | 943 | 944 | ### streamQuery.ask(query, [options]) ⇒ Promise.<boolean> 945 | Sends a request for a ASK query 946 | 947 | **Kind**: instance method of [StreamQuery](#StreamQuery) 948 | **Overrides**: [ask](#RawQuery+ask) 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 959 | 960 | 961 | 962 | 964 | 965 | 967 | 968 |
ParamTypeDefaultDescription
querystring

ASK query

958 |
[options]Object
[options.headers]Headers

additional request headers

963 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

966 |
969 | 970 | 971 | 972 | ### streamQuery.construct(query, [options]) ⇒ Readable 973 | Sends a request for a CONSTRUCT or DESCRIBE query 974 | 975 | **Kind**: instance method of [StreamQuery](#StreamQuery) 976 | **Overrides**: [construct](#RawQuery+construct) 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 987 | 988 | 989 | 990 | 992 | 993 | 995 | 996 |
ParamTypeDefaultDescription
querystring

CONSTRUCT or DESCRIBE query

986 |
[options]Object
[options.headers]Headers

additional request headers

991 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

994 |
997 | 998 | 999 | 1000 | ### streamQuery.select(query, [options]) ⇒ Readable 1001 | Sends a request for a SELECT query 1002 | 1003 | **Kind**: instance method of [StreamQuery](#StreamQuery) 1004 | **Overrides**: [select](#RawQuery+select) 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1015 | 1016 | 1017 | 1018 | 1020 | 1021 | 1023 | 1024 |
ParamTypeDefaultDescription
querystring

SELECT query

1014 |
[options]Object
[options.headers]Headers

additional request headers

1019 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''get'

SPARQL Protocol operation

1022 |
1025 | 1026 | 1027 | 1028 | ### streamQuery.update(query, [options]) ⇒ Promise.<void> 1029 | Sends a request for an update query 1030 | 1031 | **Kind**: instance method of [StreamQuery](#StreamQuery) 1032 | **Overrides**: [update](#RawQuery+update) 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1043 | 1044 | 1045 | 1046 | 1048 | 1049 | 1051 | 1052 |
ParamTypeDefaultDescription
querystring

update query

1042 |
[options]Object
[options.headers]Headers

additional request headers

1047 |
[options.operation]'get' | 'postUrlencoded' | 'postDirect''postUrlencoded'

SPARQL Protocol operation

1050 |
1053 | 1054 | 1055 | 1056 | ## StreamStore 1057 | A store implementation that parses and serializes SPARQL Graph Store responses and requests into/from Readable 1058 | streams. 1059 | 1060 | **Kind**: global class 1061 | 1062 | * [StreamStore](#StreamStore) 1063 | * [new StreamStore(options)](#new_StreamStore_new) 1064 | * [.get([graph])](#StreamStore+get) ⇒ Promise.<Readable> 1065 | * [.post(stream, [options])](#StreamStore+post) ⇒ Promise.<void> 1066 | * [.put(stream, [options])](#StreamStore+put) ⇒ Promise.<void> 1067 | * [.read([options])](#StreamStore+read) ⇒ Readable 1068 | * [.write([options], [graph], method, stream)](#StreamStore+write) ⇒ Promise.<void> 1069 | 1070 | 1071 | 1072 | ### new StreamStore(options) 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1085 | 1086 |
ParamTypeDescription
optionsObject
options.clientSimpleClient

client that provides the HTTP I/O

1084 |
1087 | 1088 | 1089 | 1090 | ### streamStore.get([graph]) ⇒ Promise.<Readable> 1091 | Sends a GET request to the Graph Store 1092 | 1093 | **Kind**: instance method of [StreamStore](#StreamStore) 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1104 | 1105 |
ParamTypeDescription
[graph]NamedNode

source graph

1103 |
1106 | 1107 | 1108 | 1109 | ### streamStore.post(stream, [options]) ⇒ Promise.<void> 1110 | Sends a POST request to the Graph Store 1111 | 1112 | **Kind**: instance method of [StreamStore](#StreamStore) 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1123 | 1124 | 1125 | 1126 | 1128 | 1129 |
ParamTypeDescription
streamReadable

triples/quads to write

1122 |
[options]Object
[options.graph]Term

target graph

1127 |
1130 | 1131 | 1132 | 1133 | ### streamStore.put(stream, [options]) ⇒ Promise.<void> 1134 | Sends a PUT request to the Graph Store 1135 | 1136 | **Kind**: instance method of [StreamStore](#StreamStore) 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1147 | 1148 | 1149 | 1150 | 1152 | 1153 |
ParamTypeDescription
streamReadable

triples/quads to write

1146 |
[options]Object
[options.graph]Term

target graph

1151 |
1154 | 1155 | 1156 | 1157 | ### streamStore.read([options]) ⇒ Readable 1158 | Generic read request to the Graph Store 1159 | 1160 | **Kind**: instance method of [StreamStore](#StreamStore) 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1173 | 1174 | 1176 | 1177 |
ParamTypeDescription
[options]Object
[options.graph]Term

source graph

1172 |
options.methodstring

HTTP method

1175 |
1178 | 1179 | 1180 | 1181 | ### streamStore.write([options], [graph], method, stream) ⇒ Promise.<void> 1182 | Generic write request to the Graph Store 1183 | 1184 | **Kind**: instance method of [StreamStore](#StreamStore) 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1197 | 1198 | 1200 | 1201 | 1203 | 1204 |
ParamTypeDescription
[options]Object
[graph]Term

target graph

1196 |
methodstring

HTTP method

1199 |
streamReadable

triples/quads to write

1202 |
1205 | 1206 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sparql-http-client - Simplified SPARQL HTTP request client 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/graph-store.js: -------------------------------------------------------------------------------- 1 | import rdf from 'rdf-ext' 2 | import SparqlClient from '../StreamClient.js' 3 | 4 | const client = new SparqlClient({ 5 | storeUrl: 'http://localhost:3030/test/data', 6 | factory: rdf 7 | }) 8 | 9 | async function main () { 10 | const stream = client.store.get(rdf.defaultGraph()) 11 | 12 | stream.on('data', quad => { 13 | console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) 14 | }) 15 | } 16 | 17 | main() 18 | -------------------------------------------------------------------------------- /examples/query-to-graph-store.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example 4 | - uses a DESCRIBE query to fetch triples from wikidata 5 | - writes the result into a local triplestore using graph store 6 | - runs a SELECT query on the local triplestore 7 | 8 | To get this example running, you need to start a triplestore with: 9 | - endpoint URL: http://localhost:3030/test/sparql 10 | - graph store URL: http://localhost:3030/test/data 11 | 12 | Using Fuseki is the easiest way to get the required setup running. 13 | The following steps are required: 14 | - download and install Fuseki from https://jena.apache.org/documentation/fuseki2/ 15 | - start the server with the command `fuseki-server` 16 | - go to the web interface at http://localhost:3030/ and create a in memory dataset with the name test 17 | 18 | */ 19 | 20 | import rdf from 'rdf-ext' 21 | import SparqlClient from '../StreamClient.js' 22 | 23 | const ns = { 24 | rdf: rdf.namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'), 25 | rdfs: rdf.namespace('http://www.w3.org/2000/01/rdf-schema#'), 26 | wd: rdf.namespace('http://www.wikidata.org/entity/') 27 | } 28 | 29 | const dbpedia = new SparqlClient({ endpointUrl: 'https://query.wikidata.org/sparql' }) 30 | const local = new SparqlClient({ 31 | endpointUrl: 'http://localhost:3030/test/sparql', 32 | storeUrl: 'http://localhost:3030/test/data' 33 | }) 34 | 35 | const describeQuery = `DESCRIBE <${ns.wd.Q243.value}>` 36 | const selectQuery = `SELECT ?label WHERE { ?s <${ns.rdfs.label.value}> ?label . }` 37 | 38 | async function main () { 39 | // read all triples related to Eiffel Tower via describe construct query as a quad stream 40 | const input = dbpedia.query.construct(describeQuery) 41 | 42 | // import the quad stream into a local store (remove literals with empty language strings) 43 | await local.store.put(input) 44 | 45 | // run a select query on the local store that will return all labels 46 | const result = local.query.select(selectQuery) 47 | 48 | // write all labels + language to the console 49 | result.on('data', row => { 50 | console.log(`${row.label.value} (${row.label.language})`) 51 | }) 52 | } 53 | 54 | main() 55 | -------------------------------------------------------------------------------- /examples/select-json.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example uses the SimpleClient to make a SELECT query and manually processes the response. 4 | 5 | */ 6 | 7 | import SparqlClient from '../SimpleClient.js' 8 | 9 | const endpointUrl = 'https://query.wikidata.org/sparql' 10 | const query = ` 11 | PREFIX wd: 12 | PREFIX p: 13 | PREFIX ps: 14 | PREFIX pq: 15 | 16 | SELECT ?value WHERE { 17 | wd:Q243 p:P2048 ?height. 18 | 19 | ?height pq:P518 wd:Q24192182; 20 | ps:P2048 ?value . 21 | }` 22 | 23 | async function main () { 24 | const client = new SparqlClient({ endpointUrl }) 25 | const res = await client.query.select(query) 26 | 27 | if (!res.ok) { 28 | return console.error(res.statusText) 29 | } 30 | 31 | const content = await res.json() 32 | 33 | console.log(JSON.stringify(content, null, 2)) 34 | } 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /examples/select-parsing.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example uses the SimpleClient and upgrades it to a ParsingClient to make a SELECT query and processes the result. 4 | 5 | */ 6 | 7 | import ParsingClient from '../ParsingClient.js' 8 | import SimpleClient from '../SimpleClient.js' 9 | 10 | const endpointUrl = 'https://query.wikidata.org/sparql' 11 | const query = ` 12 | PREFIX wd: 13 | PREFIX p: 14 | PREFIX ps: 15 | PREFIX pq: 16 | 17 | SELECT ?value WHERE { 18 | wd:Q243 p:P2048 ?height. 19 | 20 | ?height pq:P518 wd:Q24192182; 21 | ps:P2048 ?value . 22 | }` 23 | 24 | async function main () { 25 | const simpleClient = new SimpleClient({ endpointUrl }) 26 | const parsingClient = new ParsingClient(simpleClient) 27 | const result = await parsingClient.query.select(query) 28 | 29 | for (const row of result) { 30 | for (const [key, value] of Object.entries(row)) { 31 | console.log(`${key}: ${value.value} (${value.termType})`) 32 | } 33 | } 34 | } 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /examples/select-raw-post-urlencoded.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example uses the SimpleClient to make a SELECT query using a URL encoded POST request. 4 | 5 | */ 6 | 7 | import SparqlClient from '../SimpleClient.js' 8 | 9 | const endpointUrl = 'https://query.wikidata.org/sparql' 10 | const query = ` 11 | PREFIX wd: 12 | PREFIX p: 13 | PREFIX ps: 14 | PREFIX pq: 15 | 16 | SELECT ?value WHERE { 17 | wd:Q243 p:P2048 ?height. 18 | 19 | ?height pq:P518 wd:Q24192182; 20 | ps:P2048 ?value . 21 | }` 22 | 23 | async function main () { 24 | const client = new SparqlClient({ endpointUrl }) 25 | const res = await client.query.select(query, { operation: 'postUrlencoded' }) 26 | 27 | if (!res.ok) { 28 | return console.error(res.statusText) 29 | } 30 | 31 | const content = await res.json() 32 | 33 | for (const row of content.results.bindings) { 34 | for (const [key, value] of Object.entries(row)) { 35 | console.log(`${key}: ${value.value}`) 36 | } 37 | } 38 | } 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /examples/select-raw.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example uses the SimpleClient to make a SELECT query and manually processes the response. 4 | 5 | */ 6 | 7 | import SparqlClient from '../SimpleClient.js' 8 | 9 | const endpointUrl = 'https://query.wikidata.org/sparql' 10 | const query = ` 11 | PREFIX wd: 12 | PREFIX p: 13 | PREFIX ps: 14 | PREFIX pq: 15 | 16 | SELECT ?value WHERE { 17 | wd:Q243 p:P2048 ?height. 18 | 19 | ?height pq:P518 wd:Q24192182; 20 | ps:P2048 ?value . 21 | }` 22 | 23 | async function main () { 24 | const client = new SparqlClient({ endpointUrl }) 25 | const res = await client.query.select(query) 26 | 27 | if (!res.ok) { 28 | return console.error(res.statusText) 29 | } 30 | 31 | const content = await res.json() 32 | 33 | for (const row of content.results.bindings) { 34 | for (const [key, value] of Object.entries(row)) { 35 | console.log(`${key}: ${value.value}`) 36 | } 37 | } 38 | } 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /examples/select-stream.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example uses the default Client to make a SELECT query and processes the stream events for each row. 4 | 5 | */ 6 | 7 | import SparqlClient from '../StreamClient.js' 8 | 9 | const endpointUrl = 'https://query.wikidata.org/sparql' 10 | const query = ` 11 | PREFIX wd: 12 | PREFIX p: 13 | PREFIX ps: 14 | PREFIX pq: 15 | 16 | SELECT ?value WHERE { 17 | wd:Q243 p:P2048 ?height. 18 | 19 | ?height pq:P518 wd:Q24192182; 20 | ps:P2048 ?value . 21 | }` 22 | 23 | async function main () { 24 | const client = new SparqlClient({ endpointUrl }) 25 | const stream = client.query.select(query) 26 | 27 | stream.on('data', row => { 28 | for (const [key, value] of Object.entries(row)) { 29 | console.log(`${key}: ${value.value} (${value.termType})`) 30 | } 31 | }) 32 | 33 | stream.on('error', err => { 34 | console.error(err) 35 | }) 36 | } 37 | 38 | main() 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import ParsingClient from './ParsingClient.js' 2 | import ParsingQuery from './ParsingQuery.js' 3 | import RawQuery from './RawQuery.js' 4 | import ResultParser from './ResultParser.js' 5 | import SimpleClient from './SimpleClient.js' 6 | import StreamClient from './StreamClient.js' 7 | import StreamQuery from './StreamQuery.js' 8 | import StreamStore from './StreamStore.js' 9 | 10 | export { 11 | StreamClient as default, 12 | ParsingClient, 13 | ParsingQuery, 14 | RawQuery, 15 | ResultParser, 16 | SimpleClient, 17 | StreamClient, 18 | StreamQuery, 19 | StreamStore 20 | } 21 | -------------------------------------------------------------------------------- /lib/asyncToReadabe.js: -------------------------------------------------------------------------------- 1 | import toReadable from 'duplex-to/readable.js' 2 | import { PassThrough } from 'readable-stream' 3 | 4 | function asyncToReadabe (func) { 5 | const stream = new PassThrough({ objectMode: true }) 6 | 7 | setTimeout(async () => { 8 | try { 9 | (await func()).pipe(stream) 10 | } catch (err) { 11 | stream.destroy(err) 12 | } 13 | }, 0) 14 | 15 | return toReadable(stream) 16 | } 17 | 18 | export default asyncToReadabe 19 | -------------------------------------------------------------------------------- /lib/checkResponse.js: -------------------------------------------------------------------------------- 1 | async function checkResponse (res) { 2 | if (res.ok) { 3 | return 4 | } 5 | 6 | const message = await res.text() 7 | const err = new Error(`${res.statusText} (${res.status}): ${message}`) 8 | err.status = res.status 9 | 10 | throw err 11 | } 12 | 13 | export default checkResponse 14 | -------------------------------------------------------------------------------- /lib/isDataFactory.js: -------------------------------------------------------------------------------- 1 | function isDataFactory (factory) { 2 | if (!factory) { 3 | return false 4 | } 5 | 6 | if (typeof factory.blankNode !== 'function') { 7 | return false 8 | } 9 | 10 | if (typeof factory.defaultGraph !== 'function') { 11 | return false 12 | } 13 | 14 | if (typeof factory.literal !== 'function') { 15 | return false 16 | } 17 | 18 | if (typeof factory.namedNode !== 'function') { 19 | return false 20 | } 21 | 22 | if (typeof factory.quad !== 'function') { 23 | return false 24 | } 25 | 26 | return true 27 | } 28 | 29 | export default isDataFactory 30 | -------------------------------------------------------------------------------- /lib/isDatasetCoreFactory.js: -------------------------------------------------------------------------------- 1 | function isDatasetCoreFactory (factory) { 2 | if (!factory) { 3 | return false 4 | } 5 | 6 | if (typeof factory.dataset !== 'function') { 7 | return false 8 | } 9 | 10 | return true 11 | } 12 | 13 | export default isDatasetCoreFactory 14 | -------------------------------------------------------------------------------- /lib/mergeHeaders.js: -------------------------------------------------------------------------------- 1 | function mergeHeaders (...all) { 2 | const merged = new Headers() 3 | 4 | for (const headers of all) { 5 | if (!headers) { 6 | continue 7 | } 8 | 9 | const entries = headers.entries ? headers.entries() : Object.entries(headers) 10 | 11 | for (const [key, value] of entries) { 12 | merged.set(key, value) 13 | } 14 | } 15 | 16 | return merged 17 | } 18 | 19 | export default mergeHeaders 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sparql-http-client", 3 | "version": "3.0.1", 4 | "description": "Simplified SPARQL HTTP request client", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "build:docs": "jsdoc2md --no-gfm -f *.js lib/* > docs/api.md", 9 | "prepare": "simple-git-hooks", 10 | "test": "stricter-standard && c8 --reporter=lcov --reporter=text-summary mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/rdf-ext/sparql-http-client.git" 15 | }, 16 | "keywords": [ 17 | "sparql", 18 | "http", 19 | "rdf" 20 | ], 21 | "author": "Thomas Bergwinkl (https://www.bergnet.org/people/bergi/card#me)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/rdf-ext/sparql-http-client/issues" 25 | }, 26 | "homepage": "https://rdf-ext.github.io/sparql-http-client/", 27 | "dependencies": { 28 | "@bergos/jsonparse": "^1.4.1", 29 | "@rdfjs/data-model": "^2.0.2", 30 | "@rdfjs/dataset": "^2.0.2", 31 | "@rdfjs/environment": "^1.0.0", 32 | "@rdfjs/parser-n3": "^2.0.2", 33 | "@rdfjs/to-ntriples": "^3.0.1", 34 | "duplex-to": "^2.0.0", 35 | "nodeify-fetch": "^3.1.0", 36 | "rdf-transform-triple-to-quad": "^2.0.0", 37 | "readable-stream": "^4.5.2", 38 | "stream-chunks": "^1.0.0" 39 | }, 40 | "devDependencies": { 41 | "c8": "^10.1.2", 42 | "express": "^5.1.0", 43 | "express-as-promise": "^2.0.0", 44 | "is-stream": "^4.0.1", 45 | "jsdoc-to-markdown": "^9.0.0", 46 | "lodash": "^4.17.21", 47 | "mocha": "^11.0.1", 48 | "rdf-ext": "^2.5.1", 49 | "rdf-test": "^0.1.0", 50 | "simple-git-hooks": "^2.9.0", 51 | "stricter-standard": "^0.3.0" 52 | }, 53 | "simple-git-hooks": { 54 | "pre-commit": "npm run build:docs && git add docs/api.md" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/ParsingClient.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, strictEqual, throws } from 'node:assert' 2 | import DataModelFactory from '@rdfjs/data-model/Factory.js' 3 | import Environment from '@rdfjs/environment' 4 | import omit from 'lodash/omit.js' 5 | import pick from 'lodash/pick.js' 6 | import { describe, it } from 'mocha' 7 | import ParsingClient from '../ParsingClient.js' 8 | import ParsingQuery from '../ParsingQuery.js' 9 | import SimpleClient from '../SimpleClient.js' 10 | 11 | describe('ParsingClient', () => { 12 | it('should be a constructor', () => { 13 | strictEqual(typeof ParsingClient, 'function') 14 | }) 15 | 16 | it('should throw an error if the given factory does not implement the DatasetCoreFactory interface', () => { 17 | throws(() => { 18 | new ParsingClient({ // eslint-disable-line no-new 19 | endpointUrl: 'test', 20 | factory: new Environment([DataModelFactory]) 21 | }) 22 | }, { 23 | message: /DatasetCoreFactory/ 24 | }) 25 | }) 26 | 27 | it('should use StreamQuery to create the query instance', () => { 28 | const client = new ParsingClient({ endpointUrl: 'test' }) 29 | 30 | strictEqual(client.query instanceof ParsingQuery, true) 31 | }) 32 | 33 | it('should forward the client to the query instance', () => { 34 | const client = new ParsingClient({ endpointUrl: 'test' }) 35 | 36 | strictEqual(client.query.client, client) 37 | }) 38 | 39 | it('should be possible to create an instance from a SimpleClient', () => { 40 | const options = { 41 | endpointUrl: 'sparql', 42 | headers: new Headers({ 'user-agent': 'sparql-http-client' }), 43 | password: 'pwd', 44 | storeUrl: 'graph', 45 | updateUrl: 'update', 46 | user: 'usr' 47 | } 48 | 49 | const simpleClient = new SimpleClient(options) 50 | const client = new ParsingClient(simpleClient) 51 | const result = pick(client, Object.keys(options)) 52 | 53 | deepStrictEqual(omit(result, 'headers'), omit(options, 'headers')) 54 | deepStrictEqual([...result.headers.entries()], [...simpleClient.headers.entries()]) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/ParsingQuery.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, rejects, strictEqual } from 'node:assert' 2 | import DataModelFactory from '@rdfjs/data-model/Factory.js' 3 | import DatasetFactory from '@rdfjs/dataset/Factory.js' 4 | import Environment from '@rdfjs/environment' 5 | import express from 'express' 6 | import withServer from 'express-as-promise/withServer.js' 7 | import { describe, it } from 'mocha' 8 | import rdf from 'rdf-ext' 9 | import { datasetEqual } from 'rdf-test/assert.js' 10 | import isDataset from 'rdf-test/isDataset.js' 11 | import ParsingQuery from '../ParsingQuery.js' 12 | import SimpleClient from '../SimpleClient.js' 13 | import { message, quads, constructQuery, selectQuery } from './support/examples.js' 14 | import isServerError from './support/isServerError.js' 15 | import isSocketError from './support/isSocketError.js' 16 | import * as ns from './support/namespaces.js' 17 | import testFactory from './support/testFactory.js' 18 | 19 | const factory = new Environment([DataModelFactory, DatasetFactory]) 20 | 21 | describe('ParsingQuery', () => { 22 | describe('.construct', () => { 23 | it('should be a method', () => { 24 | const query = new ParsingQuery({}) 25 | 26 | strictEqual(typeof query.construct, 'function') 27 | }) 28 | 29 | it('should return a DatasetCore object', async () => { 30 | await withServer(async server => { 31 | server.app.get('/', async (req, res) => { 32 | res.status(204).end() 33 | }) 34 | 35 | const endpointUrl = await server.listen() 36 | const client = new SimpleClient({ endpointUrl, factory }) 37 | const query = new ParsingQuery({ client }) 38 | 39 | const result = await query.construct(constructQuery) 40 | 41 | strictEqual(isDataset(result), true) 42 | }) 43 | }) 44 | 45 | it('should parse the N-Triples', async () => { 46 | await withServer(async server => { 47 | server.app.get('/', async (req, res) => { 48 | res.end(quads.toString()) 49 | }) 50 | 51 | const endpointUrl = await server.listen() 52 | const client = new SimpleClient({ endpointUrl, factory }) 53 | const query = new ParsingQuery({ client }) 54 | 55 | const result = await query.construct(constructQuery) 56 | 57 | datasetEqual(result, quads) 58 | }) 59 | }) 60 | 61 | it('should send a GET request', async () => { 62 | await withServer(async server => { 63 | let called = false 64 | 65 | server.app.get('/', async (req, res) => { 66 | called = true 67 | 68 | res.status(204).end() 69 | }) 70 | 71 | const endpointUrl = await server.listen() 72 | const client = new SimpleClient({ endpointUrl, factory }) 73 | const query = new ParsingQuery({ client }) 74 | 75 | await query.construct(constructQuery) 76 | 77 | strictEqual(called, true) 78 | }) 79 | }) 80 | 81 | it('should send the query string as query parameter', async () => { 82 | await withServer(async server => { 83 | let parameter = null 84 | 85 | server.app.get('/', async (req, res) => { 86 | parameter = req.query.query 87 | 88 | res.status(204).end() 89 | }) 90 | 91 | const endpointUrl = await server.listen() 92 | const client = new SimpleClient({ endpointUrl, factory }) 93 | const query = new ParsingQuery({ client }) 94 | 95 | await query.construct(constructQuery) 96 | 97 | strictEqual(parameter, constructQuery) 98 | }) 99 | }) 100 | 101 | it('should use the given factory', async () => { 102 | await withServer(async server => { 103 | const quads = rdf.dataset([ 104 | rdf.quad(rdf.blankNode(), ns.ex.predicate, rdf.literal('test')) 105 | ]) 106 | const factory = testFactory() 107 | 108 | server.app.get('/', async (req, res) => { 109 | res.end(quads.toString()) 110 | }) 111 | 112 | const endpointUrl = await server.listen() 113 | const client = new SimpleClient({ endpointUrl, factory }) 114 | const query = new ParsingQuery({ client }) 115 | 116 | await query.construct(constructQuery) 117 | 118 | deepStrictEqual(factory.used, { 119 | blankNode: true, 120 | dataset: true, 121 | defaultGraph: true, 122 | literal: true, 123 | namedNode: true, 124 | quad: true 125 | }) 126 | }) 127 | }) 128 | 129 | it('should use the given operation for the request', async () => { 130 | await withServer(async server => { 131 | let parameter = null 132 | 133 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 134 | parameter = req.body.query 135 | 136 | res.status(204).end() 137 | }) 138 | 139 | const endpointUrl = await server.listen() 140 | const client = new SimpleClient({ endpointUrl, factory }) 141 | const query = new ParsingQuery({ client }) 142 | 143 | await query.construct(constructQuery, { operation: 'postUrlencoded' }) 144 | 145 | strictEqual(parameter, constructQuery) 146 | }) 147 | }) 148 | 149 | it('should send an accept header with the value application/n-triples, text/turtle', async () => { 150 | await withServer(async server => { 151 | let accept = null 152 | 153 | server.app.get('/', async (req, res) => { 154 | accept = req.headers.accept 155 | 156 | res.end() 157 | }) 158 | 159 | const endpointUrl = await server.listen() 160 | const client = new SimpleClient({ endpointUrl, factory }) 161 | const query = new ParsingQuery({ client }) 162 | 163 | await query.construct(constructQuery) 164 | 165 | strictEqual(accept, 'application/n-triples, text/turtle') 166 | }) 167 | }) 168 | 169 | it('should handle server socket errors', async () => { 170 | await withServer(async server => { 171 | server.app.get('/', async (req, res) => { 172 | req.client.destroy() 173 | }) 174 | 175 | const endpointUrl = await server.listen() 176 | const client = new SimpleClient({ endpointUrl, factory }) 177 | const query = new ParsingQuery({ client }) 178 | 179 | await rejects(async () => { 180 | await query.construct(constructQuery) 181 | }, err => isSocketError(err)) 182 | }) 183 | }) 184 | 185 | it('should handle server errors', async () => { 186 | await withServer(async server => { 187 | server.app.get('/', async (req, res) => { 188 | res.status(500).end(message) 189 | }) 190 | 191 | const endpointUrl = await server.listen() 192 | const client = new SimpleClient({ endpointUrl, factory }) 193 | const query = new ParsingQuery({ client }) 194 | 195 | await rejects(async () => { 196 | await query.construct(constructQuery) 197 | }, err => isServerError(err, message)) 198 | }) 199 | }) 200 | }) 201 | 202 | describe('.select', () => { 203 | it('should be a method', () => { 204 | const query = new ParsingQuery({}) 205 | 206 | strictEqual(typeof query.select, 'function') 207 | }) 208 | 209 | it('should return an array', async () => { 210 | await withServer(async server => { 211 | server.app.get('/', async (req, res) => { 212 | res.status(204).end() 213 | }) 214 | 215 | const endpointUrl = await server.listen() 216 | const client = new SimpleClient({ endpointUrl }) 217 | const query = new ParsingQuery({ client }) 218 | 219 | const result = await query.select(selectQuery) 220 | 221 | strictEqual(Array.isArray(result), true) 222 | }) 223 | }) 224 | 225 | it('should parse the SPARQL JSON result', async () => { 226 | await withServer(async server => { 227 | const content = { 228 | results: { 229 | bindings: [{ 230 | a: { type: 'uri', value: 'http://example.org/0' } 231 | }, { 232 | a: { type: 'uri', value: 'http://example.org/1' } 233 | }] 234 | } 235 | } 236 | 237 | server.app.get('/', async (req, res) => { 238 | res.end(JSON.stringify(content)) 239 | }) 240 | 241 | const endpointUrl = await server.listen() 242 | const client = new SimpleClient({ endpointUrl, factory }) 243 | const query = new ParsingQuery({ client }) 244 | 245 | const result = await query.select(selectQuery) 246 | 247 | strictEqual(result[0].a.termType, 'NamedNode') 248 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 249 | strictEqual(result[1].a.termType, 'NamedNode') 250 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 251 | }) 252 | }) 253 | 254 | it('should send a GET request', async () => { 255 | await withServer(async server => { 256 | let called = false 257 | 258 | server.app.get('/', async (req, res) => { 259 | called = true 260 | 261 | res.status(204).end() 262 | }) 263 | 264 | const endpointUrl = await server.listen() 265 | const client = new SimpleClient({ endpointUrl }) 266 | const query = new ParsingQuery({ client }) 267 | 268 | await query.select(selectQuery) 269 | 270 | strictEqual(called, true) 271 | }) 272 | }) 273 | 274 | it('should send the query string as query parameter', async () => { 275 | await withServer(async server => { 276 | let parameter = null 277 | 278 | server.app.get('/', async (req, res) => { 279 | parameter = req.query.query 280 | 281 | res.status(204).end() 282 | }) 283 | 284 | const endpointUrl = await server.listen() 285 | const client = new SimpleClient({ endpointUrl }) 286 | const query = new ParsingQuery({ client }) 287 | 288 | await query.select(selectQuery) 289 | 290 | strictEqual(parameter, selectQuery) 291 | }) 292 | }) 293 | 294 | it('should use the given factory', async () => { 295 | await withServer(async server => { 296 | const content = { 297 | results: { 298 | bindings: [{ 299 | a: { type: 'bnode', value: 'b0' } 300 | }, { 301 | a: { type: 'literal', value: '0' } 302 | }, { 303 | a: { type: 'uri', value: 'http://example.org/0' } 304 | }] 305 | } 306 | } 307 | const factory = testFactory() 308 | 309 | server.app.get('/', async (req, res) => { 310 | res.end(JSON.stringify(content)) 311 | }) 312 | 313 | const endpointUrl = await server.listen() 314 | const client = new SimpleClient({ endpointUrl, factory }) 315 | const query = new ParsingQuery({ client }) 316 | 317 | await query.select(selectQuery) 318 | 319 | deepStrictEqual(factory.used, { 320 | blankNode: true, 321 | literal: true, 322 | namedNode: true 323 | }) 324 | }) 325 | }) 326 | 327 | it('should use the given operation for the request', async () => { 328 | await withServer(async server => { 329 | let parameter = null 330 | 331 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 332 | parameter = req.body.query 333 | 334 | res.status(204).end() 335 | }) 336 | 337 | const endpointUrl = await server.listen() 338 | const client = new SimpleClient({ endpointUrl }) 339 | const query = new ParsingQuery({ client }) 340 | 341 | await query.select(selectQuery, { operation: 'postUrlencoded' }) 342 | 343 | strictEqual(parameter, selectQuery) 344 | }) 345 | }) 346 | 347 | it('should handle server socket errors', async () => { 348 | await withServer(async server => { 349 | server.app.get('/', async (req, res) => { 350 | req.client.destroy() 351 | }) 352 | 353 | const endpointUrl = await server.listen() 354 | const client = new SimpleClient({ endpointUrl }) 355 | const query = new ParsingQuery({ client }) 356 | 357 | await rejects(async () => { 358 | await query.select(selectQuery) 359 | }, err => isSocketError(err)) 360 | }) 361 | }) 362 | 363 | it('should handle server errors', async () => { 364 | await withServer(async server => { 365 | server.app.get('/', async (req, res) => { 366 | res.status(500).end(message) 367 | }) 368 | 369 | const endpointUrl = await server.listen() 370 | const client = new SimpleClient({ endpointUrl }) 371 | const query = new ParsingQuery({ client }) 372 | 373 | await rejects(async () => { 374 | await query.select(selectQuery) 375 | }, err => isServerError(err, message)) 376 | }) 377 | }) 378 | }) 379 | }) 380 | -------------------------------------------------------------------------------- /test/RawQuery.test.js: -------------------------------------------------------------------------------- 1 | import { rejects, strictEqual } from 'node:assert' 2 | import express from 'express' 3 | import withServer from 'express-as-promise/withServer.js' 4 | import { describe, it } from 'mocha' 5 | import RawQuery from '../RawQuery.js' 6 | import SimpleClient from '../SimpleClient.js' 7 | import { message, askQuery, constructQuery, selectQuery, updateQuery } from './support/examples.js' 8 | import isSocketError from './support/isSocketError.js' 9 | 10 | describe('RawQuery', () => { 11 | it('should be a constructor', () => { 12 | strictEqual(typeof RawQuery, 'function') 13 | }) 14 | 15 | describe('.ask', () => { 16 | it('should be a method', () => { 17 | const query = new RawQuery({}) 18 | 19 | strictEqual(typeof query.ask, 'function') 20 | }) 21 | 22 | it('should return a response object', async () => { 23 | await withServer(async server => { 24 | server.app.get('/', async (req, res) => { 25 | res.end() 26 | }) 27 | 28 | const endpointUrl = await server.listen() 29 | const client = new SimpleClient({ endpointUrl }) 30 | const query = new RawQuery({ client }) 31 | 32 | const res = await query.ask(askQuery) 33 | 34 | strictEqual(typeof res, 'object') 35 | strictEqual(typeof res.text, 'function') 36 | }) 37 | }) 38 | 39 | it('should send a GET request', async () => { 40 | await withServer(async server => { 41 | let called = false 42 | 43 | server.app.get('/', async (req, res) => { 44 | called = true 45 | 46 | res.end() 47 | }) 48 | 49 | const endpointUrl = await server.listen() 50 | const client = new SimpleClient({ endpointUrl }) 51 | const query = new RawQuery({ client }) 52 | 53 | await query.ask(askQuery) 54 | 55 | strictEqual(called, true) 56 | }) 57 | }) 58 | 59 | it('should send the query string as query parameter', async () => { 60 | await withServer(async server => { 61 | let parameter = null 62 | 63 | server.app.get('/', async (req, res) => { 64 | parameter = req.query.query 65 | 66 | res.end() 67 | }) 68 | 69 | const endpointUrl = await server.listen() 70 | const client = new SimpleClient({ endpointUrl }) 71 | const query = new RawQuery({ client }) 72 | 73 | await query.ask(askQuery) 74 | 75 | strictEqual(parameter, askQuery) 76 | }) 77 | }) 78 | 79 | it('should keep existing query params', async () => { 80 | await withServer(async server => { 81 | let parameters = null 82 | const key = 'auth_token' 83 | const value = '12345' 84 | 85 | server.app.get('/', async (req, res) => { 86 | parameters = req.query 87 | 88 | res.end() 89 | }) 90 | 91 | const endpointUrl = await server.listen() 92 | const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) 93 | const query = new RawQuery({ client }) 94 | 95 | await query.ask(askQuery) 96 | 97 | strictEqual(parameters[key], value) 98 | }) 99 | }) 100 | 101 | it('should send an accept header with the value application/sparql-results+json', async () => { 102 | await withServer(async server => { 103 | let accept = null 104 | 105 | server.app.get('/', async (req, res) => { 106 | accept = req.headers.accept 107 | 108 | res.end() 109 | }) 110 | 111 | const endpointUrl = await server.listen() 112 | const client = new SimpleClient({ endpointUrl }) 113 | const query = new RawQuery({ client }) 114 | 115 | await query.ask(askQuery) 116 | 117 | strictEqual(accept, 'application/sparql-results+json') 118 | }) 119 | }) 120 | 121 | it('should merge the headers given in the method call', async () => { 122 | await withServer(async server => { 123 | let header = null 124 | const value = 'Bearer foo' 125 | 126 | server.app.get('/', async (req, res) => { 127 | header = req.headers.authorization 128 | 129 | res.end() 130 | }) 131 | 132 | const endpointUrl = await server.listen() 133 | const client = new SimpleClient({ endpointUrl }) 134 | const query = new RawQuery({ client }) 135 | 136 | await query.ask(askQuery, { 137 | headers: { 138 | authorization: value 139 | } 140 | }) 141 | 142 | strictEqual(header, value) 143 | }) 144 | }) 145 | 146 | it('should prioritize the headers from the method call', async () => { 147 | await withServer(async server => { 148 | let header = null 149 | const value = 'Bearer foo' 150 | 151 | server.app.get('/', async (req, res) => { 152 | header = req.headers.authorization 153 | 154 | res.end() 155 | }) 156 | 157 | const endpointUrl = await server.listen() 158 | const client = new SimpleClient({ 159 | endpointUrl, 160 | headers: { 161 | authorization: 'Bearer bar' 162 | } 163 | }) 164 | const query = new RawQuery({ client }) 165 | 166 | await query.ask(askQuery, { 167 | headers: { 168 | authorization: value 169 | } 170 | }) 171 | 172 | strictEqual(header, value) 173 | }) 174 | }) 175 | 176 | it('should use the given operation for the request', async () => { 177 | await withServer(async server => { 178 | let parameter = null 179 | 180 | server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { 181 | parameter = req.body.query 182 | 183 | res.end() 184 | }) 185 | 186 | const endpointUrl = await server.listen() 187 | const client = new SimpleClient({ endpointUrl }) 188 | const query = new RawQuery({ client }) 189 | 190 | await query.ask(askQuery, { operation: 'postUrlencoded' }) 191 | 192 | strictEqual(parameter, askQuery) 193 | }) 194 | }) 195 | 196 | it('should handle server socket errors', async () => { 197 | await withServer(async server => { 198 | server.app.get('/', async (req, res) => { 199 | req.client.destroy() 200 | }) 201 | 202 | const endpointUrl = await server.listen() 203 | const client = new SimpleClient({ endpointUrl }) 204 | const query = new RawQuery({ client }) 205 | 206 | await rejects(async () => { 207 | await query.ask(askQuery) 208 | }, err => isSocketError(err)) 209 | }) 210 | }) 211 | 212 | it('should not handle server errors', async () => { 213 | await withServer(async server => { 214 | server.app.get('/', async (req, res) => { 215 | res.status(500).end(message) 216 | }) 217 | 218 | const endpointUrl = await server.listen() 219 | const client = new SimpleClient({ endpointUrl }) 220 | const query = new RawQuery({ client }) 221 | 222 | await query.ask(askQuery) 223 | }) 224 | }) 225 | }) 226 | 227 | describe('.construct', () => { 228 | it('should be a method', () => { 229 | const query = new RawQuery({}) 230 | 231 | strictEqual(typeof query.construct, 'function') 232 | }) 233 | 234 | it('should return a response object', async () => { 235 | await withServer(async server => { 236 | server.app.get('/', async (req, res) => { 237 | res.end() 238 | }) 239 | 240 | const endpointUrl = await server.listen() 241 | const client = new SimpleClient({ endpointUrl }) 242 | const query = new RawQuery({ client }) 243 | 244 | const res = await query.construct(constructQuery) 245 | 246 | strictEqual(typeof res, 'object') 247 | strictEqual(typeof res.text, 'function') 248 | }) 249 | }) 250 | 251 | it('should send a GET request', async () => { 252 | await withServer(async server => { 253 | let called = false 254 | 255 | server.app.get('/', async (req, res) => { 256 | called = true 257 | 258 | res.end() 259 | }) 260 | 261 | const endpointUrl = await server.listen() 262 | const client = new SimpleClient({ endpointUrl }) 263 | const query = new RawQuery({ client }) 264 | 265 | await query.construct(constructQuery) 266 | 267 | strictEqual(called, true) 268 | }) 269 | }) 270 | 271 | it('should send the query string as query parameter', async () => { 272 | await withServer(async server => { 273 | let parameter = null 274 | 275 | server.app.get('/', async (req, res) => { 276 | parameter = req.query.query 277 | 278 | res.end() 279 | }) 280 | 281 | const endpointUrl = await server.listen() 282 | const client = new SimpleClient({ endpointUrl }) 283 | const query = new RawQuery({ client }) 284 | 285 | await query.construct(constructQuery) 286 | 287 | strictEqual(parameter, constructQuery) 288 | }) 289 | }) 290 | 291 | it('should keep existing query params', async () => { 292 | await withServer(async server => { 293 | let parameters = null 294 | const key = 'auth_token' 295 | const value = '12345' 296 | 297 | server.app.get('/', async (req, res) => { 298 | parameters = req.query 299 | 300 | res.end() 301 | }) 302 | 303 | const endpointUrl = await server.listen() 304 | const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) 305 | const query = new RawQuery({ client }) 306 | 307 | await query.construct(constructQuery) 308 | 309 | strictEqual(parameters[key], value) 310 | }) 311 | }) 312 | 313 | it('should send an accept header with the value application/n-triples', async () => { 314 | await withServer(async server => { 315 | let accept = null 316 | 317 | server.app.get('/', async (req, res) => { 318 | accept = req.headers.accept 319 | 320 | res.end() 321 | }) 322 | 323 | const endpointUrl = await server.listen() 324 | const client = new SimpleClient({ endpointUrl }) 325 | const query = new RawQuery({ client }) 326 | 327 | await query.construct(constructQuery) 328 | 329 | strictEqual(accept, 'application/n-triples') 330 | }) 331 | }) 332 | 333 | it('should merge the headers given in the method call', async () => { 334 | await withServer(async server => { 335 | let header = null 336 | const value = 'Bearer foo' 337 | 338 | server.app.get('/', async (req, res) => { 339 | header = req.headers.authorization 340 | 341 | res.end() 342 | }) 343 | 344 | const endpointUrl = await server.listen() 345 | const client = new SimpleClient({ endpointUrl }) 346 | const query = new RawQuery({ client }) 347 | 348 | await query.construct(constructQuery, { 349 | headers: { 350 | authorization: value 351 | } 352 | }) 353 | 354 | strictEqual(header, value) 355 | }) 356 | }) 357 | 358 | it('should prioritize the headers from the method call', async () => { 359 | await withServer(async server => { 360 | let header = null 361 | const value = 'Bearer foo' 362 | 363 | server.app.get('/', async (req, res) => { 364 | header = req.headers.authorization 365 | 366 | res.end() 367 | }) 368 | 369 | const endpointUrl = await server.listen() 370 | const client = new SimpleClient({ 371 | endpointUrl, 372 | headers: { 373 | authorization: 'Bearer bar' 374 | } 375 | }) 376 | const query = new RawQuery({ client }) 377 | 378 | await query.construct(constructQuery, { 379 | headers: { 380 | authorization: value 381 | } 382 | }) 383 | 384 | strictEqual(header, value) 385 | }) 386 | }) 387 | 388 | it('should use the given operation for the request', async () => { 389 | await withServer(async server => { 390 | let parameter = null 391 | 392 | server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { 393 | parameter = req.body.query 394 | 395 | res.end() 396 | }) 397 | 398 | const endpointUrl = await server.listen() 399 | const client = new SimpleClient({ endpointUrl }) 400 | const query = new RawQuery({ client }) 401 | 402 | await query.construct(constructQuery, { operation: 'postUrlencoded' }) 403 | 404 | strictEqual(parameter, constructQuery) 405 | }) 406 | }) 407 | 408 | it('should handle server socket errors', async () => { 409 | await withServer(async server => { 410 | server.app.get('/', async (req, res) => { 411 | req.client.destroy() 412 | }) 413 | 414 | const endpointUrl = await server.listen() 415 | const client = new SimpleClient({ endpointUrl }) 416 | const query = new RawQuery({ client }) 417 | 418 | await rejects(async () => { 419 | await query.construct(constructQuery) 420 | }, err => isSocketError(err)) 421 | }) 422 | }) 423 | 424 | it('should not handle server errors', async () => { 425 | await withServer(async server => { 426 | server.app.get('/', async (req, res) => { 427 | res.status(500).end(message) 428 | }) 429 | 430 | const endpointUrl = await server.listen() 431 | const client = new SimpleClient({ endpointUrl }) 432 | const query = new RawQuery({ client }) 433 | 434 | await query.construct(constructQuery) 435 | }) 436 | }) 437 | }) 438 | 439 | describe('.select', () => { 440 | it('should be a method', () => { 441 | const query = new RawQuery({}) 442 | 443 | strictEqual(typeof query.select, 'function') 444 | }) 445 | 446 | it('should return a response object', async () => { 447 | await withServer(async server => { 448 | server.app.get('/', async (req, res) => { 449 | res.end() 450 | }) 451 | 452 | const endpointUrl = await server.listen() 453 | const client = new SimpleClient({ endpointUrl }) 454 | const query = new RawQuery({ client }) 455 | 456 | const res = await query.select(selectQuery) 457 | 458 | strictEqual(typeof res, 'object') 459 | strictEqual(typeof res.text, 'function') 460 | }) 461 | }) 462 | 463 | it('should send a GET request', async () => { 464 | await withServer(async server => { 465 | let called = false 466 | 467 | server.app.get('/', async (req, res) => { 468 | called = true 469 | 470 | res.end() 471 | }) 472 | 473 | const endpointUrl = await server.listen() 474 | const client = new SimpleClient({ endpointUrl }) 475 | const query = new RawQuery({ client }) 476 | 477 | await query.select(selectQuery) 478 | 479 | strictEqual(called, true) 480 | }) 481 | }) 482 | 483 | it('should send the query string as query parameter', async () => { 484 | await withServer(async server => { 485 | let parameter = null 486 | 487 | server.app.get('/', async (req, res) => { 488 | parameter = req.query.query 489 | 490 | res.end() 491 | }) 492 | 493 | const endpointUrl = await server.listen() 494 | const client = new SimpleClient({ endpointUrl }) 495 | const query = new RawQuery({ client }) 496 | 497 | await query.select(selectQuery) 498 | 499 | strictEqual(parameter, selectQuery) 500 | }) 501 | }) 502 | 503 | it('should keep existing query params', async () => { 504 | await withServer(async server => { 505 | let parameters = null 506 | const key = 'auth_token' 507 | const value = '12345' 508 | 509 | server.app.get('/', async (req, res) => { 510 | parameters = req.query 511 | 512 | res.end() 513 | }) 514 | 515 | const endpointUrl = await server.listen() 516 | const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) 517 | const query = new RawQuery({ client }) 518 | 519 | await query.select(selectQuery) 520 | 521 | strictEqual(parameters[key], value) 522 | }) 523 | }) 524 | 525 | it('should send an accept header with the value application/sparql-results+json', async () => { 526 | await withServer(async server => { 527 | let accept = null 528 | 529 | server.app.get('/', async (req, res) => { 530 | accept = req.headers.accept 531 | 532 | res.end() 533 | }) 534 | 535 | const endpointUrl = await server.listen() 536 | const client = new SimpleClient({ endpointUrl }) 537 | const query = new RawQuery({ client }) 538 | 539 | await query.select(selectQuery) 540 | 541 | strictEqual(accept, 'application/sparql-results+json') 542 | }) 543 | }) 544 | 545 | it('should merge the headers given in the method call', async () => { 546 | await withServer(async server => { 547 | let header = null 548 | const value = 'Bearer foo' 549 | 550 | server.app.get('/', async (req, res) => { 551 | header = req.headers.authorization 552 | 553 | res.end() 554 | }) 555 | 556 | const endpointUrl = await server.listen() 557 | const client = new SimpleClient({ endpointUrl }) 558 | const query = new RawQuery({ client }) 559 | 560 | await query.select(selectQuery, { 561 | headers: { 562 | authorization: value 563 | } 564 | }) 565 | 566 | strictEqual(header, value) 567 | }) 568 | }) 569 | 570 | it('should prioritize the headers from the method call', async () => { 571 | await withServer(async server => { 572 | let header = null 573 | const value = 'Bearer foo' 574 | 575 | server.app.get('/', async (req, res) => { 576 | header = req.headers.authorization 577 | 578 | res.end() 579 | }) 580 | 581 | const endpointUrl = await server.listen() 582 | const client = new SimpleClient({ 583 | endpointUrl, 584 | headers: { 585 | authorization: 'Bearer bar' 586 | } 587 | }) 588 | const query = new RawQuery({ client }) 589 | 590 | await query.select(selectQuery, { 591 | headers: { 592 | authorization: value 593 | } 594 | }) 595 | 596 | strictEqual(header, value) 597 | }) 598 | }) 599 | 600 | it('should use the given operation for the request', async () => { 601 | await withServer(async server => { 602 | let parameter = null 603 | 604 | server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { 605 | parameter = req.body.query 606 | 607 | res.end() 608 | }) 609 | const endpointUrl = await server.listen() 610 | const client = new SimpleClient({ endpointUrl }) 611 | const query = new RawQuery({ client }) 612 | 613 | await query.select(selectQuery, { operation: 'postUrlencoded' }) 614 | 615 | strictEqual(parameter, selectQuery) 616 | }) 617 | }) 618 | 619 | it('should handle server socket errors', async () => { 620 | await withServer(async server => { 621 | server.app.get('/', async (req, res) => { 622 | req.client.destroy() 623 | }) 624 | 625 | const endpointUrl = await server.listen() 626 | const client = new SimpleClient({ endpointUrl }) 627 | const query = new RawQuery({ client }) 628 | 629 | await rejects(async () => { 630 | await query.select(selectQuery) 631 | }, err => isSocketError(err)) 632 | }) 633 | }) 634 | 635 | it('should not handle server errors', async () => { 636 | await withServer(async server => { 637 | server.app.get('/', async (req, res) => { 638 | res.status(500).end(message) 639 | }) 640 | 641 | const endpointUrl = await server.listen() 642 | const client = new SimpleClient({ endpointUrl }) 643 | const query = new RawQuery({ client }) 644 | 645 | await query.select(selectQuery) 646 | }) 647 | }) 648 | }) 649 | 650 | describe('.update', () => { 651 | it('should be a method', () => { 652 | const query = new RawQuery({}) 653 | 654 | strictEqual(typeof query.update, 'function') 655 | }) 656 | 657 | it('should return a response object', async () => { 658 | await withServer(async server => { 659 | server.app.post('/', async (req, res) => { 660 | res.end() 661 | }) 662 | 663 | const updateUrl = await server.listen() 664 | const client = new SimpleClient({ updateUrl }) 665 | const query = new RawQuery({ client }) 666 | 667 | const res = await query.update(updateQuery) 668 | 669 | strictEqual(typeof res, 'object') 670 | strictEqual(typeof res.text, 'function') 671 | }) 672 | }) 673 | 674 | it('should send a POST request', async () => { 675 | await withServer(async server => { 676 | let called = false 677 | 678 | server.app.post('/', async (req, res) => { 679 | called = true 680 | 681 | res.end() 682 | }) 683 | 684 | const updateUrl = await server.listen() 685 | const client = new SimpleClient({ updateUrl }) 686 | const query = new RawQuery({ client }) 687 | 688 | await query.update(updateQuery) 689 | 690 | strictEqual(called, true) 691 | }) 692 | }) 693 | 694 | it('should keep existing query params', async () => { 695 | await withServer(async server => { 696 | let parameters = null 697 | const key = 'auth_token' 698 | const value = '12345' 699 | 700 | server.app.post('/', async (req, res) => { 701 | parameters = req.query 702 | 703 | res.end() 704 | }) 705 | 706 | const updateUrl = await server.listen() 707 | const client = new SimpleClient({ updateUrl: `${updateUrl}?${key}=${value}` }) 708 | const query = new RawQuery({ client }) 709 | 710 | await query.update(updateQuery) 711 | 712 | strictEqual(parameters[key], value) 713 | }) 714 | }) 715 | 716 | it('should send an accept header with the value */*', async () => { 717 | await withServer(async server => { 718 | let accept = null 719 | 720 | server.app.post('/', async (req, res) => { 721 | accept = req.headers.accept 722 | 723 | res.end() 724 | }) 725 | 726 | const updateUrl = await server.listen() 727 | const client = new SimpleClient({ updateUrl }) 728 | const query = new RawQuery({ client }) 729 | 730 | await query.update(updateQuery) 731 | 732 | strictEqual(accept, '*/*') 733 | }) 734 | }) 735 | 736 | it('should send a content-type header with the value application/x-www-form-urlencoded', async () => { 737 | await withServer(async server => { 738 | let contentType = null 739 | 740 | server.app.post('/', async (req, res) => { 741 | contentType = req.headers['content-type'] 742 | 743 | res.end() 744 | }) 745 | 746 | const updateUrl = await server.listen() 747 | const client = new SimpleClient({ updateUrl }) 748 | const query = new RawQuery({ client }) 749 | 750 | await query.update(updateQuery) 751 | 752 | strictEqual(contentType, 'application/x-www-form-urlencoded') 753 | }) 754 | }) 755 | 756 | it('should send the query string urlencoded in the request body', async () => { 757 | await withServer(async server => { 758 | let parameter = null 759 | 760 | server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { 761 | parameter = req.body.update 762 | 763 | res.end() 764 | }) 765 | 766 | const updateUrl = await server.listen() 767 | const client = new SimpleClient({ updateUrl }) 768 | const query = new RawQuery({ client }) 769 | 770 | await query.update(updateQuery) 771 | 772 | strictEqual(parameter, updateQuery) 773 | }) 774 | }) 775 | 776 | it('should merge the headers given in the method call', async () => { 777 | await withServer(async server => { 778 | let header = null 779 | const value = 'Bearer foo' 780 | 781 | server.app.post('/', async (req, res) => { 782 | header = req.headers.authorization 783 | 784 | res.end() 785 | }) 786 | 787 | const updateUrl = await server.listen() 788 | const client = new SimpleClient({ updateUrl }) 789 | const query = new RawQuery({ client }) 790 | 791 | await query.update(updateQuery, { 792 | headers: { 793 | authorization: value 794 | } 795 | }) 796 | 797 | strictEqual(header, value) 798 | }) 799 | }) 800 | 801 | it('should prioritize the headers from the method call', async () => { 802 | await withServer(async server => { 803 | let header = null 804 | const value = 'Bearer foo' 805 | 806 | server.app.post('/', async (req, res) => { 807 | header = req.headers.authorization 808 | 809 | res.end() 810 | }) 811 | 812 | const updateUrl = await server.listen() 813 | const client = new SimpleClient({ 814 | updateUrl, 815 | headers: { 816 | authorization: 'Bearer bar' 817 | } 818 | }) 819 | const query = new RawQuery({ client }) 820 | 821 | await query.update(updateQuery, { 822 | headers: { 823 | authorization: value 824 | } 825 | }) 826 | 827 | strictEqual(header, value) 828 | }) 829 | }) 830 | 831 | it('should use the given operation for the request', async () => { 832 | await withServer(async server => { 833 | let content = null 834 | 835 | server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { 836 | content = req.body 837 | 838 | res.end() 839 | }) 840 | 841 | const updateUrl = await server.listen() 842 | const client = new SimpleClient({ updateUrl }) 843 | const query = new RawQuery({ client }) 844 | 845 | await query.update(updateQuery, { operation: 'postDirect' }) 846 | 847 | strictEqual(content, updateQuery) 848 | }) 849 | }) 850 | 851 | it('should handle server socket errors', async () => { 852 | await withServer(async server => { 853 | server.app.post('/', async (req, res) => { 854 | req.client.destroy() 855 | }) 856 | 857 | const updateUrl = await server.listen() 858 | const client = new SimpleClient({ updateUrl }) 859 | const query = new RawQuery({ client }) 860 | 861 | await rejects(async () => { 862 | await query.update(updateQuery) 863 | }, err => isSocketError(err)) 864 | }) 865 | }) 866 | 867 | it('should not handle server errors', async () => { 868 | await withServer(async server => { 869 | server.app.post('/', async (req, res) => { 870 | res.status(500).end(message) 871 | }) 872 | 873 | const updateUrl = await server.listen() 874 | const client = new SimpleClient({ updateUrl }) 875 | const query = new RawQuery({ client }) 876 | 877 | await query.update(updateQuery) 878 | }) 879 | }) 880 | }) 881 | }) 882 | -------------------------------------------------------------------------------- /test/ResultParser.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, rejects, strictEqual } from 'node:assert' 2 | import factory from '@rdfjs/data-model' 3 | import { describe, it } from 'mocha' 4 | import chunks from 'stream-chunks/chunks.js' 5 | import ResultParser from '../ResultParser.js' 6 | import testFactory from './support/testFactory.js' 7 | 8 | describe('ResultParser', () => { 9 | it('should be a constructor', () => { 10 | strictEqual(typeof ResultParser, 'function') 11 | }) 12 | 13 | it('should throw an error if the content is not JSON', async () => { 14 | const parser = new ResultParser({ factory }) 15 | 16 | parser.end('this is not json') 17 | 18 | await rejects(async () => { 19 | await chunks(parser) 20 | }, { 21 | message: /Unexpected/ 22 | }) 23 | }) 24 | 25 | it('should not emit any chunk if the json doesn\'t contain results.bindings', async () => { 26 | const parser = new ResultParser({ factory }) 27 | 28 | parser.end('{}') 29 | 30 | const result = await chunks(parser) 31 | 32 | deepStrictEqual(result, []) 33 | }) 34 | 35 | it('should not emit any chunk when Stardog GROUP BY bug shows up', async () => { 36 | const parser = new ResultParser({ factory }) 37 | const content = { 38 | results: { 39 | bindings: [{}] 40 | } 41 | } 42 | 43 | parser.end(JSON.stringify(content)) 44 | 45 | const result = await chunks(parser) 46 | 47 | deepStrictEqual(result, []) 48 | }) 49 | 50 | it('should parse named node values', async () => { 51 | const parser = new ResultParser({ factory }) 52 | const content = { 53 | results: { 54 | bindings: [{ 55 | a: { type: 'uri', value: 'http://example.org/0' } 56 | }, { 57 | a: { type: 'uri', value: 'http://example.org/1' } 58 | }] 59 | } 60 | } 61 | 62 | parser.end(JSON.stringify(content)) 63 | 64 | const result = await chunks(parser) 65 | 66 | strictEqual(result[0].a.termType, 'NamedNode') 67 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 68 | strictEqual(result[1].a.termType, 'NamedNode') 69 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 70 | }) 71 | 72 | it('should parse blank node values', async () => { 73 | const parser = new ResultParser({ factory }) 74 | const content = { 75 | results: { 76 | bindings: [{ 77 | a: { type: 'bnode', value: 'b0' } 78 | }, { 79 | a: { type: 'bnode', value: 'b1' } 80 | }] 81 | } 82 | } 83 | 84 | parser.end(JSON.stringify(content)) 85 | 86 | const result = await chunks(parser) 87 | 88 | strictEqual(result[0].a.termType, 'BlankNode') 89 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 90 | strictEqual(result[1].a.termType, 'BlankNode') 91 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 92 | }) 93 | 94 | it('should parse literal values', async () => { 95 | const parser = new ResultParser({ factory }) 96 | const content = { 97 | results: { 98 | bindings: [{ 99 | a: { type: 'literal', value: '0' } 100 | }, { 101 | a: { type: 'literal', value: '1' } 102 | }] 103 | } 104 | } 105 | 106 | parser.end(JSON.stringify(content)) 107 | 108 | const result = await chunks(parser) 109 | 110 | strictEqual(result[0].a.termType, 'Literal') 111 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 112 | strictEqual(result[1].a.termType, 'Literal') 113 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 114 | }) 115 | 116 | it('should parse typed literal values', async () => { 117 | const parser = new ResultParser({ factory }) 118 | const content = { 119 | results: { 120 | bindings: [{ 121 | a: { type: 'literal', value: '0', datatype: 'http://example.org/datatype/0' } 122 | }, { 123 | a: { type: 'literal', value: '1', datatype: 'http://example.org/datatype/0' } 124 | }] 125 | } 126 | } 127 | 128 | parser.end(JSON.stringify(content)) 129 | 130 | const result = await chunks(parser) 131 | 132 | strictEqual(result[0].a.termType, 'Literal') 133 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 134 | strictEqual(result[0].a.datatype.value, content.results.bindings[0].a.datatype) 135 | strictEqual(result[1].a.termType, 'Literal') 136 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 137 | strictEqual(result[1].a.datatype.value, content.results.bindings[1].a.datatype) 138 | }) 139 | 140 | it('should parse Virtuoso style typed literal values', async () => { 141 | const parser = new ResultParser({ factory }) 142 | const content = { 143 | results: { 144 | bindings: [{ 145 | a: { type: 'typed-literal', value: '0', datatype: 'http://example.org/datatype/0' } 146 | }, { 147 | a: { type: 'typed-literal', value: '1', datatype: 'http://example.org/datatype/0' } 148 | }] 149 | } 150 | } 151 | 152 | parser.end(JSON.stringify(content)) 153 | 154 | const result = await chunks(parser) 155 | 156 | strictEqual(result[0].a.termType, 'Literal') 157 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 158 | strictEqual(result[0].a.datatype.value, content.results.bindings[0].a.datatype) 159 | strictEqual(result[1].a.termType, 'Literal') 160 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 161 | strictEqual(result[1].a.datatype.value, content.results.bindings[1].a.datatype) 162 | }) 163 | 164 | it('should parse language literal values', async () => { 165 | const parser = new ResultParser({ factory }) 166 | const content = { 167 | results: { 168 | bindings: [{ 169 | a: { type: 'literal', value: '0', 'xml:lang': 'de' } 170 | }, { 171 | a: { type: 'literal', value: '1', 'xml:lang': 'en' } 172 | }] 173 | } 174 | } 175 | 176 | parser.end(JSON.stringify(content)) 177 | 178 | const result = await chunks(parser) 179 | 180 | strictEqual(result[0].a.termType, 'Literal') 181 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 182 | strictEqual(result[0].a.language, content.results.bindings[0].a['xml:lang']) 183 | strictEqual(result[1].a.termType, 'Literal') 184 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 185 | strictEqual(result[1].a.language, content.results.bindings[1].a['xml:lang']) 186 | }) 187 | 188 | it('should parse multiple variables', async () => { 189 | const parser = new ResultParser({ factory }) 190 | const content = { 191 | results: { 192 | bindings: [{ 193 | a: { type: 'uri', value: 'http://example.org/0' }, 194 | b: { type: 'uri', value: 'http://example.org/1' } 195 | }, { 196 | a: { type: 'uri', value: 'http://example.org/2' }, 197 | b: { type: 'uri', value: 'http://example.org/3' } 198 | }] 199 | } 200 | } 201 | 202 | parser.end(JSON.stringify(content)) 203 | 204 | const result = await chunks(parser) 205 | 206 | strictEqual(result[0].a.termType, 'NamedNode') 207 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 208 | strictEqual(result[0].b.termType, 'NamedNode') 209 | strictEqual(result[0].b.value, content.results.bindings[0].b.value) 210 | strictEqual(result[1].a.termType, 'NamedNode') 211 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 212 | strictEqual(result[1].b.termType, 'NamedNode') 213 | strictEqual(result[1].b.value, content.results.bindings[1].b.value) 214 | }) 215 | 216 | it('should use the given factory', async () => { 217 | const content = { 218 | results: { 219 | bindings: [{ 220 | a: { type: 'bnode', value: 'b0' } 221 | }, { 222 | a: { type: 'literal', value: '0' } 223 | }, { 224 | a: { type: 'uri', value: 'http://example.org/0' } 225 | }] 226 | } 227 | } 228 | const factory = testFactory() 229 | const parser = new ResultParser({ factory }) 230 | 231 | parser.end(JSON.stringify(content)) 232 | 233 | await chunks(parser) 234 | 235 | deepStrictEqual(factory.used, { 236 | blankNode: true, 237 | literal: true, 238 | namedNode: true 239 | }) 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /test/SimpleClient.test.js: -------------------------------------------------------------------------------- 1 | import { rejects, strictEqual, throws } from 'node:assert' 2 | import express from 'express' 3 | import withServer from 'express-as-promise/withServer.js' 4 | import { describe, it } from 'mocha' 5 | import RawQuery from '../RawQuery.js' 6 | import SimpleClient from '../SimpleClient.js' 7 | import { message, selectQuery, updateQuery } from './support/examples.js' 8 | import isSocketError from './support/isSocketError.js' 9 | 10 | describe('SimpleClient', () => { 11 | it('should be a constructor', () => { 12 | strictEqual(typeof SimpleClient, 'function') 13 | }) 14 | 15 | it('should throw an error if endpointUrl, storeUrl, or updateUrl is given', () => { 16 | throws(() => { 17 | new SimpleClient({}) // eslint-disable-line no-new 18 | }, { 19 | message: /endpointUrl/ 20 | }) 21 | }) 22 | 23 | it('should use RawQuery to create the query instance', () => { 24 | const client = new SimpleClient({ endpointUrl: 'test' }) 25 | 26 | strictEqual(client.query instanceof RawQuery, true) 27 | }) 28 | 29 | it('should set authorization header if user and password are given', () => { 30 | const client = new SimpleClient({ 31 | endpointUrl: 'test', 32 | user: 'abc', 33 | password: 'def' 34 | }) 35 | 36 | strictEqual(client.headers.get('authorization'), 'Basic YWJjOmRlZg==') 37 | }) 38 | 39 | it('should forward client to Query constructor', () => { 40 | class Query { 41 | constructor ({ client }) { 42 | this.client = client 43 | } 44 | } 45 | 46 | const client = new SimpleClient({ endpointUrl: 'test', Query }) 47 | 48 | strictEqual(client.query.client, client) 49 | }) 50 | 51 | it('should forward client to Store constructor', () => { 52 | class Store { 53 | constructor ({ client }) { 54 | this.client = client 55 | } 56 | } 57 | 58 | const client = new SimpleClient({ endpointUrl: 'test', Store }) 59 | 60 | strictEqual(client.store.client, client) 61 | }) 62 | 63 | describe('.get', () => { 64 | it('should be a method', () => { 65 | const client = new SimpleClient({ endpointUrl: 'test' }) 66 | 67 | strictEqual(typeof client.get, 'function') 68 | }) 69 | 70 | it('should return a response object', async () => { 71 | await withServer(async server => { 72 | server.app.get('/', async (req, res) => { 73 | res.end() 74 | }) 75 | 76 | const endpointUrl = await server.listen() 77 | const client = new SimpleClient({ endpointUrl }) 78 | 79 | const res = await client.get(selectQuery) 80 | 81 | strictEqual(typeof res, 'object') 82 | strictEqual(typeof res.text, 'function') 83 | }) 84 | }) 85 | 86 | it('should send a GET request', async () => { 87 | await withServer(async server => { 88 | let called = false 89 | 90 | server.app.get('/', async (req, res) => { 91 | called = true 92 | 93 | res.end() 94 | }) 95 | 96 | const endpointUrl = await server.listen() 97 | const client = new SimpleClient({ endpointUrl }) 98 | 99 | await client.get(selectQuery) 100 | 101 | strictEqual(called, true) 102 | }) 103 | }) 104 | 105 | it('should send the query string as query parameter', async () => { 106 | await withServer(async server => { 107 | let parameter = null 108 | 109 | server.app.get('/', async (req, res) => { 110 | parameter = req.query.query 111 | 112 | res.end() 113 | }) 114 | 115 | const endpointUrl = await server.listen() 116 | const client = new SimpleClient({ endpointUrl }) 117 | 118 | await client.get(selectQuery) 119 | 120 | strictEqual(parameter, selectQuery) 121 | }) 122 | }) 123 | 124 | it('should keep existing query params', async () => { 125 | await withServer(async server => { 126 | let parameters = null 127 | const key = 'auth_token' 128 | const value = '12345' 129 | 130 | server.app.get('/', async (req, res) => { 131 | parameters = req.query 132 | 133 | res.end() 134 | }) 135 | 136 | const endpointUrl = await server.listen() 137 | const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) 138 | 139 | await client.get(selectQuery) 140 | 141 | strictEqual(parameters[key], value) 142 | }) 143 | }) 144 | 145 | it('should merge the headers given in the method call', async () => { 146 | await withServer(async server => { 147 | let header = null 148 | const value = 'Bearer foo' 149 | 150 | server.app.get('/', async (req, res) => { 151 | header = req.headers.authorization 152 | 153 | res.end() 154 | }) 155 | 156 | const endpointUrl = await server.listen() 157 | const client = new SimpleClient({ endpointUrl }) 158 | 159 | await client.get(selectQuery, { 160 | headers: { 161 | authorization: value 162 | } 163 | }) 164 | 165 | strictEqual(header, value) 166 | }) 167 | }) 168 | 169 | it('should prioritize the headers from the method call', async () => { 170 | await withServer(async server => { 171 | let header = null 172 | const value = 'Bearer foo' 173 | 174 | server.app.get('/', async (req, res) => { 175 | header = req.headers.authorization 176 | 177 | res.end() 178 | }) 179 | 180 | const endpointUrl = await server.listen() 181 | const client = new SimpleClient({ 182 | endpointUrl, 183 | headers: { 184 | authorization: 'Bearer bar' 185 | } 186 | }) 187 | 188 | await client.get(selectQuery, { 189 | headers: { 190 | authorization: value 191 | } 192 | }) 193 | 194 | strictEqual(header, value) 195 | }) 196 | }) 197 | 198 | it('should use the updateUrl and update param if update is true', async () => { 199 | await withServer(async server => { 200 | let parameters = null 201 | 202 | server.app.get('/', async (req, res) => { 203 | parameters = req.query 204 | 205 | res.end() 206 | }) 207 | 208 | const updateUrl = await server.listen() 209 | const client = new SimpleClient({ updateUrl }) 210 | 211 | await client.get(updateQuery, { update: true }) 212 | 213 | strictEqual(parameters.update, updateQuery) 214 | }) 215 | }) 216 | 217 | it('should keep existing update query params', async () => { 218 | await withServer(async server => { 219 | let parameters = null 220 | const key = 'auth_token' 221 | const value = '12345' 222 | 223 | server.app.get('/', async (req, res) => { 224 | parameters = req.query 225 | 226 | res.end() 227 | }) 228 | 229 | const updateUrl = await server.listen() 230 | const client = new SimpleClient({ updateUrl: `${updateUrl}?${key}=${value}` }) 231 | 232 | await client.get(updateQuery, { update: true }) 233 | 234 | strictEqual(parameters[key], value) 235 | }) 236 | }) 237 | 238 | it('should handle server socket errors', async () => { 239 | await withServer(async server => { 240 | server.app.get('/', async (req, res) => { 241 | req.client.destroy() 242 | }) 243 | 244 | const endpointUrl = await server.listen() 245 | const client = new SimpleClient({ endpointUrl }) 246 | 247 | await rejects(async () => { 248 | await client.get(selectQuery) 249 | }, err => isSocketError(err)) 250 | }) 251 | }) 252 | 253 | it('should not handle server errors', async () => { 254 | await withServer(async server => { 255 | server.app.get('/', async (req, res) => { 256 | res.status(500).end(message) 257 | }) 258 | 259 | const endpointUrl = await server.listen() 260 | const client = new SimpleClient({ endpointUrl }) 261 | 262 | await client.get(selectQuery) 263 | }) 264 | }) 265 | }) 266 | 267 | describe('.postDirect', () => { 268 | it('should be a method', () => { 269 | const client = new SimpleClient({ endpointUrl: 'test' }) 270 | 271 | strictEqual(typeof client.postDirect, 'function') 272 | }) 273 | 274 | it('should return a response object', async () => { 275 | await withServer(async server => { 276 | server.app.post('/', async (req, res) => { 277 | res.end() 278 | }) 279 | 280 | const endpointUrl = await server.listen() 281 | const client = new SimpleClient({ endpointUrl }) 282 | 283 | const res = await client.postDirect(selectQuery) 284 | 285 | strictEqual(typeof res, 'object') 286 | strictEqual(typeof res.text, 'function') 287 | }) 288 | }) 289 | 290 | it('should send a POST request', async () => { 291 | await withServer(async server => { 292 | let called = false 293 | 294 | server.app.post('/', async (req, res) => { 295 | called = true 296 | 297 | res.end() 298 | }) 299 | 300 | const endpointUrl = await server.listen() 301 | const client = new SimpleClient({ endpointUrl }) 302 | 303 | await client.postDirect(selectQuery) 304 | 305 | strictEqual(called, true) 306 | }) 307 | }) 308 | 309 | it('should send a content type header with the value application/sparql-query & charset utf-8', async () => { 310 | await withServer(async server => { 311 | let contentType = null 312 | 313 | server.app.post('/', async (req, res) => { 314 | contentType = req.headers['content-type'] 315 | 316 | res.end() 317 | }) 318 | 319 | const endpointUrl = await server.listen() 320 | const client = new SimpleClient({ endpointUrl }) 321 | 322 | await client.postDirect(selectQuery) 323 | 324 | strictEqual(contentType, 'application/sparql-query; charset=utf-8') 325 | }) 326 | }) 327 | 328 | it('should send the query string in the request body', async () => { 329 | await withServer(async server => { 330 | let content = null 331 | 332 | server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { 333 | content = req.body 334 | 335 | res.end() 336 | }) 337 | 338 | const endpointUrl = await server.listen() 339 | const client = new SimpleClient({ endpointUrl }) 340 | 341 | await client.postDirect(selectQuery) 342 | 343 | strictEqual(content, selectQuery) 344 | }) 345 | }) 346 | 347 | it('should merge the headers given in the method call', async () => { 348 | await withServer(async server => { 349 | let header = null 350 | const value = 'Bearer foo' 351 | 352 | server.app.post('/', async (req, res) => { 353 | header = req.headers.authorization 354 | 355 | res.end() 356 | }) 357 | 358 | const endpointUrl = await server.listen() 359 | const client = new SimpleClient({ endpointUrl }) 360 | 361 | await client.postDirect(selectQuery, { 362 | headers: { 363 | authorization: value 364 | } 365 | }) 366 | 367 | strictEqual(header, value) 368 | }) 369 | }) 370 | 371 | it('should prioritize the headers from the method call', async () => { 372 | await withServer(async server => { 373 | let header = null 374 | const value = 'Bearer foo' 375 | 376 | server.app.post('/', async (req, res) => { 377 | header = req.headers.authorization 378 | 379 | res.end() 380 | }) 381 | 382 | const endpointUrl = await server.listen() 383 | const client = new SimpleClient({ 384 | endpointUrl, 385 | headers: { 386 | authorization: 'Bearer bar' 387 | } 388 | }) 389 | 390 | await client.postDirect(selectQuery, { 391 | headers: { 392 | authorization: value 393 | } 394 | }) 395 | 396 | strictEqual(header, value) 397 | }) 398 | }) 399 | 400 | it('should use the updateUrl if update is true', async () => { 401 | await withServer(async server => { 402 | let called = false 403 | 404 | server.app.post('/', async (req, res) => { 405 | called = true 406 | 407 | res.end() 408 | }) 409 | 410 | const updateUrl = await server.listen() 411 | const client = new SimpleClient({ updateUrl }) 412 | 413 | await client.postDirect(updateQuery, { update: true }) 414 | 415 | strictEqual(called, true) 416 | }) 417 | }) 418 | 419 | it('should handle server socket errors', async () => { 420 | await withServer(async server => { 421 | server.app.post('/', async (req, res) => { 422 | req.client.destroy() 423 | }) 424 | 425 | const endpointUrl = await server.listen() 426 | const client = new SimpleClient({ endpointUrl }) 427 | 428 | await rejects(async () => { 429 | await client.postDirect(selectQuery) 430 | }, err => isSocketError(err)) 431 | }) 432 | }) 433 | 434 | it('should not handle server errors', async () => { 435 | await withServer(async server => { 436 | server.app.post('/', async (req, res) => { 437 | res.status(500).end(message) 438 | }) 439 | 440 | const endpointUrl = await server.listen() 441 | const client = new SimpleClient({ endpointUrl }) 442 | 443 | await client.postDirect(selectQuery) 444 | }) 445 | }) 446 | }) 447 | 448 | describe('.postUrlencoded', () => { 449 | it('should be a method', () => { 450 | const client = new SimpleClient({ endpointUrl: 'test' }) 451 | 452 | strictEqual(typeof client.postUrlencoded, 'function') 453 | }) 454 | 455 | it('should return a response object', async () => { 456 | await withServer(async server => { 457 | server.app.post('/', async (req, res) => { 458 | res.end() 459 | }) 460 | 461 | const endpointUrl = await server.listen() 462 | const client = new SimpleClient({ endpointUrl }) 463 | 464 | const res = await client.postUrlencoded(selectQuery) 465 | 466 | strictEqual(typeof res, 'object') 467 | strictEqual(typeof res.text, 'function') 468 | }) 469 | }) 470 | 471 | it('should send a POST request', async () => { 472 | await withServer(async server => { 473 | let called = false 474 | 475 | server.app.post('/', async (req, res) => { 476 | called = true 477 | 478 | res.end() 479 | }) 480 | 481 | const endpointUrl = await server.listen() 482 | const client = new SimpleClient({ endpointUrl }) 483 | 484 | await client.postUrlencoded(selectQuery) 485 | 486 | strictEqual(called, true) 487 | }) 488 | }) 489 | 490 | it('should send a content type header with the value application/x-www-form-urlencoded', async () => { 491 | await withServer(async server => { 492 | let contentType = null 493 | 494 | server.app.post('/', async (req, res) => { 495 | contentType = req.headers['content-type'] 496 | 497 | res.end() 498 | }) 499 | 500 | const endpointUrl = await server.listen() 501 | const client = new SimpleClient({ endpointUrl }) 502 | 503 | await client.postUrlencoded(selectQuery) 504 | 505 | strictEqual(contentType, 'application/x-www-form-urlencoded') 506 | }) 507 | }) 508 | 509 | it('should send the query string urlencoded in the request body', async () => { 510 | await withServer(async server => { 511 | let parameter = null 512 | 513 | server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { 514 | parameter = req.body.query 515 | 516 | res.end() 517 | }) 518 | 519 | const endpointUrl = await server.listen() 520 | const client = new SimpleClient({ endpointUrl }) 521 | 522 | await client.postUrlencoded(selectQuery) 523 | 524 | strictEqual(parameter, selectQuery) 525 | }) 526 | }) 527 | 528 | it('should merge the headers given in the method call', async () => { 529 | await withServer(async server => { 530 | let header = null 531 | const value = 'Bearer foo' 532 | 533 | server.app.post('/', async (req, res) => { 534 | header = req.headers.authorization 535 | 536 | res.end() 537 | }) 538 | 539 | const endpointUrl = await server.listen() 540 | const client = new SimpleClient({ endpointUrl }) 541 | 542 | await client.postUrlencoded(selectQuery, { 543 | headers: { 544 | authorization: value 545 | } 546 | }) 547 | 548 | strictEqual(header, value) 549 | }) 550 | }) 551 | 552 | it('should prioritize the headers from the method call', async () => { 553 | await withServer(async server => { 554 | let header = null 555 | const value = 'Bearer foo' 556 | 557 | server.app.post('/', async (req, res) => { 558 | header = req.headers.authorization 559 | 560 | res.end() 561 | }) 562 | 563 | const endpointUrl = await server.listen() 564 | const client = new SimpleClient({ 565 | endpointUrl, 566 | headers: { 567 | authorization: 'Bearer bar' 568 | } 569 | }) 570 | 571 | await client.postUrlencoded(selectQuery, { 572 | headers: { 573 | authorization: value 574 | } 575 | }) 576 | 577 | strictEqual(header, value) 578 | }) 579 | }) 580 | 581 | it('should use the updateUrl if update is true', async () => { 582 | await withServer(async server => { 583 | let called = false 584 | 585 | server.app.post('/', async (req, res) => { 586 | called = true 587 | 588 | res.end() 589 | }) 590 | 591 | const updateUrl = await server.listen() 592 | const client = new SimpleClient({ updateUrl }) 593 | 594 | await client.postUrlencoded(updateQuery, { update: true }) 595 | 596 | strictEqual(called, true) 597 | }) 598 | }) 599 | 600 | it('should handle server socket errors', async () => { 601 | await withServer(async server => { 602 | server.app.post('/', async (req, res) => { 603 | req.client.destroy() 604 | }) 605 | 606 | const endpointUrl = await server.listen() 607 | const client = new SimpleClient({ endpointUrl }) 608 | 609 | await rejects(async () => { 610 | await client.postUrlencoded(selectQuery) 611 | }, err => isSocketError(err)) 612 | }) 613 | }) 614 | 615 | it('should not handle server errors', async () => { 616 | await withServer(async server => { 617 | server.app.post('/', async (req, res) => { 618 | res.status(500).end(message) 619 | }) 620 | 621 | const endpointUrl = await server.listen() 622 | const client = new SimpleClient({ endpointUrl }) 623 | 624 | await client.postUrlencoded(selectQuery) 625 | }) 626 | }) 627 | }) 628 | }) 629 | -------------------------------------------------------------------------------- /test/StreamClient.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, strictEqual, throws } from 'node:assert' 2 | import omit from 'lodash/omit.js' 3 | import pick from 'lodash/pick.js' 4 | import { describe, it } from 'mocha' 5 | import SimpleClient from '../SimpleClient.js' 6 | import StreamClient from '../StreamClient.js' 7 | import StreamQuery from '../StreamQuery.js' 8 | import StreamStore from '../StreamStore.js' 9 | 10 | describe('StreamClient', () => { 11 | it('should be a constructor', () => { 12 | strictEqual(typeof StreamClient, 'function') 13 | }) 14 | 15 | it('should throw an error if the given factory does not implement the DataFactory interface', () => { 16 | throws(() => { 17 | new StreamClient({ // eslint-disable-line no-new 18 | endpointUrl: 'test', 19 | factory: {} 20 | }) 21 | }, { 22 | message: /DataFactory/ 23 | }) 24 | }) 25 | 26 | it('should use StreamQuery to create the query instance', () => { 27 | const client = new StreamClient({ endpointUrl: 'test' }) 28 | 29 | strictEqual(client.query instanceof StreamQuery, true) 30 | }) 31 | 32 | it('should forward the client to the query instance', () => { 33 | const client = new StreamClient({ endpointUrl: 'test' }) 34 | 35 | strictEqual(client.query.client, client) 36 | }) 37 | 38 | it('should use StreamStore to create the store instance', () => { 39 | const client = new StreamClient({ endpointUrl: 'test' }) 40 | 41 | strictEqual(client.store instanceof StreamStore, true) 42 | }) 43 | 44 | it('should forward the client to the store instance', () => { 45 | const client = new StreamClient({ endpointUrl: 'test' }) 46 | 47 | strictEqual(client.store.client, client) 48 | }) 49 | 50 | it('should be possible to create an instance from a SimpleClient', () => { 51 | const options = { 52 | endpointUrl: 'sparql', 53 | headers: new Headers({ 'user-agent': 'sparql-http-client' }), 54 | password: 'pwd', 55 | storeUrl: 'graph', 56 | updateUrl: 'update', 57 | user: 'usr' 58 | } 59 | 60 | const simpleClient = new SimpleClient(options) 61 | const client = new StreamClient(simpleClient) 62 | const result = pick(client, Object.keys(options)) 63 | 64 | deepStrictEqual(omit(result, 'headers'), omit(options, 'headers')) 65 | deepStrictEqual([...result.headers.entries()], [...simpleClient.headers.entries()]) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/StreamQuery.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, rejects, strictEqual } from 'node:assert' 2 | import factory from '@rdfjs/data-model' 3 | import express from 'express' 4 | import withServer from 'express-as-promise/withServer.js' 5 | import { isReadableStream, isWritableStream } from 'is-stream' 6 | import { describe, it } from 'mocha' 7 | import rdf from 'rdf-ext' 8 | import { datasetEqual } from 'rdf-test/assert.js' 9 | import chunks from 'stream-chunks/chunks.js' 10 | import SimpleClient from '../SimpleClient.js' 11 | import StreamQuery from '../StreamQuery.js' 12 | import { message, quads, askQuery, constructQuery, selectQuery, updateQuery } from './support/examples.js' 13 | import isServerError from './support/isServerError.js' 14 | import isSocketError from './support/isSocketError.js' 15 | import * as ns from './support/namespaces.js' 16 | import testFactory from './support/testFactory.js' 17 | 18 | describe('StreamQuery', () => { 19 | describe('.ask', () => { 20 | it('should be a method', () => { 21 | const query = new StreamQuery({}) 22 | 23 | strictEqual(typeof query.ask, 'function') 24 | }) 25 | 26 | it('should return a boolean value', async () => { 27 | await withServer(async server => { 28 | server.app.get('/', async (req, res) => { 29 | res.json({ 30 | boolean: true 31 | }) 32 | }) 33 | 34 | const endpointUrl = await server.listen() 35 | const client = new SimpleClient({ endpointUrl }) 36 | const query = new StreamQuery({ client }) 37 | 38 | const result = await query.ask(askQuery) 39 | 40 | strictEqual(typeof result, 'boolean') 41 | }) 42 | }) 43 | 44 | it('should parse the SPARQL JSON result', async () => { 45 | await withServer(async server => { 46 | server.app.get('/', async (req, res) => { 47 | res.json({ 48 | boolean: true 49 | }) 50 | }) 51 | 52 | const endpointUrl = await server.listen() 53 | const client = new SimpleClient({ endpointUrl }) 54 | const query = new StreamQuery({ client }) 55 | 56 | const result = await query.ask(askQuery) 57 | 58 | strictEqual(result, true) 59 | }) 60 | }) 61 | 62 | it('should send a GET request', async () => { 63 | await withServer(async server => { 64 | let called = false 65 | 66 | server.app.get('/', async (req, res) => { 67 | called = true 68 | 69 | res.end('{}') 70 | }) 71 | 72 | const endpointUrl = await server.listen() 73 | const client = new SimpleClient({ endpointUrl }) 74 | const query = new StreamQuery({ client }) 75 | 76 | await query.ask(askQuery) 77 | 78 | strictEqual(called, true) 79 | }) 80 | }) 81 | 82 | it('should send the query string as query parameter', async () => { 83 | await withServer(async server => { 84 | let parameter = null 85 | 86 | server.app.get('/', async (req, res) => { 87 | parameter = req.query.query 88 | 89 | res.end('{}') 90 | }) 91 | 92 | const endpointUrl = await server.listen() 93 | const client = new SimpleClient({ endpointUrl }) 94 | const query = new StreamQuery({ client }) 95 | 96 | await query.ask(askQuery) 97 | 98 | strictEqual(parameter, askQuery) 99 | }) 100 | }) 101 | 102 | it('should use the given operation for the request', async () => { 103 | await withServer(async server => { 104 | let parameter = null 105 | 106 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 107 | parameter = req.body.query 108 | 109 | res.json({ 110 | boolean: true 111 | }) 112 | }) 113 | 114 | const endpointUrl = await server.listen() 115 | const client = new SimpleClient({ endpointUrl }) 116 | const query = new StreamQuery({ client }) 117 | 118 | await query.ask(askQuery, { operation: 'postUrlencoded' }) 119 | 120 | strictEqual(parameter, askQuery) 121 | }) 122 | }) 123 | 124 | it('should handle server socket errors', async () => { 125 | await withServer(async server => { 126 | server.app.get('/', async (req, res) => { 127 | req.destroy() 128 | }) 129 | 130 | const endpointUrl = await server.listen() 131 | const client = new SimpleClient({ endpointUrl }) 132 | const query = new StreamQuery({ client }) 133 | 134 | await rejects(async () => { 135 | await query.ask(askQuery) 136 | }, err => isSocketError(err)) 137 | }) 138 | }) 139 | 140 | it('should handle server errors', async () => { 141 | await withServer(async server => { 142 | server.app.get('/', async (req, res) => { 143 | res.status(500).end(message) 144 | }) 145 | 146 | const endpointUrl = await server.listen() 147 | const client = new SimpleClient({ endpointUrl }) 148 | const query = new StreamQuery({ client }) 149 | 150 | await rejects(async () => { 151 | await query.ask(askQuery) 152 | }, err => isServerError(err, message)) 153 | }) 154 | }) 155 | }) 156 | 157 | describe('.construct', () => { 158 | it('should be a method', () => { 159 | const query = new StreamQuery({}) 160 | 161 | strictEqual(typeof query.construct, 'function') 162 | }) 163 | 164 | it('should return a Readable stream object', async () => { 165 | await withServer(async server => { 166 | server.app.get('/', async (req, res) => { 167 | res.status(204).end() 168 | }) 169 | 170 | const endpointUrl = await server.listen() 171 | const client = new SimpleClient({ endpointUrl }) 172 | const query = new StreamQuery({ client }) 173 | 174 | const result = query.construct(constructQuery) 175 | 176 | strictEqual(isReadableStream(result), true) 177 | strictEqual(isWritableStream(result), false) 178 | 179 | await chunks(result) 180 | }) 181 | }) 182 | 183 | it('should parse the N-Triples', async () => { 184 | await withServer(async server => { 185 | server.app.get('/', async (req, res) => { 186 | res.end(quads.toString()) 187 | }) 188 | 189 | const endpointUrl = await server.listen() 190 | const client = new SimpleClient({ endpointUrl }) 191 | const query = new StreamQuery({ client }) 192 | 193 | const stream = query.construct(constructQuery) 194 | const result = await chunks(stream) 195 | 196 | datasetEqual(result, quads) 197 | }) 198 | }) 199 | 200 | it('should send a GET request', async () => { 201 | await withServer(async server => { 202 | let called = false 203 | 204 | server.app.get('/', async (req, res) => { 205 | called = true 206 | 207 | res.status(204).end() 208 | }) 209 | 210 | const endpointUrl = await server.listen() 211 | const client = new SimpleClient({ endpointUrl }) 212 | const query = new StreamQuery({ client }) 213 | 214 | const stream = query.construct(constructQuery) 215 | await chunks(stream) 216 | 217 | strictEqual(called, true) 218 | }) 219 | }) 220 | 221 | it('should send the query string as query parameter', async () => { 222 | await withServer(async server => { 223 | let parameter = null 224 | 225 | server.app.get('/', async (req, res) => { 226 | parameter = req.query.query 227 | 228 | res.status(204).end() 229 | }) 230 | 231 | const endpointUrl = await server.listen() 232 | const client = new SimpleClient({ endpointUrl }) 233 | const query = new StreamQuery({ client }) 234 | 235 | const stream = query.construct(constructQuery) 236 | await chunks(stream) 237 | 238 | strictEqual(parameter, constructQuery) 239 | }) 240 | }) 241 | 242 | it('should use the given factory', async () => { 243 | await withServer(async server => { 244 | const quads = rdf.dataset([ 245 | rdf.quad(rdf.blankNode(), ns.ex.predicate, rdf.literal('test')) 246 | ]) 247 | const factory = testFactory() 248 | 249 | server.app.get('/', async (req, res) => { 250 | res.end(quads.toString()) 251 | }) 252 | 253 | const endpointUrl = await server.listen() 254 | const client = new SimpleClient({ endpointUrl, factory }) 255 | const query = new StreamQuery({ client }) 256 | 257 | const stream = query.construct(constructQuery) 258 | await chunks(stream) 259 | 260 | deepStrictEqual(factory.used, { 261 | blankNode: true, 262 | defaultGraph: true, 263 | literal: true, 264 | namedNode: true, 265 | quad: true 266 | }) 267 | }) 268 | }) 269 | 270 | it('should use the given operation for the request', async () => { 271 | await withServer(async server => { 272 | let parameter = null 273 | 274 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 275 | parameter = req.body.query 276 | 277 | res.status(204).end() 278 | }) 279 | 280 | const endpointUrl = await server.listen() 281 | const client = new SimpleClient({ endpointUrl }) 282 | const query = new StreamQuery({ client }) 283 | 284 | const stream = query.construct(constructQuery, { operation: 'postUrlencoded' }) 285 | await chunks(stream) 286 | 287 | strictEqual(parameter, constructQuery) 288 | }) 289 | }) 290 | 291 | it('should send an accept header with the value application/n-triples, text/turtle', async () => { 292 | await withServer(async server => { 293 | let accept = null 294 | 295 | server.app.get('/', async (req, res) => { 296 | accept = req.headers.accept 297 | 298 | res.end() 299 | }) 300 | 301 | const endpointUrl = await server.listen() 302 | const client = new SimpleClient({ endpointUrl }) 303 | const query = new StreamQuery({ client }) 304 | 305 | const stream = query.construct(constructQuery) 306 | await chunks(stream) 307 | 308 | strictEqual(accept, 'application/n-triples, text/turtle') 309 | }) 310 | }) 311 | 312 | it('should handle server socket errors', async () => { 313 | await withServer(async server => { 314 | server.app.get('/', async (req, res) => { 315 | req.destroy() 316 | }) 317 | 318 | const endpointUrl = await server.listen() 319 | const client = new SimpleClient({ endpointUrl }) 320 | const query = new StreamQuery({ client }) 321 | 322 | await rejects(async () => { 323 | const stream = query.construct(constructQuery) 324 | await chunks(stream) 325 | }, err => isSocketError(err)) 326 | }) 327 | }) 328 | 329 | it('should handle server errors', async () => { 330 | await withServer(async server => { 331 | server.app.get('/', async (req, res) => { 332 | res.status(500).end(message) 333 | }) 334 | 335 | const endpointUrl = await server.listen() 336 | const client = new SimpleClient({ endpointUrl }) 337 | const query = new StreamQuery({ client }) 338 | 339 | await rejects(async () => { 340 | const stream = query.construct(constructQuery) 341 | await chunks(stream) 342 | }, err => isServerError(err, message)) 343 | }) 344 | }) 345 | }) 346 | 347 | describe('.select', () => { 348 | it('should be a method', () => { 349 | const query = new StreamQuery({}) 350 | 351 | strictEqual(typeof query.select, 'function') 352 | }) 353 | 354 | it('should return a Readable stream object', async () => { 355 | await withServer(async server => { 356 | server.app.get('/', async (req, res) => { 357 | res.status(204).end() 358 | }) 359 | 360 | const endpointUrl = await server.listen() 361 | const client = new SimpleClient({ endpointUrl }) 362 | const query = new StreamQuery({ client }) 363 | 364 | const result = query.select(selectQuery) 365 | 366 | strictEqual(isReadableStream(result), true) 367 | strictEqual(isWritableStream(result), false) 368 | 369 | await chunks(result) 370 | }) 371 | }) 372 | 373 | it('should parse the SPARQL JSON result', async () => { 374 | await withServer(async server => { 375 | const content = { 376 | results: { 377 | bindings: [{ 378 | a: { type: 'uri', value: 'http://example.org/0' } 379 | }, { 380 | a: { type: 'uri', value: 'http://example.org/1' } 381 | }] 382 | } 383 | } 384 | 385 | server.app.get('/', async (req, res) => { 386 | res.end(JSON.stringify(content)) 387 | }) 388 | 389 | const endpointUrl = await server.listen() 390 | const client = new SimpleClient({ endpointUrl, factory }) 391 | const query = new StreamQuery({ client }) 392 | 393 | const stream = query.select(selectQuery) 394 | const result = await chunks(stream) 395 | 396 | strictEqual(result[0].a.termType, 'NamedNode') 397 | strictEqual(result[0].a.value, content.results.bindings[0].a.value) 398 | strictEqual(result[1].a.termType, 'NamedNode') 399 | strictEqual(result[1].a.value, content.results.bindings[1].a.value) 400 | }) 401 | }) 402 | 403 | it('should send a GET request', async () => { 404 | await withServer(async server => { 405 | let called = false 406 | 407 | server.app.get('/', async (req, res) => { 408 | called = true 409 | 410 | res.status(204).end() 411 | }) 412 | 413 | const endpointUrl = await server.listen() 414 | const client = new SimpleClient({ endpointUrl }) 415 | const query = new StreamQuery({ client }) 416 | 417 | const stream = query.select(selectQuery) 418 | await chunks(stream) 419 | 420 | strictEqual(called, true) 421 | }) 422 | }) 423 | 424 | it('should send the query string as query parameter', async () => { 425 | await withServer(async server => { 426 | let parameter = null 427 | 428 | server.app.get('/', async (req, res) => { 429 | parameter = req.query.query 430 | 431 | res.status(204).end() 432 | }) 433 | 434 | const endpointUrl = await server.listen() 435 | const client = new SimpleClient({ endpointUrl }) 436 | const query = new StreamQuery({ client }) 437 | 438 | const stream = query.construct(selectQuery) 439 | await chunks(stream) 440 | 441 | strictEqual(parameter, selectQuery) 442 | }) 443 | }) 444 | 445 | it('should use the given factory', async () => { 446 | await withServer(async server => { 447 | const content = { 448 | results: { 449 | bindings: [{ 450 | a: { type: 'bnode', value: 'b0' } 451 | }, { 452 | a: { type: 'literal', value: '0' } 453 | }, { 454 | a: { type: 'uri', value: 'http://example.org/0' } 455 | }] 456 | } 457 | } 458 | const factory = testFactory() 459 | 460 | server.app.get('/', async (req, res) => { 461 | res.end(JSON.stringify(content)) 462 | }) 463 | 464 | const endpointUrl = await server.listen() 465 | const client = new SimpleClient({ endpointUrl, factory }) 466 | const query = new StreamQuery({ client }) 467 | 468 | const stream = query.select(selectQuery) 469 | await chunks(stream) 470 | 471 | deepStrictEqual(factory.used, { 472 | blankNode: true, 473 | literal: true, 474 | namedNode: true 475 | }) 476 | }) 477 | }) 478 | 479 | it('should use the given operation for the request', async () => { 480 | await withServer(async server => { 481 | let parameter = null 482 | 483 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 484 | parameter = req.body.query 485 | 486 | res.status(204).end() 487 | }) 488 | 489 | const endpointUrl = await server.listen() 490 | const client = new SimpleClient({ endpointUrl }) 491 | const query = new StreamQuery({ client }) 492 | 493 | const stream = query.select(selectQuery, { operation: 'postUrlencoded' }) 494 | await chunks(stream) 495 | 496 | strictEqual(parameter, selectQuery) 497 | }) 498 | }) 499 | 500 | it('should handle server socket errors', async () => { 501 | await withServer(async server => { 502 | server.app.get('/', async (req, res) => { 503 | req.destroy() 504 | }) 505 | 506 | const endpointUrl = await server.listen() 507 | const client = new SimpleClient({ endpointUrl }) 508 | const query = new StreamQuery({ client }) 509 | 510 | await rejects(async () => { 511 | const stream = query.select(selectQuery) 512 | await chunks(stream) 513 | }, err => isSocketError(err)) 514 | }) 515 | }) 516 | 517 | it('should handle server errors', async () => { 518 | await withServer(async server => { 519 | server.app.get('/', async (req, res) => { 520 | res.status(500).end(message) 521 | }) 522 | 523 | const endpointUrl = await server.listen() 524 | const client = new SimpleClient({ endpointUrl }) 525 | const query = new StreamQuery({ client }) 526 | 527 | await rejects(async () => { 528 | const stream = query.select(selectQuery) 529 | await chunks(stream) 530 | }, err => isServerError(err, message)) 531 | }) 532 | }) 533 | }) 534 | 535 | describe('.update', () => { 536 | it('should be a method', () => { 537 | const query = new StreamQuery({}) 538 | 539 | strictEqual(typeof query.update, 'function') 540 | }) 541 | 542 | it('should send a POST request', async () => { 543 | await withServer(async server => { 544 | let called = false 545 | 546 | server.app.post('/', async (req, res) => { 547 | called = true 548 | 549 | res.status(204).end() 550 | }) 551 | 552 | const updateUrl = await server.listen() 553 | const client = new SimpleClient({ updateUrl }) 554 | const query = new StreamQuery({ client }) 555 | 556 | await query.update(updateQuery) 557 | 558 | strictEqual(called, true) 559 | }) 560 | }) 561 | 562 | it('should send the query string urlencoded in the request body', async () => { 563 | await withServer(async server => { 564 | let parameter = null 565 | 566 | server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { 567 | parameter = req.body.update 568 | 569 | res.status(204).end() 570 | }) 571 | 572 | const updateUrl = await server.listen() 573 | const client = new SimpleClient({ updateUrl }) 574 | const query = new StreamQuery({ client }) 575 | 576 | await query.update(updateQuery) 577 | 578 | strictEqual(parameter, updateQuery) 579 | }) 580 | }) 581 | 582 | it('should use the given operation for the request', async () => { 583 | await withServer(async server => { 584 | let content = null 585 | 586 | server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { 587 | content = req.body 588 | 589 | res.status(204).end() 590 | }) 591 | 592 | const updateUrl = await server.listen() 593 | const client = new SimpleClient({ updateUrl }) 594 | const query = new StreamQuery({ client }) 595 | 596 | await query.update(updateQuery, { operation: 'postDirect' }) 597 | 598 | strictEqual(content, updateQuery) 599 | }) 600 | }) 601 | 602 | it('should handle server socket errors', async () => { 603 | await withServer(async server => { 604 | server.app.post('/', async (req, res) => { 605 | req.destroy() 606 | }) 607 | 608 | const updateUrl = await server.listen() 609 | const client = new SimpleClient({ updateUrl }) 610 | const query = new StreamQuery({ client }) 611 | 612 | await rejects(async () => { 613 | await query.update(updateQuery) 614 | }, err => isSocketError(err)) 615 | }) 616 | }) 617 | 618 | it('should handle server errors', async () => { 619 | await withServer(async server => { 620 | server.app.post('/', async (req, res) => { 621 | res.status(500).end(message) 622 | }) 623 | 624 | const updateUrl = await server.listen() 625 | const client = new SimpleClient({ updateUrl }) 626 | const query = new StreamQuery({ client }) 627 | 628 | await rejects(async () => { 629 | await query.update(updateQuery) 630 | }, err => isServerError(err, message)) 631 | }) 632 | }) 633 | }) 634 | }) 635 | -------------------------------------------------------------------------------- /test/StreamStore.test.js: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, rejects, strictEqual } from 'node:assert' 2 | import withServer from 'express-as-promise/withServer.js' 3 | import { isReadableStream, isWritableStream } from 'is-stream' 4 | import { describe, it } from 'mocha' 5 | import rdf from 'rdf-ext' 6 | import { datasetEqual } from 'rdf-test/assert.js' 7 | import { Readable } from 'readable-stream' 8 | import chunks from 'stream-chunks/chunks.js' 9 | import decode from 'stream-chunks/decode.js' 10 | import SimpleClient from '../SimpleClient.js' 11 | import StreamStore from '../StreamStore.js' 12 | import { graph, message, quads } from './support/examples.js' 13 | import isServerError from './support/isServerError.js' 14 | import isSocketError from './support/isSocketError.js' 15 | import * as ns from './support/namespaces.js' 16 | import testFactory from './support/testFactory.js' 17 | 18 | describe('StreamStore', () => { 19 | describe('.read', () => { 20 | it('should be a method', () => { 21 | const store = new StreamStore({}) 22 | 23 | strictEqual(typeof store.read, 'function') 24 | }) 25 | 26 | it('should return a Readable stream object', async () => { 27 | await withServer(async server => { 28 | server.app.get('/', async (req, res) => { 29 | res.status(204).end() 30 | }) 31 | 32 | const storeUrl = await server.listen() 33 | const client = new SimpleClient({ storeUrl }) 34 | const store = new StreamStore({ client }) 35 | 36 | const result = store.read({ method: 'GET', graph }) 37 | 38 | strictEqual(isReadableStream(result), true) 39 | strictEqual(isWritableStream(result), false) 40 | 41 | await chunks(result) 42 | }) 43 | }) 44 | 45 | it('should use the given method', async () => { 46 | await withServer(async server => { 47 | let called = false 48 | 49 | server.app.get('/', async (req, res) => { 50 | called = true 51 | 52 | res.status(204).end() 53 | }) 54 | 55 | const storeUrl = await server.listen() 56 | const client = new SimpleClient({ storeUrl }) 57 | const store = new StreamStore({ client }) 58 | 59 | const stream = store.read({ method: 'GET', graph }) 60 | await chunks(stream) 61 | 62 | strictEqual(called, true) 63 | }) 64 | }) 65 | 66 | it('should send the requested graph as a query parameter', async () => { 67 | await withServer(async server => { 68 | let graphParameter = null 69 | 70 | server.app.get('/', async (req, res) => { 71 | graphParameter = req.query.graph 72 | 73 | res.status(204).end() 74 | }) 75 | 76 | const storeUrl = await server.listen() 77 | const client = new SimpleClient({ storeUrl }) 78 | const store = new StreamStore({ client }) 79 | 80 | const stream = store.read({ method: 'GET', graph }) 81 | await chunks(stream) 82 | 83 | strictEqual(graphParameter, graph.value) 84 | }) 85 | }) 86 | 87 | it('should send the default parameter if the default graph is requested', async () => { 88 | await withServer(async server => { 89 | let defaultParameter = null 90 | let graphParameter = null 91 | 92 | server.app.get('/', async (req, res) => { 93 | defaultParameter = req.query.default 94 | graphParameter = req.query.graph 95 | 96 | res.status(204).end() 97 | }) 98 | 99 | const storeUrl = await server.listen() 100 | const client = new SimpleClient({ storeUrl }) 101 | const store = new StreamStore({ client }) 102 | 103 | const stream = store.read({ method: 'GET', graph: rdf.defaultGraph() }) 104 | await chunks(stream) 105 | 106 | strictEqual(defaultParameter, '') 107 | strictEqual(graphParameter, undefined) 108 | }) 109 | }) 110 | 111 | it('should request content with media type application/n-triples', async () => { 112 | await withServer(async server => { 113 | let mediaType = null 114 | 115 | server.app.get('/', async (req, res) => { 116 | mediaType = req.get('accept') 117 | 118 | res.status(204).end() 119 | }) 120 | 121 | const storeUrl = await server.listen() 122 | const client = new SimpleClient({ storeUrl }) 123 | const store = new StreamStore({ client }) 124 | 125 | const stream = store.read({ method: 'GET', graph }) 126 | await chunks(stream) 127 | 128 | strictEqual(mediaType, 'application/n-triples') 129 | }) 130 | }) 131 | 132 | it('should parse the N-Triples and return them as a quad stream', async () => { 133 | await withServer(async server => { 134 | server.app.get('/', async (req, res) => { 135 | res.end(quads.toString()) 136 | }) 137 | 138 | const storeUrl = await server.listen() 139 | const client = new SimpleClient({ storeUrl }) 140 | const store = new StreamStore({ client }) 141 | 142 | const stream = store.read({ method: 'GET', graph }) 143 | const result = await chunks(stream) 144 | 145 | datasetEqual(result, rdf.dataset(quads, graph)) 146 | }) 147 | }) 148 | 149 | it('should not send the graph query parameter if the default graph is requested', async () => { 150 | await withServer(async server => { 151 | server.app.get('/', async (req, res) => { 152 | res.status(500).end(message) 153 | }) 154 | 155 | const storeUrl = await server.listen() 156 | const client = new SimpleClient({ storeUrl }) 157 | const store = new StreamStore({ client }) 158 | 159 | await rejects(async () => { 160 | const stream = store.read({ method: 'GET', graph }) 161 | await chunks(stream) 162 | }, err => { 163 | return isServerError(err, message) 164 | }) 165 | }) 166 | }) 167 | 168 | it('should use the given factory', async () => { 169 | await withServer(async server => { 170 | const quads = rdf.dataset([ 171 | rdf.quad(rdf.blankNode(), ns.ex.predicate1, rdf.literal('test'), graph) 172 | ]) 173 | const factory = testFactory() 174 | 175 | server.app.get('/', async (req, res) => { 176 | res.end(quads.toString()) 177 | }) 178 | 179 | const storeUrl = await server.listen() 180 | const client = new SimpleClient({ factory, storeUrl }) 181 | const store = new StreamStore({ client }) 182 | 183 | const stream = store.read({ method: 'GET', graph }) 184 | await chunks(stream) 185 | 186 | deepStrictEqual(factory.used, { 187 | blankNode: true, 188 | defaultGraph: true, 189 | literal: true, 190 | namedNode: true, 191 | quad: true 192 | }) 193 | }) 194 | }) 195 | 196 | it('should use the given user and password', async () => { 197 | await withServer(async server => { 198 | let authorization = null 199 | 200 | server.app.get('/', async (req, res) => { 201 | authorization = req.headers.authorization 202 | 203 | res.status(204).end() 204 | }) 205 | 206 | const storeUrl = await server.listen() 207 | const client = new SimpleClient({ storeUrl, user: 'abc', password: 'def' }) 208 | const store = new StreamStore({ client }) 209 | 210 | const stream = store.read({ method: 'GET', graph }) 211 | await chunks(stream) 212 | 213 | strictEqual(authorization, 'Basic YWJjOmRlZg==') 214 | }) 215 | }) 216 | 217 | it('should handle server socket errors', async () => { 218 | await withServer(async server => { 219 | server.app.get('/', async (req, res) => { 220 | req.client.destroy() 221 | }) 222 | 223 | const storeUrl = await server.listen() 224 | const client = new SimpleClient({ storeUrl }) 225 | const store = new StreamStore({ client }) 226 | 227 | await rejects(async () => { 228 | const stream = store.read({ method: 'GET', graph }) 229 | await chunks(stream) 230 | }, err => isSocketError(err)) 231 | }) 232 | }) 233 | 234 | it('should handle server errors', async () => { 235 | await withServer(async server => { 236 | server.app.get('/', async (req, res) => { 237 | res.status(500).end(message) 238 | }) 239 | 240 | const storeUrl = await server.listen() 241 | const client = new SimpleClient({ storeUrl }) 242 | const store = new StreamStore({ client }) 243 | 244 | await rejects(async () => { 245 | const stream = store.read({ method: 'GET', graph }) 246 | await chunks(stream) 247 | }, err => isServerError(err, message)) 248 | }) 249 | }) 250 | }) 251 | 252 | describe('.write', () => { 253 | it('should be a method', () => { 254 | const store = new StreamStore({}) 255 | 256 | strictEqual(typeof store.write, 'function') 257 | }) 258 | 259 | it('should use the given method', async () => { 260 | await withServer(async server => { 261 | let called = false 262 | 263 | server.app.post('/', async (req, res) => { 264 | called = true 265 | 266 | res.status(204).end() 267 | }) 268 | 269 | const storeUrl = await server.listen() 270 | const client = new SimpleClient({ storeUrl }) 271 | const store = new StreamStore({ client }) 272 | 273 | await store.write({ method: 'POST', stream: quads.toStream() }) 274 | 275 | strictEqual(called, true) 276 | }) 277 | }) 278 | 279 | it('should send content with media type application/n-triples', async () => { 280 | await withServer(async server => { 281 | let mediaType = null 282 | 283 | server.app.post('/', async (req, res) => { 284 | mediaType = req.get('content-type') 285 | 286 | res.status(204).end() 287 | }) 288 | 289 | const storeUrl = await server.listen() 290 | const client = new SimpleClient({ storeUrl }) 291 | const store = new StreamStore({ client }) 292 | 293 | await store.write({ method: 'POST', stream: quads.toStream() }) 294 | 295 | strictEqual(mediaType, 'application/n-triples') 296 | }) 297 | }) 298 | 299 | it('should send the quad stream as N-Triples', async () => { 300 | await withServer(async server => { 301 | let content 302 | 303 | server.app.post('/', async (req, res) => { 304 | content = await decode(req) 305 | 306 | res.status(204).end() 307 | }) 308 | 309 | const storeUrl = await server.listen() 310 | const client = new SimpleClient({ storeUrl }) 311 | const store = new StreamStore({ client }) 312 | 313 | await store.write({ method: 'POST', stream: quads.toStream() }) 314 | 315 | strictEqual(content, quads.toString()) 316 | }) 317 | }) 318 | 319 | it('should handle streams with no data', async () => { 320 | await withServer(async server => { 321 | server.app.post('/', async (req, res) => { 322 | res.status(204).end() 323 | }) 324 | 325 | const storeUrl = await server.listen() 326 | const client = new SimpleClient({ storeUrl }) 327 | const store = new StreamStore({ client }) 328 | 329 | await store.write({ method: 'POST', stream: Readable.from([]) }) 330 | }) 331 | }) 332 | 333 | it('should use the given user and password', async () => { 334 | await withServer(async server => { 335 | let authorization = null 336 | 337 | server.app.post('/', async (req, res) => { 338 | authorization = req.headers.authorization 339 | 340 | res.status(204).end() 341 | }) 342 | 343 | const storeUrl = await server.listen() 344 | const client = new SimpleClient({ storeUrl, user: 'abc', password: 'def' }) 345 | const store = new StreamStore({ client }) 346 | 347 | await store.write({ method: 'POST', stream: quads.toStream() }) 348 | 349 | strictEqual(authorization, 'Basic YWJjOmRlZg==') 350 | }) 351 | }) 352 | 353 | it('should handle server socket errors', async () => { 354 | await withServer(async server => { 355 | server.app.post('/', async req => { 356 | req.client.destroy() 357 | }) 358 | 359 | const storeUrl = await server.listen() 360 | const client = new SimpleClient({ storeUrl }) 361 | const store = new StreamStore({ client }) 362 | 363 | await rejects(async () => { 364 | await store.write({ method: 'POST', stream: quads.toStream() }) 365 | }, err => isSocketError(err)) 366 | }) 367 | }) 368 | 369 | it('should handle server errors', async () => { 370 | await withServer(async server => { 371 | server.app.post('/', async (req, res) => { 372 | res.status(500).end(message) 373 | }) 374 | 375 | const storeUrl = await server.listen() 376 | const client = new SimpleClient({ storeUrl }) 377 | const store = new StreamStore({ client }) 378 | 379 | await rejects(async () => { 380 | await store.write({ method: 'POST', stream: quads.toStream() }) 381 | }, err => isServerError(err, message)) 382 | }) 383 | }) 384 | }) 385 | 386 | describe('.get', () => { 387 | it('should be a method', () => { 388 | const store = new StreamStore({}) 389 | 390 | strictEqual(typeof store.get, 'function') 391 | }) 392 | 393 | it('should return a Readable stream object', async () => { 394 | await withServer(async server => { 395 | server.app.get('/', async (req, res) => { 396 | res.status(204).end() 397 | }) 398 | 399 | const storeUrl = await server.listen() 400 | const client = new SimpleClient({ storeUrl }) 401 | const store = new StreamStore({ client }) 402 | 403 | const result = store.get(graph) 404 | 405 | strictEqual(isReadableStream(result), true) 406 | strictEqual(isWritableStream(result), false) 407 | 408 | await chunks(result) 409 | }) 410 | }) 411 | 412 | it('should send a GET request', async () => { 413 | await withServer(async server => { 414 | let called = false 415 | 416 | server.app.get('/', async (req, res) => { 417 | called = true 418 | 419 | res.status(204).end() 420 | }) 421 | 422 | const storeUrl = await server.listen() 423 | const client = new SimpleClient({ storeUrl }) 424 | const store = new StreamStore({ client }) 425 | 426 | const stream = store.get(graph) 427 | await chunks(stream) 428 | 429 | strictEqual(called, true) 430 | }) 431 | }) 432 | 433 | it('should send the requested graph as a query parameter', async () => { 434 | await withServer(async server => { 435 | let graphParameter = null 436 | 437 | server.app.get('/', async (req, res) => { 438 | graphParameter = req.query.graph 439 | 440 | res.status(204).end() 441 | }) 442 | 443 | const storeUrl = await server.listen() 444 | const client = new SimpleClient({ storeUrl }) 445 | const store = new StreamStore({ client }) 446 | 447 | const stream = store.get(graph) 448 | await chunks(stream) 449 | 450 | strictEqual(graphParameter, graph.value) 451 | }) 452 | }) 453 | 454 | it('should not send the graph query parameter if the default graph is requested', async () => { 455 | await withServer(async server => { 456 | let graphParameter = null 457 | 458 | server.app.get('/', async (req, res) => { 459 | graphParameter = req.query.graph 460 | 461 | res.status(204).end() 462 | }) 463 | 464 | const storeUrl = await server.listen() 465 | const client = new SimpleClient({ storeUrl }) 466 | const store = new StreamStore({ client }) 467 | 468 | const stream = store.get(rdf.defaultGraph()) 469 | await chunks(stream) 470 | 471 | strictEqual(graphParameter, undefined) 472 | }) 473 | }) 474 | 475 | it('should request content with media type application/n-triples', async () => { 476 | await withServer(async server => { 477 | let mediaType = null 478 | 479 | server.app.get('/', async (req, res) => { 480 | mediaType = req.get('accept') 481 | 482 | res.status(204).end() 483 | }) 484 | 485 | const storeUrl = await server.listen() 486 | const client = new SimpleClient({ storeUrl }) 487 | const store = new StreamStore({ client }) 488 | 489 | const stream = store.get(graph) 490 | await chunks(stream) 491 | 492 | strictEqual(mediaType, 'application/n-triples') 493 | }) 494 | }) 495 | 496 | it('should parse the N-Triples and return them as a quad stream', async () => { 497 | await withServer(async server => { 498 | server.app.get('/', async (req, res) => { 499 | res.end(quads.toString()) 500 | }) 501 | 502 | const storeUrl = await server.listen() 503 | const client = new SimpleClient({ storeUrl }) 504 | const store = new StreamStore({ client }) 505 | 506 | const stream = store.get(graph) 507 | const result = await chunks(stream) 508 | 509 | datasetEqual(result, rdf.dataset(quads, graph)) 510 | }) 511 | }) 512 | 513 | it('should handle server errors', async () => { 514 | await withServer(async server => { 515 | server.app.get('/', async (req, res) => { 516 | res.status(500).end(message) 517 | }) 518 | 519 | const storeUrl = await server.listen() 520 | const client = new SimpleClient({ storeUrl }) 521 | const store = new StreamStore({ client }) 522 | 523 | await rejects(async () => { 524 | const stream = store.get(graph) 525 | await chunks(stream) 526 | }, err => isServerError(err, message)) 527 | }) 528 | }) 529 | }) 530 | 531 | describe('.post', () => { 532 | it('should be a method', () => { 533 | const store = new StreamStore({}) 534 | 535 | strictEqual(typeof store.post, 'function') 536 | }) 537 | 538 | it('should send a POST request', async () => { 539 | await withServer(async server => { 540 | let called = false 541 | 542 | server.app.post('/', async (req, res) => { 543 | called = true 544 | 545 | res.status(204).end() 546 | }) 547 | 548 | const storeUrl = await server.listen() 549 | const client = new SimpleClient({ storeUrl }) 550 | const store = new StreamStore({ client }) 551 | 552 | await store.post(quads.toStream()) 553 | 554 | strictEqual(called, true) 555 | }) 556 | }) 557 | 558 | it('should send content with media type application/n-triples', async () => { 559 | await withServer(async server => { 560 | let mediaType = null 561 | const quads = rdf.dataset([ 562 | rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1) 563 | ]) 564 | 565 | server.app.post('/', async (req, res) => { 566 | mediaType = req.get('content-type') 567 | 568 | res.status(204).end() 569 | }) 570 | 571 | const storeUrl = await server.listen() 572 | const client = new SimpleClient({ storeUrl }) 573 | const store = new StreamStore({ client }) 574 | 575 | await store.post(quads.toStream()) 576 | 577 | strictEqual(mediaType, 'application/n-triples') 578 | }) 579 | }) 580 | 581 | it('should send the quad stream as N-Triples', async () => { 582 | await withServer(async server => { 583 | let content 584 | 585 | server.app.post('/', async (req, res) => { 586 | content = await decode(req) 587 | 588 | res.status(204).end() 589 | }) 590 | 591 | const storeUrl = await server.listen() 592 | const client = new SimpleClient({ storeUrl }) 593 | const store = new StreamStore({ client }) 594 | 595 | await store.post(quads.toStream()) 596 | 597 | strictEqual(content, quads.toString()) 598 | }) 599 | }) 600 | 601 | it('should support default graph', async () => { 602 | await withServer(async server => { 603 | let graph = true 604 | let content 605 | 606 | server.app.post('/', async (req, res) => { 607 | graph = req.query.graph 608 | content = await decode(req) 609 | 610 | res.status(204).end() 611 | }) 612 | 613 | const storeUrl = await server.listen() 614 | const client = new SimpleClient({ storeUrl }) 615 | const store = new StreamStore({ client }) 616 | 617 | await store.post(quads.toStream()) 618 | 619 | strictEqual(graph, undefined) 620 | strictEqual(content, quads.toString()) 621 | }) 622 | }) 623 | 624 | it('should handle server errors', async () => { 625 | await withServer(async server => { 626 | server.app.post('/', async (req, res) => { 627 | res.status(500).end(message) 628 | }) 629 | 630 | const storeUrl = await server.listen() 631 | const client = new SimpleClient({ storeUrl }) 632 | const store = new StreamStore({ client }) 633 | 634 | await rejects(async () => { 635 | await store.post(quads.toStream()) 636 | }, err => isServerError(err, message)) 637 | }) 638 | }) 639 | }) 640 | 641 | describe('.put', () => { 642 | it('should be a method', () => { 643 | const store = new StreamStore({}) 644 | 645 | strictEqual(typeof store.put, 'function') 646 | }) 647 | 648 | it('should send a PUT request', async () => { 649 | await withServer(async server => { 650 | let called = false 651 | 652 | server.app.put('/', async (req, res) => { 653 | called = true 654 | 655 | res.status(204).end() 656 | }) 657 | 658 | const storeUrl = await server.listen() 659 | const client = new SimpleClient({ storeUrl }) 660 | const store = new StreamStore({ client }) 661 | 662 | await store.put(quads.toStream()) 663 | 664 | strictEqual(called, true) 665 | }) 666 | }) 667 | 668 | it('should send content with media type application/n-triples', async () => { 669 | await withServer(async server => { 670 | let mediaType = null 671 | 672 | server.app.put('/', async (req, res) => { 673 | mediaType = req.get('content-type') 674 | 675 | res.status(204).end() 676 | }) 677 | 678 | const storeUrl = await server.listen() 679 | const client = new SimpleClient({ storeUrl }) 680 | const store = new StreamStore({ client }) 681 | 682 | await store.put(quads.toStream()) 683 | 684 | strictEqual(mediaType, 'application/n-triples') 685 | }) 686 | }) 687 | 688 | it('should send the quad stream as N-Triples', async () => { 689 | await withServer(async server => { 690 | let content 691 | 692 | server.app.put('/', async (req, res) => { 693 | content = await decode(req) 694 | 695 | res.status(204).end() 696 | }) 697 | 698 | const storeUrl = await server.listen() 699 | const client = new SimpleClient({ storeUrl }) 700 | const store = new StreamStore({ client }) 701 | 702 | await store.put(quads.toStream()) 703 | 704 | strictEqual(content, quads.toString()) 705 | }) 706 | }) 707 | 708 | it('should support default graph', async () => { 709 | await withServer(async server => { 710 | let graph = true 711 | let content 712 | 713 | server.app.put('/', async (req, res) => { 714 | graph = req.query.graph 715 | content = await decode(req) 716 | 717 | res.status(204).end() 718 | }) 719 | 720 | const storeUrl = await server.listen() 721 | const client = new SimpleClient({ storeUrl }) 722 | const store = new StreamStore({ client }) 723 | 724 | await store.put(quads.toStream()) 725 | 726 | strictEqual(graph, undefined) 727 | strictEqual(content, quads.toString()) 728 | }) 729 | }) 730 | 731 | it('should handle server errors', async () => { 732 | await withServer(async server => { 733 | server.app.put('/', async (req, res) => { 734 | res.status(500).end(message) 735 | }) 736 | 737 | const storeUrl = await server.listen() 738 | const client = new SimpleClient({ storeUrl }) 739 | const store = new StreamStore({ client }) 740 | 741 | await rejects(async () => { 742 | await store.put(quads.toStream()) 743 | }, err => isServerError(err, message)) 744 | }) 745 | }) 746 | }) 747 | }) 748 | -------------------------------------------------------------------------------- /test/support/examples.js: -------------------------------------------------------------------------------- 1 | import rdf from 'rdf-ext' 2 | import * as ns from './namespaces.js' 3 | 4 | const graph = ns.ex.graph1 5 | 6 | const message = 'test message' 7 | 8 | const quads = rdf.dataset([ 9 | rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), 10 | rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), 11 | rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), 12 | rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) 13 | ]) 14 | 15 | const askQuery = 'ASK {}' 16 | const constructQuery = 'CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}' 17 | const selectQuery = 'SELECT * WHERE {?s ?p ?o}' 18 | const updateQuery = 'INSERT { "object"} WHERE {}' 19 | 20 | export { 21 | graph, 22 | message, 23 | quads, 24 | askQuery, 25 | constructQuery, 26 | selectQuery, 27 | updateQuery 28 | } 29 | -------------------------------------------------------------------------------- /test/support/isServerError.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | 3 | function isServerError (err, message) { 4 | strictEqual(err.message.includes('Internal Server Error'), true) 5 | strictEqual(err.message.includes('500'), true) 6 | strictEqual(err.message.includes(message), true) 7 | strictEqual(err.status, 500) 8 | 9 | return true 10 | } 11 | 12 | export default isServerError 13 | -------------------------------------------------------------------------------- /test/support/isSocketError.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | 3 | function isSocketError (err) { 4 | strictEqual(err.message.includes('socket hang up'), true) 5 | strictEqual(err.status, undefined) 6 | 7 | return true 8 | } 9 | 10 | export default isSocketError 11 | -------------------------------------------------------------------------------- /test/support/namespaces.js: -------------------------------------------------------------------------------- 1 | import rdf from 'rdf-ext' 2 | 3 | const ex = rdf.namespace('http://example.org/') 4 | 5 | export { 6 | ex 7 | } 8 | -------------------------------------------------------------------------------- /test/support/testFactory.js: -------------------------------------------------------------------------------- 1 | import dataModelFactory from '@rdfjs/data-model' 2 | import datasetFactory from '@rdfjs/dataset' 3 | 4 | function testFactory () { 5 | const factory = { 6 | blankNode: () => { 7 | factory.used.blankNode = true 8 | return dataModelFactory.blankNode() 9 | }, 10 | dataset: (quads, graph) => { 11 | factory.used.dataset = true 12 | return datasetFactory.dataset(quads, graph) 13 | }, 14 | defaultGraph: () => { 15 | factory.used.defaultGraph = true 16 | return dataModelFactory.defaultGraph() 17 | }, 18 | literal: value => { 19 | factory.used.literal = true 20 | return dataModelFactory.literal(value) 21 | }, 22 | namedNode: value => { 23 | factory.used.namedNode = true 24 | return dataModelFactory.namedNode(value) 25 | }, 26 | quad: (s, p, o, g) => { 27 | factory.used.quad = true 28 | return dataModelFactory.quad(s, p, o, g) 29 | }, 30 | variable: value => { 31 | factory.used.variable = true 32 | return dataModelFactory.variable(value) 33 | }, 34 | used: {} 35 | } 36 | 37 | return factory 38 | } 39 | 40 | export default testFactory 41 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { describe, it } from 'mocha' 3 | import * as index from '../index.js' 4 | import ParsingClient from '../ParsingClient.js' 5 | import ParsingQuery from '../ParsingQuery.js' 6 | import RawQuery from '../RawQuery.js' 7 | import ResultParser from '../ResultParser.js' 8 | import SimpleClient from '../SimpleClient.js' 9 | import StreamClient from '../StreamClient.js' 10 | import StreamQuery from '../StreamQuery.js' 11 | import StreamStore from '../StreamStore.js' 12 | 13 | // TODO: remove all awaits that are not required 14 | 15 | describe('sparql-http-client', () => { 16 | it('should export the StreamClient as default export', () => { 17 | strictEqual(index.default, StreamClient) 18 | }) 19 | 20 | it('should export the ParsingClient', () => { 21 | strictEqual(index.ParsingClient, ParsingClient) 22 | }) 23 | 24 | it('should export the ParsingQuery', () => { 25 | strictEqual(index.ParsingQuery, ParsingQuery) 26 | }) 27 | 28 | it('should export the RawQuery', () => { 29 | strictEqual(index.RawQuery, RawQuery) 30 | }) 31 | 32 | it('should export the ResultParser', () => { 33 | strictEqual(index.ResultParser, ResultParser) 34 | }) 35 | 36 | it('should export the SimpleClient', () => { 37 | strictEqual(index.SimpleClient, SimpleClient) 38 | }) 39 | 40 | it('should export the StreamClient', () => { 41 | strictEqual(index.StreamClient, StreamClient) 42 | }) 43 | 44 | it('should export the StreamQuery', () => { 45 | strictEqual(index.StreamQuery, StreamQuery) 46 | }) 47 | 48 | it('should export the StreamStore', () => { 49 | strictEqual(index.StreamStore, StreamStore) 50 | }) 51 | }) 52 | --------------------------------------------------------------------------------