@existdb/node-exist
Advanced tools
| const { promisify } = require('util') | ||
| const stream = require('stream') | ||
| const { isGeneratorFunction } = require('util').types | ||
| const pipeline = promisify(stream.pipeline) | ||
| const { getMimeType } = require('./util') | ||
| /** | ||
| * remove leading slash from path | ||
| * @param {string} path database path | ||
| * @returns {string} normalized path | ||
| */ | ||
| function normalizeDBPath (path) { | ||
| return path.startsWith('/') ? path.substring(1) : path | ||
| } | ||
| /** | ||
| * create resource in DB | ||
| * @param {Object} restClient Got-instance | ||
| * @param {string | Buffer | stream.Readable | Generator | AsyncGenerator | FormData} body contents of the resource | ||
| * @param {string} path where to create resource | ||
| * @param {string | undefined} [mimetype] enforce specific mimetype | ||
| * @returns {Object} Response with headers | ||
| */ | ||
| async function put (restClient, body, path, mimetype) { | ||
| const url = normalizeDBPath(path) | ||
| const contentType = getMimeType(path, mimetype) | ||
| if (body instanceof stream.Readable) { | ||
| const writeStream = restClient.stream.put({ | ||
| url, | ||
| headers: { | ||
| 'content-type': contentType | ||
| } | ||
| }) | ||
| let _response | ||
| writeStream.on('response', response => { | ||
| _response = response | ||
| }) | ||
| await pipeline( | ||
| body, | ||
| writeStream, | ||
| new stream.PassThrough() // necessary to receive read errors | ||
| ) | ||
| return _response | ||
| } | ||
| if (isGeneratorFunction(body)) { | ||
| return restClient.put({ | ||
| url, | ||
| headers: { | ||
| 'content-type': contentType | ||
| }, | ||
| body: body() | ||
| }) | ||
| } | ||
| return restClient.put({ | ||
| url, | ||
| headers: { | ||
| 'content-type': contentType, | ||
| 'content-length': body.length | ||
| }, | ||
| body | ||
| }) | ||
| } | ||
| const postAttributeNames = [ | ||
| 'start', | ||
| 'max', | ||
| 'session', | ||
| 'cache' | ||
| ] | ||
| const existResultRegex = /^<exist:result/ | ||
| /** | ||
| * Tests if body is a wrapped result from exist=db | ||
| * | ||
| * @param {String} body server response body | ||
| * @returns {Boolean} | ||
| */ | ||
| function isExistResult (body) { | ||
| return existResultRegex.test(body) | ||
| } | ||
| const sessionRegex = /exist:session="(\d+)"/ | ||
| const hitsRegex = /exist:hits="(\d+)"/ | ||
| const startRegex = /exist:start="(\d+)"/ | ||
| const countRegex = /exist:count="(\d+)"/ | ||
| const compilationTimeRegex = /exist:compilation-time="(\d+)"/ | ||
| const executionTimeRegex = /exist:execution-time="(\d+)"/ | ||
| /** | ||
| * Extract numerical attribute value with a regular expression from exist:result | ||
| * Returns -1, if the attribute is not set(the regular expression does not match) | ||
| * | ||
| * @param {RegExp} regex regular expression to extract numerical attribute value | ||
| * @returns {Number} parsed attribute value or -1 | ||
| */ | ||
| function getAttributeValueByRegex (existResult, regex) { | ||
| return regex.test(existResult) ? parseInt(regex.exec(existResult)[1], 10) : -1 | ||
| } | ||
| function extendIfWrapped (response) { | ||
| const { body } = response | ||
| if (!isExistResult(body)) { | ||
| return response | ||
| } | ||
| const session = getAttributeValueByRegex(body, sessionRegex) | ||
| const hits = getAttributeValueByRegex(body, hitsRegex) | ||
| const start = getAttributeValueByRegex(body, startRegex) | ||
| const count = getAttributeValueByRegex(body, countRegex) | ||
| const compilationTime = getAttributeValueByRegex(body, compilationTimeRegex) | ||
| const executionTime = getAttributeValueByRegex(body, executionTimeRegex) | ||
| return Object.assign(response, { | ||
| session, | ||
| hits, | ||
| start, | ||
| count, | ||
| compilationTime, | ||
| executionTime | ||
| }) | ||
| } | ||
| /** | ||
| * create resource in DB | ||
| * @param {Object} restClient Got-instance | ||
| * @param {string | Buffer } query XQuery main module | ||
| * @param {string} path context path | ||
| * @param {Object} [options] query options | ||
| * @returns {Object} Response with headers and exist specific values | ||
| */ | ||
| async function post (restClient, query, path, options) { | ||
| const url = normalizeDBPath(path) | ||
| const attributes = [] | ||
| const properties = [] | ||
| if (options) { | ||
| for (const attributeIndex in postAttributeNames) { | ||
| const attributeName = postAttributeNames[attributeIndex] | ||
| if (attributeName in options) { | ||
| attributes.push(`${attributeName}="${options[attributeName]}"`) | ||
| delete options[attributeName] | ||
| } | ||
| } | ||
| for (const option in options) { | ||
| properties.push(`<property name="${option}" value="${options[option]}"/>`) | ||
| } | ||
| } | ||
| const body = `<query xmlns="http://exist.sourceforge.net/NS/exist" | ||
| ${attributes.join(' ')}> | ||
| <text><![CDATA[ | ||
| ${query.toString()} | ||
| ]]></text> | ||
| <properties> | ||
| ${properties.join('\n')} | ||
| </properties> | ||
| </query>` | ||
| const response = await restClient.post({ | ||
| url, | ||
| headers: { | ||
| 'content-type': 'application/xml', | ||
| 'content-length': body.length | ||
| }, | ||
| body | ||
| }) | ||
| return extendIfWrapped(response) | ||
| } | ||
| /** | ||
| * read resources and collection contents in DB | ||
| * @param {Object} restClient Got-instance | ||
| * @param {string} path which resource to read | ||
| * @param {Object} [searchParams] query options | ||
| * @param {stream.Writable | undefined} [writableStream] if provided allows to stream onto the file system for instance | ||
| * @returns {Object} Response with body and headers | ||
| */ | ||
| async function get (restClient, path, searchParams, writableStream) { | ||
| const url = normalizeDBPath(path) | ||
| if (writableStream instanceof stream.Writable) { | ||
| const readStream = restClient.stream({ url, searchParams }) | ||
| let _response | ||
| readStream.on('response', response => { | ||
| _response = response | ||
| }) | ||
| await pipeline( | ||
| readStream, | ||
| writableStream | ||
| ) | ||
| return extendIfWrapped(_response) | ||
| } | ||
| const response = await restClient.get({ url, searchParams }) | ||
| return extendIfWrapped(response) | ||
| } | ||
| /** | ||
| * delete a resource from the database | ||
| * @param {Object} restClient Got-instance | ||
| * @param {string} path which resource to delete | ||
| * @returns {Object} Response with body and headers | ||
| */ | ||
| function del (restClient, path) { | ||
| const url = normalizeDBPath(path) | ||
| return restClient({ | ||
| url, | ||
| method: 'delete' | ||
| }) | ||
| } | ||
| module.exports = { | ||
| get, | ||
| post, | ||
| put, | ||
| del | ||
| } |
| const mime = require('mime') | ||
| /** | ||
| * determine mimetype from path, allowing override | ||
| * @param {string} path database path | ||
| * @param {string | undefined} [mimetype] mimetype to enforce | ||
| * @returns {string} mimetype, defaults to 'application/octet-stream' | ||
| */ | ||
| function getMimeType (path, mimetype) { | ||
| return mimetype || mime.getType(path) || 'application/octet-stream' | ||
| } | ||
| module.exports = { | ||
| getMimeType | ||
| } |
@@ -33,2 +33,13 @@ const xmlrpc = require('xmlrpc') | ||
| const defaultRestOptions = { | ||
| host: 'localhost', | ||
| protocol: 'https', | ||
| port: '8443', | ||
| path: '/exist/rest', | ||
| basic_auth: { | ||
| user: 'guest', | ||
| pass: 'guest' | ||
| } | ||
| } | ||
| function isLocalDB (host) { | ||
@@ -49,2 +60,7 @@ return ( | ||
| function basicAuth (name, pass) { | ||
| const payload = pass ? `${name}:${pass}` : name | ||
| return 'Basic ' + Buffer.from(payload).toString('base64') | ||
| } | ||
| /** | ||
@@ -59,2 +75,3 @@ * Connect to database via XML-RPC | ||
| let client | ||
| if (useSecureConnection(options)) { | ||
@@ -67,10 +84,9 @@ // allow invalid and self-signed certificates on localhost, if not explicitly | ||
| const secureClient = xmlrpc.createSecureClient(_options) | ||
| secureClient.promisedMethodCall = promisedMethodCall(secureClient) | ||
| return secureClient | ||
| client = xmlrpc.createSecureClient(_options) | ||
| } else { | ||
| if (!isLocalDB(_options.host)) { | ||
| console.warn('Connecting to DB using an unencrypted channel.') | ||
| } | ||
| client = xmlrpc.createClient(_options) | ||
| } | ||
| if (!isLocalDB(_options.host)) { | ||
| console.warn('Connecting to DB using an unencrypted channel.') | ||
| } | ||
| const client = xmlrpc.createClient(_options) | ||
| client.promisedMethodCall = promisedMethodCall(client) | ||
@@ -80,2 +96,33 @@ return client | ||
| async function restConnection (options) { | ||
| const { got } = await import('got') | ||
| const _options = assign({}, defaultRestOptions, options) | ||
| const authorization = basicAuth(_options.basic_auth.user, _options.basic_auth.pass) | ||
| const rejectUnauthorized = ('rejectUnauthorized' in _options) | ||
| ? _options.rejectUnauthorized | ||
| : !isLocalDB(_options.host) | ||
| if (!isLocalDB(_options.host) && _options.protocol === 'http') { | ||
| console.warn('Connecting to remote DB using an unencrypted channel.') | ||
| } | ||
| const port = _options.port ? ':' + _options.port : '' | ||
| const path = _options.path.startsWith('/') ? _options.path : '/' + _options.path | ||
| const prefixUrl = `${_options.protocol}://${_options.host}${port}${path}` | ||
| const client = got.extend( | ||
| { | ||
| prefixUrl, | ||
| headers: { | ||
| 'user-agent': 'node-exist', | ||
| authorization | ||
| }, | ||
| https: { rejectUnauthorized } | ||
| } | ||
| ) | ||
| return client | ||
| } | ||
| /** | ||
@@ -116,3 +163,5 @@ * Read connection options from ENV | ||
| readOptionsFromEnv, | ||
| defaultRPCoptions | ||
| restConnection, | ||
| defaultRPCoptions, | ||
| defaultRestOptions | ||
| } |
@@ -1,2 +0,2 @@ | ||
| const mime = require('mime') | ||
| const { getMimeType } = require('./util') | ||
@@ -9,3 +9,3 @@ function upload (client, contentBuffer) { | ||
| // set default values | ||
| const mimeType = options.mimetype || mime.getType(filename) | ||
| const mimeType = getMimeType(filename, options.mimetype) | ||
| const replace = options.replace || true | ||
@@ -12,0 +12,0 @@ |
+15
-0
@@ -29,2 +29,3 @@ /** | ||
| const app = require('./components/app') | ||
| const rest = require('./components/rest') | ||
@@ -81,2 +82,15 @@ // exist specific mime types | ||
| async function getRestClient (options) { | ||
| const restClient = await connection.restConnection(options) | ||
| const { get, put, post, del } = applyEachWith(rest, restClient) | ||
| return { | ||
| restClient, | ||
| get, | ||
| put, | ||
| post, | ||
| del | ||
| } | ||
| } | ||
| exports.readOptionsFromEnv = connection.readOptionsFromEnv | ||
@@ -91,1 +105,2 @@ exports.connect = connect | ||
| } | ||
| exports.getRestClient = getRestClient |
+2
-1
@@ -50,2 +50,3 @@ { | ||
| "dependencies": { | ||
| "got": "^12.1.0", | ||
| "lodash.assign": "^4.0.2", | ||
@@ -55,3 +56,3 @@ "mime": "^3.0.0", | ||
| }, | ||
| "version": "5.0.2" | ||
| "version": "5.1.0" | ||
| } |
+65
-2
@@ -6,3 +6,3 @@ # node-exist | ||
| Mostly a shallow wrapper for [eXist's XML-RPC API](http://exist-db.org/exist/apps/doc/devguide_xmlrpc.xml). | ||
| Mostly a shallow wrapper for [eXist's XML-RPC API](http://exist-db.org/exist/apps/doc/devguide_xmlrpc.xml) and [eXist's REST API](https://exist-db.org/exist/apps/doc/devguide_rest.xml). | ||
| Attempts to translate terminologies into node world. Uses promises. | ||
@@ -28,2 +28,65 @@ | ||
| In addition to eXist-db's XML-RPC API you do now also have the option to | ||
| leverage the potential of its REST-API. | ||
| This allows to choose the best tool for any particular task. | ||
| Both APIs are used in combination in the [upload example](spec/examples/exist-upload). | ||
| ### REST | ||
| Status: unstable | ||
| __NOTE:__ eXist-db's REST-API has its on methodology and available options | ||
| may differ between instances. Especially, the ability | ||
| to download the source code of XQuery files is | ||
| prohibited by default (`_source=yes` is only availabe if enabled in `descriptor.xml`). | ||
| For details of available `options` for each method please see the [REST-API documentation](https://exist-db.org/exist/apps/doc/devguide_rest.xml) | ||
| First, we need an instance of the restClient (see also [Configuration](#configuration) for connection options). | ||
| ```js | ||
| import { getRestClient } from '@existdb/node-exist' | ||
| const rc = await getRestClient() | ||
| ``` | ||
| The rest client has 4 methods | ||
| - `post(query, path[, options])`: execute `query` in the context of `path`. | ||
| The `query` is expected to be a XQuery main module and will be wrapped in the XML-fragment that exist expects. | ||
| ```js | ||
| await rc.post('count(//p)', '/db') | ||
| ``` | ||
| - `put(data, path)` which allows to create resources in the database. | ||
| If sub-collections are missing they will be created for you. | ||
| The server will respond with StatusCode 400, Bad Request, for not-well- | ||
| formed XML resources. | ||
| ```js | ||
| await rc.put('<root />', '/db/rest-test/test.xml') | ||
| ``` | ||
| - `get(path [, options][, writableStream])` to read data from the database. | ||
| The response body will contain the contents of the resource or | ||
| a file listing if the provided path is a collection. | ||
| If a writableStream is passed in the response body will be streamed into it. | ||
| ```js | ||
| const { body } = await rc.get('/db/rest-test/test.xml') | ||
| console.log(body) | ||
| ``` | ||
| - `del(path)`: remove resources and collections from an existdb instance | ||
| ```js | ||
| await rc.del('/db/rest-test/test.xml') | ||
| ``` | ||
| Have a look at [the rest-client example](spec/examples/rest.mjs). | ||
| The REST-client uses the [Got](https://github.com/sindresorhus/got#readme) library | ||
| and works with streams and generators. | ||
| Look at the [rest tests](spec/tests/rest.js) to see examples. | ||
| ### XML-RPC | ||
| Creating, reading and removing a collection: | ||
@@ -521,3 +584,3 @@ | ||
| - [ ] switch to use eXist-db's REST-API. | ||
| - [x] switch to use eXist-db's REST-API (available through [rest-client](#rest-client)) | ||
| - [ ] refactor to ES6 modules | ||
@@ -524,0 +587,0 @@ - [ ] better type hints |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
44301
29.94%20
11.11%740
56.12%597
11.8%4
33.33%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added