Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@existdb/node-exist

Package Overview
Dependencies
Maintainers
3
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@existdb/node-exist - npm Package Compare versions

Comparing version
5.0.2
to
5.1.0
+225
components/rest.js
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
}
+57
-8

@@ -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
}
+2
-2

@@ -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 @@

@@ -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

@@ -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"
}

@@ -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