diff --git a/.github/workflows/ci.yaml b/.github/workflows/test.yaml similarity index 53% rename from .github/workflows/ci.yaml rename to .github/workflows/test.yaml index c1e7a47..06d8634 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/test.yaml @@ -1,19 +1,21 @@ -name: CI -on: push +name: Test +on: + - pull_request + - push jobs: test: runs-on: ubuntu-20.04 strategy: matrix: node: - - '14' - - '16' - '18' - - '19' + - '20' + - '21' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - run: npm install - run: npm test + - uses: codecov/codecov-action@v3 diff --git a/BaseClient.js b/BaseClient.js deleted file mode 100644 index 3a26c51..0000000 --- a/BaseClient.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * An abstract base client which connects the query, store and endpoint together - * - * Store and Query parameters are both optional and only necessary when the client will connect to SPARQL Graph Store - * or SPARQL Query endpoints respectively - * - * @class - */ -class BaseClient { - /** - * @param {Object} init - * @param {Endpoint} init.endpoint object to connect to SPARQL endpoint - * @param {Query} [init.Query] SPARQL Query/Update executor constructor - * @param {Store} [init.Store] SPARQL Graph Store connector constructor - * @param {factory} [init.factory] RDF/JS DataFactory - * @param {{...(key:value)}} [init.options] any additional arguments passed to Query and Store constructors - */ - constructor ({ endpoint, Query, Store, factory, ...options }) { - /** @member {RawQuery} */ - this.query = Query ? new Query({ endpoint, factory, ...options }) : null - /** @member {StreamStore} */ - this.store = Store ? new Store({ endpoint, factory, ...options }) : null - } -} - -module.exports = BaseClient diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7029bd8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.0.0] - 2024-02-18 + +### Added + +- ESM support +- exports for all classes in the `index.js` package entrypoint + +### Changed + +- options like `endpointUrl`, `user`, and `password` are attached to the client object, allowing creating new client + instances from existing instances +- methods that return a `Readable` stream objects are sync +- updated dependencies + +### Removed + +- CommonJS support +- `BaseClient` and `Endpoint` class +- automatic request splitting for Graph Store uploads diff --git a/Endpoint.js b/Endpoint.js deleted file mode 100644 index 766b417..0000000 --- a/Endpoint.js +++ /dev/null @@ -1,136 +0,0 @@ -const defaultFetch = require('nodeify-fetch') - -/** - * Represents a SPARQL endpoint and exposes a low-level methods, close to the underlying HTTP interface - * - * It directly returns HTTP response objects - */ -class Endpoint { - /** - * @param {Object} init - * @param {string} init.endpointUrl SPARQL Query endpoint URL - * @param {fetch} [init.fetch=nodeify-fetch] fetch implementation - * @param {HeadersInit} [init.headers] HTTP headers to send with every endpoint request - * @param {string} [init.password] password used for basic authentication - * @param {string} [init.storeUrl] Graph Store URL - * @param {string} [init.updateUrl] SPARQL Update endpoint URL - * @param {string} [init.user] user used for basic authentication - */ - constructor ({ endpointUrl, fetch, headers, password, storeUrl, updateUrl, user }) { - this.endpointUrl = endpointUrl - this.fetch = fetch || defaultFetch - this.headers = new this.fetch.Headers(headers) - this.storeUrl = storeUrl - this.updateUrl = updateUrl - - if (typeof user === 'string' && typeof password === 'string') { - this.headers.set('authorization', 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64')) - } - } - - /** - * Sends the query as GET request with query string - * @param {string} query SPARQL Query/Update - * @param {Object} options - * @param {HeadersInit} [options.headers] per-request HTTP headers - * @param {boolean} [options.update=false] if true, performs a SPARQL Update - * @return {Promise} - */ - async get (query, { headers, update = false } = {}) { - let url = null - - if (!update) { - url = new URL(this.endpointUrl) - url.searchParams.append('query', query) - } else { - url = new URL(this.updateUrl) - url.searchParams.append('update', query) - } - - return this.fetch(url.toString().replace(/\+/g, '%20'), { - method: 'GET', - headers: this.mergeHeaders(headers) - }) - } - - /** - * Sends the query as POST request with application/sparql-query body - * @param {string} query SPARQL Query/Update - * @param {Object} options - * @param {HeadersInit} [options.headers] per-request HTTP headers - * @param {boolean} [options.update=false] if true, performs a SPARQL Update - * @return {Promise} - */ - async postDirect (query, { headers, update = false } = {}) { - let url = null - - if (!update) { - url = new URL(this.endpointUrl) - } else { - url = new URL(this.updateUrl) - } - - headers = this.mergeHeaders(headers) - - if (!headers.has('content-type')) { - headers.set('content-type', 'application/sparql-query; charset=utf-8') - } - - return this.fetch(url, { - method: 'POST', - headers, - body: query - }) - } - - /** - * Sends the query as POST request with application/x-www-form-urlencoded body - * @param {string} query SPARQL Query/Update - * @param {Object} options - * @param {HeadersInit} [options.headers] per-request HTTP headers - * @param {boolean} [options.update=false] if true, performs a SPARQL Update - * @return {Promise} - */ - async postUrlencoded (query, { headers, update = false } = {}) { - let url = null - let body = null - - if (!update) { - url = new URL(this.endpointUrl) - body = 'query=' + encodeURIComponent(query) - } else { - url = new URL(this.updateUrl) - body = 'update=' + encodeURIComponent(query) - } - - headers = this.mergeHeaders(headers) - - if (!headers.has('content-type')) { - headers.set('content-type', 'application/x-www-form-urlencoded') - } - - return this.fetch(url, { - method: 'POST', - headers, - body - }) - } - - mergeHeaders (args = {}) { - const merged = new this.fetch.Headers() - - // client headers - for (const [key, value] of this.headers) { - merged.set(key, value) - } - - // argument headers - for (const [key, value] of new this.fetch.Headers(args)) { - merged.set(key, value) - } - - return merged - } -} - -module.exports = Endpoint diff --git a/ParsingClient.js b/ParsingClient.js index 891f2e4..a965a12 100644 --- a/ParsingClient.js +++ b/ParsingClient.js @@ -1,33 +1,85 @@ -const Endpoint = require('./Endpoint') -const ParsingQuery = require('./ParsingQuery') -const BaseClient = require('./BaseClient') +import DataModelFactory from '@rdfjs/data-model/Factory.js' +import DatasetFactory from '@rdfjs/dataset/Factory.js' +import Environment from '@rdfjs/environment' +import isDatasetCoreFactory from './lib/isDatasetCoreFactory.js' +import ParsingQuery from './ParsingQuery.js' +import SimpleClient from './SimpleClient.js' + +const defaultFactory = new Environment([DataModelFactory, DatasetFactory]) /** - * A client implementation which parses SPARQL responses into RDF/JS dataset (CONSTRUCT/DESCRIBE) or JSON objects (SELECT) - * - * It does not provide a store + * A client implementation based on {@link ParsingQuery} that parses SPARQL results into RDF/JS DatasetCore objects + * (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). It does not provide a store interface. * + * @extends SimpleClient * @property {ParsingQuery} query + * + * @example + * // read the height of the Eiffel Tower from Wikidata with a SELECT query + * + * import ParsingClient from 'sparql-http-client/ParsingClient.js' + * + * const endpointUrl = 'https://query.wikidata.org/sparql' + * const query = ` + * PREFIX wd: + * PREFIX p: + * PREFIX ps: + * PREFIX pq: + * + * SELECT ?value WHERE { + * wd:Q243 p:P2048 ?height. + * + * ?height pq:P518 wd:Q24192182; + * ps:P2048 ?value . + * }` + * + * const client = new ParsingClient({ endpointUrl }) + * const result = await client.query.select(query) + * + * for (const row of result) { + * for (const [key, value] of Object.entries(row)) { + * console.log(`${key}: ${value.value} (${value.termType})`) + * } + * } */ -class ParsingClient extends BaseClient { +class ParsingClient extends SimpleClient { /** * @param {Object} options - * @param {string} options.endpointUrl SPARQL Query endpoint URL + * @param {string} [options.endpointUrl] SPARQL query endpoint URL + * @param {factory} [options.factory] RDF/JS factory * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation - * @param {HeadersInit} [options.headers] HTTP headers to send with every endpoint request + * @param {Headers} [options.headers] headers sent with every request * @param {string} [options.password] password used for basic authentication - * @param {string} [options.storeUrl] Graph Store URL - * @param {string} [options.updateUrl] SPARQL Update endpoint URL + * @param {string} [options.storeUrl] SPARQL Graph Store URL + * @param {string} [options.updateUrl] SPARQL update endpoint URL * @param {string} [options.user] user used for basic authentication - * @param {factory} [options.factory] RDF/JS DataFactory */ - constructor (options) { + constructor ({ + endpointUrl, + factory = defaultFactory, + fetch, + headers, + password, + storeUrl, + updateUrl, + user + }) { super({ - endpoint: new Endpoint(options), - factory: options.factory, + endpointUrl, + factory, + fetch, + headers, + password, + storeUrl, + updateUrl, + user, Query: ParsingQuery }) + + if (!isDatasetCoreFactory(this.factory)) { + throw new Error('the given factory doesn\'t implement the DatasetCoreFactory interface') + } } } -module.exports = ParsingClient +export default ParsingClient diff --git a/ParsingQuery.js b/ParsingQuery.js index 101698c..10543a3 100644 --- a/ParsingQuery.js +++ b/ParsingQuery.js @@ -1,47 +1,40 @@ -const { array } = require('get-stream') -const StreamQuery = require('./StreamQuery') +import chunks from 'stream-chunks/chunks.js' +import StreamQuery from './StreamQuery.js' /** - * Extends StreamQuery by materialising the SPARQL response streams + * A query implementation that wraps the results of the {@link StreamQuery} into RDF/JS DatasetCore objects + * (CONSTRUCT/DESCRIBE) or an array of objects (SELECT). + * + * @extends StreamQuery */ class ParsingQuery extends StreamQuery { /** - * @param {Object} init - * @param {Endpoint} init.endpoint - */ - constructor ({ endpoint }) { - super({ endpoint }) - } - - /** - * Performs a query which returns triples + * Sends a request for a CONSTRUCT or DESCRIBE query * - * @param {string} query - * @param {Object} [options] - * @param {HeadersInit} [options.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] - * @return {Promise} + * @param {string} query CONSTRUCT or DESCRIBE query + * @param {Object} options + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation + * @return {Promise} */ - async construct (query, options = {}) { - const stream = await super.construct(query, options) + async construct (query, { headers, operation } = {}) { + const quads = await chunks(await super.construct(query, { headers, operation })) - return array(stream) + return this.client.factory.dataset(quads) } /** - * Performs a SELECT query which returns binding tuples + * Sends a request for a SELECT query * - * @param {string} query + * @param {string} query SELECT query * @param {Object} [options] - * @param {HeadersInit} [options.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation * @return {Promise>>} */ - async select (query, options = {}) { - const stream = await super.select(query, options) - - return array(stream) + async select (query, { headers, operation } = {}) { + return chunks(await super.select(query, { headers, operation })) } } -module.exports = ParsingQuery +export default ParsingQuery diff --git a/README.md b/README.md index 810c739..9a84f9d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ # sparql-http-client -[![build status](https://img.shields.io/github/actions/workflow/status/bergos/sparql-http-client/ci.yaml?branch=master)](https://github.com/bergos/sparql-http-client/actions/workflows/ci.yaml) +[![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) [![npm version](https://img.shields.io/npm/v/sparql-http-client.svg)](https://www.npmjs.com/package/sparql-http-client) SPARQL client for easier handling of SPARQL Queries and Graph Store requests. 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/). The [SPARQL Graph Store Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/) is used to manage Named Graphs. -## Getting started example +It provides client implementations in different flavors. +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. -TL;DR; the package exports a `StreamClient` class which run SPARQL queries on an endpoint. +## Usage + +The example below shows how to use the client to run a `SELECT` query against the Wikidata endpoint. +Check the [documentation](https://rdf-ext.github.io/sparql-http-client/) for more details. ```javascript -const SparqlClient = require('sparql-http-client') +import SparqlClient from 'sparql-http-client' const endpointUrl = 'https://query.wikidata.org/sparql' const query = ` @@ -29,17 +33,15 @@ SELECT ?value WHERE { }` const client = new SparqlClient({ endpointUrl }) -const stream = await client.query.select(query) +const stream = client.query.select(query) stream.on('data', row => { - Object.entries(row).forEach(([key, value]) => { + for (const [key, value] of Object.entries(row)) { console.log(`${key}: ${value.value} (${value.termType})`) - }) + } }) stream.on('error', err => { console.error(err) }) ``` - -Find more details on [https://bergos.github.io/sparql-http-client/](https://bergos.github.io/sparql-http-client/) diff --git a/RawQuery.js b/RawQuery.js index 5aa6bfc..7f36807 100644 --- a/RawQuery.js +++ b/RawQuery.js @@ -1,95 +1,92 @@ +import mergeHeaders from './lib/mergeHeaders.js' + /** - * A base query class which performs HTTP requests for the different SPARQL query forms + * A query implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. */ class RawQuery { /** - * @param {Object} init - * @param {Endpoint} init.endpoint + * @param {Object} options + * @param {SimpleClient} options.client client that provides the HTTP I/O */ - constructor ({ endpoint }) { - /** @member {Endpoint} */ - this.endpoint = endpoint + constructor ({ client }) { + this.client = client } /** - * Performs an ASK query - * By default uses HTTP GET with query string + * Sends a request for a ASK query * - * @param {string} query SPARQL ASK query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @param {string} query ASK query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation * @return {Promise} */ async ask (query, { headers, operation = 'get' } = {}) { - headers = this.endpoint.mergeHeaders(headers) + headers = mergeHeaders(headers) if (!headers.has('accept')) { headers.set('accept', 'application/sparql-results+json') } - return this.endpoint[operation](query, { headers }) + return this.client[operation](query, { headers }) } /** - * Performs a CONSTRUCT/DESCRIBE query - * By default uses HTTP GET with query string + * Sends a request for a CONSTRUCT or DESCRIBE query * - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @param {string} query CONSTRUCT or DESCRIBE query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation * @return {Promise} */ async construct (query, { headers, operation = 'get' } = {}) { - headers = new this.endpoint.fetch.Headers(headers) + headers = mergeHeaders(headers) if (!headers.has('accept')) { headers.set('accept', 'application/n-triples') } - return this.endpoint[operation](query, { headers }) + return this.client[operation](query, { headers }) } /** - * Performs a SELECT query - * By default uses HTTP GET with query string + * Sends a request for a SELECT query * - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @param {string} query SELECT query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation * @return {Promise} */ async select (query, { headers, operation = 'get' } = {}) { - headers = this.endpoint.mergeHeaders(headers) + headers = mergeHeaders(headers) if (!headers.has('accept')) { headers.set('accept', 'application/sparql-results+json') } - return this.endpoint[operation](query, { headers }) + return this.client[operation](query, { headers }) } /** - * Performs a SELECT query - * By default uses HTTP POST with form-encoded body + * Sends a request for an update query * - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='postUrlencoded'] + * @param {string} query update query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='postUrlencoded'] SPARQL Protocol operation * @return {Promise} */ async update (query, { headers, operation = 'postUrlencoded' } = {}) { - headers = new this.endpoint.fetch.Headers(headers) + headers = mergeHeaders(headers) if (!headers.has('accept')) { headers.set('accept', '*/*') } - return this.endpoint[operation](query, { headers, update: true }) + return this.client[operation](query, { headers, update: true }) } } -module.exports = RawQuery +export default RawQuery diff --git a/ResultParser.js b/ResultParser.js index afe2ecd..d22f8ee 100644 --- a/ResultParser.js +++ b/ResultParser.js @@ -1,51 +1,52 @@ -const jsonStream = require('jsonstream2') -const delay = require('promise-the-world/delay') -const rdf = require('@rdfjs/data-model') -const { Duplex, finished } = require('readable-stream') +import JsonParser from '@bergos/jsonparse' +import { Transform } from 'readable-stream' /** - * A stream which parses SPARQL SELECT bindings + * A Transform stream that parses JSON SPARQL results and emits one object per row with the variable names as keys and + * RDF/JS terms as values. */ -class ResultParser extends Duplex { - constructor ({ factory = rdf } = {}) { +class ResultParser extends Transform { + /** + * @param {Object} options + * @param {DataFactory} options.factory RDF/JS DataFactory used to create the quads and terms + */ + constructor ({ factory }) { super({ readableObjectMode: true }) this.factory = factory - this.jsonParser = jsonStream.parse('results.bindings.*') - - finished(this.jsonParser, err => { - this.destroy(err) - }) + this.jsonParser = new JsonParser() + this.jsonParser.onError = err => this.destroy(err) + this.jsonParser.onValue = value => this.onValue(value) } _write (chunk, encoding, callback) { - this.jsonParser.write(chunk, encoding, callback) + this.jsonParser.write(chunk) + + callback() } - async _read () { - while (true) { - const raw = this.jsonParser.read() + onValue (raw) { + if (this.jsonParser.stack.length !== 3) { + return + } - if (!raw || Object.keys(raw).length === 0) { - if (!this.writable) { - return this.push(null) - } + if (this.jsonParser.stack[1].key !== 'results' || this.jsonParser.stack[2].key !== 'bindings') { + return + } - await delay(0) - } else { - const row = Object.entries(raw).reduce((row, [key, value]) => { - row[key] = this.valueToTerm(value) + if (Object.keys(raw).length === 0) { + return + } - return row - }, {}) + const row = {} - if (!this.push(row)) { - return - } - } + for (const [key, value] of Object.entries(raw)) { + row[key] = this.valueToTerm(value) } + + this.push(row) } valueToTerm (value) { @@ -67,4 +68,4 @@ class ResultParser extends Duplex { } } -module.exports = ResultParser +export default ResultParser diff --git a/SimpleClient.js b/SimpleClient.js index 9d4238b..17682ac 100644 --- a/SimpleClient.js +++ b/SimpleClient.js @@ -1,31 +1,192 @@ -const Endpoint = require('./Endpoint') -const RawQuery = require('./RawQuery') -const BaseClient = require('./BaseClient') +import defaultFetch from 'nodeify-fetch' +import mergeHeaders from './lib/mergeHeaders.js' +import RawQuery from './RawQuery.js' /** - * A basic client implementation which uses RawQuery and no Store + * A client implementation based on {@link RawQuery} that prepares URLs and headers for SPARQL queries and returns the + * raw fetch response. It does not provide a store interface. * * @property {RawQuery} query + * @property {string} endpointUrl + * @property {RawQuery} factory + * @property {factory} fetch + * @property {Headers} headers + * @property {string} password + * @property {string} storeUrl + * @property {string} updateUrl + * @property {string} user + * @property {string} updateUrl + * + * @example + * // read the height of the Eiffel Tower from Wikidata with a SELECT query + * + * import SparqlClient from 'sparql-http-client/SimpleClient.js' + * + * const endpointUrl = 'https://query.wikidata.org/sparql' + * const query = ` + * PREFIX wd: + * PREFIX p: + * PREFIX ps: + * PREFIX pq: + * + * SELECT ?value WHERE { + * wd:Q243 p:P2048 ?height. + * + * ?height pq:P518 wd:Q24192182; + * ps:P2048 ?value . + * }` + * + * const client = new SparqlClient({ endpointUrl }) + * const res = await client.query.select(query) + * + * if (!res.ok) { + * return console.error(res.statusText) + * } + * + * const content = await res.json() + * + * console.log(JSON.stringify(content, null, 2)) */ -class SimpleClient extends BaseClient { +class SimpleClient { /** * @param {Object} options - * @param {string} options.endpointUrl SPARQL Query endpoint URL + * @param {string} [options.endpointUrl] SPARQL query endpoint URL + * @param {factory} [options.factory] RDF/JS factory * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation - * @param {HeadersInit} [options.headers] HTTP headers to send with every endpoint request + * @param {Headers} [options.headers] headers sent with every request * @param {string} [options.password] password used for basic authentication - * @param {string} [options.storeUrl] Graph Store URL - * @param {string} [options.updateUrl] SPARQL Update endpoint URL + * @param {string} [options.storeUrl] SPARQL Graph Store URL + * @param {string} [options.updateUrl] SPARQL update endpoint URL * @param {string} [options.user] user used for basic authentication - * @param {factory} [options.factory] RDF/JS DataFactory + * @param {Query} [options.Query] Constructor of a query implementation + * @param {Store} [options.Store] Constructor of a store implementation + */ + constructor ({ + endpointUrl, + factory, + fetch = defaultFetch, + headers, + password, + storeUrl, + updateUrl, + user, + Query = RawQuery, + Store + }) { + if (!endpointUrl && !storeUrl && !updateUrl) { + throw new Error('no endpointUrl, storeUrl, or updateUrl given') + } + + this.endpointUrl = endpointUrl + this.factory = factory + this.fetch = fetch + this.headers = new Headers(headers) + this.password = password + this.storeUrl = storeUrl + this.updateUrl = updateUrl + this.user = user + this.query = Query ? new Query({ client: this }) : null + this.store = Store ? new Store({ client: this }) : null + + if (typeof user === 'string' && typeof password === 'string') { + this.headers.set('authorization', `Basic ${btoa(`${user}:${password}`)}`) + } + } + + /** + * Sends a GET request as defined in the + * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get SPARQL Protocol specification}. + * + * @param {string} query SPARQL query + * @param {Object} options + * @param {Headers} [options.headers] additional request headers + * @param {boolean} [options.update=false] send the request to the updateUrl + * @return {Promise} + */ + async get (query, { headers, update = false } = {}) { + let url = null + + if (!update) { + url = new URL(this.endpointUrl) + url.searchParams.append('query', query) + } else { + url = new URL(this.updateUrl) + url.searchParams.append('update', query) + } + + return this.fetch(url.toString().replace(/\+/g, '%20'), { + method: 'GET', + headers: mergeHeaders(this.headers, headers) + }) + } + + /** + * Sends a POST directly request as defined in the + * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct SPARQL Protocol specification}. + * + * + * @param {string} query SPARQL query + * @param {Object} options + * @param {Headers} [options.headers] additional request headers + * @param {boolean} [options.update=false] send the request to the updateUrl + * @return {Promise} */ - constructor (options) { - super({ - endpoint: new Endpoint(options), - factory: options.factory, - Query: RawQuery + async postDirect (query, { headers, update = false } = {}) { + let url = null + + if (!update) { + url = new URL(this.endpointUrl) + } else { + url = new URL(this.updateUrl) + } + + headers = mergeHeaders(this.headers, headers) + + if (!headers.has('content-type')) { + headers.set('content-type', 'application/sparql-query; charset=utf-8') + } + + return this.fetch(url, { + method: 'POST', + headers, + body: query + }) + } + + /** + * Sends a POST URL-encoded request as defined in the + * {@link https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded SPARQL Protocol specification}. + * + * @param {string} query SPARQL query + * @param {Object} options + * @param {Headers} [options.headers] additional request headers + * @param {boolean} [options.update=false] send the request to the updateUrl + * @return {Promise} + */ + async postUrlencoded (query, { headers, update = false } = {}) { + let url = null + let body = null + + if (!update) { + url = new URL(this.endpointUrl) + body = `query=${encodeURIComponent(query)}` + } else { + url = new URL(this.updateUrl) + body = `update=${encodeURIComponent(query)}` + } + + headers = mergeHeaders(this.headers, headers) + + if (!headers.has('content-type')) { + headers.set('content-type', 'application/x-www-form-urlencoded') + } + + return this.fetch(url, { + method: 'POST', + headers, + body }) } } -module.exports = SimpleClient +export default SimpleClient diff --git a/StreamClient.js b/StreamClient.js index 3a5c503..8a63570 100644 --- a/StreamClient.js +++ b/StreamClient.js @@ -1,35 +1,106 @@ -const BaseClient = require('./BaseClient') -const Endpoint = require('./Endpoint') -const StreamQuery = require('./StreamQuery') -const StreamStore = require('./StreamStore') +import defaultFactory from '@rdfjs/data-model' +import isDataFactory from './lib/isDataFactory.js' +import SimpleClient from './SimpleClient.js' +import StreamQuery from './StreamQuery.js' +import StreamStore from './StreamStore.js' /** - * The default client implementation which returns SPARQL response as RDF/JS streams + * The default client implementation based on {@link StreamQuery} and {@link StreamStore} parses SPARQL results into + * Readable streams of RDF/JS Quad objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). Graph Store + * read and write operations are handled using Readable streams. * + * @extends SimpleClient * @property {StreamQuery} query * @property {StreamStore} store + * + * @example + * // read the height of the Eiffel Tower from Wikidata with a SELECT query + * + * import SparqlClient from 'sparql-http-client' + * + * const endpointUrl = 'https://query.wikidata.org/sparql' + * const query = ` + * PREFIX wd: + * PREFIX p: + * PREFIX ps: + * PREFIX pq: + * + * SELECT ?value WHERE { + * wd:Q243 p:P2048 ?height. + * + * ?height pq:P518 wd:Q24192182; + * ps:P2048 ?value . + * }` + * + * const client = new SparqlClient({ endpointUrl }) + * const stream = client.query.select(query) + * + * stream.on('data', row => { + * for (const [key, value] of Object.entries(row)) { + * console.log(`${key}: ${value.value} (${value.termType})`) + * } + * }) + * + * stream.on('error', err => { + * console.error(err) + * }) + * + * @example + * // read all quads from a local triplestore using the Graph Store protocol + * + * import rdf from 'rdf-ext' + * import SparqlClient from 'sparql-http-client' + * + * const client = new SparqlClient({ + * storeUrl: 'http://localhost:3030/test/data', + * factory: rdf + * }) + * + * const stream = local.store.get(rdf.defaultGraph()) + * + * stream.on('data', quad => { + * console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) + * }) */ -class StreamClient extends BaseClient { +class StreamClient extends SimpleClient { /** * @param {Object} options - * @param {string} options.endpointUrl SPARQL Query endpoint URL + * @param {string} [options.endpointUrl] SPARQL query endpoint URL + * @param {factory} [options.factory] RDF/JS factory * @param {fetch} [options.fetch=nodeify-fetch] fetch implementation - * @param {HeadersInit} [options.headers] HTTP headers to send with every endpoint request + * @param {Headers} [options.headers] headers sent with every request * @param {string} [options.password] password used for basic authentication - * @param {string} [options.storeUrl] Graph Store URL - * @param {string} [options.updateUrl] SPARQL Update endpoint URL + * @param {string} [options.storeUrl] SPARQL Graph Store URL + * @param {string} [options.updateUrl] SPARQL update endpoint URL * @param {string} [options.user] user used for basic authentication - * @param {factory} [options.factory] RDF/JS DataFactory */ - constructor (options) { + constructor ({ + endpointUrl, + factory = defaultFactory, + fetch, + headers, + password, + storeUrl, + updateUrl, + user + }) { super({ - endpoint: new Endpoint(options), - factory: options.factory, + endpointUrl, + factory, + fetch, + headers, + password, + storeUrl, + updateUrl, + user, Query: StreamQuery, - Store: StreamStore, - ...options + Store: StreamStore }) + + if (!isDataFactory(this.factory)) { + throw new Error('the given factory doesn\'t implement the DataFactory interface') + } } } -module.exports = StreamClient +export default StreamClient diff --git a/StreamQuery.js b/StreamQuery.js index 163b325..b34a6dc 100644 --- a/StreamQuery.js +++ b/StreamQuery.js @@ -1,30 +1,24 @@ -const rdf = require('@rdfjs/data-model') -const N3Parser = require('@rdfjs/parser-n3') -const checkResponse = require('./lib/checkResponse') -const RawQuery = require('./RawQuery') -const ResultParser = require('./ResultParser') +import N3Parser from '@rdfjs/parser-n3' +import asyncToReadabe from './lib/asyncToReadabe.js' +import checkResponse from './lib/checkResponse.js' +import mergeHeaders from './lib/mergeHeaders.js' +import RawQuery from './RawQuery.js' +import ResultParser from './ResultParser.js' /** - * Extends RawQuery by wrapping response body streams as RDF/JS Streams + * A query implementation based on {@link RawQuery} that parses SPARQL results into Readable streams of RDF/JS Quad + * objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). + * + * @extends RawQuery */ class StreamQuery extends RawQuery { /** - * @param {Object} init - * @param {Endpoint} init.endpoint - * @param {DataFactory} [init.factory=@rdfjs/data-model] - */ - constructor ({ endpoint, factory = rdf }) { - super({ endpoint }) - - /** @member {DataFactory} */ - this.factory = factory - } - - /** - * @param {string} query SPARQL ASK query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * Sends a request for a ASK query + * + * @param {string} query ASK query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation * @return {Promise} */ async ask (query, { headers, operation } = {}) { @@ -38,50 +32,60 @@ class StreamQuery extends RawQuery { } /** - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] - * @return {Promise} + * Sends a request for a CONSTRUCT or DESCRIBE query + * + * @param {string} query CONSTRUCT or DESCRIBE query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation + * @return {Readable} */ - async construct (query, { headers, operation } = {}) { - headers = new this.endpoint.fetch.Headers(headers) + construct (query, { headers, operation } = {}) { + return asyncToReadabe(async () => { + headers = mergeHeaders(headers) - if (!headers.has('accept')) { - headers.set('accept', 'application/n-triples, text/turtle') - } + if (!headers.has('accept')) { + headers.set('accept', 'application/n-triples, text/turtle') + } - const res = await super.construct(query, { headers, operation }) + const res = await super.construct(query, { headers, operation }) - await checkResponse(res) + await checkResponse(res) - const parser = new N3Parser({ factory: this.factory }) + const parser = new N3Parser({ factory: this.client.factory }) - return parser.import(res.body) + return parser.import(res.body) + }) } /** - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] - * @return {Promise} + * Sends a request for a SELECT query + * + * @param {string} query SELECT query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='get'] SPARQL Protocol operation + * @return {Readable} */ - async select (query, { headers, operation } = {}) { - const res = await super.select(query, { headers, operation }) + select (query, { headers, operation } = {}) { + return asyncToReadabe(async () => { + const res = await super.select(query, { headers, operation }) - await checkResponse(res) + await checkResponse(res) - const parser = new ResultParser({ factory: this.factory }) + const parser = new ResultParser({ factory: this.client.factory }) - return res.body.pipe(parser) + return res.body.pipe(parser) + }) } /** - * @param {string} query SPARQL query - * @param {Object} [init] - * @param {HeadersInit} [init.headers] HTTP request headers - * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='postUrlencoded'] + * Sends a request for an update query + * + * @param {string} query update query + * @param {Object} [options] + * @param {Headers} [options.headers] additional request headers + * @param {'get'|'postUrlencoded'|'postDirect'} [options.operation='postUrlencoded'] SPARQL Protocol operation * @return {Promise} */ async update (query, { headers, operation } = {}) { @@ -91,4 +95,4 @@ class StreamQuery extends RawQuery { } } -module.exports = StreamQuery +export default StreamQuery diff --git a/StreamStore.js b/StreamStore.js index ce7562b..518fb57 100644 --- a/StreamStore.js +++ b/StreamStore.js @@ -1,115 +1,127 @@ -const { promisify } = require('util') -const TripleToQuadTransform = require('rdf-transform-triple-to-quad') -const rdf = require('@rdfjs/data-model') -const N3Parser = require('@rdfjs/parser-n3') -const { finished } = require('readable-stream') -const checkResponse = require('./lib/checkResponse') -const QuadStreamSeparator = require('./lib/QuadStreamSeparator') +import N3Parser from '@rdfjs/parser-n3' +import toNT from '@rdfjs/to-ntriples' +import TripleToQuadTransform from 'rdf-transform-triple-to-quad' +import { Transform } from 'readable-stream' +import asyncToReadabe from './lib/asyncToReadabe.js' +import checkResponse from './lib/checkResponse.js' +import mergeHeaders from './lib/mergeHeaders.js' /** - * Accesses stores with SPARQL Graph Protocol + * A store implementation that parses and serializes SPARQL Graph Store responses and requests into/from Readable + * streams. */ class StreamStore { /** - * - * @param {Object} init - * @param {Endpoint} init.endpoint - * @param {DataFactory} [init.factory=@rdfjs/data-model] - * @param {number} [maxQuadsPerRequest] + * @param {Object} options + * @param {SimpleClient} options.client client that provides the HTTP I/O */ - constructor ({ endpoint, factory = rdf, maxQuadsPerRequest }) { - this.endpoint = endpoint - this.factory = factory - this.maxQuadsPerRequest = maxQuadsPerRequest + constructor ({ client }) { + this.client = client } /** - * Gets a graph triples from the store - * @param {NamedNode} graph - * @return {Promise} + * Sends a GET request to the Graph Store + * + * @param {NamedNode} [graph] source graph + * @return {Promise} */ - async get (graph) { + get (graph) { return this.read({ method: 'GET', graph }) } /** - * Adds triples to a graph - * @param {Stream} stream + * Sends a POST request to the Graph Store + * + * @param {Readable} stream triples/quads to write + * @param {Object} [options] + * @param {Term} [options.graph] target graph * @return {Promise} */ - async post (stream) { - return this.write({ method: 'POST', stream }) + async post (stream, { graph } = {}) { + return this.write({ graph, method: 'POST', stream }) } /** - * Replaces graph with triples - * @param {Stream} stream + * Sends a PUT request to the Graph Store + * + * @param {Readable} stream triples/quads to write + * @param {Object} [options] + * @param {Term} [options.graph] target graph * @return {Promise} */ - async put (stream) { - return this.write({ firstMethod: 'PUT', method: 'POST', stream }) + async put (stream, { graph } = {}) { + return this.write({ graph, method: 'PUT', stream }) } - async read ({ method, graph }) { - const url = new URL(this.endpoint.storeUrl) + /** + * Generic read request to the Graph Store + * + * @param {Object} [options] + * @param {Term} [options.graph] source graph + * @param {string} options.method HTTP method + * @returns {Readable} + */ + read ({ graph, method }) { + return asyncToReadabe(async () => { + const url = new URL(this.client.storeUrl) + + if (graph && graph.termType !== 'DefaultGraph') { + url.searchParams.append('graph', graph.value) + } - if (graph.termType !== 'DefaultGraph') { - url.searchParams.append('graph', graph.value) - } + const res = await this.client.fetch(url, { + method, + headers: mergeHeaders(this.client.headers, { accept: 'application/n-triples' }) + }) - return this.endpoint.fetch(url, { - method, - headers: this.endpoint.mergeHeaders({ accept: 'application/n-triples' }) - }).then(async res => { await checkResponse(res) - const parser = new N3Parser({ factory: this.factory }) - const tripleToQuad = new TripleToQuadTransform(graph, { factory: this.factory }) + const parser = new N3Parser({ factory: this.client.factory }) + const tripleToQuad = new TripleToQuadTransform(graph, { factory: this.client.factory }) return parser.import(res.body).pipe(tripleToQuad) }) } - async write ({ firstMethod, method, stream }) { - const seen = new Set() - let requestEnd = null - - const splitter = new QuadStreamSeparator({ - maxQuadsPerStream: this.maxQuadsPerRequest, - change: async stream => { - if (requestEnd) { - await requestEnd - } + /** + * Generic write request to the Graph Store + * + * @param {Object} [options] + * @param {Term} [graph] target graph + * @param {string} method HTTP method + * @param {Readable} stream triples/quads to write + * @returns {Promise} + */ + async write ({ graph, method, stream }) { + const url = new URL(this.client.storeUrl) - const currentMethod = seen.has(splitter.graph.value) ? method : (firstMethod || method) + if (graph && graph.termType !== 'DefaultGraph') { + url.searchParams.append('graph', graph.value) + } - requestEnd = this.writeRequest(currentMethod, splitter.graph, stream) + const serialize = new Transform({ + writableObjectMode: true, + transform (quad, encoding, callback) { + const triple = { + subject: quad.subject, + predicate: quad.predicate, + object: quad.object, + graph: { termType: 'DefaultGraph' } + } - seen.add(splitter.graph.value) + callback(null, `${toNT(triple)}\n`) } }) - stream.pipe(splitter) - - await promisify(finished)(splitter) - await requestEnd - } - - async writeRequest (method, graph, stream) { - const url = new URL(this.endpoint.storeUrl) - - if (graph.termType !== 'DefaultGraph') { - url.searchParams.append('graph', graph.value) - } - - const res = await this.endpoint.fetch(url, { + const res = await this.client.fetch(url, { method, - headers: this.endpoint.mergeHeaders({ 'content-type': 'application/n-triples' }), - body: stream + headers: mergeHeaders(this.client.headers, { 'content-type': 'application/n-triples' }), + body: stream.pipe(serialize), + duplex: 'half' }) await checkResponse(res) } } -module.exports = StreamStore +export default StreamStore diff --git a/docs/README.md b/docs/README.md index 1281e65..69e97e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,18 @@ # sparql-http-client -[![Build Status](https://travis-ci.org/zazuko/sparql-http-client.svg?branch=master)](https://travis-ci.org/zazuko/sparql-http-client) -[![NPM Version](https://img.shields.io/npm/v/sparql-http-client.svg?style=flat)](https://npm.im/sparql-http-client) +[![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) +[![npm version](https://img.shields.io/npm/v/sparql-http-client.svg)](https://www.npmjs.com/package/sparql-http-client) -## Contents of this package +This package provides a handful of clients that can talk to SPARQL endpoints using Query, Update, and Graph Store Protocols. -This package provides a handful of clients which can talk to SPARQL endpoints using Query, Update and/or Graph Store Protocols. +- [`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`). + Graph Store read and write operations are handled using Readable streams. +- [`ParsingClient`](parsing-client.md): A client implementation that parses SPARQL results into RDF/JS DatasetCore objects (`CONSTRUCT`/`DESCRIBE`) or an array of objects (`SELECT`). + It does not provide a store interface. +- [`SimpleClient`](simple-client.md): A client implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. + It does not provide a store interface. -1. [`StreamClient`](stream-client.md) - returns [streams of RDF/JS triples](http://rdf.js.org/stream-spec/#stream-interface) or SELECT bindings -2. [`ParsingClient`](parsing-client.md) - extends the `StreamClient` to return fully parsed [RDF/JS Dataset](https://rdf.js.org/dataset-spec/#datasetcore-interface) or JS array of SELECT bindings -3. [`SimpleClient`](simple-client.md) - returns raw underlying [fetch `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Response_objects) - -[`StreamClient`](stream-client.md) is the default export of this package's main module. - -Every client implementation inherits from a `BaseClient`. They all share the most of the base API and parameters. +[`StreamClient`](stream-client.md) is the default export of the package. ## Usage @@ -21,64 +20,43 @@ Every client implementation inherits from a `BaseClient`. They all share the mos The constructor of all clients accepts a single object argument with the following properties: -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| endpointUrl | string | | URL of the endpoint for SPARQL Queries | -| updateUrl | string | | URL of the endpoint for SPARQL Updates | -| storeUrl | string | | URL of the endpoint for Graph Store Protocol requests | -| headers | HeadersInit | | Headers that will be merged into all requests as fetch | -| user | string | | User for basic authentication | -| password | string | | Password used for basic authentication | -| factory | factory | `@rdfjs/data-model` | A RDF/JS factory used to create all Terms and Quads | -| fetch | fetch | nodeify-fetch | fetch implementation | - -All properties are optional but omitting some of them will disable certain capabilities. Also not all properties are supported by all implementations. Check their respective pages. - -Clients come with a [fetch](https://www.npmjs.com/package/nodeify-fetch) library and parsers for triple results or `SELECT` and `ASK` queries. - -### Queries - -All methods for SPARQL Queries or SPARQL Updates are attached to the instance property `query`. They are all async. - -Their return types depend on the client specific client implementation. - -The following query methods are available: +| Param | Type | Default | Description | +|-------------|-----------------------|----------------------------------------|---------------------------------------------| +| endpointUrl | string | | SPARQL query endpoint URL | +| updateUrl | string | | SPARQL update endpoint URL | +| storeUrl | string | | SPARQL Graph Store URL | +| user | string | | user used for basic authentication | +| password | string | | password used for basic authentication | +| headers | Headers | | headers sent with every request | +| factory | factory | `@rdfjs/data-model` & `@rdfjs/dataset` | RDF/JS factory | +| fetch | fetch | `nodeify-fetch` | fetch implementation | -#### query.ask (query, { headers, operation }) +At least one URL argument must be given. +Besides that, all properties are optional, but omitting some of them will disable certain capabilities. +Also, not all properties are supported by all implementations. Check their respective pages. -Runs an `ASK` query against the given `endpointUrl`. +A client object has all properties attached required to create a new instance. +It is, therefore, possible to create a new client object of a different type based on an existing client object: -#### query.construct (query, { headers, operation }) - -Runs a `CONSTRUCT` or `DESCRIBE` query against the given `endpointUrl`. +```javascript +const simpleClient = new SimpleClient({ endpointUrl }) +const parsingClient = new ParsingClient(simpleClient) +``` -#### query.select (query, { headers, operation }) +### Examples -Runs a `SELECT` query against the given `endpointUrl`. +The [API](api.md) section contains examples for all clients. -#### query.update (query, { headers, operation }) +### Queries -Runs an `INSERT`, `UPDATE` or `DELETE` query against the given `updateUrl`. +All methods for SPARQL Queries and Updates are attached to the instance property `query`. +Their return types are implementation-specific. +See the [API](api.md) section for more details on the individual methods. ### Graph Store -All methods for SPARQL Graph Store requests are attached to the instance property `store`. - -Their return types depend on the client specific client implementation. - -The following store methods are available: - -#### store.get (graph) - -Makes a `GET` request to the given `storeUrl` and the given `graph` argument. - -#### store.post (stream) - -Makes a `POST` request to the given `storeUrl` and sends the RDF/JS `Quads` of the given stream as request content. Named Graph changes are detected and requests are split accordingly. - -#### store.put (stream) - -Makes a `PUT` request to the given `storeUrl` and sends the RDF/JS `Quads` of the given stream as request content. Named Graph changes are detected and requests are split accordingly. +All methods for SPARQL Graph Store are attached to the instance property `store`. +See the [API](api.md) section for more details on the individual methods. ### Advanced Topics @@ -87,12 +65,11 @@ Makes a `PUT` request to the given `storeUrl` and sends the RDF/JS `Quads` of th HTTP requests to the SPARQL endpoint can have additional headers added to them. For example, to pass authorization information. -One method for doing so is to set headers on a single query or update: +One method for doing so is to set headers on the method call: ```javascript const client = new SparqlClient({ endpointUrl: 'https://query.wikidata.org/sparql' }) -// authorize a single query client.query.select(query, { headers: { Authorization: 'Bearer token' @@ -101,10 +78,10 @@ client.query.select(query, { ``` It is also possible to set headers in the constructor of the client. + The headers will be sent on all requests originating from the instance of the client: ```javascript -// authorize all requests const client = new SparqlClient({ endpointUrl: 'https://query.wikidata.org/sparql', headers: { @@ -116,12 +93,7 @@ const client = new SparqlClient({ #### Operation SPARQL queries and updates over the SPARQL Protocol can be done with different [operations](https://www.w3.org/TR/sparql11-protocol/#protocol). -By default all read queries use `get` and updates use `postUrlencoded`. +By default, all read queries use `get`, and updates use `postUrlencoded`. Very long queries may exceed the maximum request header length. For those cases, it's useful to switch to operations that use a `POST` request. This can be done by the optional `operation` argument. -The value must be a string with one of the following values: - -- `get` -- `postUrlencoded` -- `postDirect` diff --git a/docs/_sidebar.md b/docs/_sidebar.md index ca37873..c444a41 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,9 +1,3 @@ * sparql-http-client * [About](/) - * [StreamClient](stream-client.md) - * [ParsingClient](parsing-client.md) - * [SimpleClient](simple-client.md) - * [BaseClient](base-client.md) -* Reference - * [JSDoc](api.md) - * [TypeScript](https://npm.im/@types/sparql-http-client) + * [API](api.md) diff --git a/docs/api.md b/docs/api.md index cfcb017..645558c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,113 +1,72 @@ ## Classes
-
BaseClient
-

