'use strict'
var util = require('./_util')
/**
* A representation of a FaunaDB Query Expression. Generally, you shouldn't need
* to use this class directly; use the Query helpers defined in {@link module:query}.
*
* @param {Object} obj The object that represents a Query to be treated as an Expression.
* @constructor
*/
function Expr(obj) {
this.raw = obj
}
Expr.prototype._isFaunaExpr = true
Expr.prototype.toJSON = function() {
return this.raw
}
Expr.prototype.toFQL = function() {
return exprToString(this.raw)
}
var varArgsFunctions = [
'Do',
'Call',
'Union',
'Intersection',
'Difference',
'Equals',
'Add',
'BitAnd',
'BitOr',
'BitXor',
'Divide',
'Max',
'Min',
'Modulo',
'Multiply',
'Subtract',
'LT',
'LTE',
'GT',
'GTE',
'And',
'Or',
]
// FQL function names come across the wire as all lowercase letters
// (like the key of this object). Not all are properly snake-cased
// on the Core side, which causes improper capitalizations.
//
// JS Driver patch: https://faunadb.atlassian.net/browse/FE-540
// Core update: https://faunadb.atlassian.net/browse/ENG-2110
var specialCases = {
containsstrregex: 'ContainsStrRegex',
containsstr: 'ContainsStr',
endswith: 'EndsWith',
findstr: 'FindStr',
findstrregex: 'FindStrRegex',
gt: 'GT',
gte: 'GTE',
is_nonempty: 'is_non_empty',
lowercase: 'LowerCase',
lt: 'LT',
lte: 'LTE',
ltrim: 'LTrim',
ngram: 'NGram',
rtrim: 'RTrim',
regexescape: 'RegexEscape',
replacestr: 'ReplaceStr',
replacestrregex: 'ReplaceStrRegex',
startswith: 'StartsWith',
substring: 'SubString',
titlecase: 'TitleCase',
uppercase: 'UpperCase',
}
/**
*
* @param {Expr} expression A FQL expression
* @returns {Boolean} Returns true for valid expressions
* @private
*/
function isExpr(expression) {
return (
expression instanceof Expr ||
util.checkInstanceHasProperty(expression, '_isFaunaExpr')
)
}
/**
*
* @param {Object} obj An object to print
* @returns {String} String representation of object
* @private
*/
function printObject(obj) {
return (
'{' +
Object.keys(obj)
.map(function(k) {
return '"' + k + '"' + ': ' + exprToString(obj[k])
})
.join(', ') +
'}'
)
}
/**
*
* @param {Array} arr An array to print
* @param {Function} toStr Function used for stringification
* @returns {String} String representation of array
* @private
*/
function printArray(arr, toStr) {
return arr
.map(function(item) {
return toStr(item)
})
.join(', ')
}
/**
*
* @param {String} fn A snake-case FQL function name
* @returns {String} The correpsonding camel-cased FQL function name
* @private
*/
function convertToCamelCase(fn) {
// For FQL functions with special formatting concerns, we
// use the specialCases object above to define their casing.
if (fn in specialCases) fn = specialCases[fn]
return fn
.split('_')
.map(function(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
})
.join('')
}
var exprToString = function(expr, caller) {
// If expr is a Expr, we want to parse expr.raw instead
if (isExpr(expr)) {
if ('value' in expr) return expr.toString()
expr = expr.raw
}
// Return early to avoid extra work if null
if (expr === null) {
return 'null'
}
// Return stringified value if expression is not an Object or Array
switch (typeof expr) {
case 'string':
return JSON.stringify(expr)
case 'symbol':
case 'number':
case 'boolean':
return expr.toString()
case 'undefined':
return 'undefined'
}
// Handle expression Arrays
if (Array.isArray(expr)) {
var array = printArray(expr, exprToString)
return varArgsFunctions.indexOf(caller) != -1 ? array : '[' + array + ']'
}
// Parse expression Objects
if ('match' in expr) {
var matchStr = exprToString(expr['match'])
var terms = expr['terms'] || []
if (isExpr(terms)) terms = terms.raw
if (Array.isArray(terms) && terms.length == 0)
return 'Match(' + matchStr + ')'
if (Array.isArray(terms)) {
return (
'Match(' + matchStr + ', [' + printArray(terms, exprToString) + '])'
)
}
return 'Match(' + matchStr + ', ' + exprToString(terms) + ')'
}
if ('paginate' in expr) {
var exprKeys = Object.keys(expr)
if (exprKeys.length === 1) {
return 'Paginate(' + exprToString(expr['paginate']) + ')'
}
var expr2 = Object.assign({}, expr)
delete expr2['paginate']
return (
'Paginate(' +
exprToString(expr['paginate']) +
', ' +
printObject(expr2) +
')'
)
}
if ('let' in expr && 'in' in expr) {
var letExpr = ''
if (Array.isArray(expr['let']))
letExpr = '[' + printArray(expr['let'], printObject) + ']'
else letExpr = printObject(expr['let'])
return 'Let(' + letExpr + ', ' + exprToString(expr['in']) + ')'
}
if ('object' in expr) return printObject(expr['object'])
if ('merge' in expr) {
if (expr.lambda) {
return (
'Merge(' +
exprToString(expr.merge) +
', ' +
exprToString(expr.with) +
', ' +
exprToString(expr.lambda) +
')'
)
}
return (
'Merge(' + exprToString(expr.merge) + ', ' + exprToString(expr.with) + ')'
)
}
if ('lambda' in expr) {
return (
'Lambda(' +
exprToString(expr['lambda']) +
', ' +
exprToString(expr['expr']) +
')'
)
}
if ('filter' in expr) {
return (
'Filter(' +
exprToString(expr['collection']) +
', ' +
exprToString(expr['filter']) +
')'
)
}
if ('call' in expr) {
return (
'Call(' +
exprToString(expr['call']) +
', ' +
exprToString(expr['arguments']) +
')'
)
}
if ('map' in expr) {
return (
'Map(' +
exprToString(expr['collection']) +
', ' +
exprToString(expr['map']) +
')'
)
}
if ('foreach' in expr) {
return (
'Foreach(' +
exprToString(expr['collection']) +
', ' +
exprToString(expr['foreach']) +
')'
)
}
var keys = Object.keys(expr)
var fn = keys[0]
fn = convertToCamelCase(fn)
// The filter prevents zero arity functions from having a null argument
// This only works under the assumptions
// that there are no functions where a single 'null' argument makes sense.
var args = keys
.filter(k => expr[k] !== null || keys.length > 1)
.map(k => exprToString(expr[k], fn))
.join(', ')
return fn + '(' + args + ')'
}
Expr.toString = exprToString
module.exports = Expr