'use strict'
var packageJson = require('../package.json')
var PageHelper = require('./PageHelper')
var RequestResult = require('./RequestResult')
var errors = require('./errors')
var http = require('./_http')
var json = require('./_json')
var query = require('./query')
var stream = require('./stream')
var util = require('./_util')
var values = require('./values')
/**
* The callback that will be executed after every completed request.
*
* @callback Client~observerCallback
* @param {RequestResult} res
*/
/**
* **WARNING: This is an experimental feature. There are no guarantees to
* its API stability and/or service availability. DO NOT USE IT IN
* PRODUCTION**.
*
* Creates a subscription to the result of the given read-only expression. When
* executed, the expression must only perform reads and produce a single
* streamable type, such as a reference or a version. Expressions that attempt
* to perform writes or produce non-streamable types will result in an error.
* Otherwise, any expression can be used to initiate a stream, including
* user-defined function calls.
*
* The subscription returned by this method does not issue any requests until
* the {@link module:stream~Subscription#start} method is called. Make sure to
* subscribe to the events of interest, otherwise the received events are simply
* ignored. For example:
*
* ```
* client.stream(document.ref)
* .on('version', version => console.log(version))
* .on('error', error => console.log(error))
* .start()
* ```
*
* Please note that streams are not temporal, meaning that there is no option to
* configure its starting timestamp. The stream will, however, state its initial
* subscription time via the {@link module:stream~Subscription#event:start}
* event. A common programming mistake is to read a document, then initiate a
* subscription. This approach can miss events that occurred between the initial
* read and the subscription request. To prevent event loss, make sure the
* subscription has started before performing a data load. The following example
* buffer events until the document's data is loaded:
*
* ```
* var buffer = []
* var loaded = false
*
* client.stream(document.ref)
* .on('start', ts => {
* loadData(ts).then(data => {
* processData(data)
* processBuffer(buffer)
* loaded = true
* })
* })
* .on('version', version => {
* if (loaded) {
* processVersion(version)
* } else {
* buffer.push(version)
* }
* })
* .start()
* ```
*
* The reduce boilerplate, the `document` helper implements a similar
* functionality, except it discards events prior to the document's snapshot
* time. The expression given to this helper must be a reference as it
* internally runs a {@link module:query~Get} call with it. The example above
* can be rewritten as:
*
* ```
* client.stream.document(document.ref)
* .on('snapshot', data => processData(data))
* .on('version', version => processVersion(version))
* .start()
* ```
*
* Be aware that streams are not available in all browsers. If the client can't
* initiate a stream, an error event with the {@link
* module:errors~StreamsNotSupported} error will be emmited.
*
* To stop a subscription, call the {@link module:stream~Subscription#close}
* method:
*
* ```
* var subscription = client.stream(document.ref)
* .on('version', version => processVersion(version))
* .start()
*
* // ...
* subscription.close()
* ```
*
* @param {module:query~ExprArg} expression
* The expression to subscribe to. Created from {@link module:query}
* functions.
*
* @param {?module:stream~Options} options
* Object that configures the stream.
*
* @property {function} document
* A document stream helper. See {@link Client#stream} for more information.
*
* @see module:stream~Subscription
*
* @function
* @name Client#stream
* @returns {module:stream~Subscription} A new subscription instance.
*/
/**
* A client for interacting with FaunaDB.
*
* Users will mainly call the {@link Client#query} method to execute queries, or
* the {@link Client#stream} method to subscribe to streams.
*
* See the [FaunaDB Documentation](https://fauna.com/documentation) for detailed examples.
*
* All methods return promises containing a JSON object that represents the FaunaDB response.
* Literal types in the response object will remain as strings, Arrays, and objects.
* FaunaDB types, such as {@link Ref}, {@link SetRef}, {@link FaunaTime}, and {@link FaunaDate} will
* be converted into the appropriate object.
*
* (So if a response contains `{ "@ref": "collections/frogs/123" }`,
* it will be returned as `new Ref("collections/frogs/123")`.)
*
* @constructor
* @param {?Object} options
* Object that configures this FaunaDB client.
* @param {?string} options.endpoint
* Full URL for the FaunaDB server.
* @param {?string} options.domain
* Base URL for the FaunaDB server.
* @param {?('http'|'https')} options.scheme
* HTTP scheme to use.
* @param {?number} options.port
* Port of the FaunaDB server.
* @param {?string} options.secret FaunaDB secret (see [Reference Documentation](https://app.fauna.com/documentation/intro/security))
* @param {?number} options.timeout Read timeout in seconds.
* @param {?Client~observerCallback} options.observer
* Callback that will be called after every completed request.
* @param {?boolean} options.keepAlive
* Configures http/https keepAlive option (ignored in browser environments)
* @param {?{ string: string }} options.headers
* Optional headers to send with requests
* @param {?fetch} options.fetch
* a fetch compatible [API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) for making a request
* @param {?number} options.queryTimeout
* Sets the maximum amount of time (in milliseconds) for query execution on the server
* @param {?number} options.http2SessionIdleTime
* Sets the maximum amount of time (in milliseconds) an HTTP2 session may live
* when there's no activity. Must be a non-negative integer, with a maximum value of 5000.
* If an invalid value is passed a default of 500 ms is applied. If a value
* exceeding 5000 ms is passed (e.g. Infinity) the maximum of 5000 ms is applied.
* Only applicable for NodeJS environment (when http2 module is used).
* can also be configured via the FAUNADB_HTTP2_SESSION_IDLE_TIME environment variable
* which has the highest priority and overrides the option passed into the Client constructor.
* @param {?boolean} options.checkNewVersion
* Enabled by default. Prints a message to the terminal when a newer driver is available.
* @param {?boolean} options.metrics
* Disabled by default. Controls whether or not query metrics are returned.
*/
function Client(options) {
const http2SessionIdleTime = getHttp2SessionIdleTime(
options ? options.http2SessionIdleTime : undefined
)
if (options) options.http2SessionIdleTime = http2SessionIdleTime
options = util.applyDefaults(options, {
endpoint: null,
domain: 'db.fauna.com',
scheme: 'https',
port: null,
secret: null,
timeout: 60,
observer: null,
keepAlive: true,
headers: {},
fetch: undefined,
queryTimeout: null,
http2SessionIdleTime,
checkNewVersion: false,
})
this._observer = options.observer
this._http = new http.HttpClient(options)
this.stream = stream.StreamAPI(this)
}
/**
* Current API version.
*
* @type {string}
*/
Client.apiVersion = packageJson.apiVersion
/**
* Executes a query via the FaunaDB Query API.
* See the [docs](https://app.fauna.com/documentation/reference/queryapi),
* and the query functions in this documentation.
* @param expression {module:query~ExprArg}
* The query to execute. Created from {@link module:query} functions.
* @param {?Object} options
* Object that configures the current query, overriding FaunaDB client options.
* @param {?string} options.secret FaunaDB secret (see [Reference Documentation](https://app.fauna.com/documentation/intro/security))
* @return {external:Promise<Object>} FaunaDB response object.
*/
Client.prototype.query = function(expression, options) {
query.arity.between(1, 2, arguments, 'Client.prototype.query')
options = Object.assign({}, this._globalQueryOptions, options)
return this._execute('POST', '', query.wrap(expression), null, options)
}
/**
* Returns a {@link PageHelper} for the given Query expression.
* This provides a helpful API for paginating over FaunaDB responses.
* @param expression {Expr}
* The Query expression to paginate over.
* @param params {Object}
* Options to be passed to the paginate function. See [paginate](https://app.fauna.com/documentation/reference/queryapi#read-functions).
* @param options {?Object}
* Object that configures the current pagination queries, overriding FaunaDB client options.
* @param {?string} options.secret FaunaDB secret (see [Reference Documentation](https://app.fauna.com/documentation/intro/security))
* @returns {PageHelper} A PageHelper that wraps the provided expression.
*/
Client.prototype.paginate = function(expression, params, options) {
params = util.defaults(params, {})
options = util.defaults(options, {})
return new PageHelper(this, expression, params, options)
}
/**
* Sends a `ping` request to FaunaDB.
* @return {external:Promise<string>} Ping response.
*/
Client.prototype.ping = function(scope, timeout) {
return this._execute('GET', 'ping', null, { scope: scope, timeout: timeout })
}
/**
* Get the freshest timestamp reported to this client.
* @returns {number} the last seen transaction time
*/
Client.prototype.getLastTxnTime = function() {
return this._http.getLastTxnTime()
}
/**
* Sync the freshest timestamp seen by this client.
*
* This has no effect if staler than currently stored timestamp.
* WARNING: This should be used only when coordinating timestamps across
* multiple clients. Moving the timestamp arbitrarily forward into
* the future will cause transactions to stall.
* @param time {number} the last seen transaction time
*/
Client.prototype.syncLastTxnTime = function(time) {
this._http.syncLastTxnTime(time)
}
/**
* Closes the client session and cleans up any held resources.
* By default, it will wait for any ongoing requests to complete on their own;
* streaming requests are terminated forcibly. Any subsequent requests will
* error after the .close method is called.
* Should be used at application termination in order to release any open resources
* and allow the process to terminate e.g. when the custom http2SessionIdleTime parameter is used.
*
* @param {?object} opts Close options.
* @param {?boolean} opts.force Specifying this property will force any ongoing
* requests to terminate instead of gracefully waiting until they complete.
* This may result in an ERR_HTTP2_STREAM_CANCEL error for NodeJS.
* @returns {Promise<void>}
*/
Client.prototype.close = function(opts) {
return this._http.close(opts)
}
/**
* Executes a query via the FaunaDB Query API.
* See the [docs](https://app.fauna.com/documentation/reference/queryapi),
* and the query functions in this documentation.
* @param expression {module:query~ExprArg}
* The query to execute. Created from {@link module:query} functions.
* @param {?Object} options
* Object that configures the current query, overriding FaunaDB client options.
* @param {?string} options.secret FaunaDB secret (see [Reference Documentation](https://app.fauna.com/documentation/intro/security))
* @return {external:Promise<Object>} {value, metrics} An object containing the FaunaDB response object and the list of query metrics incurred by the request.
*/
Client.prototype.queryWithMetrics = function(expression, options) {
query.arity.between(1, 2, arguments, 'Client.prototype.query')
return this._execute('POST', '', query.wrap(expression), null, options, true)
}
Client.prototype._execute = function(
method,
path,
data,
query,
options,
returnMetrics = false
) {
query = util.defaults(query, null)
if (
path instanceof values.Ref ||
util.checkInstanceHasProperty(path, '_isFaunaRef')
) {
path = path.value
}
if (query !== null) {
query = util.removeUndefinedValues(query)
}
var startTime = Date.now()
var self = this
var body =
['GET', 'HEAD'].indexOf(method) >= 0 ? undefined : JSON.stringify(data)
return this._http
.execute(
Object.assign({}, options, {
path: path,
query: query,
method: method,
body: body,
})
)
.then(function(response) {
var endTime = Date.now()
var responseObject = json.parseJSON(response.body)
var result = new RequestResult(
method,
path,
query,
body,
data,
response.body,
responseObject,
response.status,
response.headers,
startTime,
endTime
)
self._handleRequestResult(response, result, options)
const metricsHeaders = [
'x-compute-ops',
'x-byte-read-ops',
'x-byte-write-ops',
'x-query-time',
'x-txn-retries',
]
if (returnMetrics) {
return {
value: responseObject['resource'],
metrics: Object.fromEntries(
Array.from(Object.entries(response.headers))
.filter(([k, v]) => metricsHeaders.includes(k))
.map(([k, v]) => [k, parseInt(v)])
),
}
} else {
return responseObject['resource']
}
})
}
Client.prototype._handleRequestResult = function(response, result, options) {
var txnTimeHeaderKey = 'x-txn-time'
if (response.headers[txnTimeHeaderKey] != null) {
this.syncLastTxnTime(parseInt(response.headers[txnTimeHeaderKey], 10))
}
var observers = [this._observer, options && options.observer]
observers.forEach(observer => {
if (typeof observer == 'function') {
observer(result, this)
}
})
errors.FaunaHTTPError.raiseForStatusCode(result)
}
function getHttp2SessionIdleTime(configuredIdleTime) {
const maxIdleTime = 5000
const defaultIdleTime = 500
const envIdleTime = util.getEnvVariable('FAUNADB_HTTP2_SESSION_IDLE_TIME')
var value = defaultIdleTime
// attemp to set the idle time to the env value and then the configured value
const values = [envIdleTime, configuredIdleTime]
for (const rawValue of values) {
const parsedValue =
rawValue === 'Infinity' ? Number.MAX_SAFE_INTEGER : parseInt(rawValue, 10)
const isNegative = parsedValue < 0
const isGreaterThanMax = parsedValue > maxIdleTime
// if we didn't get infinity or a positive integer move to the next value
if (isNegative || !parsedValue) continue
// if we did get something valid constrain it to the ceiling
value = parsedValue
if (isGreaterThanMax) value = maxIdleTime
break
}
return value
}
module.exports = Client