An abstract base client which connects the query, store and endpoint together

-

Store and Query parameters are both optional and only necessary when the client will connect to SPARQL Graph Store -or SPARQL Query endpoints respectively

+
ParsingClientSimpleClient
+

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

-
Endpoint
-

Represents a SPARQL endpoint and exposes a low-level methods, close to the underlying HTTP interface

-

It directly returns HTTP response objects

-
-
ParsingClient
-

A client implementation which parses SPARQL responses into RDF/JS dataset (CONSTRUCT/DESCRIBE) or JSON objects (SELECT)

-

It does not provide a store

-
-
ParsingQuery
-

Extends StreamQuery by materialising the SPARQL response streams

+
ParsingQueryStreamQuery
+

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

RawQuery
-

A base query class which performs HTTP requests for the different SPARQL query forms

+

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

ResultParser
-

A stream which parses SPARQL SELECT bindings

+

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

SimpleClient
-

A basic client implementation which uses RawQuery and no Store

+

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

-
StreamClient
-

The default client implementation which returns SPARQL response as RDF/JS streams

+
StreamClientSimpleClient
+

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

-
StreamQuery
-

Extends RawQuery by wrapping response body streams as RDF/JS Streams

+
StreamQueryRawQuery
+

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

StreamStore
-

Accesses stores with SPARQL Graph Protocol

+

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

