salesforce/lib/query-builder.js (194 lines of code) (raw):
// Copyright Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
const _ = require("lodash/core");
const SalesForceDate = require("./date");
const { escapeString, isArray } = require("./helper");
const opMap = {
"=": "=",
"$eq": "=",
"!=": "!=",
"$ne": "!=",
">": ">",
"$gt": ">",
"<": "<",
"$lt": "<",
">=": ">=",
"$gte": ">=",
"<=": "<=",
"$lte": "<=",
"$like": "LIKE",
"$nlike": "NOT LIKE",
"$in": "IN",
"$nin": "NOT IN",
"$includes": "INCLUDES",
"$excludes": "EXCLUDES",
"$exists": "EXISTS"
};
/**
* Class containing various method to build query string
* @protected
* @constructor
*/
module.exports = class {
/**
*Creates Query String
* @private
* @param {Object} query
* @returns soql
*/
createQuery(query) {
let soql = [
"SELECT ",
this.createFieldsClause(query.fields, query.includes),
" FROM ",
query.table
].join("");
let cond = this.createConditionClause(query.conditions);
if (cond) {
soql += " WHERE " + cond;
}
let orderby = this.createOrderByClause(query.sort);
if (orderby) {
soql += " ORDER BY " + orderby;
}
if (query.limit) {
soql += " LIMIT " + query.limit;
}
if (query.offset) {
soql += " OFFSET " + query.offset;
}
if (query.innerjoin) {
soql += " WHERE " + query.table + "." + query.innerjoin.parent.field + " IN (SELECT " + query.innerjoin.child.table + "." + query.innerjoin.child.field + " FROM " + query.innerjoin.child.table + " )";
}
return soql;
}
/**
*createFieldsClause Create fields string required in query
* @private
* @param {Array<Object>} fields
* @param {Array<String>} childQueries
* @returns string of concatinated fields
*/
createFieldsClause(fields, childQueries) {
childQueries = _.map(_.values(childQueries || {}), () => "(' + createSOQL(cquery) + ')");
return (fields || ["Id"]).concat(childQueries).join(", ");
}
/**
*createConditionClause - conditon string used in query
* @private
* @param {Array|String} conditions
* @param {String} operator
* @param {Number} depth
* @returns conds - Array of condition
*/
createConditionClause(conditions, operator, depth) {
if (_.isString(conditions)) {
return conditions;
}
conditions = conditions || [];
operator = operator || "AND";
depth = depth || 0;
if (!isArray(conditions)) {
conditions = _.keys(conditions).map((key) => ({
key: key,
value: conditions[key]
}));
} else {
conditions = conditions.map((cond) => {
let conds = [];
for (let key in cond) {
conds.push({
key: key,
value: cond[key]
});
}
return conds.length > 1 ? conds : conds[0];
});
}
conditions = conditions.map((cond) => {
let d = depth + 1;
let op;
switch (cond.key) {
case "$or":
case "$and":
case "$not":
if (operator !== "NOT" && conditions.length === 1) {
d = depth;
}
op = cond.key === "$or" ? "OR"
: cond.key === "$and" ? "AND"
: "NOT";
return this.createConditionClause(cond.value, op, d);
default:
return this.createFieldExpression(cond.key, cond.value);
}
}).filter((expr) => expr);
let paren;
if (operator === "NOT") {
paren = depth > 0;
return (paren ? "(" : "") + "NOT " + conditions[0] + (paren ? ")" : "");
} else {
paren = depth > 0 && conditions.length > 1;
return (paren ? "(" : "") + conditions.join(" " + operator + " ") + (paren ? ")" : "");
}
}
/**
*createFieldExpression - creates string from expression query
* @private
* @param {String} field
* @param {Object|Array} value
* @returns str
*/
createFieldExpression(field, value) {
let op = "$eq";
// Assume the `$in` operator if value is an array and none was supplied.
if (_.isArray(value)) {
op = "$in";
} else if (_.isObject(value)) {
for (let k in value) {
if (k[0] === "$") {
op = k;
value = value[k];
break;
}
}
}
let sfop = opMap[op];
if (!sfop || _.isUndefined(value)) { return null; }
let valueExpr = this.createValueExpression(value);
if (_.isUndefined(valueExpr)) { return null; }
switch (sfop) {
case "NOT LIKE":
return "(" + ["NOT", field, "LIKE", valueExpr].join(" ") + ")";
case "EXISTS":
return [field, value ? "!=" : "=", "null"].join(" ");
default:
return [field, sfop, valueExpr].join(" ");
}
}
/**
*createValueExpression
* @private
* @param {Array|String|Number} value
* @returns {String} value- value expression
*/
createValueExpression(value) {
if (isArray(value)) {
return value.length > 0
? "(" + value.map(this.createValueExpression).join(", ") + ")"
: undefined;
}
if (value instanceof SalesForceDate) {
return value._literal.toString();
}
if (_.isString(value)) {
return "'" + escapeString(value) + "'";
}
if (_.isNumber(value)) {
return (value).toString();
}
if (_.isNull(value)) {
return "null";
}
return value;
}
/**
*createOrderByClause
* @private
* @param {Array|String} sort
* @returns stringfy value for orderby
*/
createOrderByClause(sort) {
sort = sort || [];
if (_.isString(sort)) {
if (/,|\s+(asc|desc)\s*$/.test(sort)) {
return sort;
}
// sort order in "FieldA -FieldB" => "ORDER BY FieldA ASC, FieldB DESC".
sort = sort.split(/\s+/).map((field) => {
let dir = "ASC";
let flag = field[0];
if (flag === "-") {
dir = "DESC";
field = field.substring(1);
} else if (flag === "+") {
field = field.substring(1);
}
return [field, dir];
});
} else if (!isArray(sort)) {
sort = _.keys(sort).map((field) => {
let dir = sort[field];
return [field, dir];
});
}
return sort.map((s) => {
let field = s[0];
let dir = s[1];
switch (String(dir)) {
case "DESC":
case "desc":
case "descending":
case "-":
case "-1":
dir = "DESC";
break;
default:
dir = "ASC";
}
return field + " " + dir;
}).join(", ");
}
};