Skip to content

Commit

Permalink
feat(rest): add REST client for resources
Browse files Browse the repository at this point in the history
node-exist exports a new async function `getRestClient` accepting the
same connection options as connect did.
The returned client has three methods `get`, `put` and `del` to
read, create and remove resources from exist using the REST API.

```
const rc = await getRestClient(connectionOptions)
await rc.put('<root />', '/db/test.xml')
await rc.get('/db/test.xml')
await rc.del('/db/test.xml')
```
  • Loading branch information
line-o committed Aug 8, 2022
1 parent 7f1d4d1 commit 7c369cb
Show file tree
Hide file tree
Showing 8 changed files with 2,517 additions and 1,430 deletions.
65 changes: 57 additions & 8 deletions components/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ const defaultRPCoptions = {
}
}

const defaultRestOptions = {
host: 'localhost',
protocol: 'https',
port: '8443',
path: '/exist/rest',
basic_auth: {
user: 'guest',
pass: 'guest'
}
}

function isLocalDB (host) {
return (
host === 'localhost' ||
Expand All @@ -45,6 +56,11 @@ function useSecureConnection (options) {
return true
}

function basicAuth (name, pass) {
const payload = pass ? `${name}:${pass}` : name
return 'Basic ' + Buffer.from(payload).toString('base64')
}

/**
* Connect to database via XML-RPC
* @param {NodeExistConnectionOptions} options
Expand All @@ -54,25 +70,56 @@ function connect (options) {
const _options = assign({}, defaultRPCoptions, options)
delete _options.secure // prevent pollution of XML-RPC options

let client
if (useSecureConnection(options)) {
// allow invalid and self-signed certificates on localhost, if not explicitly
// enforced by setting options.rejectUnauthorized to true
_options.rejectUnauthorized = ('rejectUnauthorized' in _options)
? _options.rejectUnauthorized
: !isLocalDB(_options.host)

const secureClient = xmlrpc.createSecureClient(_options)
secureClient.promisedMethodCall = promisedMethodCall(secureClient)
return secureClient
}
if (!isLocalDB(_options.host)) {
console.warn('Connecting to DB using an unencrypted channel.')
client = xmlrpc.createSecureClient(_options)
} else {
if (!isLocalDB(_options.host)) {
console.warn('Connecting to DB using an unencrypted channel.')
}
client = xmlrpc.createClient(_options)
}
const client = xmlrpc.createClient(_options)
client.promisedMethodCall = promisedMethodCall(client)
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
}

/**
* Read connection options from ENV
* NOTE: The connection options returned from this function
Expand Down Expand Up @@ -108,5 +155,7 @@ function readOptionsFromEnv () {
module.exports = {
connect,
readOptionsFromEnv,
defaultRPCoptions
restConnection,
defaultRPCoptions,
defaultRestOptions
}
4 changes: 2 additions & 2 deletions components/documents.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const mime = require('mime')
const { getMimeType } = require('./util')

function upload (client, contentBuffer) {
return client.promisedMethodCall('upload', [contentBuffer, contentBuffer.length])
}

function parseLocal (client, handle, filename, options) {
// set default values
const mimeType = options.mimetype || mime.getType(filename)
const mimeType = getMimeType(filename, options.mimetype)
const replace = options.replace || true

return client.promisedMethodCall('parseLocal', [handle, filename, replace, mimeType])
Expand Down
116 changes: 116 additions & 0 deletions components/rest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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)) {
console.log('GENERATOR FUNCTION')
return restClient.put({
url,
headers: {
'content-type': contentType
},
body: body()
})
}

return restClient.put({
url,
headers: {
'content-type': contentType,
'content-length': body.length
},
body
})
}

/**
* read resource in DB
* @param {Object} restClient Got-instance
* @param {string} path which resource to read
* @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, writableStream) {
const url = normalizeDBPath(path)

if (writableStream instanceof stream.Writable) {
const readStream = restClient.stream({ url })

let _response
readStream.on('response', response => {
_response = response
})

await pipeline(
readStream,
writableStream
)
return _response
}

return restClient.get(url)
}

/**
* 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 = {
put,
get,
del
}
15 changes: 15 additions & 0 deletions components/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
}
14 changes: 14 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const collections = require('./components/collections')
const indices = require('./components/indices')
const users = require('./components/users')
const app = require('./components/app')
const rest = require('./components/rest')

// exist specific mime types
mime.define({
Expand Down Expand Up @@ -77,6 +78,18 @@ function connect (options) {
}
}

async function getRestClient (options) {
const restClient = await connection.restConnection(options)
const { del, put, get } = applyEachWith(rest, restClient)

return {
restClient,
del,
put,
get
}
}

exports.readOptionsFromEnv = connection.readOptionsFromEnv
exports.connect = connect
exports.defineMimeTypes = function (mimeTypes) {
Expand All @@ -86,3 +99,4 @@ exports.defineMimeTypes = function (mimeTypes) {
exports.getMimeType = function (path) {
return mime.getType(path)
}
exports.getRestClient = getRestClient
Loading

0 comments on commit 7c369cb

Please sign in to comment.