- - -## BaseClient -An abstract base client which connects the query, store and endpoint together + -Store and Query parameters are both optional and only necessary when the client will connect to SPARQL Graph Store -or SPARQL Query endpoints respectively +## ParsingClient ⇐ [SimpleClient](#SimpleClient) +A client implementation based on [ParsingQuery](#ParsingQuery) that parses SPARQL results into RDF/JS DatasetCore objects +(CONSTRUCT/DESCRIBE) or an array of objects (SELECT). It does not provide a store interface. **Kind**: global class +**Extends**: [SimpleClient](#SimpleClient) +**Properties** -* [BaseClient](#BaseClient) - * [new BaseClient(init)](#new_BaseClient_new) - * [.query](#BaseClient+query) : [RawQuery](#RawQuery) - * [.store](#BaseClient+store) : [StreamStore](#StreamStore) - - - -### new BaseClient(init) - + - - - - - - - - - - - +
ParamTypeDescriptionNameType
initObject
init.endpointEndpoint

object to connect to SPARQL endpoint

-
[init.Query]Query

SPARQL Query/Update executor constructor

-
[init.Store]Store

SPARQL Graph Store connector constructor

-
[init.factory]factory

RDF/JS DataFactory

-
[init.options]Object

any additional arguments passed to Query and Store constructors

-
queryParsingQuery
- - -### baseClient.query : [RawQuery](#RawQuery) -**Kind**: instance property of [BaseClient](#BaseClient) - - -### baseClient.store : [StreamStore](#StreamStore) -**Kind**: instance property of [BaseClient](#BaseClient) - -## Endpoint -Represents a SPARQL endpoint and exposes a low-level methods, close to the underlying HTTP interface +* [ParsingClient](#ParsingClient) ⇐ [SimpleClient](#SimpleClient) + * [new ParsingClient(options)](#new_ParsingClient_new) + * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> + * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> + * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> -It directly returns HTTP response objects - -**Kind**: global class - -* [Endpoint](#Endpoint) - * [new Endpoint(init)](#new_Endpoint_new) - * [.get(query, options)](#Endpoint+get) ⇒ Promise.<Response> - * [.postDirect(query, options)](#Endpoint+postDirect) ⇒ Promise.<Response> - * [.postUrlencoded(query, options)](#Endpoint+postUrlencoded) ⇒ Promise.<Response> - - + -### new Endpoint(init) +### new ParsingClient(options) @@ -116,37 +75,71 @@ It directly returns HTTP response objects - + - - - - - - - + +
initObjectoptionsObject
init.endpointUrlstring

SPARQL Query endpoint URL

+
[options.endpointUrl]string

SPARQL query endpoint URL

[init.fetch]fetchnodeify-fetch

fetch implementation

+
[options.factory]factory

RDF/JS factory

[init.headers]HeadersInit

HTTP headers to send with every endpoint request

+
[options.fetch]fetchnodeify-fetch

fetch implementation

[init.password]string

password used for basic authentication

+
[options.headers]Headers

headers sent with every request

[init.storeUrl]string

Graph Store URL

+
[options.password]string

password used for basic authentication

[init.updateUrl]string

SPARQL Update endpoint URL

+
[options.storeUrl]string

SPARQL Graph Store URL

[init.user]string

user used for basic authentication

+
[options.updateUrl]string

SPARQL update endpoint URL

+
[options.user]string

user used for basic authentication

- +**Example** +```js +// read the height of the Eiffel Tower from Wikidata with a SELECT query + +import ParsingClient from 'sparql-http-client/ParsingClient.js' + +const endpointUrl = 'https://query.wikidata.org/sparql' +const query = ` +PREFIX wd: +PREFIX p: +PREFIX ps: +PREFIX pq: + +SELECT ?value WHERE { + wd:Q243 p:P2048 ?height. + + ?height pq:P518 wd:Q24192182; + ps:P2048 ?value . +}` -### endpoint.get(query, options) ⇒ Promise.<Response> -Sends the query as GET request with query string +const client = new ParsingClient({ endpointUrl }) +const result = await client.query.select(query) -**Kind**: instance method of [Endpoint](#Endpoint) +for (const row of result) { + for (const [key, value] of Object.entries(row)) { + console.log(`${key}: ${value.value} (${value.termType})`) + } +} +``` + + +### parsingClient.get(query, options) ⇒ Promise.<Response> +Sends a GET request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). + +**Kind**: instance method of [ParsingClient](#ParsingClient) +**Overrides**: [get](#SimpleClient+get) @@ -155,25 +148,27 @@ Sends the query as GET request with query string - - -
querystring

SPARQL Query/Update

+
querystring

SPARQL query

optionsObject
[options.headers]HeadersInit

per-request HTTP headers

+
[options.headers]Headers

additional request headers

[options.update]booleanfalse

if true, performs a SPARQL Update

+
[options.update]booleanfalse

send the request to the updateUrl

- + -### endpoint.postDirect(query, options) ⇒ Promise.<Response> -Sends the query as POST request with application/sparql-query body +### parsingClient.postDirect(query, options) ⇒ Promise.<Response> +Sends a POST directly request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). -**Kind**: instance method of [Endpoint](#Endpoint) +**Kind**: instance method of [ParsingClient](#ParsingClient) +**Overrides**: [postDirect](#SimpleClient+postDirect) @@ -182,25 +177,27 @@ Sends the query as POST request with application/sparql-query body - - -
querystring

SPARQL Query/Update

+
querystring

SPARQL query

optionsObject
[options.headers]HeadersInit

per-request HTTP headers

+
[options.headers]Headers

additional request headers

[options.update]booleanfalse

if true, performs a SPARQL Update

+
[options.update]booleanfalse

send the request to the updateUrl

- + -### endpoint.postUrlencoded(query, options) ⇒ Promise.<Response> -Sends the query as POST request with application/x-www-form-urlencoded body +### parsingClient.postUrlencoded(query, options) ⇒ Promise.<Response> +Sends a POST URL-encoded request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). -**Kind**: instance method of [Endpoint](#Endpoint) +**Kind**: instance method of [ParsingClient](#ParsingClient) +**Overrides**: [postUrlencoded](#SimpleClient+postUrlencoded) @@ -209,44 +206,41 @@ Sends the query as POST request with application/x-www-form-urlencoded body - - -
querystring

SPARQL Query/Update

+
querystring

SPARQL query

optionsObject
[options.headers]HeadersInit

per-request HTTP headers

+
[options.headers]Headers

additional request headers

[options.update]booleanfalse

if true, performs a SPARQL Update

+
[options.update]booleanfalse

send the request to the updateUrl

- - -## ParsingClient -A client implementation which parses SPARQL responses into RDF/JS dataset (CONSTRUCT/DESCRIBE) or JSON objects (SELECT) + -It does not provide a store +## ParsingQuery ⇐ [StreamQuery](#StreamQuery) +A query implementation that wraps the results of the [StreamQuery](#StreamQuery) into RDF/JS DatasetCore objects +(CONSTRUCT/DESCRIBE) or an array of objects (SELECT). **Kind**: global class -**Properties** +**Extends**: [StreamQuery](#StreamQuery) - - - - - - - - - - -
NameType
queryParsingQuery
+* [ParsingQuery](#ParsingQuery) ⇐ [StreamQuery](#StreamQuery) + * [.construct(query, options)](#ParsingQuery+construct) ⇒ Promise.<DatasetCore> + * [.select(query, [options])](#ParsingQuery+select) ⇒ Promise.<Array.<Object.<string, Term>>> + * [.ask(query, [options])](#StreamQuery+ask) ⇒ Promise.<boolean> + * [.update(query, [options])](#StreamQuery+update) ⇒ Promise.<void> - + -### new ParsingClient(options) +### parsingQuery.construct(query, options) ⇒ Promise.<DatasetCore> +Sends a request for a CONSTRUCT or DESCRIBE query + +**Kind**: instance method of [ParsingQuery](#ParsingQuery) +**Overrides**: [construct](#StreamQuery+construct) @@ -255,67 +249,51 @@ It does not provide a store - - - - - - - - - - + - - - -
optionsObject
options.endpointUrlstring

SPARQL Query endpoint URL

-
[options.fetch]fetchnodeify-fetch

fetch implementation

+
querystring

CONSTRUCT or DESCRIBE query

[options.headers]HeadersInit

HTTP headers to send with every endpoint request

-
[options.password]string

password used for basic authentication

-
[options.storeUrl]string

Graph Store URL

-
optionsObject
[options.updateUrl]string

SPARQL Update endpoint URL

+
[options.headers]Headers

additional request headers

[options.user]string

user used for basic authentication

-
[options.factory]factory

RDF/JS DataFactory

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

SPARQL Protocol operation

- - -## ParsingQuery -Extends StreamQuery by materialising the SPARQL response streams - -**Kind**: global class - -* [ParsingQuery](#ParsingQuery) - * [new ParsingQuery(init)](#new_ParsingQuery_new) - * [.construct(query, [options])](#ParsingQuery+construct) ⇒ Promise.<Array.<Quad>> - * [.select(query, [options])](#ParsingQuery+select) ⇒ Promise.<Array.<Object.<string, Term>>> + - +### parsingQuery.select(query, [options]) ⇒ Promise.<Array.<Object.<string, Term>>> +Sends a request for a SELECT query -### new ParsingQuery(init) +**Kind**: instance method of [ParsingQuery](#ParsingQuery) +**Overrides**: [select](#StreamQuery+select) - + - + - + + + + +
ParamTypeParamTypeDefaultDescription
initObjectquerystring

SELECT query

+
init.endpointEndpoint[options]Object
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
- + -### parsingQuery.construct(query, [options]) ⇒ Promise.<Array.<Quad>> -Performs a query which returns triples +### parsingQuery.ask(query, [options]) ⇒ Promise.<boolean> +Sends a request for a ASK query **Kind**: instance method of [ParsingQuery](#ParsingQuery) @@ -326,21 +304,23 @@ Performs a query which returns triples - + - - +
querystringquerystring

ASK query

+
[options]Object
[options.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
- + -### parsingQuery.select(query, [options]) ⇒ Promise.<Array.<Object.<string, Term>>> -Performs a SELECT query which returns binding tuples +### parsingQuery.update(query, [options]) ⇒ Promise.<void> +Sends a request for an update query **Kind**: instance method of [ParsingQuery](#ParsingQuery) @@ -351,58 +331,55 @@ Performs a SELECT query which returns binding tuples - + - - +
querystringquerystring

update query

+
[options]Object
[options.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
## RawQuery -A base query class which performs HTTP requests for the different SPARQL query forms +A query implementation that prepares URLs and headers for SPARQL queries and returns the raw fetch response. **Kind**: global class * [RawQuery](#RawQuery) - * [new RawQuery(init)](#new_RawQuery_new) - * [.endpoint](#RawQuery+endpoint) : [Endpoint](#Endpoint) - * [.ask(query, [init])](#RawQuery+ask) ⇒ Promise.<Response> - * [.construct(query, [init])](#RawQuery+construct) ⇒ Promise.<Response> - * [.select(query, [init])](#RawQuery+select) ⇒ Promise.<Response> - * [.update(query, [init])](#RawQuery+update) ⇒ Promise.<Response> + * [new RawQuery(options)](#new_RawQuery_new) + * [.ask(query, [options])](#RawQuery+ask) ⇒ Promise.<Response> + * [.construct(query, [options])](#RawQuery+construct) ⇒ Promise.<Response> + * [.select(query, [options])](#RawQuery+select) ⇒ Promise.<Response> + * [.update(query, [options])](#RawQuery+update) ⇒ Promise.<Response> -### new RawQuery(init) +### new RawQuery(options) - + - + - +
ParamTypeParamTypeDescription
initObjectoptionsObject
init.endpointEndpointoptions.clientSimpleClient

client that provides the HTTP I/O

+
- - -### rawQuery.endpoint : [Endpoint](#Endpoint) -**Kind**: instance property of [RawQuery](#RawQuery) -### rawQuery.ask(query, [init]) ⇒ Promise.<Response> -Performs an ASK query -By default uses HTTP GET with query string +### rawQuery.ask(query, [options]) ⇒ Promise.<Response> +Sends a request for a ASK query **Kind**: instance method of [RawQuery](#RawQuery) @@ -413,23 +390,23 @@ By default uses HTTP GET with query string - - + - - +
querystring

SPARQL ASK query

+
querystring

ASK query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### rawQuery.construct(query, [init]) ⇒ Promise.<Response> -Performs a CONSTRUCT/DESCRIBE query -By default uses HTTP GET with query string +### rawQuery.construct(query, [options]) ⇒ Promise.<Response> +Sends a request for a CONSTRUCT or DESCRIBE query **Kind**: instance method of [RawQuery](#RawQuery) @@ -440,23 +417,23 @@ By default uses HTTP GET with query string - - + - - +
querystring

SPARQL query

+
querystring

CONSTRUCT or DESCRIBE query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### rawQuery.select(query, [init]) ⇒ Promise.<Response> -Performs a SELECT query -By default uses HTTP GET with query string +### rawQuery.select(query, [options]) ⇒ Promise.<Response> +Sends a request for a SELECT query **Kind**: instance method of [RawQuery](#RawQuery) @@ -467,23 +444,23 @@ By default uses HTTP GET with query string - - + - - +
querystring

SPARQL query

+
querystring

SELECT query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### rawQuery.update(query, [init]) ⇒ Promise.<Response> -Performs a SELECT query -By default uses HTTP POST with form-encoded body +### rawQuery.update(query, [options]) ⇒ Promise.<Response> +Sends a request for an update query **Kind**: instance method of [RawQuery](#RawQuery) @@ -494,28 +471,49 @@ By default uses HTTP POST with form-encoded body - - + - - +
querystring

SPARQL query

+
querystring

update query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
## ResultParser -A stream which parses SPARQL SELECT bindings +A Transform stream that parses JSON SPARQL results and emits one object per row with the variable names as keys and +RDF/JS terms as values. **Kind**: global class + + +### new ResultParser(options) + + + + + + + + + + + + +
ParamTypeDescription
optionsObject
options.factoryDataFactory

RDF/JS DataFactory used to create the quads and terms

+
+ ## SimpleClient -A basic client implementation which uses RawQuery and no Store +A client implementation based on [RawQuery](#RawQuery) that prepares URLs and headers for SPARQL queries and returns the +raw fetch response. It does not provide a store interface. **Kind**: global class **Properties** @@ -529,9 +527,34 @@ A basic client implementation which uses RawQuery and no Store queryRawQuery + + endpointUrlstring + + factoryRawQuery + + fetchfactory + + headersHeaders + + passwordstring + + storeUrlstring + + updateUrlstring + + userstring + + updateUrlstring + +* [SimpleClient](#SimpleClient) + * [new SimpleClient(options)](#new_SimpleClient_new) + * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> + * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> + * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> + ### new SimpleClient(options) @@ -545,38 +568,162 @@ A basic client implementation which uses RawQuery and no Store optionsObject - options.endpointUrlstring

SPARQL Query endpoint URL

+ [options.endpointUrl]string

SPARQL query endpoint URL

+ + + [options.factory]factory

RDF/JS factory

[options.fetch]fetchnodeify-fetch

fetch implementation

- [options.headers]HeadersInit

HTTP headers to send with every endpoint request

+ [options.headers]Headers

headers sent with every request

[options.password]string

password used for basic authentication

- [options.storeUrl]string

Graph Store URL

+ [options.storeUrl]string

SPARQL Graph Store URL

- [options.updateUrl]string

SPARQL Update endpoint URL

+ [options.updateUrl]string

SPARQL update endpoint URL

[options.user]string

user used for basic authentication

- [options.factory]factory

RDF/JS DataFactory

+ [options.Query]Query

Constructor of a query implementation

+ + + [options.Store]Store

Constructor of a store implementation

+ + + + +**Example** +```js +// read the height of the Eiffel Tower from Wikidata with a SELECT query + +import SparqlClient from 'sparql-http-client/SimpleClient.js' + +const endpointUrl = 'https://query.wikidata.org/sparql' +const query = ` +PREFIX wd: +PREFIX p: +PREFIX ps: +PREFIX pq: + +SELECT ?value WHERE { + wd:Q243 p:P2048 ?height. + + ?height pq:P518 wd:Q24192182; + ps:P2048 ?value . +}` + +const client = new SparqlClient({ endpointUrl }) +const res = await client.query.select(query) + +if (!res.ok) { +return console.error(res.statusText) +} + +const content = await res.json() + +console.log(JSON.stringify(content, null, 2)) +``` + + +### simpleClient.get(query, options) ⇒ Promise.<Response> +Sends a GET request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). + +**Kind**: instance method of [SimpleClient](#SimpleClient) + + + + + + + + + + + + + + + + +
ParamTypeDefaultDescription
querystring

SPARQL query

+
optionsObject
[options.headers]Headers

additional request headers

+
[options.update]booleanfalse

send the request to the updateUrl

+
+ + + +### simpleClient.postDirect(query, options) ⇒ Promise.<Response> +Sends a POST directly request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). + +**Kind**: instance method of [SimpleClient](#SimpleClient) + + + + + + + + + + + + + + + + +
ParamTypeDefaultDescription
querystring

SPARQL query

+
optionsObject
[options.headers]Headers

additional request headers

+
[options.update]booleanfalse

send the request to the updateUrl

+
+ + + +### simpleClient.postUrlencoded(query, options) ⇒ Promise.<Response> +Sends a POST URL-encoded request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). + +**Kind**: instance method of [SimpleClient](#SimpleClient) + + + + + + + + + + + + + + +
ParamTypeDefaultDescription
querystring

SPARQL query

+
optionsObject
[options.headers]Headers

additional request headers

+
[options.update]booleanfalse

send the request to the updateUrl

-## StreamClient -The default client implementation which returns SPARQL response as RDF/JS streams +## StreamClient ⇐ [SimpleClient](#SimpleClient) +The default client implementation based on [StreamQuery](#StreamQuery) and [StreamStore](#StreamStore) parses SPARQL results into +Readable streams of RDF/JS Quad objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). Graph Store +read and write operations are handled using Readable streams. **Kind**: global class +**Extends**: [SimpleClient](#SimpleClient) **Properties** @@ -593,6 +740,13 @@ The default client implementation which returns SPARQL response as RDF/JS stream
+ +* [StreamClient](#StreamClient) ⇐ [SimpleClient](#SimpleClient) + * [new StreamClient(options)](#new_StreamClient_new) + * [.get(query, options)](#SimpleClient+get) ⇒ Promise.<Response> + * [.postDirect(query, options)](#SimpleClient+postDirect) ⇒ Promise.<Response> + * [.postUrlencoded(query, options)](#SimpleClient+postUrlencoded) ⇒ Promise.<Response> + ### new StreamClient(options) @@ -606,74 +760,192 @@ The default client implementation which returns SPARQL response as RDF/JS stream optionsObject - options.endpointUrlstring

SPARQL Query endpoint URL

+ [options.endpointUrl]string

SPARQL query endpoint URL

+ + + [options.factory]factory

RDF/JS factory

[options.fetch]fetchnodeify-fetch

fetch implementation

- [options.headers]HeadersInit

HTTP headers to send with every endpoint request

+ [options.headers]Headers

headers sent with every request

[options.password]string

password used for basic authentication

- [options.storeUrl]string

Graph Store URL

+ [options.storeUrl]string

SPARQL Graph Store URL

- [options.updateUrl]string

SPARQL Update endpoint URL

+ [options.updateUrl]string

SPARQL update endpoint URL

[options.user]string

user used for basic authentication

+ + + + +**Example** +```js +// read the height of the Eiffel Tower from Wikidata with a SELECT query + +import SparqlClient from 'sparql-http-client' + +const endpointUrl = 'https://query.wikidata.org/sparql' +const query = ` +PREFIX wd: +PREFIX p: +PREFIX ps: +PREFIX pq: + +SELECT ?value WHERE { + wd:Q243 p:P2048 ?height. + + ?height pq:P518 wd:Q24192182; + ps:P2048 ?value . +}` + +const client = new SparqlClient({ endpointUrl }) +const stream = client.query.select(query) + +stream.on('data', row => { + for (const [key, value] of Object.entries(row)) { + console.log(`${key}: ${value.value} (${value.termType})`) + } +}) + +stream.on('error', err => { + console.error(err) +}) +``` +**Example** +```js +// read all quads from a local triplestore using the Graph Store protocol + +import rdf from 'rdf-ext' +import SparqlClient from 'sparql-http-client' + +const client = new SparqlClient({ + storeUrl: 'http://localhost:3030/test/data', + factory: rdf +}) + +const stream = local.store.get(rdf.defaultGraph()) + +stream.on('data', quad => { + console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) +}) +``` + + +### streamClient.get(query, options) ⇒ Promise.<Response> +Sends a GET request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-get). + +**Kind**: instance method of [StreamClient](#StreamClient) +**Overrides**: [get](#SimpleClient+get) + + + + + + + + + + + + + -
ParamTypeDefaultDescription
querystring

SPARQL query

+
optionsObject
[options.headers]Headers

additional request headers

[options.factory]factory

RDF/JS DataFactory

+
[options.update]booleanfalse

send the request to the updateUrl

- + -## StreamQuery -Extends RawQuery by wrapping response body streams as RDF/JS Streams +### streamClient.postDirect(query, options) ⇒ Promise.<Response> +Sends a POST directly request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-direct). -**Kind**: global class +**Kind**: instance method of [StreamClient](#StreamClient) +**Overrides**: [postDirect](#SimpleClient+postDirect) + + + + + + + + + + + + + + + + +
ParamTypeDefaultDescription
querystring

SPARQL query

+
optionsObject
[options.headers]Headers

additional request headers

+
[options.update]booleanfalse

send the request to the updateUrl

+
-* [StreamQuery](#StreamQuery) - * [new StreamQuery(init)](#new_StreamQuery_new) - * [.factory](#StreamQuery+factory) : DataFactory - * [.ask(query, [init])](#StreamQuery+ask) ⇒ Promise.<boolean> - * [.construct(query, [init])](#StreamQuery+construct) ⇒ Promise.<Stream> - * [.select(query, [init])](#StreamQuery+select) ⇒ Promise.<Stream> - * [.update(query, [init])](#StreamQuery+update) ⇒ Promise.<void> + - +### streamClient.postUrlencoded(query, options) ⇒ Promise.<Response> +Sends a POST URL-encoded request as defined in the +[SPARQL Protocol specification](https://www.w3.org/TR/2013/REC-sparql11-protocol-20130321/#query-via-post-urlencoded). -### new StreamQuery(init) +**Kind**: instance method of [StreamClient](#StreamClient) +**Overrides**: [postUrlencoded](#SimpleClient+postUrlencoded) - + - + - + - + + +
ParamTypeDefaultParamTypeDefaultDescription
initObjectquerystring

SPARQL query

+
init.endpointEndpointoptionsObject
[init.factory]DataFactory@rdfjs/data-model[options.headers]Headers

additional request headers

+
[options.update]booleanfalse

send the request to the updateUrl

+
- + + +## StreamQuery ⇐ [RawQuery](#RawQuery) +A query implementation based on [RawQuery](#RawQuery) that parses SPARQL results into Readable streams of RDF/JS Quad +objects (CONSTRUCT/DESCRIBE) or Readable streams of objects (SELECT). + +**Kind**: global class +**Extends**: [RawQuery](#RawQuery) + +* [StreamQuery](#StreamQuery) ⇐ [RawQuery](#RawQuery) + * [.ask(query, [options])](#StreamQuery+ask) ⇒ Promise.<boolean> + * [.construct(query, [options])](#StreamQuery+construct) ⇒ Readable + * [.select(query, [options])](#StreamQuery+select) ⇒ Readable + * [.update(query, [options])](#StreamQuery+update) ⇒ Promise.<void> -### streamQuery.factory : DataFactory -**Kind**: instance property of [StreamQuery](#StreamQuery) -### streamQuery.ask(query, [init]) ⇒ Promise.<boolean> +### streamQuery.ask(query, [options]) ⇒ Promise.<boolean> +Sends a request for a ASK query + **Kind**: instance method of [StreamQuery](#StreamQuery) +**Overrides**: [ask](#RawQuery+ask) @@ -682,22 +954,26 @@ Extends RawQuery by wrapping response body streams as RDF/JS Streams - - + - - +
querystring

SPARQL ASK query

+
querystring

ASK query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### streamQuery.construct(query, [init]) ⇒ Promise.<Stream> +### streamQuery.construct(query, [options]) ⇒ Readable +Sends a request for a CONSTRUCT or DESCRIBE query + **Kind**: instance method of [StreamQuery](#StreamQuery) +**Overrides**: [construct](#RawQuery+construct) @@ -706,22 +982,26 @@ Extends RawQuery by wrapping response body streams as RDF/JS Streams - - + - - +
querystring

SPARQL query

+
querystring

CONSTRUCT or DESCRIBE query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### streamQuery.select(query, [init]) ⇒ Promise.<Stream> +### streamQuery.select(query, [options]) ⇒ Readable +Sends a request for a SELECT query + **Kind**: instance method of [StreamQuery](#StreamQuery) +**Overrides**: [select](#RawQuery+select) @@ -730,22 +1010,26 @@ Extends RawQuery by wrapping response body streams as RDF/JS Streams - - + - - +
querystring

SPARQL query

+
querystring

SELECT query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
-### streamQuery.update(query, [init]) ⇒ Promise.<void> +### streamQuery.update(query, [options]) ⇒ Promise.<void> +Sends a request for an update query + **Kind**: instance method of [StreamQuery](#StreamQuery) +**Overrides**: [update](#RawQuery+update) @@ -754,103 +1038,168 @@ Extends RawQuery by wrapping response body streams as RDF/JS Streams - - + - - +
querystring

SPARQL query

+
querystring

update query

[init]Object[options]Object
[init.headers]HeadersInit

HTTP request headers

+
[options.headers]Headers

additional request headers

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

SPARQL Protocol operation

+
## StreamStore -Accesses stores with SPARQL Graph Protocol +A store implementation that parses and serializes SPARQL Graph Store responses and requests into/from Readable +streams. **Kind**: global class * [StreamStore](#StreamStore) - * [new StreamStore(init, [maxQuadsPerRequest])](#new_StreamStore_new) - * [.get(graph)](#StreamStore+get) ⇒ Promise.<Stream> - * [.post(stream)](#StreamStore+post) ⇒ Promise.<void> - * [.put(stream)](#StreamStore+put) ⇒ Promise.<void> + * [new StreamStore(options)](#new_StreamStore_new) + * [.get([graph])](#StreamStore+get) ⇒ Promise.<Readable> + * [.post(stream, [options])](#StreamStore+post) ⇒ Promise.<void> + * [.put(stream, [options])](#StreamStore+put) ⇒ Promise.<void> + * [.read([options])](#StreamStore+read) ⇒ Readable + * [.write([options], [graph], method, stream)](#StreamStore+write) ⇒ Promise.<void> -### new StreamStore(init, [maxQuadsPerRequest]) +### new StreamStore(options) - + - - - + - - - +
ParamTypeDefaultParamTypeDescription
initObject
init.endpointEndpointoptionsObject
[init.factory]DataFactory@rdfjs/data-model
[maxQuadsPerRequest]numberoptions.clientSimpleClient

client that provides the HTTP I/O

+
-### streamStore.get(graph) ⇒ Promise.<Stream> -Gets a graph triples from the store +### streamStore.get([graph]) ⇒ Promise.<Readable> +Sends a GET request to the Graph Store **Kind**: instance method of [StreamStore](#StreamStore) - + - +
ParamTypeParamTypeDescription
graphNamedNode[graph]NamedNode

source graph

+
-### streamStore.post(stream) ⇒ Promise.<void> -Adds triples to a graph +### streamStore.post(stream, [options]) ⇒ Promise.<void> +Sends a POST request to the Graph Store **Kind**: instance method of [StreamStore](#StreamStore) - + - + + + + +
ParamTypeParamTypeDescription
streamStreamstreamReadable

triples/quads to write

+
[options]Object
[options.graph]Term

target graph

+
-### streamStore.put(stream) ⇒ Promise.<void> -Replaces graph with triples +### streamStore.put(stream, [options]) ⇒ Promise.<void> +Sends a PUT request to the Graph Store **Kind**: instance method of [StreamStore](#StreamStore) - + - + + + + + + +
ParamTypeParamTypeDescription
streamStreamstreamReadable

triples/quads to write

+
[options]Object
[options.graph]Term

target graph

+
+ + + +### streamStore.read([options]) ⇒ Readable +Generic read request to the Graph Store + +**Kind**: instance method of [StreamStore](#StreamStore) + + + + + + + + + + + + + + +
ParamTypeDescription
[options]Object
[options.graph]Term

source graph

+
options.methodstring

HTTP method

+
+ + + +### streamStore.write([options], [graph], method, stream) ⇒ Promise.<void> +Generic write request to the Graph Store + +**Kind**: instance method of [StreamStore](#StreamStore) + + + + + + + + + + + + + + +
ParamTypeDescription
[options]Object
[graph]Term

target graph

+
methodstring

HTTP method

+
streamReadable

triples/quads to write

+
diff --git a/docs/base-client.md b/docs/base-client.md deleted file mode 100644 index c0ad3fb..0000000 --- a/docs/base-client.md +++ /dev/null @@ -1,28 +0,0 @@ -# BaseClient - -It is also possible to build a custom client by manually constructing an `Endpoint` and providing classes which implement the `Query` and `Store` objects. - -Please refer to [JSDoc page](api.md) for details about parameters. - -## A parsing query and stream store - -The example below combines the `query` object of `ParsingClient` with the `StreamStore`'s store implementation. - - - -```javascript -const BaseClient = require('sparql-http-client/BaseClient') -const Endpoint = require('sparql-http-client/Endpoint') -const ParsingQuery = require('sparql-http-client/ParsingQuery') -const StreamStore = require('sparql-http-client/StreamStore') - -const endpointUrl = 'https://query.wikidata.org/sparql' - -new BaseClient({ - endpoint: new Endpoint({ endpointUrl }), - Query: ParsingQuery, - Store: StreamStore -}) -``` - - diff --git a/docs/index.html b/docs/index.html index 12250e5..b17a17f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ sparql-http-client - Simplified SPARQL HTTP request client - + @@ -13,12 +13,11 @@ - diff --git a/docs/parsing-client.md b/docs/parsing-client.md deleted file mode 100644 index b509b7a..0000000 --- a/docs/parsing-client.md +++ /dev/null @@ -1,43 +0,0 @@ -# ParsingClient - -Query results returned by the `ParsingClient` are not streams but actual triples or arrays. - -* `select` returns an array of objects which represent rows of query results. -* `describe` and `construct` return an RDF/JS Dataset -* `ask` returns a `boolean` -* `update` does not return a value - -The `ParsingClient` does not support the Graph Store protocol - -## Query Example - - - -```javascript -const ParsingClient = require('sparql-http-client/ParsingClient') - -const endpointUrl = 'https://query.wikidata.org/sparql' -const query = ` -PREFIX wd: -PREFIX p: -PREFIX ps: -PREFIX pq: - -SELECT ?value WHERE { - wd:Q243 p:P2048 ?height. - - ?height pq:P518 wd:Q24192182; - ps:P2048 ?value . -}` - -const client = new ParsingClient({ endpointUrl }) -const bindings = await client.query.select(query) - -bindings.forEach(row => - Object.entries(row).forEach(([key, value]) => { - console.log(`${key}: ${value.value} (${value.termType})`) - }) -) -``` - - diff --git a/docs/simple-client.md b/docs/simple-client.md deleted file mode 100644 index 8565cc3..0000000 --- a/docs/simple-client.md +++ /dev/null @@ -1,38 +0,0 @@ -# SimpleClient - -For full control of the SPARQL response, the `SimpleClient` can be used. - -All query methods return a fetch `Response` object. - -`SimpleClient` does not support Graph Store protocol. - - - -```javascript -const SimpleClient = require('sparql-http-client/SimpleClient') - -const endpointUrl = 'https://query.wikidata.org/sparql' -const query = ` -PREFIX wd: -PREFIX p: -PREFIX ps: -PREFIX pq: - -SELECT ?value WHERE { - wd:Q243 p:P2048 ?height. - - ?height pq:P518 wd:Q24192182; - ps:P2048 ?value . -}` - -const client = new SimpleClient({ endpointUrl }) -const response = await client.query.select(query, { - headers: { - accept: 'application/sparql-results+xml' - } -}) - -await response.text() -``` - - diff --git a/docs/stream-client.md b/docs/stream-client.md deleted file mode 100644 index 463f012..0000000 --- a/docs/stream-client.md +++ /dev/null @@ -1,70 +0,0 @@ -# StreamClient - -The query methods of `StreamClient` return streams. - -* `select` stream emits each row as a single object with the variable as key and the value as RDF/JS Term object -* `describe` and `construct` streams emit RDF/JS Quads. -* `ask` returns a `boolean` -* `update` does not return a value - -The store methods of `StreamClient` return and consume RDF/JS streams. - -## Query Example -The following example makes a `SELECT` query to the endpoint of wikidata to figure out the height of the Eiffel Tower. - - - -```javascript -const SparqlClient = require('sparql-http-client') - -const endpointUrl = 'https://query.wikidata.org/sparql' -const query = ` -PREFIX wd: -PREFIX p: -PREFIX ps: -PREFIX pq: - -SELECT ?value WHERE { - wd:Q243 p:P2048 ?height. - - ?height pq:P518 wd:Q24192182; - ps:P2048 ?value . -}` - -const client = new SparqlClient({ endpointUrl }) -const stream = await client.query.select(query) - -stream.on('data', row => { - Object.entries(row).forEach(([key, value]) => { - console.log(`${key}: ${value.value} (${value.termType})`) - }) -}) - -stream.on('error', err => { - console.error(err) -}) -``` - - - -## Store example - -The following example reads all quads from the default graph from a triplestore running on `http://localhost:3030/test/data`. -`rdf-ext` is used as a factory. -The `Term` objects of `rdf-ext` have a `toString()` method which is used in the `console.log()`: - -```javascript -const rdf = require('rdf-ext') -const SparqlClient = require('sparql-http-client') - -const local = new SparqlClient({ - storeUrl: 'http://localhost:3030/test/data', - factory: rdf -}) - -const stream = await local.store.get(rdf.defaultGraph()) - -stream.on('data', quad => { - console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) -}) -``` diff --git a/examples/graph-store.js b/examples/graph-store.js index 192cb4f..f50da99 100644 --- a/examples/graph-store.js +++ b/examples/graph-store.js @@ -1,13 +1,13 @@ -const rdf = require('rdf-ext') -const SparqlClient = require('..') +import rdf from 'rdf-ext' +import SparqlClient from '../StreamClient.js' -const local = new SparqlClient({ +const client = new SparqlClient({ storeUrl: 'http://localhost:3030/test/data', factory: rdf }) async function main () { - const stream = await local.store.get(rdf.defaultGraph()) + const stream = client.store.get(rdf.defaultGraph()) stream.on('data', quad => { console.log(`${quad.subject} ${quad.predicate} ${quad.object}`) diff --git a/examples/query-to-graph-store.js b/examples/query-to-graph-store.js index 44d6caa..a091c67 100644 --- a/examples/query-to-graph-store.js +++ b/examples/query-to-graph-store.js @@ -17,13 +17,13 @@ The following steps are required: */ -const namespace = require('@rdfjs/namespace') -const SparqlClient = require('..') +import rdf from 'rdf-ext' +import SparqlClient from '../StreamClient.js' const ns = { - rdf: namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'), - rdfs: namespace('http://www.w3.org/2000/01/rdf-schema#'), - wd: namespace('http://www.wikidata.org/entity/') + rdf: rdf.namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'), + rdfs: rdf.namespace('http://www.w3.org/2000/01/rdf-schema#'), + wd: rdf.namespace('http://www.wikidata.org/entity/') } const dbpedia = new SparqlClient({ endpointUrl: 'https://query.wikidata.org/sparql' }) @@ -37,13 +37,13 @@ const selectQuery = `SELECT ?label WHERE { ?s <${ns.rdfs.label.value}> ?label . async function main () { // read all triples related to Eiffel Tower via describe construct query as a quad stream - const input = await dbpedia.query.construct(describeQuery) + const input = dbpedia.query.construct(describeQuery) // import the quad stream into a local store (remove literals with empty language strings) await local.store.put(input) // run a select query on the local store that will return all labels - const result = await local.query.select(selectQuery) + const result = local.query.select(selectQuery) // write all labels + language to the console result.on('data', row => { diff --git a/examples/select-json.js b/examples/select-json.js new file mode 100644 index 0000000..6ce1d65 --- /dev/null +++ b/examples/select-json.js @@ -0,0 +1,36 @@ +/* + +This example uses the SimpleClient to make a SELECT query and manually processes the response. + +*/ + +import SparqlClient from '../SimpleClient.js' + +const endpointUrl = 'https://query.wikidata.org/sparql' +const query = ` +PREFIX wd: +PREFIX p: +PREFIX ps: +PREFIX pq: + +SELECT ?value WHERE { + wd:Q243 p:P2048 ?height. + + ?height pq:P518 wd:Q24192182; + ps:P2048 ?value . +}` + +async function main () { + const client = new SparqlClient({ endpointUrl }) + const res = await client.query.select(query) + + if (!res.ok) { + return console.error(res.statusText) + } + + const content = await res.json() + + console.log(JSON.stringify(content, null, 2)) +} + +main() diff --git a/examples/select-parsing.js b/examples/select-parsing.js new file mode 100644 index 0000000..5929f03 --- /dev/null +++ b/examples/select-parsing.js @@ -0,0 +1,36 @@ +/* + +This example uses the SimpleClient and upgrades it to a ParsingClient to make a SELECT query and processes the result. + +*/ + +import ParsingClient from '../ParsingClient.js' +import SimpleClient from '../SimpleClient.js' + +const endpointUrl = 'https://query.wikidata.org/sparql' +const query = ` +PREFIX wd: +PREFIX p: +PREFIX ps: +PREFIX pq: + +SELECT ?value WHERE { + wd:Q243 p:P2048 ?height. + + ?height pq:P518 wd:Q24192182; + ps:P2048 ?value . +}` + +async function main () { + const simpleClient = new SimpleClient({ endpointUrl }) + const parsingClient = new ParsingClient(simpleClient) + const result = await parsingClient.query.select(query) + + for (const row of result) { + for (const [key, value] of Object.entries(row)) { + console.log(`${key}: ${value.value} (${value.termType})`) + } + } +} + +main() diff --git a/examples/select-raw-post-urlencoded.js b/examples/select-raw-post-urlencoded.js index 919a7c0..ea18db0 100644 --- a/examples/select-raw-post-urlencoded.js +++ b/examples/select-raw-post-urlencoded.js @@ -4,7 +4,7 @@ This example uses the SimpleClient to make a SELECT query using a URL encoded PO */ -const SparqlClient = require('../SimpleClient') +import SparqlClient from '../SimpleClient.js' const endpointUrl = 'https://query.wikidata.org/sparql' const query = ` @@ -31,9 +31,9 @@ async function main () { const content = await res.json() for (const row of content.results.bindings) { - Object.entries(row).forEach(([key, value]) => { - console.log(`${key}: ${value.value} (${value.type})`) - }) + for (const [key, value] of Object.entries(row)) { + console.log(`${key}: ${value.value}`) + } } } diff --git a/examples/select-raw.js b/examples/select-raw.js index e1a0e00..a33b71a 100644 --- a/examples/select-raw.js +++ b/examples/select-raw.js @@ -4,7 +4,7 @@ This example uses the SimpleClient to make a SELECT query and manually processes */ -const SparqlClient = require('../SimpleClient') +import SparqlClient from '../SimpleClient.js' const endpointUrl = 'https://query.wikidata.org/sparql' const query = ` @@ -31,9 +31,9 @@ async function main () { const content = await res.json() for (const row of content.results.bindings) { - Object.entries(row).forEach(([key, value]) => { - console.log(`${key}: ${value.value} (${value.type})`) - }) + for (const [key, value] of Object.entries(row)) { + console.log(`${key}: ${value.value}`) + } } } diff --git a/examples/select-stream.js b/examples/select-stream.js index 6e6642a..d9a410e 100644 --- a/examples/select-stream.js +++ b/examples/select-stream.js @@ -4,7 +4,7 @@ This example uses the default Client to make a SELECT query and processes the st */ -const SparqlClient = require('..') +import SparqlClient from '../StreamClient.js' const endpointUrl = 'https://query.wikidata.org/sparql' const query = ` @@ -22,12 +22,12 @@ SELECT ?value WHERE { async function main () { const client = new SparqlClient({ endpointUrl }) - const stream = await client.query.select(query) + const stream = client.query.select(query) stream.on('data', row => { - Object.entries(row).forEach(([key, value]) => { + for (const [key, value] of Object.entries(row)) { console.log(`${key}: ${value.value} (${value.termType})`) - }) + } }) stream.on('error', err => { diff --git a/index.js b/index.js index 8f400bc..f0d1de2 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,20 @@ -const StreamClient = require('./StreamClient') +import ParsingClient from './ParsingClient.js' +import ParsingQuery from './ParsingQuery.js' +import RawQuery from './RawQuery.js' +import ResultParser from './ResultParser.js' +import SimpleClient from './SimpleClient.js' +import StreamClient from './StreamClient.js' +import StreamQuery from './StreamQuery.js' +import StreamStore from './StreamStore.js' -module.exports = StreamClient +export { + StreamClient as default, + ParsingClient, + ParsingQuery, + RawQuery, + ResultParser, + SimpleClient, + StreamClient, + StreamQuery, + StreamStore +} diff --git a/lib/QuadStreamSeparator.js b/lib/QuadStreamSeparator.js deleted file mode 100644 index 1f57c17..0000000 --- a/lib/QuadStreamSeparator.js +++ /dev/null @@ -1,34 +0,0 @@ -const factory = require('@rdfjs/data-model') -const { quadToNTriples } = require('@rdfjs/to-ntriples') -const SeparateStream = require('separate-stream') - -class QuadStreamSeparator extends SeparateStream { - constructor ({ change, maxQuadsPerStream = Infinity }) { - super({ - change: async (stream, quad) => { - this.graph = quad.graph - - return change(stream) - }, - map: quad => { - return quadToNTriples(factory.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }, - split: quad => { - this.count++ - - if (this.count >= maxQuadsPerStream) { - this.count = 0 - - return true - } - - return !quad.graph.equals(this.graph) - } - }) - - this.count = 0 - this.graph = null - } -} - -module.exports = QuadStreamSeparator diff --git a/lib/asyncToReadabe.js b/lib/asyncToReadabe.js new file mode 100644 index 0000000..9462859 --- /dev/null +++ b/lib/asyncToReadabe.js @@ -0,0 +1,18 @@ +import toReadable from 'duplex-to/readable.js' +import { PassThrough } from 'readable-stream' + +function asyncToReadabe (func) { + const stream = new PassThrough({ objectMode: true }) + + setTimeout(async () => { + try { + (await func()).pipe(stream) + } catch (err) { + stream.destroy(err) + } + }, 0) + + return toReadable(stream) +} + +export default asyncToReadabe diff --git a/lib/checkResponse.js b/lib/checkResponse.js index 278a843..4bc72e6 100644 --- a/lib/checkResponse.js +++ b/lib/checkResponse.js @@ -10,4 +10,4 @@ async function checkResponse (res) { throw err } -module.exports = checkResponse +export default checkResponse diff --git a/lib/isDataFactory.js b/lib/isDataFactory.js new file mode 100644 index 0000000..c1d19bb --- /dev/null +++ b/lib/isDataFactory.js @@ -0,0 +1,29 @@ +function isDataFactory (factory) { + if (!factory) { + return false + } + + if (typeof factory.blankNode !== 'function') { + return false + } + + if (typeof factory.defaultGraph !== 'function') { + return false + } + + if (typeof factory.literal !== 'function') { + return false + } + + if (typeof factory.namedNode !== 'function') { + return false + } + + if (typeof factory.quad !== 'function') { + return false + } + + return true +} + +export default isDataFactory diff --git a/lib/isDatasetCoreFactory.js b/lib/isDatasetCoreFactory.js new file mode 100644 index 0000000..2fa3db5 --- /dev/null +++ b/lib/isDatasetCoreFactory.js @@ -0,0 +1,13 @@ +function isDatasetCoreFactory (factory) { + if (!factory) { + return false + } + + if (typeof factory.dataset !== 'function') { + return false + } + + return true +} + +export default isDatasetCoreFactory diff --git a/lib/mergeHeaders.js b/lib/mergeHeaders.js new file mode 100644 index 0000000..9e01797 --- /dev/null +++ b/lib/mergeHeaders.js @@ -0,0 +1,19 @@ +function mergeHeaders (...all) { + const merged = new Headers() + + for (const headers of all) { + if (!headers) { + continue + } + + const entries = headers.entries ? headers.entries() : Object.entries(headers) + + for (const [key, value] of entries) { + merged.set(key, value) + } + } + + return merged +} + +export default mergeHeaders diff --git a/package.json b/package.json index 883f045..476be25 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,16 @@ "name": "sparql-http-client", "version": "2.4.2", "description": "Simplified SPARQL HTTP request client", + "type": "module", "main": "index.js", "scripts": { - "test": "standard && nyc --reporter=lcov mocha", - "docs:serve": "docsify serve docs" + "build:docs": "jsdoc2md --no-gfm -f *.js lib/* > docs/api.md", + "prepare": "simple-git-hooks", + "test": "stricter-standard && c8 --reporter=lcov --reporter=text-summary mocha" }, "repository": { "type": "git", - "url": "https://github.com/bergos/sparql-http-client.git" + "url": "https://github.com/rdf-ext/sparql-http-client.git" }, "keywords": [ "sparql", @@ -19,42 +21,36 @@ "author": "Thomas Bergwinkl (https://www.bergnet.org/people/bergi/card#me)", "license": "MIT", "bugs": { - "url": "https://github.com/bergos/sparql-http-client/issues" + "url": "https://github.com/rdf-ext/sparql-http-client/issues" }, - "homepage": "https://bergos.github.io/sparql-http-client/", + "homepage": "https://rdf-ext.github.io/sparql-http-client/", "dependencies": { - "@rdfjs/data-model": "^1.1.2", - "@rdfjs/parser-n3": "^1.1.3", - "@rdfjs/to-ntriples": "^1.0.2", - "get-stream": "^5.1.0", - "jsonstream2": "^3.0.0", - "lodash": "^4.17.15", - "nodeify-fetch": "^2.2.0", - "promise-the-world": "^1.0.1", - "rdf-transform-triple-to-quad": "^1.0.2", - "readable-stream": "^3.5.0", - "separate-stream": "^1.0.0" + "@bergos/jsonparse": "^1.4.1", + "@rdfjs/data-model": "^2.0.2", + "@rdfjs/dataset": "^2.0.2", + "@rdfjs/environment": "^1.0.0", + "@rdfjs/parser-n3": "^2.0.2", + "@rdfjs/to-ntriples": "^3.0.1", + "duplex-to": "^2.0.0", + "nodeify-fetch": "^3.1.0", + "rdf-transform-triple-to-quad": "^2.0.0", + "readable-stream": "^4.5.2", + "stream-chunks": "^1.0.0" }, "devDependencies": { - "@rdfjs/dataset": "^1.0.1", - "@rdfjs/namespace": "^1.1.0", - "body-parser": "^1.19.0", - "docsify-cli": "^4.4.0", - "express-as-promise": "0.0.1", - "get-stream": "^5.1.0", - "husky": "^4.2.5", - "into-stream": "^5.1.1", - "isstream": "^0.1.2", - "jsdoc-to-markdown": "^5.0.3", - "mocha": "^7.0.0", - "nyc": "^15.0.0", - "rdf-dataset-ext": "^1.0.0", - "rdf-ext": "^1.3.0", - "standard": "^14.3.1" + "c8": "^9.1.0", + "express": "^4.18.2", + "express-as-promise": "^1.2.0", + "is-stream": "^3.0.0", + "jsdoc-to-markdown": "^8.0.1", + "lodash": "^4.17.21", + "mocha": "^10.3.0", + "rdf-ext": "^2.5.1", + "rdf-test": "^0.1.0", + "simple-git-hooks": "^2.9.0", + "stricter-standard": "^0.3.0" }, - "husky": { - "hooks": { - "pre-commit": "jsdoc2md --no-gfm -f *.js lib/* > docs/api.md; git add docs/api.md" - } + "simple-git-hooks": { + "pre-commit": "npm run build:docs && git add docs/api.md" } } diff --git a/test/BaseClient.test.js b/test/BaseClient.test.js deleted file mode 100644 index 9c01cac..0000000 --- a/test/BaseClient.test.js +++ /dev/null @@ -1,85 +0,0 @@ -const { strictEqual } = require('assert') -const { describe, it } = require('mocha') -const BaseClient = require('../BaseClient') - -describe('BaseClient', () => { - it('should be a constructor', () => { - strictEqual(typeof BaseClient, 'function') - }) - - it('should forward endpoint to Query constructor', () => { - class Query { - constructor ({ endpoint }) { - this.endpoint = endpoint - } - } - - const client = new BaseClient({ endpoint: 'test', Query }) - - strictEqual(client.query.endpoint, 'test') - }) - - it('should forward factory to Query constructor', () => { - class Query { - constructor ({ factory }) { - this.factory = factory - } - } - - const client = new BaseClient({ factory: 'test', Query }) - - strictEqual(client.query.factory, 'test') - }) - - it('should forward any other options to Query constructor', () => { - class Query { - constructor ({ abc, def }) { - this.abc = abc - this.def = def - } - } - - const client = new BaseClient({ abc: 'test', def: '1234', Query }) - - strictEqual(client.query.abc, 'test') - strictEqual(client.query.def, '1234') - }) - - it('should forward endpoint to Store constructor', () => { - class Store { - constructor ({ endpoint }) { - this.endpoint = endpoint - } - } - - const client = new BaseClient({ endpoint: 'test', Store }) - - strictEqual(client.store.endpoint, 'test') - }) - - it('should forward factory to Store constructor', () => { - class Store { - constructor ({ factory }) { - this.factory = factory - } - } - - const client = new BaseClient({ factory: 'test', Store }) - - strictEqual(client.store.factory, 'test') - }) - - it('should forward any other options to Store constructor', () => { - class Store { - constructor ({ abc, def }) { - this.abc = abc - this.def = def - } - } - - const client = new BaseClient({ abc: 'test', def: '1234', Store }) - - strictEqual(client.store.abc, 'test') - strictEqual(client.store.def, '1234') - }) -}) diff --git a/test/Endpoint.test.js b/test/Endpoint.test.js deleted file mode 100644 index dc81c36..0000000 --- a/test/Endpoint.test.js +++ /dev/null @@ -1,528 +0,0 @@ -const { strictEqual } = require('assert') -const fetch = require('nodeify-fetch') -const { describe, it } = require('mocha') -const { text, urlencoded } = require('body-parser') -const Endpoint = require('../Endpoint') -const withServer = require('./support/withServer') - -const simpleSelectQuery = 'SELECT * WHERE {?s ?p ?o}' -const simpleConstructQuery = 'CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}' - -describe('Endpoint', () => { - it('should be a constructor', () => { - strictEqual(typeof Endpoint, 'function') - }) - - it('should set authorization header if user and password are given', () => { - const client = new Endpoint({ fetch, user: 'abc', password: 'def' }) - - strictEqual(client.headers.get('authorization'), 'Basic YWJjOmRlZg==') - }) - - describe('.get', () => { - it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - - strictEqual(typeof endpoint.get, 'function') - }) - - it('should async return a response object', async () => { - await withServer(async server => { - server.app.get('/', async (req, res) => { - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - const res = await endpoint.get(simpleSelectQuery) - - strictEqual(typeof res, 'object') - strictEqual(typeof res.text, 'function') - }) - }) - - it('should send a GET request to the endpointUrl', async () => { - await withServer(async server => { - let called = false - - server.app.get('/', async (req, res) => { - called = true - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.get(simpleSelectQuery) - - strictEqual(called, true) - }) - }) - - it('should send the query string as query parameter', async () => { - await withServer(async server => { - let parameter = null - - server.app.get('/', async (req, res) => { - parameter = req.query.query - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.get(simpleSelectQuery) - - strictEqual(parameter, simpleSelectQuery) - }) - }) - - it('should keep existing query params', async () => { - await withServer(async server => { - let parameters = null - const key = 'auth_token' - const value = '12345' - - server.app.get('/', async (req, res) => { - parameters = req.query - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl: `${endpointUrl}?${key}=${value}`, fetch }) - - await endpoint.get(simpleSelectQuery) - - strictEqual(parameters[key], value) - }) - }) - - it('should merge the headers given in the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.get('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.get(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should prioritize the headers from the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.get('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ - endpointUrl, - fetch, - headers: { - authorization: 'Bearer bar' - } - }) - - await endpoint.get(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should use the updateUrl and update param if update is true', async () => { - await withServer(async server => { - let parameters = null - - server.app.get('/', async (req, res) => { - parameters = req.query - - res.end() - }) - - const updateUrl = await server.listen() - - const endpoint = new Endpoint({ updateUrl, fetch }) - - await endpoint.get(simpleSelectQuery, { update: true }) - - strictEqual(parameters.update, simpleSelectQuery) - }) - }) - - it('should keep existing update query params', async () => { - await withServer(async server => { - let parameters = null - const key = 'auth_token' - const value = '12345' - - server.app.get('/', async (req, res) => { - parameters = req.query - - res.end() - }) - - const updateUrl = await server.listen() - - const endpoint = new Endpoint({ updateUrl: `${updateUrl}?${key}=${value}`, fetch }) - - await endpoint.get(simpleSelectQuery, { update: true }) - - strictEqual(parameters[key], value) - }) - }) - }) - - describe('.postDirect', () => { - it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - - strictEqual(typeof endpoint.postDirect, 'function') - }) - - it('should async return a response object', async () => { - await withServer(async server => { - server.app.post('/', async (req, res) => { - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - const res = await endpoint.postDirect(simpleSelectQuery) - - strictEqual(typeof res, 'object') - strictEqual(typeof res.text, 'function') - }) - }) - - it('should send a POST request to the endpointUrl', async () => { - await withServer(async server => { - let called = false - - server.app.post('/', async (req, res) => { - called = true - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postDirect(simpleSelectQuery) - - strictEqual(called, true) - }) - }) - - it('should send a content type header with the value application/sparql-query & charset utf-8', async () => { - await withServer(async server => { - let contentType = null - - server.app.post('/', async (req, res) => { - contentType = req.headers['content-type'] - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postDirect(simpleSelectQuery) - - strictEqual(contentType, 'application/sparql-query; charset=utf-8') - }) - }) - - it('should send the query string in the request body', async () => { - await withServer(async server => { - let content = null - - server.app.post('/', text({ type: '*/*' }), async (req, res) => { - content = req.body - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postDirect(simpleSelectQuery) - - strictEqual(content, simpleSelectQuery) - }) - }) - - it('should merge the headers given in the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.post('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postDirect(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should prioritize the headers from the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.post('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ - endpointUrl, - fetch, - headers: { - authorization: 'Bearer bar' - } - }) - - await endpoint.postDirect(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should use the updateUrl if update is true', async () => { - await withServer(async server => { - let called = false - - server.app.post('/', async (req, res) => { - called = true - - res.end() - }) - - const updateUrl = await server.listen() - - const endpoint = new Endpoint({ updateUrl, fetch }) - - await endpoint.postDirect(simpleSelectQuery, { update: true }) - - strictEqual(called, true) - }) - }) - }) - - describe('.postUrlencoded', () => { - it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - - strictEqual(typeof endpoint.postUrlencoded, 'function') - }) - - it('should async return a response object', async () => { - await withServer(async server => { - server.app.post('/', async (req, res) => { - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - const res = await endpoint.postUrlencoded(simpleSelectQuery) - - strictEqual(typeof res, 'object') - strictEqual(typeof res.text, 'function') - }) - }) - - it('should send a POST request to the endpointUrl', async () => { - await withServer(async server => { - let called = false - - server.app.post('/', async (req, res) => { - called = true - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postUrlencoded(simpleSelectQuery) - - strictEqual(called, true) - }) - }) - - it('should send a content type header with the value application/x-www-form-urlencoded', async () => { - await withServer(async server => { - let contentType = null - - server.app.post('/', async (req, res) => { - contentType = req.headers['content-type'] - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postUrlencoded(simpleSelectQuery) - - strictEqual(contentType, 'application/x-www-form-urlencoded') - }) - }) - - it('should send the query string urlencoded in the request body', async () => { - await withServer(async server => { - let parameter = null - - server.app.post('/', urlencoded({ extended: true }), async (req, res) => { - parameter = req.body.query - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postUrlencoded(simpleSelectQuery) - - strictEqual(parameter, simpleSelectQuery) - }) - }) - - it('should merge the headers given in the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.post('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ endpointUrl, fetch }) - - await endpoint.postUrlencoded(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should prioritize the headers from the method call', async () => { - await withServer(async server => { - let header = null - const value = 'Bearer foo' - - server.app.post('/', async (req, res) => { - header = req.headers.authorization - - res.end() - }) - - const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ - endpointUrl, - fetch, - headers: { - authorization: 'Bearer bar' - } - }) - - await endpoint.postUrlencoded(simpleConstructQuery, { - headers: { - authorization: value - } - }) - - strictEqual(header, value) - }) - }) - - it('should use the updateUrl if update is true', async () => { - await withServer(async server => { - let called = false - - server.app.post('/', async (req, res) => { - called = true - - res.end() - }) - - const updateUrl = await server.listen() - - const endpoint = new Endpoint({ updateUrl, fetch }) - - await endpoint.postUrlencoded(simpleSelectQuery, { update: true }) - - strictEqual(called, true) - }) - }) - }) -}) diff --git a/test/ParsingClient.test.js b/test/ParsingClient.test.js new file mode 100644 index 0000000..2ede0a9 --- /dev/null +++ b/test/ParsingClient.test.js @@ -0,0 +1,56 @@ +import { deepStrictEqual, strictEqual, throws } from 'node:assert' +import DataModelFactory from '@rdfjs/data-model/Factory.js' +import Environment from '@rdfjs/environment' +import omit from 'lodash/omit.js' +import pick from 'lodash/pick.js' +import { describe, it } from 'mocha' +import ParsingClient from '../ParsingClient.js' +import ParsingQuery from '../ParsingQuery.js' +import SimpleClient from '../SimpleClient.js' + +describe('ParsingClient', () => { + it('should be a constructor', () => { + strictEqual(typeof ParsingClient, 'function') + }) + + it('should throw an error if the given factory does not implement the DatasetCoreFactory interface', () => { + throws(() => { + new ParsingClient({ // eslint-disable-line no-new + endpointUrl: 'test', + factory: new Environment([DataModelFactory]) + }) + }, { + message: /DatasetCoreFactory/ + }) + }) + + it('should use StreamQuery to create the query instance', () => { + const client = new ParsingClient({ endpointUrl: 'test' }) + + strictEqual(client.query instanceof ParsingQuery, true) + }) + + it('should forward the client to the query instance', () => { + const client = new ParsingClient({ endpointUrl: 'test' }) + + strictEqual(client.query.client, client) + }) + + it('should be possible to create an instance from a SimpleClient', () => { + const options = { + endpointUrl: 'sparql', + headers: new Headers({ 'user-agent': 'sparql-http-client' }), + password: 'pwd', + storeUrl: 'graph', + updateUrl: 'update', + user: 'usr' + } + + const simpleClient = new SimpleClient(options) + const client = new ParsingClient(simpleClient) + const result = pick(client, Object.keys(options)) + + deepStrictEqual(omit(result, 'headers'), omit(options, 'headers')) + deepStrictEqual([...result.headers.entries()], [...simpleClient.headers.entries()]) + }) +}) diff --git a/test/ParsingQuery.test.js b/test/ParsingQuery.test.js new file mode 100644 index 0000000..fa888da --- /dev/null +++ b/test/ParsingQuery.test.js @@ -0,0 +1,379 @@ +import { deepStrictEqual, rejects, strictEqual } from 'node:assert' +import DataModelFactory from '@rdfjs/data-model/Factory.js' +import DatasetFactory from '@rdfjs/dataset/Factory.js' +import Environment from '@rdfjs/environment' +import express from 'express' +import withServer from 'express-as-promise/withServer.js' +import { describe, it } from 'mocha' +import rdf from 'rdf-ext' +import { datasetEqual } from 'rdf-test/assert.js' +import isDataset from 'rdf-test/isDataset.js' +import ParsingQuery from '../ParsingQuery.js' +import SimpleClient from '../SimpleClient.js' +import { message, quads, constructQuery, selectQuery } from './support/examples.js' +import isServerError from './support/isServerError.js' +import isSocketError from './support/isSocketError.js' +import * as ns from './support/namespaces.js' +import testFactory from './support/testFactory.js' + +const factory = new Environment([DataModelFactory, DatasetFactory]) + +describe('ParsingQuery', () => { + describe('.construct', () => { + it('should be a method', () => { + const query = new ParsingQuery({}) + + strictEqual(typeof query.construct, 'function') + }) + + it('should return a DatasetCore object', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + const result = await query.construct(constructQuery) + + strictEqual(isDataset(result), true) + }) + }) + + it('should parse the N-Triples', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.end(quads.toString()) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + const result = await query.construct(constructQuery) + + datasetEqual(result, quads) + }) + }) + + it('should send a GET request', async () => { + await withServer(async server => { + let called = false + + server.app.get('/', async (req, res) => { + called = true + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.construct(constructQuery) + + strictEqual(called, true) + }) + }) + + it('should send the query string as query parameter', async () => { + await withServer(async server => { + let parameter = null + + server.app.get('/', async (req, res) => { + parameter = req.query.query + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.construct(constructQuery) + + strictEqual(parameter, constructQuery) + }) + }) + + it('should use the given factory', async () => { + await withServer(async server => { + const quads = rdf.dataset([ + rdf.quad(rdf.blankNode(), ns.ex.predicate, rdf.literal('test')) + ]) + const factory = testFactory() + + server.app.get('/', async (req, res) => { + res.end(quads.toString()) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.construct(constructQuery) + + deepStrictEqual(factory.used, { + blankNode: true, + dataset: true, + defaultGraph: true, + literal: true, + namedNode: true, + quad: true + }) + }) + }) + + it('should use the given operation for the request', async () => { + await withServer(async server => { + let parameter = null + + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { + parameter = req.body.query + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.construct(constructQuery, { operation: 'postUrlencoded' }) + + strictEqual(parameter, constructQuery) + }) + }) + + it('should send an accept header with the value application/n-triples, text/turtle', async () => { + await withServer(async server => { + let accept = null + + server.app.get('/', async (req, res) => { + accept = req.headers.accept + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.construct(constructQuery) + + strictEqual(accept, 'application/n-triples, text/turtle') + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await rejects(async () => { + await query.construct(constructQuery) + }, err => isSocketError(err)) + }) + }) + + it('should handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await rejects(async () => { + await query.construct(constructQuery) + }, err => isServerError(err, message)) + }) + }) + }) + + describe('.select', () => { + it('should be a method', () => { + const query = new ParsingQuery({}) + + strictEqual(typeof query.select, 'function') + }) + + it('should return an array', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + const result = await query.select(selectQuery) + + strictEqual(Array.isArray(result), true) + }) + }) + + it('should parse the SPARQL JSON result', async () => { + await withServer(async server => { + const content = { + results: { + bindings: [{ + a: { type: 'uri', value: 'http://example.org/0' } + }, { + a: { type: 'uri', value: 'http://example.org/1' } + }] + } + } + + server.app.get('/', async (req, res) => { + res.end(JSON.stringify(content)) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + const result = await query.select(selectQuery) + + strictEqual(result[0].a.termType, 'NamedNode') + strictEqual(result[0].a.value, content.results.bindings[0].a.value) + strictEqual(result[1].a.termType, 'NamedNode') + strictEqual(result[1].a.value, content.results.bindings[1].a.value) + }) + }) + + it('should send a GET request', async () => { + await withServer(async server => { + let called = false + + server.app.get('/', async (req, res) => { + called = true + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + await query.select(selectQuery) + + strictEqual(called, true) + }) + }) + + it('should send the query string as query parameter', async () => { + await withServer(async server => { + let parameter = null + + server.app.get('/', async (req, res) => { + parameter = req.query.query + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + await query.select(selectQuery) + + strictEqual(parameter, selectQuery) + }) + }) + + it('should use the given factory', async () => { + await withServer(async server => { + const content = { + results: { + bindings: [{ + a: { type: 'bnode', value: 'b0' } + }, { + a: { type: 'literal', value: '0' } + }, { + a: { type: 'uri', value: 'http://example.org/0' } + }] + } + } + const factory = testFactory() + + server.app.get('/', async (req, res) => { + res.end(JSON.stringify(content)) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl, factory }) + const query = new ParsingQuery({ client }) + + await query.select(selectQuery) + + deepStrictEqual(factory.used, { + blankNode: true, + literal: true, + namedNode: true + }) + }) + }) + + it('should use the given operation for the request', async () => { + await withServer(async server => { + let parameter = null + + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { + parameter = req.body.query + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + await query.select(selectQuery, { operation: 'postUrlencoded' }) + + strictEqual(parameter, selectQuery) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + await rejects(async () => { + await query.select(selectQuery) + }, err => isSocketError(err)) + }) + }) + + it('should handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new ParsingQuery({ client }) + + await rejects(async () => { + await query.select(selectQuery) + }, err => isServerError(err, message)) + }) + }) + }) +}) diff --git a/test/QuadStreamSeparator.test.js b/test/QuadStreamSeparator.test.js deleted file mode 100644 index 69098d4..0000000 --- a/test/QuadStreamSeparator.test.js +++ /dev/null @@ -1,187 +0,0 @@ -const { deepStrictEqual, strictEqual } = require('assert') -const { promisify } = require('util') -const getStream = require('get-stream') -const { isReadable, isWritable } = require('isstream') -const { describe, it } = require('mocha') -const rdf = require('@rdfjs/data-model') -const namespace = require('@rdfjs/namespace') -const { quadToNTriples } = require('@rdfjs/to-ntriples') -const { finished } = require('readable-stream') -const QuadStreamSeparator = require('../lib/QuadStreamSeparator') - -const untilFinished = promisify(finished) - -const ns = { - ex: namespace('http://example.org/') -} - -describe('QuadStreamSeparator', () => { - it('should be a constructor', () => { - strictEqual(typeof QuadStreamSeparator, 'function') - }) - - it('should have a Writable interface', () => { - const stream = new QuadStreamSeparator({ change: () => {} }) - - strictEqual(isWritable(stream), true) - }) - - it('should call change after the first stream was created', async () => { - let called = false - - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - const stream = new QuadStreamSeparator({ - change: () => { - called = true - } - }) - - stream.write(quad) - stream.end() - - await untilFinished(stream) - - strictEqual(called, true) - }) - - it('should call change with the output stream as argument', async () => { - let output = null - - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - const stream = new QuadStreamSeparator({ - change: stream => { - output = stream - } - }) - - stream.write(quad) - stream.end() - - await untilFinished(stream) - - strictEqual(isReadable(output), true) - }) - - it('should forward all chunks to the first stream if they are all in the same graph', async () => { - const actual = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, ns.ex.graph1), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, ns.ex.graph1), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph1), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph1) - ] - const expected = [ - quads - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join('') - ] - - const stream = new QuadStreamSeparator({ - change: stream => { - getStream(stream).then(result => { - actual.push(result) - }) - } - }) - - quads.forEach(quad => { - stream.write(quad) - }) - stream.end() - - await untilFinished(stream) - - deepStrictEqual(actual, expected) - }) - - it('should create a new stream on graph change', async () => { - const actual = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph2), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph2) - ] - - const expected = [ - quads - .slice(0, 2) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join(''), - quads - .slice(2, 4) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join(''), - quads - .slice(4, 6) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join('') - ] - - const stream = new QuadStreamSeparator({ - change: stream => { - getStream(stream).then(result => { - actual.push(result) - }) - } - }) - - quads.forEach(quad => { - stream.write(quad) - }) - stream.end() - - await untilFinished(stream) - - deepStrictEqual(actual, expected) - }) - - it('should create a new stream when maxQuadsPerStream is reached', async () => { - const actual = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, ns.ex.graph1), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, ns.ex.graph1), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph1), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph1) - ] - - const expected = [ - quads - .slice(0, 2) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join(''), - quads - .slice(2, 4) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join(''), - quads - .slice(4, 6) - .map(quad => quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n') - .join('') - ] - - const stream = new QuadStreamSeparator({ - change: stream => { - getStream(stream).then(result => { - actual.push(result) - }) - }, - maxQuadsPerStream: 2 - }) - - quads.forEach(quad => { - stream.write(quad) - }) - stream.end() - - await untilFinished(stream) - - deepStrictEqual(actual, expected) - }) -}) diff --git a/test/RawQuery.test.js b/test/RawQuery.test.js index def9058..e6e6804 100644 --- a/test/RawQuery.test.js +++ b/test/RawQuery.test.js @@ -1,15 +1,11 @@ -const { strictEqual } = require('assert') -const { text, urlencoded } = require('body-parser') -const fetch = require('nodeify-fetch') -const { describe, it } = require('mocha') -const Endpoint = require('../Endpoint') -const RawQuery = require('../RawQuery') -const withServer = require('./support/withServer') - -const simpleAskQuery = 'ASK {}' -const simpleConstructQuery = 'CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}' -const simpleSelectQuery = 'SELECT * WHERE {?s ?p ?o}' -const simpleUpdateQuery = 'INSERT { "object"} WHERE {}' +import { rejects, strictEqual } from 'node:assert' +import express from 'express' +import withServer from 'express-as-promise/withServer.js' +import { describe, it } from 'mocha' +import RawQuery from '../RawQuery.js' +import SimpleClient from '../SimpleClient.js' +import { message, askQuery, constructQuery, selectQuery, updateQuery } from './support/examples.js' +import isSocketError from './support/isSocketError.js' describe('RawQuery', () => { it('should be a constructor', () => { @@ -18,31 +14,29 @@ describe('RawQuery', () => { describe('.ask', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({}) strictEqual(typeof query.ask, 'function') }) - it('should async return a response object', async () => { + it('should return a response object', async () => { await withServer(async server => { server.app.get('/', async (req, res) => { res.end() }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - const res = await query.ask(simpleAskQuery) + const res = await query.ask(askQuery) strictEqual(typeof res, 'object') strictEqual(typeof res.text, 'function') }) }) - it('should send a GET request to the endpointUrl', async () => { + it('should send a GET request', async () => { await withServer(async server => { let called = false @@ -53,11 +47,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.ask(simpleAskQuery) + await query.ask(askQuery) strictEqual(called, true) }) @@ -74,13 +67,12 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + await query.ask(askQuery) - await query.ask(simpleSelectQuery) - - strictEqual(parameter, simpleSelectQuery) + strictEqual(parameter, askQuery) }) }) @@ -97,11 +89,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl: `${endpointUrl}?${key}=${value}`, fetch }) - const query = new RawQuery({ endpoint }) - - await query.ask(simpleAskQuery) + await query.ask(askQuery) strictEqual(parameters[key], value) }) @@ -118,11 +109,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.ask(simpleAskQuery) + await query.ask(askQuery) strictEqual(accept, 'application/sparql-results+json') }) @@ -140,11 +130,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.ask(simpleAskQuery, { + await query.ask(askQuery, { headers: { authorization: value } @@ -166,17 +155,15 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ + const client = new SimpleClient({ endpointUrl, - fetch, headers: { authorization: 'Bearer bar' } }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({ client }) - await query.ask(simpleAskQuery, { + await query.ask(askQuery, { headers: { authorization: value } @@ -190,51 +177,78 @@ describe('RawQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: true }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { parameter = req.body.query res.end() }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + await query.ask(askQuery, { operation: 'postUrlencoded' }) - await query.ask(simpleAskQuery, { operation: 'postUrlencoded' }) + strictEqual(parameter, askQuery) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) - strictEqual(parameter, simpleAskQuery) + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await rejects(async () => { + await query.ask(askQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await query.ask(askQuery) }) }) }) describe('.construct', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({}) strictEqual(typeof query.construct, 'function') }) - it('should async return a response object', async () => { + it('should return a response object', async () => { await withServer(async server => { server.app.get('/', async (req, res) => { res.end() }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - const res = await query.construct(simpleConstructQuery) + const res = await query.construct(constructQuery) strictEqual(typeof res, 'object') strictEqual(typeof res.text, 'function') }) }) - it('should send a GET request to the endpointUrl', async () => { + it('should send a GET request', async () => { await withServer(async server => { let called = false @@ -245,11 +259,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.construct(simpleConstructQuery) + await query.construct(constructQuery) strictEqual(called, true) }) @@ -266,13 +279,12 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.construct(simpleConstructQuery) + await query.construct(constructQuery) - strictEqual(parameter, simpleConstructQuery) + strictEqual(parameter, constructQuery) }) }) @@ -289,11 +301,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl: `${endpointUrl}?${key}=${value}`, fetch }) - const query = new RawQuery({ endpoint }) - - await query.construct(simpleConstructQuery) + await query.construct(constructQuery) strictEqual(parameters[key], value) }) @@ -310,11 +321,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.construct(simpleConstructQuery) + await query.construct(constructQuery) strictEqual(accept, 'application/n-triples') }) @@ -332,11 +342,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.construct(simpleConstructQuery, { + await query.construct(constructQuery, { headers: { authorization: value } @@ -358,17 +367,15 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ + const client = new SimpleClient({ endpointUrl, - fetch, headers: { authorization: 'Bearer bar' } }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({ client }) - await query.construct(simpleConstructQuery, { + await query.construct(constructQuery, { headers: { authorization: value } @@ -382,51 +389,78 @@ describe('RawQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: true }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { parameter = req.body.query res.end() }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + await query.construct(constructQuery, { operation: 'postUrlencoded' }) - await query.construct(simpleConstructQuery, { operation: 'postUrlencoded' }) + strictEqual(parameter, constructQuery) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) - strictEqual(parameter, simpleConstructQuery) + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await rejects(async () => { + await query.construct(constructQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await query.construct(constructQuery) }) }) }) describe('.select', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({}) strictEqual(typeof query.select, 'function') }) - it('should async return a response object', async () => { + it('should return a response object', async () => { await withServer(async server => { server.app.get('/', async (req, res) => { res.end() }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - const res = await query.select(simpleSelectQuery) + const res = await query.select(selectQuery) strictEqual(typeof res, 'object') strictEqual(typeof res.text, 'function') }) }) - it('should send a GET request to the endpointUrl', async () => { + it('should send a GET request', async () => { await withServer(async server => { let called = false @@ -437,11 +471,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.select(simpleSelectQuery) + await query.select(selectQuery) strictEqual(called, true) }) @@ -458,13 +491,12 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + await query.select(selectQuery) - await query.select(simpleSelectQuery) - - strictEqual(parameter, simpleSelectQuery) + strictEqual(parameter, selectQuery) }) }) @@ -481,11 +513,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl: `${endpointUrl}?${key}=${value}`, fetch }) - const query = new RawQuery({ endpoint }) - - await query.select(simpleSelectQuery) + await query.select(selectQuery) strictEqual(parameters[key], value) }) @@ -502,11 +533,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.select(simpleSelectQuery) + await query.select(selectQuery) strictEqual(accept, 'application/sparql-results+json') }) @@ -524,11 +554,10 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.select(simpleSelectQuery, { + await query.select(selectQuery, { headers: { authorization: value } @@ -550,17 +579,15 @@ describe('RawQuery', () => { }) const endpointUrl = await server.listen() - - const endpoint = new Endpoint({ + const client = new SimpleClient({ endpointUrl, - fetch, headers: { authorization: 'Bearer bar' } }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({ client }) - await query.select(simpleSelectQuery, { + await query.select(selectQuery, { headers: { authorization: value } @@ -574,51 +601,77 @@ describe('RawQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: true }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { parameter = req.body.query res.end() }) + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await query.select(selectQuery, { operation: 'postUrlencoded' }) + + strictEqual(parameter, selectQuery) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) + + await rejects(async () => { + await query.select(selectQuery) + }, err => isSocketError(err)) + }) + }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) - await query.select(simpleSelectQuery, { operation: 'postUrlencoded' }) + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new RawQuery({ client }) - strictEqual(parameter, simpleSelectQuery) + await query.select(selectQuery) }) }) }) describe('.update', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({}) strictEqual(typeof query.update, 'function') }) - it('should async return a response object', async () => { + it('should return a response object', async () => { await withServer(async server => { - server.app.get('/', async (req, res) => { + server.app.post('/', async (req, res) => { res.end() }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - const res = await query.update(simpleUpdateQuery) + const res = await query.update(updateQuery) strictEqual(typeof res, 'object') strictEqual(typeof res.text, 'function') }) }) - it('should send a POST request to the updateUrl', async () => { + it('should send a POST request', async () => { await withServer(async server => { let called = false @@ -629,11 +682,10 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery) + await query.update(updateQuery) strictEqual(called, true) }) @@ -652,11 +704,10 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl: `${updateUrl}?${key}=${value}` }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl: `${updateUrl}?${key}=${value}`, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery) + await query.update(updateQuery) strictEqual(parameters[key], value) }) @@ -673,11 +724,10 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery) + await query.update(updateQuery) strictEqual(accept, '*/*') }) @@ -694,11 +744,10 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery) + await query.update(updateQuery) strictEqual(contentType, 'application/x-www-form-urlencoded') }) @@ -708,20 +757,19 @@ describe('RawQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: true }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { parameter = req.body.update res.end() }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery) + await query.update(updateQuery) - strictEqual(parameter, simpleUpdateQuery) + strictEqual(parameter, updateQuery) }) }) @@ -737,11 +785,10 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - const endpoint = new Endpoint({ updateUrl, fetch }) - const query = new RawQuery({ endpoint }) - - await query.update(simpleUpdateQuery, { + await query.update(updateQuery, { headers: { authorization: value } @@ -763,17 +810,15 @@ describe('RawQuery', () => { }) const updateUrl = await server.listen() - - const endpoint = new Endpoint({ + const client = new SimpleClient({ updateUrl, - fetch, headers: { authorization: 'Bearer bar' } }) - const query = new RawQuery({ endpoint }) + const query = new RawQuery({ client }) - await query.update(simpleUpdateQuery, { + await query.update(updateQuery, { headers: { authorization: value } @@ -787,20 +832,49 @@ describe('RawQuery', () => { await withServer(async server => { let content = null - server.app.post('/', text({ type: '*/*' }), async (req, res) => { + server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { content = req.body res.end() }) - const endpointUrl = await server.listen() + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) + + await query.update(updateQuery, { operation: 'postDirect' }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new RawQuery({ endpoint }) + strictEqual(content, updateQuery) + }) + }) - await query.select(simpleUpdateQuery, { operation: 'postDirect' }) + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + req.client.destroy() + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) + + await rejects(async () => { + await query.update(updateQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.status(500).end(message) + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new RawQuery({ client }) - strictEqual(content, simpleUpdateQuery) + await query.update(updateQuery) }) }) }) diff --git a/test/ResultParser.test.js b/test/ResultParser.test.js index 7875531..178a154 100644 --- a/test/ResultParser.test.js +++ b/test/ResultParser.test.js @@ -1,8 +1,9 @@ -const { deepStrictEqual, strictEqual, notStrictEqual } = require('assert') -const getStream = require('get-stream') -const { describe, it } = require('mocha') -const testFactory = require('./support/testFactory') -const ResultParser = require('../ResultParser') +import { deepStrictEqual, rejects, strictEqual } from 'node:assert' +import factory from '@rdfjs/data-model' +import { describe, it } from 'mocha' +import chunks from 'stream-chunks/chunks.js' +import ResultParser from '../ResultParser.js' +import testFactory from './support/testFactory.js' describe('ResultParser', () => { it('should be a constructor', () => { @@ -10,32 +11,29 @@ describe('ResultParser', () => { }) it('should throw an error if the content is not JSON', async () => { - let error = null - const parser = new ResultParser() + const parser = new ResultParser({ factory }) parser.end('this is not json') - try { - await getStream.array(parser) - } catch (err) { - error = err - } - - notStrictEqual(error, null) + await rejects(async () => { + await chunks(parser) + }, { + message: /Unexpected/ + }) }) it('should not emit any chunk if the json doesn\'t contain results.bindings', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) parser.end('{}') - const result = await getStream.array(parser) + const result = await chunks(parser) deepStrictEqual(result, []) }) it('should not emit any chunk when Stardog GROUP BY bug shows up', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{}] @@ -44,13 +42,13 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) deepStrictEqual(result, []) }) it('should parse named node values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -63,7 +61,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'NamedNode') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -72,7 +70,7 @@ describe('ResultParser', () => { }) it('should parse blank node values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -85,7 +83,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'BlankNode') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -94,7 +92,7 @@ describe('ResultParser', () => { }) it('should parse literal values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -107,7 +105,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'Literal') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -116,7 +114,7 @@ describe('ResultParser', () => { }) it('should parse typed literal values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -129,7 +127,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'Literal') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -140,7 +138,7 @@ describe('ResultParser', () => { }) it('should parse Virtuoso style typed literal values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -153,7 +151,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'Literal') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -164,7 +162,7 @@ describe('ResultParser', () => { }) it('should parse language literal values', async () => { - const parser = new ResultParser() + const parser = new ResultParser({ factory }) const content = { results: { bindings: [{ @@ -177,7 +175,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - const result = await getStream.array(parser) + const result = await chunks(parser) strictEqual(result[0].a.termType, 'Literal') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -187,6 +185,34 @@ describe('ResultParser', () => { strictEqual(result[1].a.language, content.results.bindings[1].a['xml:lang']) }) + it('should parse multiple variables', async () => { + const parser = new ResultParser({ factory }) + const content = { + results: { + bindings: [{ + a: { type: 'uri', value: 'http://example.org/0' }, + b: { type: 'uri', value: 'http://example.org/1' } + }, { + a: { type: 'uri', value: 'http://example.org/2' }, + b: { type: 'uri', value: 'http://example.org/3' } + }] + } + } + + parser.end(JSON.stringify(content)) + + const result = await chunks(parser) + + strictEqual(result[0].a.termType, 'NamedNode') + strictEqual(result[0].a.value, content.results.bindings[0].a.value) + strictEqual(result[0].b.termType, 'NamedNode') + strictEqual(result[0].b.value, content.results.bindings[0].b.value) + strictEqual(result[1].a.termType, 'NamedNode') + strictEqual(result[1].a.value, content.results.bindings[1].a.value) + strictEqual(result[1].b.termType, 'NamedNode') + strictEqual(result[1].b.value, content.results.bindings[1].b.value) + }) + it('should use the given factory', async () => { const content = { results: { @@ -204,7 +230,7 @@ describe('ResultParser', () => { parser.end(JSON.stringify(content)) - await getStream.array(parser) + await chunks(parser) deepStrictEqual(factory.used, { blankNode: true, diff --git a/test/SimpleClient.test.js b/test/SimpleClient.test.js index 621f2b1..72ad279 100644 --- a/test/SimpleClient.test.js +++ b/test/SimpleClient.test.js @@ -1,16 +1,628 @@ -const { strictEqual } = require('assert') -const { describe, it } = require('mocha') -const SimpleClient = require('../SimpleClient') -const RawQuery = require('../RawQuery') +import { rejects, strictEqual, throws } from 'node:assert' +import express from 'express' +import withServer from 'express-as-promise/withServer.js' +import { describe, it } from 'mocha' +import RawQuery from '../RawQuery.js' +import SimpleClient from '../SimpleClient.js' +import { message, selectQuery, updateQuery } from './support/examples.js' +import isSocketError from './support/isSocketError.js' describe('SimpleClient', () => { it('should be a constructor', () => { strictEqual(typeof SimpleClient, 'function') }) + it('should throw an error if endpointUrl, storeUrl, or updateUrl is given', () => { + throws(() => { + new SimpleClient({}) // eslint-disable-line no-new + }, { + message: /endpointUrl/ + }) + }) + it('should use RawQuery to create the query instance', () => { - const client = new SimpleClient({}) + const client = new SimpleClient({ endpointUrl: 'test' }) strictEqual(client.query instanceof RawQuery, true) }) + + it('should set authorization header if user and password are given', () => { + const client = new SimpleClient({ + endpointUrl: 'test', + user: 'abc', + password: 'def' + }) + + strictEqual(client.headers.get('authorization'), 'Basic YWJjOmRlZg==') + }) + + it('should forward client to Query constructor', () => { + class Query { + constructor ({ client }) { + this.client = client + } + } + + const client = new SimpleClient({ endpointUrl: 'test', Query }) + + strictEqual(client.query.client, client) + }) + + it('should forward client to Store constructor', () => { + class Store { + constructor ({ client }) { + this.client = client + } + } + + const client = new SimpleClient({ endpointUrl: 'test', Store }) + + strictEqual(client.store.client, client) + }) + + describe('.get', () => { + it('should be a method', () => { + const client = new SimpleClient({ endpointUrl: 'test' }) + + strictEqual(typeof client.get, 'function') + }) + + it('should return a response object', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + const res = await client.get(selectQuery) + + strictEqual(typeof res, 'object') + strictEqual(typeof res.text, 'function') + }) + }) + + it('should send a GET request', async () => { + await withServer(async server => { + let called = false + + server.app.get('/', async (req, res) => { + called = true + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.get(selectQuery) + + strictEqual(called, true) + }) + }) + + it('should send the query string as query parameter', async () => { + await withServer(async server => { + let parameter = null + + server.app.get('/', async (req, res) => { + parameter = req.query.query + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.get(selectQuery) + + strictEqual(parameter, selectQuery) + }) + }) + + it('should keep existing query params', async () => { + await withServer(async server => { + let parameters = null + const key = 'auth_token' + const value = '12345' + + server.app.get('/', async (req, res) => { + parameters = req.query + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl: `${endpointUrl}?${key}=${value}` }) + + await client.get(selectQuery) + + strictEqual(parameters[key], value) + }) + }) + + it('should merge the headers given in the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.get('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.get(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should prioritize the headers from the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.get('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ + endpointUrl, + headers: { + authorization: 'Bearer bar' + } + }) + + await client.get(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should use the updateUrl and update param if update is true', async () => { + await withServer(async server => { + let parameters = null + + server.app.get('/', async (req, res) => { + parameters = req.query + + res.end() + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + + await client.get(updateQuery, { update: true }) + + strictEqual(parameters.update, updateQuery) + }) + }) + + it('should keep existing update query params', async () => { + await withServer(async server => { + let parameters = null + const key = 'auth_token' + const value = '12345' + + server.app.get('/', async (req, res) => { + parameters = req.query + + res.end() + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl: `${updateUrl}?${key}=${value}` }) + + await client.get(updateQuery, { update: true }) + + strictEqual(parameters[key], value) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + req.client.destroy() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await rejects(async () => { + await client.get(selectQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.get(selectQuery) + }) + }) + }) + + describe('.postDirect', () => { + it('should be a method', () => { + const client = new SimpleClient({ endpointUrl: 'test' }) + + strictEqual(typeof client.postDirect, 'function') + }) + + it('should return a response object', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + const res = await client.postDirect(selectQuery) + + strictEqual(typeof res, 'object') + strictEqual(typeof res.text, 'function') + }) + }) + + it('should send a POST request', async () => { + await withServer(async server => { + let called = false + + server.app.post('/', async (req, res) => { + called = true + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postDirect(selectQuery) + + strictEqual(called, true) + }) + }) + + it('should send a content type header with the value application/sparql-query & charset utf-8', async () => { + await withServer(async server => { + let contentType = null + + server.app.post('/', async (req, res) => { + contentType = req.headers['content-type'] + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postDirect(selectQuery) + + strictEqual(contentType, 'application/sparql-query; charset=utf-8') + }) + }) + + it('should send the query string in the request body', async () => { + await withServer(async server => { + let content = null + + server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { + content = req.body + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postDirect(selectQuery) + + strictEqual(content, selectQuery) + }) + }) + + it('should merge the headers given in the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.post('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postDirect(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should prioritize the headers from the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.post('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ + endpointUrl, + headers: { + authorization: 'Bearer bar' + } + }) + + await client.postDirect(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should use the updateUrl if update is true', async () => { + await withServer(async server => { + let called = false + + server.app.post('/', async (req, res) => { + called = true + + res.end() + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + + await client.postDirect(updateQuery, { update: true }) + + strictEqual(called, true) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + req.client.destroy() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await rejects(async () => { + await client.postDirect(selectQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postDirect(selectQuery) + }) + }) + }) + + describe('.postUrlencoded', () => { + it('should be a method', () => { + const client = new SimpleClient({ endpointUrl: 'test' }) + + strictEqual(typeof client.postUrlencoded, 'function') + }) + + it('should return a response object', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + const res = await client.postUrlencoded(selectQuery) + + strictEqual(typeof res, 'object') + strictEqual(typeof res.text, 'function') + }) + }) + + it('should send a POST request', async () => { + await withServer(async server => { + let called = false + + server.app.post('/', async (req, res) => { + called = true + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postUrlencoded(selectQuery) + + strictEqual(called, true) + }) + }) + + it('should send a content type header with the value application/x-www-form-urlencoded', async () => { + await withServer(async server => { + let contentType = null + + server.app.post('/', async (req, res) => { + contentType = req.headers['content-type'] + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postUrlencoded(selectQuery) + + strictEqual(contentType, 'application/x-www-form-urlencoded') + }) + }) + + it('should send the query string urlencoded in the request body', async () => { + await withServer(async server => { + let parameter = null + + server.app.post('/', express.urlencoded({ extended: true }), async (req, res) => { + parameter = req.body.query + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postUrlencoded(selectQuery) + + strictEqual(parameter, selectQuery) + }) + }) + + it('should merge the headers given in the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.post('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postUrlencoded(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should prioritize the headers from the method call', async () => { + await withServer(async server => { + let header = null + const value = 'Bearer foo' + + server.app.post('/', async (req, res) => { + header = req.headers.authorization + + res.end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ + endpointUrl, + headers: { + authorization: 'Bearer bar' + } + }) + + await client.postUrlencoded(selectQuery, { + headers: { + authorization: value + } + }) + + strictEqual(header, value) + }) + }) + + it('should use the updateUrl if update is true', async () => { + await withServer(async server => { + let called = false + + server.app.post('/', async (req, res) => { + called = true + + res.end() + }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + + await client.postUrlencoded(updateQuery, { update: true }) + + strictEqual(called, true) + }) + }) + + it('should handle server socket errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + req.client.destroy() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await rejects(async () => { + await client.postUrlencoded(selectQuery) + }, err => isSocketError(err)) + }) + }) + + it('should not handle server errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.status(500).end(message) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + + await client.postUrlencoded(selectQuery) + }) + }) + }) }) diff --git a/test/StreamClient.test.js b/test/StreamClient.test.js index 05b8134..b11e426 100644 --- a/test/StreamClient.test.js +++ b/test/StreamClient.test.js @@ -1,38 +1,67 @@ -const { strictEqual } = require('assert') -const { describe, it } = require('mocha') -const StreamClient = require('../StreamClient') -const StreamQuery = require('../StreamQuery') -const StreamStore = require('../StreamStore') +import { deepStrictEqual, strictEqual, throws } from 'node:assert' +import omit from 'lodash/omit.js' +import pick from 'lodash/pick.js' +import { describe, it } from 'mocha' +import SimpleClient from '../SimpleClient.js' +import StreamClient from '../StreamClient.js' +import StreamQuery from '../StreamQuery.js' +import StreamStore from '../StreamStore.js' describe('StreamClient', () => { it('should be a constructor', () => { strictEqual(typeof StreamClient, 'function') }) - it('should use the given factory', () => { - const factory = 'test' - - const client = new StreamClient({ factory }) - - strictEqual(client.query.factory, 'test') - strictEqual(client.store.factory, 'test') + it('should throw an error if the given factory does not implement the DataFactory interface', () => { + throws(() => { + new StreamClient({ // eslint-disable-line no-new + endpointUrl: 'test', + factory: {} + }) + }, { + message: /DataFactory/ + }) }) it('should use StreamQuery to create the query instance', () => { - const client = new StreamClient({}) + const client = new StreamClient({ endpointUrl: 'test' }) strictEqual(client.query instanceof StreamQuery, true) }) + it('should forward the client to the query instance', () => { + const client = new StreamClient({ endpointUrl: 'test' }) + + strictEqual(client.query.client, client) + }) + it('should use StreamStore to create the store instance', () => { - const client = new StreamClient({}) + const client = new StreamClient({ endpointUrl: 'test' }) strictEqual(client.store instanceof StreamStore, true) }) - it('should forward additional options', () => { - const client = new StreamClient({ maxQuadsPerRequest: 1 }) + it('should forward the client to the store instance', () => { + const client = new StreamClient({ endpointUrl: 'test' }) + + strictEqual(client.store.client, client) + }) + + it('should be possible to create an instance from a SimpleClient', () => { + const options = { + endpointUrl: 'sparql', + headers: new Headers({ 'user-agent': 'sparql-http-client' }), + password: 'pwd', + storeUrl: 'graph', + updateUrl: 'update', + user: 'usr' + } + + const simpleClient = new SimpleClient(options) + const client = new StreamClient(simpleClient) + const result = pick(client, Object.keys(options)) - strictEqual(client.store.maxQuadsPerRequest, 1) + deepStrictEqual(omit(result, 'headers'), omit(options, 'headers')) + deepStrictEqual([...result.headers.entries()], [...simpleClient.headers.entries()]) }) }) diff --git a/test/StreamQuery.test.js b/test/StreamQuery.test.js index 1022668..a0936d6 100644 --- a/test/StreamQuery.test.js +++ b/test/StreamQuery.test.js @@ -1,36 +1,65 @@ -const { deepStrictEqual, rejects, strictEqual } = require('assert') -const { text, urlencoded } = require('body-parser') -const getStream = require('get-stream') -const { describe, it } = require('mocha') -const fetch = require('nodeify-fetch') -const { toCanonical } = require('rdf-dataset-ext') -const rdf = { ...require('@rdfjs/data-model'), ...require('@rdfjs/dataset') } -const namespace = require('@rdfjs/namespace') -const { quadToNTriples } = require('@rdfjs/to-ntriples') -const testFactory = require('./support/testFactory') -const withServer = require('./support/withServer') -const Endpoint = require('../Endpoint') -const StreamQuery = require('../StreamQuery') - -const ns = { - ex: namespace('http://example.org/') -} - -const simpleAskQuery = 'ASK {}' -const simpleConstructQuery = 'CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}' -const simpleSelectQuery = 'SELECT * WHERE {?s ?p ?o}' -const simpleUpdateQuery = 'INSERT { "object"} WHERE {}' +import { deepStrictEqual, rejects, strictEqual } from 'node:assert' +import factory from '@rdfjs/data-model' +import express from 'express' +import withServer from 'express-as-promise/withServer.js' +import { isReadableStream, isWritableStream } from 'is-stream' +import { describe, it } from 'mocha' +import rdf from 'rdf-ext' +import { datasetEqual } from 'rdf-test/assert.js' +import chunks from 'stream-chunks/chunks.js' +import SimpleClient from '../SimpleClient.js' +import StreamQuery from '../StreamQuery.js' +import { message, quads, askQuery, constructQuery, selectQuery, updateQuery } from './support/examples.js' +import isServerError from './support/isServerError.js' +import isSocketError from './support/isSocketError.js' +import * as ns from './support/namespaces.js' +import testFactory from './support/testFactory.js' describe('StreamQuery', () => { describe('.ask', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new StreamQuery({ endpoint }) + const query = new StreamQuery({}) strictEqual(typeof query.ask, 'function') }) - it('should send a GET request to the endpointUrl', async () => { + it('should return a boolean value', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.json({ + boolean: true + }) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + const result = await query.ask(askQuery) + + strictEqual(typeof result, 'boolean') + }) + }) + + it('should parse the SPARQL JSON result', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.json({ + boolean: true + }) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + const result = await query.ask(askQuery) + + strictEqual(result, true) + }) + }) + + it('should send a GET request', async () => { await withServer(async server => { let called = false @@ -41,10 +70,10 @@ describe('StreamQuery', () => { }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - await query.ask(simpleAskQuery) + await query.ask(askQuery) strictEqual(called, true) }) @@ -61,175 +90,172 @@ describe('StreamQuery', () => { }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - await query.ask(simpleAskQuery) + await query.ask(askQuery) - strictEqual(parameter, simpleAskQuery) + strictEqual(parameter, askQuery) }) }) - it('should parse parse the result and return the boolean value', async () => { + it('should use the given operation for the request', async () => { await withServer(async server => { - server.app.get('/', async (req, res) => { + let parameter = null + + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { + parameter = req.body.query + res.json({ boolean: true }) }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const result = await query.ask(simpleConstructQuery) + await query.ask(askQuery, { operation: 'postUrlencoded' }) - strictEqual(result, true) + strictEqual(parameter, askQuery) }) }) - it('should use the given operation for the request', async () => { + it('should handle server socket errors', async () => { await withServer(async server => { - let parameter = null - - server.app.post('/', urlencoded({ extended: false }), async (req, res) => { - parameter = req.body.query - - res.json({ - boolean: true - }) + server.app.get('/', async (req, res) => { + req.destroy() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - await query.ask(simpleAskQuery, { operation: 'postUrlencoded' }) - - strictEqual(parameter, simpleAskQuery) + await rejects(async () => { + await query.ask(askQuery) + }, err => isSocketError(err)) }) }) it('should handle server errors', async () => { await withServer(async server => { - const message = 'test message' - server.app.get('/', async (req, res) => { res.status(500).end(message) }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) await rejects(async () => { - await query.ask(simpleAskQuery) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) - - return true - }) + await query.ask(askQuery) + }, err => isServerError(err, message)) }) }) }) describe('.construct', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new StreamQuery({ endpoint }) + const query = new StreamQuery({}) strictEqual(typeof query.construct, 'function') }) - it('should send a GET request to the endpointUrl', async () => { + it('should return a Readable stream object', async () => { await withServer(async server => { - let called = false - server.app.get('/', async (req, res) => { - called = true - res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.construct(simpleConstructQuery) - await getStream.array(stream) + const result = query.construct(constructQuery) - strictEqual(called, true) + strictEqual(isReadableStream(result), true) + strictEqual(isWritableStream(result), false) + + await chunks(result) }) }) - it('should send the query string as query parameter', async () => { + it('should parse the N-Triples', async () => { await withServer(async server => { - let parameter = null + server.app.get('/', async (req, res) => { + res.end(quads.toString()) + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + const stream = query.construct(constructQuery) + const result = await chunks(stream) + + datasetEqual(result, quads) + }) + }) + + it('should send a GET request', async () => { + await withServer(async server => { + let called = false server.app.get('/', async (req, res) => { - parameter = req.query.query + called = true res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.construct(simpleConstructQuery) - await getStream.array(stream) + const stream = query.construct(constructQuery) + await chunks(stream) - strictEqual(parameter, simpleConstructQuery) + strictEqual(called, true) }) }) - it('should parse the N-Triples from the server and provide them as a quad stream', async () => { + it('should send the query string as query parameter', async () => { await withServer(async server => { - const quads = rdf.dataset([ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ]) - const content = [...quads].map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let parameter = null server.app.get('/', async (req, res) => { - res.end(content) + parameter = req.query.query + + res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.construct(simpleConstructQuery) - const result = rdf.dataset(await getStream.array(stream)) + const stream = query.construct(constructQuery) + await chunks(stream) - strictEqual(toCanonical(result), toCanonical(quads)) + strictEqual(parameter, constructQuery) }) }) it('should use the given factory', async () => { await withServer(async server => { - const quads = rdf.dataset([rdf.quad(rdf.blankNode(), ns.ex.predicate, rdf.literal('test'))]) - const content = [...quads].map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + const quads = rdf.dataset([ + rdf.quad(rdf.blankNode(), ns.ex.predicate, rdf.literal('test')) + ]) const factory = testFactory() server.app.get('/', async (req, res) => { - res.end(content) + res.end(quads.toString()) }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint, factory }) + const client = new SimpleClient({ endpointUrl, factory }) + const query = new StreamQuery({ client }) - const stream = await query.construct(simpleConstructQuery) - await getStream.array(stream) + const stream = query.construct(constructQuery) + await chunks(stream) deepStrictEqual(factory.used, { blankNode: true, @@ -245,20 +271,20 @@ describe('StreamQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: false }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { parameter = req.body.query res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.construct(simpleConstructQuery, { operation: 'postUrlencoded' }) - await getStream.array(stream) + const stream = query.construct(constructQuery, { operation: 'postUrlencoded' }) + await chunks(stream) - strictEqual(parameter, simpleConstructQuery) + strictEqual(parameter, constructQuery) }) }) @@ -273,92 +299,78 @@ describe('StreamQuery', () => { }) const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const endpoint = new Endpoint({ endpointUrl, fetch }) - const query = new StreamQuery({ endpoint }) - - await query.construct(simpleConstructQuery) + const stream = query.construct(constructQuery) + await chunks(stream) strictEqual(accept, 'application/n-triples, text/turtle') }) }) - it('should handle server errors', async () => { + it('should handle server socket errors', async () => { await withServer(async server => { - const message = 'test message' - server.app.get('/', async (req, res) => { - res.status(500).end(message) + req.destroy() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) await rejects(async () => { - await query.construct(simpleConstructQuery) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) + const stream = query.construct(constructQuery) + await chunks(stream) + }, err => isSocketError(err)) + }) + }) - return true + it('should handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + await rejects(async () => { + const stream = query.construct(constructQuery) + await chunks(stream) + }, err => isServerError(err, message)) }) }) }) describe('.select', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new StreamQuery({ endpoint }) + const query = new StreamQuery({}) strictEqual(typeof query.select, 'function') }) - it('should send a GET request to the endpointUrl', async () => { + it('should return a Readable stream object', async () => { await withServer(async server => { - let called = false - server.app.get('/', async (req, res) => { - called = true - res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.select(simpleSelectQuery) - await getStream.array(stream) + const result = query.select(selectQuery) - strictEqual(called, true) - }) - }) + strictEqual(isReadableStream(result), true) + strictEqual(isWritableStream(result), false) - it('should send the query string as query parameter', async () => { - await withServer(async server => { - let parameter = null - - server.app.get('/', async (req, res) => { - parameter = req.query.query - - res.status(204).end() - }) - - const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) - - const stream = await query.construct(simpleSelectQuery) - await getStream.array(stream) - - strictEqual(parameter, simpleSelectQuery) + await chunks(result) }) }) - it('should parse the SPARQL JSON result from the server and provide it as stream of RDF/JS key value pair objects', async () => { + it('should parse the SPARQL JSON result', async () => { await withServer(async server => { const content = { results: { @@ -375,11 +387,11 @@ describe('StreamQuery', () => { }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl, factory }) + const query = new StreamQuery({ client }) - const stream = await query.select(simpleSelectQuery) - const result = await getStream.array(stream) + const stream = query.select(selectQuery) + const result = await chunks(stream) strictEqual(result[0].a.termType, 'NamedNode') strictEqual(result[0].a.value, content.results.bindings[0].a.value) @@ -388,6 +400,48 @@ describe('StreamQuery', () => { }) }) + it('should send a GET request', async () => { + await withServer(async server => { + let called = false + + server.app.get('/', async (req, res) => { + called = true + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + const stream = query.select(selectQuery) + await chunks(stream) + + strictEqual(called, true) + }) + }) + + it('should send the query string as query parameter', async () => { + await withServer(async server => { + let parameter = null + + server.app.get('/', async (req, res) => { + parameter = req.query.query + + res.status(204).end() + }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + const stream = query.construct(selectQuery) + await chunks(stream) + + strictEqual(parameter, selectQuery) + }) + }) + it('should use the given factory', async () => { await withServer(async server => { const content = { @@ -408,11 +462,11 @@ describe('StreamQuery', () => { }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ factory, endpoint }) + const client = new SimpleClient({ endpointUrl, factory }) + const query = new StreamQuery({ client }) - const stream = await query.select(simpleSelectQuery) - await getStream.array(stream) + const stream = query.select(selectQuery) + await chunks(stream) deepStrictEqual(factory.used, { blankNode: true, @@ -426,57 +480,66 @@ describe('StreamQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: false }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { parameter = req.body.query res.status(204).end() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) - const stream = await query.select(simpleSelectQuery, { operation: 'postUrlencoded' }) - await getStream.array(stream) + const stream = query.select(selectQuery, { operation: 'postUrlencoded' }) + await chunks(stream) - strictEqual(parameter, simpleSelectQuery) + strictEqual(parameter, selectQuery) }) }) - it('should handle server errors', async () => { + it('should handle server socket errors', async () => { await withServer(async server => { - const message = 'test message' - server.app.get('/', async (req, res) => { - res.status(500).end(message) + req.destroy() }) const endpointUrl = await server.listen() - const endpoint = new Endpoint({ fetch, endpointUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) await rejects(async () => { - await query.select(simpleSelectQuery) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) + const stream = query.select(selectQuery) + await chunks(stream) + }, err => isSocketError(err)) + }) + }) - return true + it('should handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) }) + + const endpointUrl = await server.listen() + const client = new SimpleClient({ endpointUrl }) + const query = new StreamQuery({ client }) + + await rejects(async () => { + const stream = query.select(selectQuery) + await chunks(stream) + }, err => isServerError(err, message)) }) }) }) describe('.update', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const query = new StreamQuery({ endpoint }) + const query = new StreamQuery({}) strictEqual(typeof query.update, 'function') }) - it('should send a POST request to the updateUrl', async () => { + it('should send a POST request', async () => { await withServer(async server => { let called = false @@ -487,10 +550,10 @@ describe('StreamQuery', () => { }) const updateUrl = await server.listen() - const endpoint = new Endpoint({ fetch, updateUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ updateUrl }) + const query = new StreamQuery({ client }) - await query.update(simpleUpdateQuery) + await query.update(updateQuery) strictEqual(called, true) }) @@ -500,19 +563,19 @@ describe('StreamQuery', () => { await withServer(async server => { let parameter = null - server.app.post('/', urlencoded({ extended: false }), async (req, res) => { + server.app.post('/', express.urlencoded({ extended: false }), async (req, res) => { parameter = req.body.update res.status(204).end() }) const updateUrl = await server.listen() - const endpoint = new Endpoint({ fetch, updateUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ updateUrl }) + const query = new StreamQuery({ client }) - await query.update(simpleUpdateQuery) + await query.update(updateQuery) - strictEqual(parameter, simpleUpdateQuery) + strictEqual(parameter, updateQuery) }) }) @@ -520,43 +583,51 @@ describe('StreamQuery', () => { await withServer(async server => { let content = null - server.app.post('/', text({ type: '*/*' }), async (req, res) => { + server.app.post('/', express.text({ type: '*/*' }), async (req, res) => { content = req.body res.status(204).end() }) const updateUrl = await server.listen() - const endpoint = new Endpoint({ fetch, updateUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ updateUrl }) + const query = new StreamQuery({ client }) - await query.update(simpleUpdateQuery, { operation: 'postDirect' }) + await query.update(updateQuery, { operation: 'postDirect' }) - strictEqual(content, simpleUpdateQuery) + strictEqual(content, updateQuery) }) }) - it('should handle server errors', async () => { + it('should handle server socket errors', async () => { await withServer(async server => { - const message = 'test message' - server.app.post('/', async (req, res) => { - res.status(500).end(message) + req.destroy() }) const updateUrl = await server.listen() - const endpoint = new Endpoint({ fetch, updateUrl }) - const query = new StreamQuery({ endpoint }) + const client = new SimpleClient({ updateUrl }) + const query = new StreamQuery({ client }) await rejects(async () => { - await query.update(simpleUpdateQuery) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) + await query.update(updateQuery) + }, err => isSocketError(err)) + }) + }) - return true + it('should handle server errors', async () => { + await withServer(async server => { + server.app.post('/', async (req, res) => { + res.status(500).end(message) }) + + const updateUrl = await server.listen() + const client = new SimpleClient({ updateUrl }) + const query = new StreamQuery({ client }) + + await rejects(async () => { + await query.update(updateQuery) + }, err => isServerError(err, message)) }) }) }) diff --git a/test/StreamStore.test.js b/test/StreamStore.test.js index 50a56df..c1a1f4d 100644 --- a/test/StreamStore.test.js +++ b/test/StreamStore.test.js @@ -1,34 +1,50 @@ -const { deepStrictEqual, notStrictEqual, rejects, strictEqual } = require('assert') -const getStream = require('get-stream') -const intoStream = require('into-stream') -const { describe, it } = require('mocha') -const fetch = require('nodeify-fetch') -const { toCanonical } = require('rdf-dataset-ext') -const rdf = require('@rdfjs/data-model') -const namespace = require('@rdfjs/namespace') -const { quadToNTriples } = require('@rdfjs/to-ntriples') -const testFactory = require('./support/testFactory') -const withServer = require('./support/withServer') -const Endpoint = require('../Endpoint') -const StreamStore = require('../StreamStore') - -const ns = { - ex: namespace('http://example.org/') -} +import { deepStrictEqual, rejects, strictEqual } from 'node:assert' +import withServer from 'express-as-promise/withServer.js' +import { isReadableStream, isWritableStream } from 'is-stream' +import { describe, it } from 'mocha' +import rdf from 'rdf-ext' +import { datasetEqual } from 'rdf-test/assert.js' +import { Readable } from 'readable-stream' +import chunks from 'stream-chunks/chunks.js' +import decode from 'stream-chunks/decode.js' +import SimpleClient from '../SimpleClient.js' +import StreamStore from '../StreamStore.js' +import { graph, message, quads } from './support/examples.js' +import isServerError from './support/isServerError.js' +import isSocketError from './support/isSocketError.js' +import * as ns from './support/namespaces.js' +import testFactory from './support/testFactory.js' describe('StreamStore', () => { describe('.read', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const store = new StreamStore({ endpoint }) + const store = new StreamStore({}) strictEqual(typeof store.read, 'function') }) + it('should return a Readable stream object', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(204).end() + }) + + const storeUrl = await server.listen() + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) + + const result = store.read({ method: 'GET', graph }) + + strictEqual(isReadableStream(result), true) + strictEqual(isWritableStream(result), false) + + await chunks(result) + }) + }) + it('should use the given method', async () => { await withServer(async server => { let called = false - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { called = true @@ -37,11 +53,11 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) strictEqual(called, true) }) @@ -50,7 +66,6 @@ describe('StreamStore', () => { it('should send the requested graph as a query parameter', async () => { await withServer(async server => { let graphParameter = null - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { graphParameter = req.query.graph @@ -59,11 +74,11 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) strictEqual(graphParameter, graph.value) }) @@ -72,7 +87,6 @@ describe('StreamStore', () => { it('should not send the graph query parameter if the default graph is requested', async () => { await withServer(async server => { let graphParameter = null - const graph = rdf.defaultGraph() server.app.get('/', async (req, res) => { graphParameter = req.query.graph @@ -81,21 +95,19 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph: rdf.defaultGraph() }) + await chunks(stream) - strictEqual(typeof graphParameter, 'undefined') + strictEqual(graphParameter, undefined) }) }) - it('should request content with media type application/n-triples from the server', async () => { + it('should request content with media type application/n-triples', async () => { await withServer(async server => { let mediaType = null - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { mediaType = req.get('accept') @@ -104,86 +116,69 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) strictEqual(mediaType, 'application/n-triples') }) }) - it('should parse the N-Triples from the server and provide them as a quad stream', async () => { + it('should parse the N-Triples and return them as a quad stream', async () => { await withServer(async server => { - const graph = ns.ex.graph1 - const expected = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, graph), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, graph), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, graph), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, graph) - ] - const content = expected.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') - server.app.get('/', async (req, res) => { - res.end(content) + res.end(quads.toString()) }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - const quads = await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + const result = await chunks(stream) - strictEqual(toCanonical(quads), toCanonical(expected)) + datasetEqual(result, rdf.dataset(quads, graph)) }) }) it('should not send the graph query parameter if the default graph is requested', async () => { await withServer(async server => { - let error = null - const graph = ns.ex.graph1 - server.app.get('/', async (req, res) => { - res.status(500).end() + res.status(500).end(message) }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - try { - await store.read({ method: 'GET', graph }) - } catch (err) { - error = err - } + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - notStrictEqual(error, null) + await rejects(async () => { + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) + }, err => { + return isServerError(err, message) + }) }) }) it('should use the given factory', async () => { await withServer(async server => { - const graph = ns.ex.graph1 - const expected = [rdf.quad(rdf.blankNode(), ns.ex.predicate1, rdf.literal('test'), graph)] - const content = expected.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + const quads = rdf.dataset([ + rdf.quad(rdf.blankNode(), ns.ex.predicate1, rdf.literal('test'), graph) + ]) const factory = testFactory() server.app.get('/', async (req, res) => { - res.end(content) + res.end(quads.toString()) }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ factory, endpoint }) + const client = new SimpleClient({ factory, storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) deepStrictEqual(factory.used, { blankNode: true, @@ -198,7 +193,6 @@ describe('StreamStore', () => { it('should use the given user and password', async () => { await withServer(async server => { let authorization = null - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { authorization = req.headers.authorization @@ -207,47 +201,54 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl, user: 'abc', password: 'def' }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl, user: 'abc', password: 'def' }) + const store = new StreamStore({ client }) - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) strictEqual(authorization, 'Basic YWJjOmRlZg==') }) }) - it('should handle server errors', async () => { + it('should handle server socket errors', async () => { await withServer(async server => { - const message = 'test message' - const graph = ns.ex.graph1 - server.app.get('/', async (req, res) => { - res.status(500).end(message) + req.client.destroy() }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) await rejects(async () => { - const stream = await store.read({ method: 'GET', graph }) - await getStream.array(stream) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) + }, err => isSocketError(err)) + }) + }) - return true + it('should handle server errors', async () => { + await withServer(async server => { + server.app.get('/', async (req, res) => { + res.status(500).end(message) }) + + const storeUrl = await server.listen() + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) + + await rejects(async () => { + const stream = store.read({ method: 'GET', graph }) + await chunks(stream) + }, err => isServerError(err, message)) }) }) }) describe('.write', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const store = new StreamStore({ endpoint }) + const store = new StreamStore({}) strictEqual(typeof store.write, 'function') }) @@ -255,7 +256,6 @@ describe('StreamStore', () => { it('should use the given method', async () => { await withServer(async server => { let called = false - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) server.app.post('/', async (req, res) => { called = true @@ -264,20 +264,18 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.write({ method: 'POST', stream }) + await store.write({ method: 'POST', stream: quads.toStream() }) strictEqual(called, true) }) }) - it('should send content with media type application/n-triples to the server', async () => { + it('should send content with media type application/n-triples', async () => { await withServer(async server => { let mediaType = null - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) server.app.post('/', async (req, res) => { mediaType = req.get('content-type') @@ -286,307 +284,131 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.write({ method: 'POST', stream }) + await store.write({ method: 'POST', stream: quads.toStream() }) strictEqual(mediaType, 'application/n-triples') }) }) - it('should send the quad stream as N-Triples to the server', async () => { + it('should send the quad stream as N-Triples', async () => { await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, ns.ex.graph1), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, ns.ex.graph1) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') - - server.app.post('/', async (req, res) => { - content[req.query.graph] = await getStream(req) - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - await store.write({ method: 'POST', stream }) - - strictEqual(content[quads[0].graph.value], expected) - }) - }) - - it('should support default graph', async () => { - await withServer(async server => { - let graph = true - let content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let content server.app.post('/', async (req, res) => { - graph = req.query.graph - content = await getStream(req) + content = await decode(req) res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.write({ method: 'POST', stream }) + await store.write({ method: 'POST', stream: quads.toStream() }) - strictEqual(typeof graph, 'undefined') - strictEqual(content, expected) + strictEqual(content, quads.toString()) }) }) - it('should use multiple requests to send multiple graphs', async () => { + it('should handle streams with no data', async () => { await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph2), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph2) - ] - const expected = quads.reduce((expected, quad) => { - const graphIri = quad.graph.value || '' - - expected[graphIri] = (expected[graphIri] || '') + - quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - - return expected - }, {}) - server.app.post('/', async (req, res) => { - content[typeof req.query.graph === 'string' ? req.query.graph : ''] = await getStream(req) - res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.write({ method: 'POST', stream }) - - Object.entries(expected).forEach(([graphIri, graphContent]) => { - strictEqual(graphContent, content[graphIri]) - }) + await store.write({ method: 'POST', stream: Readable.from([]) }) }) }) - it('should use multiple requests if maxQuadsPerRequest is reached', async () => { + it('should use the given user and password', async () => { await withServer(async server => { - const content = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expected = [ - quadToNTriples(quads[0]) + '\n' + quadToNTriples(quads[1]) + '\n', - quadToNTriples(quads[2]) + '\n' + quadToNTriples(quads[3]) + '\n' - ] + let authorization = null server.app.post('/', async (req, res) => { - content.push(await getStream(req)) + authorization = req.headers.authorization res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint, maxQuadsPerRequest: 2 }) - - await store.write({ method: 'POST', stream }) - - deepStrictEqual(content, expected) - }) - }) + const client = new SimpleClient({ storeUrl, user: 'abc', password: 'def' }) + const store = new StreamStore({ client }) - it('should handle streams with no data', async () => { - await withServer(async server => { - const quads = [] - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + await store.write({ method: 'POST', stream: quads.toStream() }) - await store.write({ method: 'POST', stream }) + strictEqual(authorization, 'Basic YWJjOmRlZg==') }) }) it('should handle server socket errors', async () => { await withServer(async server => { - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - server.app.post('/', async req => { req.client.destroy() }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) await rejects(async () => { - await store.write({ method: 'POST', stream }) - }, err => { - strictEqual(err.message.includes('socket hang up'), true) - - return true - }) + await store.write({ method: 'POST', stream: quads.toStream() }) + }, err => isSocketError(err)) }) }) it('should handle server errors', async () => { await withServer(async server => { - const message = 'test message' - - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - server.app.post('/', async (req, res) => { res.status(500).end(message) }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) await rejects(async () => { - await store.write({ method: 'POST', stream }) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) - - return true - }) + await store.write({ method: 'POST', stream: quads.toStream() }) + }, err => isServerError(err, message)) }) }) + }) - it('should handle server socket errors in separated requests', async () => { - await withServer(async server => { - const quad1 = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - const quad2 = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph2) - - server.app.post('/', async (req, res) => { - if (req.query.graph === quad2.graph.value) { - return req.client.destroy() - } - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object([quad1, quad2]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - await rejects(async () => { - await store.write({ method: 'POST', stream }) - }, err => { - strictEqual(err.message.includes('socket hang up'), true) + describe('.get', () => { + it('should be a method', () => { + const store = new StreamStore({}) - return true - }) - }) + strictEqual(typeof store.get, 'function') }) - it('should handle server errors in separated requests', async () => { + it('should return a Readable stream object', async () => { await withServer(async server => { - const message = 'test message' - - const quad1 = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - const quad2 = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph2) - - server.app.post('/', async (req, res) => { - if (req.query.graph === quad2.graph.value) { - return res.status(500).end(message) - } - + server.app.get('/', async (req, res) => { res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object([quad1, quad2]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await rejects(async () => { - await store.write({ method: 'POST', stream }) - }, err => { - strictEqual(err.message.includes('Internal Server Error'), true) - strictEqual(err.message.includes('500'), true) - strictEqual(err.message.includes(message), true) + const result = store.get(graph) - return true - }) - }) - }) - - it('should use the given user and password', async () => { - await withServer(async server => { - let authorization = null - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - - server.app.post('/', async (req, res) => { - authorization = req.headers.authorization - - res.status(204).end() - }) + strictEqual(isReadableStream(result), true) + strictEqual(isWritableStream(result), false) - const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl, user: 'abc', password: 'def' }) - const store = new StreamStore({ endpoint }) - - await store.write({ method: 'POST', stream }) - - strictEqual(authorization, 'Basic YWJjOmRlZg==') + await chunks(result) }) }) - }) - - describe('.get', () => { - it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const store = new StreamStore({ endpoint }) - - strictEqual(typeof store.get, 'function') - }) it('should send a GET request', async () => { await withServer(async server => { let called = false - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { called = true @@ -595,11 +417,11 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.get(graph) - await getStream.array(stream) + const stream = store.get(graph) + await chunks(stream) strictEqual(called, true) }) @@ -608,7 +430,6 @@ describe('StreamStore', () => { it('should send the requested graph as a query parameter', async () => { await withServer(async server => { let graphParameter = null - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { graphParameter = req.query.graph @@ -617,11 +438,11 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.get(graph) - await getStream.array(stream) + const stream = store.get(graph) + await chunks(stream) strictEqual(graphParameter, graph.value) }) @@ -630,7 +451,6 @@ describe('StreamStore', () => { it('should not send the graph query parameter if the default graph is requested', async () => { await withServer(async server => { let graphParameter = null - const graph = rdf.defaultGraph() server.app.get('/', async (req, res) => { graphParameter = req.query.graph @@ -639,21 +459,19 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - const stream = await store.get(graph) - await getStream.array(stream) + const stream = store.get(rdf.defaultGraph()) + await chunks(stream) - strictEqual(typeof graphParameter, 'undefined') + strictEqual(graphParameter, undefined) }) }) - it('should request content with media type application/n-triples from the server', async () => { + it('should request content with media type application/n-triples', async () => { await withServer(async server => { let mediaType = null - const graph = ns.ex.graph1 server.app.get('/', async (req, res) => { mediaType = req.get('accept') @@ -662,72 +480,54 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.get(graph) - await getStream.array(stream) + const stream = store.get(graph) + await chunks(stream) strictEqual(mediaType, 'application/n-triples') }) }) - it('should parse the N-Triples from the server and provide them as a quad stream', async () => { + it('should parse the N-Triples and return them as a quad stream', async () => { await withServer(async server => { - const graph = ns.ex.graph1 - const expected = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, graph), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, graph), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, graph), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, graph) - ] - const content = expected.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') - server.app.get('/', async (req, res) => { - res.end(content) + res.end(quads.toString()) }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - const stream = await store.get(graph) - const quads = await getStream.array(stream) + const stream = store.get(graph) + const result = await chunks(stream) - strictEqual(toCanonical(quads), toCanonical(expected)) + datasetEqual(result, rdf.dataset(quads, graph)) }) }) - it('should not send the graph query parameter if the default graph is requested', async () => { + it('should handle server errors', async () => { await withServer(async server => { - let error = null - const graph = ns.ex.graph1 - server.app.get('/', async (req, res) => { - res.status(500).end() + res.status(500).end(message) }) const storeUrl = await server.listen() - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - try { - await store.get(graph) - } catch (err) { - error = err - } - - notStrictEqual(error, null) + await rejects(async () => { + const stream = store.get(graph) + await chunks(stream) + }, err => isServerError(err, message)) }) }) }) describe('.post', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const store = new StreamStore({ endpoint }) + const store = new StreamStore({}) strictEqual(typeof store.post, 'function') }) @@ -735,7 +535,6 @@ describe('StreamStore', () => { it('should send a POST request', async () => { await withServer(async server => { let called = false - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) server.app.post('/', async (req, res) => { called = true @@ -744,20 +543,21 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.post(stream) + await store.post(quads.toStream()) strictEqual(called, true) }) }) - it('should send content with media type application/n-triples to the server', async () => { + it('should send content with media type application/n-triples', async () => { await withServer(async server => { let mediaType = null - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) + const quads = rdf.dataset([ + rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1) + ]) server.app.post('/', async (req, res) => { mediaType = req.get('content-type') @@ -766,179 +566,78 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.post(stream) + await store.post(quads.toStream()) strictEqual(mediaType, 'application/n-triples') }) }) - it('should send the quad stream as N-Triples to the server', async () => { + it('should send the quad stream as N-Triples', async () => { await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, ns.ex.graph1), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, ns.ex.graph1) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let content server.app.post('/', async (req, res) => { - content[req.query.graph] = await getStream(req) + content = await decode(req) res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.post(stream) + await store.post(quads.toStream()) - strictEqual(content[quads[0].graph.value], expected) + strictEqual(content, quads.toString()) }) }) it('should support default graph', async () => { await withServer(async server => { let graph = true - let content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let content server.app.post('/', async (req, res) => { graph = req.query.graph - content = await getStream(req) + content = await decode(req) res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.post(stream) + await store.post(quads.toStream()) - strictEqual(typeof graph, 'undefined') - strictEqual(content, expected) - }) - }) - - it('should use multiple requests to send multiple graphs', async () => { - await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph2), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph2) - ] - const expected = quads.reduce((expected, quad) => { - const graphIri = quad.graph.value || '' - - expected[graphIri] = (expected[graphIri] || '') + - quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - - return expected - }, {}) - - server.app.post('/', async (req, res) => { - content[typeof req.query.graph === 'string' ? req.query.graph : ''] = await getStream(req) - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - await store.post(stream) - - Object.entries(expected).forEach(([graphIri, graphContent]) => { - strictEqual(graphContent, content[graphIri]) - }) - }) - }) - - it('should use multiple requests if maxQuadsPerRequest is reached', async () => { - await withServer(async server => { - const content = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expected = [ - quadToNTriples(quads[0]) + '\n' + quadToNTriples(quads[1]) + '\n', - quadToNTriples(quads[2]) + '\n' + quadToNTriples(quads[3]) + '\n' - ] - - server.app.post('/', async (req, res) => { - content.push(await getStream(req)) - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint, maxQuadsPerRequest: 2 }) - - await store.post(stream) - - deepStrictEqual(content, expected) + strictEqual(graph, undefined) + strictEqual(content, quads.toString()) }) }) it('should handle server errors', async () => { await withServer(async server => { - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - server.app.post('/', async (req, res) => { - res.status(500).end() + res.status(500).end(message) }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - let error = null - - try { - await store.post(stream) - } catch (err) { - error = err - } - - notStrictEqual(error, null) + await rejects(async () => { + await store.post(quads.toStream()) + }, err => isServerError(err, message)) }) }) }) describe('.put', () => { it('should be a method', () => { - const endpoint = new Endpoint({ fetch }) - const store = new StreamStore({ endpoint }) + const store = new StreamStore({}) strictEqual(typeof store.put, 'function') }) @@ -946,7 +645,6 @@ describe('StreamStore', () => { it('should send a PUT request', async () => { await withServer(async server => { let called = false - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) server.app.put('/', async (req, res) => { called = true @@ -955,20 +653,18 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.put(stream) + await store.put(quads.toStream()) strictEqual(called, true) }) }) - it('should send content with media type application/n-triples to the server', async () => { + it('should send content with media type application/n-triples', async () => { await withServer(async server => { let mediaType = null - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) server.app.put('/', async (req, res) => { mediaType = req.get('content-type') @@ -977,177 +673,71 @@ describe('StreamStore', () => { }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.put(stream) + await store.put(quads.toStream()) strictEqual(mediaType, 'application/n-triples') }) }) - it('should send the quad stream as N-Triples to the server', async () => { + it('should send the quad stream as N-Triples', async () => { await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3, ns.ex.graph1), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4, ns.ex.graph1) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let content server.app.put('/', async (req, res) => { - content[req.query.graph] = await getStream(req) + content = await decode(req) res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.put(stream) + await store.put(quads.toStream()) - strictEqual(content[quads[0].graph.value], expected) + strictEqual(content, quads.toString()) }) }) it('should support default graph', async () => { await withServer(async server => { let graph = true - let content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expected = quads.map(quad => { - return quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - }).join('') + let content server.app.put('/', async (req, res) => { graph = req.query.graph - content = await getStream(req) - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) - - await store.put(stream) - - strictEqual(typeof graph, 'undefined') - strictEqual(content, expected) - }) - }) - - it('should use multiple requests to send multiple graphs', async () => { - await withServer(async server => { - const content = {} - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2, ns.ex.graph1), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4), - rdf.quad(ns.ex.subject5, ns.ex.predicate5, ns.ex.object5, ns.ex.graph2), - rdf.quad(ns.ex.subject6, ns.ex.predicate6, ns.ex.object6, ns.ex.graph2) - ] - const expected = quads.reduce((expected, quad) => { - const graphIri = quad.graph.value || '' - - expected[graphIri] = (expected[graphIri] || '') + - quadToNTriples(rdf.quad(quad.subject, quad.predicate, quad.object)) + '\n' - - return expected - }, {}) - - server.app.put('/', async (req, res) => { - content[typeof req.query.graph === 'string' ? req.query.graph : ''] = await getStream(req) + content = await decode(req) res.status(204).end() }) const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - await store.put(stream) + await store.put(quads.toStream()) - Object.entries(expected).forEach(([graphIri, graphContent]) => { - strictEqual(graphContent, content[graphIri]) - }) - }) - }) - - it('should use PUT and POST methods to combine multiple requests split by maxQuadsPerRequest', async () => { - await withServer(async server => { - const contentPut = [] - const contentPost = [] - const quads = [ - rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), - rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), - rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), - rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) - ] - const expectedPut = [quadToNTriples(quads[0]) + '\n' + quadToNTriples(quads[1]) + '\n'] - const expectedPost = [quadToNTriples(quads[2]) + '\n' + quadToNTriples(quads[3]) + '\n'] - - server.app.post('/', async (req, res) => { - contentPost.push(await getStream(req)) - - res.status(204).end() - }) - - server.app.put('/', async (req, res) => { - contentPut.push(await getStream(req)) - - res.status(204).end() - }) - - const storeUrl = await server.listen() - const stream = intoStream.object(quads) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint, maxQuadsPerRequest: 2 }) - - await store.put(stream) - - deepStrictEqual(contentPut, expectedPut) - deepStrictEqual(contentPost, expectedPost) + strictEqual(graph, undefined) + strictEqual(content, quads.toString()) }) }) it('should handle server errors', async () => { await withServer(async server => { - const quad = rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1, ns.ex.graph1) - server.app.put('/', async (req, res) => { - res.status(500).end() + res.status(500).end(message) }) const storeUrl = await server.listen() - const stream = intoStream.object([quad]) - const endpoint = new Endpoint({ fetch, storeUrl }) - const store = new StreamStore({ endpoint }) + const client = new SimpleClient({ storeUrl }) + const store = new StreamStore({ client }) - let error = null - - try { - await store.put(stream) - } catch (err) { - error = err - } - - notStrictEqual(error, null) + await rejects(async () => { + await store.put(quads.toStream()) + }, err => isServerError(err, message)) }) }) }) diff --git a/test/support/examples.js b/test/support/examples.js new file mode 100644 index 0000000..3eea87a --- /dev/null +++ b/test/support/examples.js @@ -0,0 +1,28 @@ +import rdf from 'rdf-ext' +import * as ns from './namespaces.js' + +const graph = ns.ex.graph1 + +const message = 'test message' + +const quads = rdf.dataset([ + rdf.quad(ns.ex.subject1, ns.ex.predicate1, ns.ex.object1), + rdf.quad(ns.ex.subject2, ns.ex.predicate2, ns.ex.object2), + rdf.quad(ns.ex.subject3, ns.ex.predicate3, ns.ex.object3), + rdf.quad(ns.ex.subject4, ns.ex.predicate4, ns.ex.object4) +]) + +const askQuery = 'ASK {}' +const constructQuery = 'CONSTRUCT {?s ?p ?o} WHERE {?s ?p ?o}' +const selectQuery = 'SELECT * WHERE {?s ?p ?o}' +const updateQuery = 'INSERT { "object"} WHERE {}' + +export { + graph, + message, + quads, + askQuery, + constructQuery, + selectQuery, + updateQuery +} diff --git a/test/support/isServerError.js b/test/support/isServerError.js new file mode 100644 index 0000000..2e4d6d3 --- /dev/null +++ b/test/support/isServerError.js @@ -0,0 +1,12 @@ +import { strictEqual } from 'node:assert' + +function isServerError (err, message) { + strictEqual(err.message.includes('Internal Server Error'), true) + strictEqual(err.message.includes('500'), true) + strictEqual(err.message.includes(message), true) + strictEqual(err.status, 500) + + return true +} + +export default isServerError diff --git a/test/support/isSocketError.js b/test/support/isSocketError.js new file mode 100644 index 0000000..e1fda14 --- /dev/null +++ b/test/support/isSocketError.js @@ -0,0 +1,10 @@ +import { strictEqual } from 'node:assert' + +function isSocketError (err) { + strictEqual(err.message.includes('socket hang up'), true) + strictEqual(err.status, undefined) + + return true +} + +export default isSocketError diff --git a/test/support/namespaces.js b/test/support/namespaces.js new file mode 100644 index 0000000..425760f --- /dev/null +++ b/test/support/namespaces.js @@ -0,0 +1,7 @@ +import rdf from 'rdf-ext' + +const ex = rdf.namespace('http://example.org/') + +export { + ex +} diff --git a/test/support/testFactory.js b/test/support/testFactory.js index 15b7df3..537440f 100644 --- a/test/support/testFactory.js +++ b/test/support/testFactory.js @@ -1,26 +1,31 @@ -const rdf = require('@rdfjs/data-model') +import dataModelFactory from '@rdfjs/data-model' +import datasetFactory from '@rdfjs/dataset' function testFactory () { const factory = { blankNode: () => { factory.used.blankNode = true - return rdf.blankNode() + return dataModelFactory.blankNode() + }, + dataset: (quads, graph) => { + factory.used.dataset = true + return datasetFactory.dataset(quads, graph) }, defaultGraph: () => { factory.used.defaultGraph = true - return rdf.defaultGraph() + return dataModelFactory.defaultGraph() }, - literal: (value) => { + literal: value => { factory.used.literal = true - return rdf.literal(value) + return dataModelFactory.literal(value) }, - namedNode: (value) => { + namedNode: value => { factory.used.namedNode = true - return rdf.namedNode(value) + return dataModelFactory.namedNode(value) }, quad: (s, p, o, g) => { factory.used.quad = true - return rdf.quad(s, p, o, g) + return dataModelFactory.quad(s, p, o, g) }, used: {} } @@ -28,4 +33,4 @@ function testFactory () { return factory } -module.exports = testFactory +export default testFactory diff --git a/test/support/withServer.js b/test/support/withServer.js deleted file mode 100644 index 704556a..0000000 --- a/test/support/withServer.js +++ /dev/null @@ -1,22 +0,0 @@ -const ExpressAsPromise = require('express-as-promise') - -async function withServer (callback) { - let error = null - const server = new ExpressAsPromise() - - try { - await callback(server) - } catch (err) { - error = err - } - - if (server.server) { - await server.stop() - } - - if (error) { - throw error - } -} - -module.exports = withServer diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..dc77579 --- /dev/null +++ b/test/test.js @@ -0,0 +1,51 @@ +import { strictEqual } from 'node:assert' +import { describe, it } from 'mocha' +import * as index from '../index.js' +import ParsingClient from '../ParsingClient.js' +import ParsingQuery from '../ParsingQuery.js' +import RawQuery from '../RawQuery.js' +import ResultParser from '../ResultParser.js' +import SimpleClient from '../SimpleClient.js' +import StreamClient from '../StreamClient.js' +import StreamQuery from '../StreamQuery.js' +import StreamStore from '../StreamStore.js' + +// TODO: remove all awaits that are not required + +describe('sparql-http-client', () => { + it('should export the StreamClient as default export', () => { + strictEqual(index.default, StreamClient) + }) + + it('should export the ParsingClient', () => { + strictEqual(index.ParsingClient, ParsingClient) + }) + + it('should export the ParsingQuery', () => { + strictEqual(index.ParsingQuery, ParsingQuery) + }) + + it('should export the RawQuery', () => { + strictEqual(index.RawQuery, RawQuery) + }) + + it('should export the ResultParser', () => { + strictEqual(index.ResultParser, ResultParser) + }) + + it('should export the SimpleClient', () => { + strictEqual(index.SimpleClient, SimpleClient) + }) + + it('should export the StreamClient', () => { + strictEqual(index.StreamClient, StreamClient) + }) + + it('should export the StreamQuery', () => { + strictEqual(index.StreamQuery, StreamQuery) + }) + + it('should export the StreamStore', () => { + strictEqual(index.StreamStore, StreamStore) + }) +})